SwiftUI多个SwiftData对象关联导致预览报错问题
SwiftUI多个SwiftData对象关联导致预览报错问题

SwiftUI多个SwiftData对象关联导致预览报错问题

本文作为SwiftData预览报错一系列的文章中比较复杂的内容,因为涉及到多个SwiftData对象关联导致的报错,与以往单个SwiftData对象导致的预览报错的环境不一样,因此单独拿出来分析。

首先是Xcode项目在单个SwiftData对象的情况下,目前预览是正常。

#Preview {
    let container = PiggyBank.preview
    let context = container.mainContext
    let places = try! context.fetch(FetchDescriptor<PiggyBank>()) // 从上下文中获取数据
    return AccessRecordsView(piggyBank: places[0])
}

接着是复现报错的场景。

问题复现

目前“存取猪猪”应用只有一个SwiftData对象,那就是PiggyBank结构,用于存储存钱罐的数据结构信息,例如存钱罐的姓名、金额等等信息。

import SwiftData
import SwiftUI

@Model
class PiggyBank {
    var name: String = ""  // 存钱罐名称
    var icon:String = ""   // 图标名称
    var initialAmount: Double = 0.0 // 初始化金额,仅首次标记,用于后续展示
    var targetAmount: Double = 0.0  // 目标金额
    var amount: Double = 0.0   // 存钱罐金额
    var creationDate: Date = Date()    // 创建日期
    var expirationDate: Date = Date()     // 截止日期
    var isExpirationDateEnabled: Bool = false   // 是否设置截止日期
    var isPrimary: Bool = false // 标记主要存钱罐
    ...
}

因为我想要实现记录存钱罐的存取记录,因此创建了一个SavingsRecord(存取记录)结构,并设置@Relationship与PiggyBank(存钱罐)结构进行关联。

import SwiftUI
import SwiftData

@Model
class SavingsRecord {
    var amount: Double     // 存钱的金额
    var date: Date  // 存钱的日期
    var note: String?  // 可选的备注信息
    
    // 反向关系:与 PiggyBank 关联
    @Relationship(inverse: \PiggyBank.records)
    var piggyBank: PiggyBank? = nil
    
    init(amount: Double, date: Date = Date(), note: String? = nil, piggyBank: PiggyBank? = nil) {
        self.amount = amount
        self.date = date
        self.note = note
        self.piggyBank = piggyBank
    }
}

设置关联后,当创建一条存取记录时,存钱罐也会同步在属性中创建一条关联信息。

@Model
class PiggyBank {
    var name: String = ""  // 存钱罐名称
    var icon:String = ""   // 图标名称
    var initialAmount: Double = 0.0 // 初始化金额,仅首次标记,用于后续展示
    var targetAmount: Double = 0.0  // 目标金额
    var amount: Double = 0.0   // 存钱罐金额
    var creationDate: Date = Date()    // 创建日期
    var expirationDate: Date = Date()     // 截止日期
    var isExpirationDateEnabled: Bool = false   // 是否设置截止日期
    var isPrimary: Bool = false // 标记主要存钱罐
    
    // 与存钱记录的关系
    @Relationship
    var records: [SavingsRecord] = []
    ...
}

新增存取记录结构并关联SwiftData后,重新返回到视图中预览,就会报错预览报错。

代码调试

首先是使用真机和模拟器测试,在模拟器上可以正常打开存取记录的视图,但在Xcode上存取记录的视图是报错的,因此本次问题和排查也只着重于Xcode预览报错。

因此,代码问题首先是定位在预览代码中。

首先,在入口文件中,将SavingsRecord.self导入到modelContainer中。

import SwiftUI
import SwiftData
@main
struct pigletApp: App {
    @State private var modelConfigManager = ModelConfigManager()
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .environment(modelConfigManager)
        .modelContainer(try! ModelContainer(for: PiggyBank.self,SavingsRecord.self,configurations: modelConfigManager.currentConfiguration))	// 添加SavingRecord.self
    }
}

修改SwiftData中PiggyBank存钱罐的静态数据,以便在预览中进行展示。

@MainActor
static var preview: ModelContainer {
    do {
        let container = try ModelContainer(
            for: PiggyBank.self, SavingsRecord.self,
            configurations: ModelConfiguration(isStoredInMemoryOnly: true)
        )
        let context = container.mainContext
        
        for piggyBank in PiggyBanks {
            context.insert(piggyBank)
            for record in piggyBank.records {
                context.insert(record)
            }
        }
        return container
    } catch {
        fatalError("Failed to create preview ModelContainer: \(error)")
    }
}

static var PiggyBanks: [PiggyBank] {
    let carPiggyBank = PiggyBank(name: "奔驰车", icon: "car", initialAmount: 0, targetAmount: 380000, amount: 0, creationDate: Date(), expirationDate: Date(), isExpirationDateEnabled: false, isPrimary: true)
    let iPhonePiggyBank = PiggyBank(name: "iPhone 15 pro Max", icon: "iphone.gen2", initialAmount: 0, targetAmount: 8999, amount: 0, creationDate: Date(), expirationDate: Date(), isExpirationDateEnabled: false, isPrimary: false)
    let housePiggyBank = PiggyBank(name: "新房子", icon: "building.2", initialAmount: 0, targetAmount: 800000, amount: 0, creationDate: Date(), expirationDate: Date(), isExpirationDateEnabled: false, isPrimary: false)
    
    let record1 = SavingsRecord(amount: 5000, date: Date(), note: "首存", piggyBank: carPiggyBank)
    let record2 = SavingsRecord(amount: 1000, date: Date(), note: "日常存款", piggyBank: iPhonePiggyBank)
    let record3 = SavingsRecord(amount: 2000, date: Date(), note: "努力攒钱", piggyBank: housePiggyBank)
    
    carPiggyBank.records.append(record1)
    iPhonePiggyBank.records.append(record2)
    housePiggyBank.records.append(record3)
    
    return [carPiggyBank, iPhonePiggyBank, housePiggyBank]
}

这是一段静态数据,首先是一个静态的ModelContainer容器,该容器包含了PiggyBank和SavingsRecord,在容器的上下文中,通过遍历下面的PiggyBanks数组,将PiggyBanks数组中的实例插入到上下文中。最后,返回这个ModelContainer容器。

在SwiftUI视图中,预览代码使用从静态ModelContainer容器中获取数据。

#Preview {
    let container = PiggyBank.preview
    let context = container.mainContext
    let places = try! context.fetch(FetchDescriptor<PiggyBank>()) // 从上下文中获取数据
    return AccessRecordsView(piggyBank: places[0])
}

但是代码仍然报错。

经过从网上查找相关资料文章,如《SwiftData ModelContainer can not be created in iOS 17.4 Beta》,发现问题可能跟iCloud同步有关。

因此,我首先将Xcode项目中的iCloud同步关掉后,重新调试预览代码。

这里实际上是将问题去分为iCloud导致的问题和预览问题,因为iCloud也会因为默认值或者不为nil导致报错。

经过一段时间的调试,发现只有静态数据中先插入存钱罐(PiggyBank)对象,再插入关联的SavingsRecord(存取记录)对象,Xcode预览才可以正常预览。

import SwiftData
import SwiftUI

@Model
class PiggyBank {
    ...
    
    @MainActor
    static var preview: ModelContainer {
        do {
            let container = try ModelContainer(
                for: PiggyBank.self, SavingsRecord.self,
                configurations: ModelConfiguration(isStoredInMemoryOnly: true)
            )
            let context = container.mainContext
            for piggyBank in PiggyBanks {
                context.insert(piggyBank)
                // 如果需要,手动插入 SavingsRecord
                let record = SavingsRecord(amount: 500,saveMoney: true,piggyBank:piggyBank)
                piggyBank.records.append(record) // 通过关系自动管理
            }
            try context.save()
            return container
        } catch {
            fatalError("Failed to create preview ModelContainer: \(error)")
        }
    }
    
    static var PiggyBanks: [PiggyBank] {
        let carPiggyBank = PiggyBank(name: "奔驰车", icon: "car", initialAmount: 0, targetAmount: 380000, amount: 0, creationDate: Date(), expirationDate: Date(), isExpirationDateEnabled: false, isPrimary: true)
        let iPhonePiggyBank = PiggyBank(name: "iPhone 15 pro Max", icon: "iphone.gen2", initialAmount: 0, targetAmount: 8999, amount: 0, creationDate: Date(), expirationDate: Date(), isExpirationDateEnabled: false, isPrimary: false)
        let housePiggyBank = PiggyBank(name: "新房子", icon: "building.2", initialAmount: 0, targetAmount: 800000, amount: 0, creationDate: Date(), expirationDate: Date(), isExpirationDateEnabled: false, isPrimary: false)
        
        return [carPiggyBank, iPhonePiggyBank, housePiggyBank]
    }
}

这个静态数据就可以在视图中正常的预览。

#Preview {
    let container = PiggyBank.preview
    let context = container.mainContext
    let places = try! context.fetch(FetchDescriptor<PiggyBank>()) // 从上下文中获取数据
    return AccessRecordsView(piggyBank: places[0])
}

在Xcode预览视图中,可以看到插入到PiggyBank对象的SavingsRecord对象。

排查iCloud问题

当重新启用iCloud后,Xcode预览视图重新报错。

报错内容为:

CrashReportError: Fatal Error in PiggyBank.swift
    
    piglet crashed due to fatalError in PiggyBank.swift at line 53.
    
    Failed to create preview ModelContainer: SwiftDataError(_error: SwiftData.SwiftDataError._Error.loadIssueModelContainer)
    
    Process: piglet[47933]
    Date/Time: 2025-01-13 04:03:28 +0000
Log File: <none></none>

经过一系列测试发现,问题并没有出在数据模型的定义中,而是在容器的配置中。

因为SwiftData数据在同步iCloud时,需要给数据模型中的属性,添加默认值,如果是关系则需要为可选类型。

import SwiftData
import SwiftUI

@Model
class PiggyBank {
    var name: String = ""  // 存钱罐名称
    var icon:String = ""   // 图标名称
    var initialAmount: Double = 0.0 // 初始化金额,仅首次标记,用于后续展示
    var targetAmount: Double = 0.0  // 目标金额
    var amount: Double = 0.0   // 存钱罐金额
    var creationDate: Date = Date()    // 创建日期
    var expirationDate: Date = Date()     // 截止日期
    var isExpirationDateEnabled: Bool = false   // 是否设置截止日期
    var isPrimary: Bool = false // 标记主要存钱罐
    
    // 与存钱记录的关系
    @Relationship
    var records: [SavingsRecord] = []
    
    init(name: String, icon: String, initialAmount: Double, targetAmount: Double, amount: Double, creationDate: Date, expirationDate: Date, isExpirationDateEnabled: Bool,isPrimary: Bool) {
        self.name = name
        self.icon = icon
        self.initialAmount = initialAmount
        self.targetAmount = targetAmount
        self.amount = amount
        self.creationDate = creationDate
        self.expirationDate = expirationDate
        self.isExpirationDateEnabled = isExpirationDateEnabled
        self.isPrimary = isPrimary
    }
    ...
}

在数据模型中,全部都设置了默认值,但是预览仍然报错。

后来,考虑到可以尝试在ModelContainer中设置不启用iCloud容器。

let container = try ModelContainer(
    for: PiggyBank.self, SavingsRecord.self,
    configurations: ModelConfiguration(isStoredInMemoryOnly: true,cloudKitDatabase: .none)
)

在配置cloudKitDatabase为none后,Xcode预览正常,问题全部得到解决。

总结

本次遇到的问题比较棘手,我在排查的过程中尝试了多个ai并借鉴了以往的预览报错文章,但问题仍然没有得到解决。直到从网上找到《SwiftData ModelContainer can not be created in iOS 17.4 Beta》,在关闭iCloud后,重新调试,问题才得以解决。

为此,我还尝试通过联系Apple人员,并考虑使用“代码级支持”来协助我处理这一问题,真的是比较复杂。

体而言,SwiftData关联报错主要有两点,一点是Xcode项目配置iCloud后,因为预览的ModelContainer容器没有cloudKitDatabase的配置,导致预览始终是报错的。因为前期没有意识到这一点,所以即使静态数据预览调试正常了,也会因为iCloud报错导致无法发现这一情况。

其次是,当SwiftData中存在多个对象关联使用的,在Xcode预览的ModelContainer中先插入主要的SwiftData对象,与之相关联的对象需要手动追加到SwiftData对象中,如前面提到的SavingsRecord追加到piggyBank中。

for piggyBank in PiggyBanks {
    context.insert(piggyBank)
    // 如果需要,手动插入 SavingsRecord
    let record = SavingsRecord(amount: 500,saveMoney: true,piggyBank:piggyBank)
    piggyBank.records.append(record) // 通过关系自动管理
}

后续遇到同类问题时,如果在数据模型中无法寻找到问题的原因,就需要考虑是否是预览的容器导致的问题,或者是Xcode项目的配置导致的问题。

真机和模拟器如果可以正常运行,就需要仔细的对比预览代码和实际代码之间的区别,检查是否是某些因素导致的报错。先把代码缩减到可运行的状态,再依次恢复代码,直至报错复现。

以上就是本文的全部内容。

相关文章

1、SwiftData中ModelContainer上下文绑定导致预览报错问题:https://fangjunyu.com/2024/12/27/swiftdata%e4%b8%admodelcontainer%e4%b8%8a%e4%b8%8b%e6%96%87%e7%bb%91%e5%ae%9a%e5%af%bc%e8%87%b4%e9%a2%84%e8%a7%88%e6%8a%a5%e9%94%99%e9%97%ae%e9%a2%98/

2、SwiftData使用静态数据预览视图:https://fangjunyu.com/2024/12/26/swiftdata%e4%bd%bf%e7%94%a8%e9%9d%99%e6%80%81%e6%95%b0%e6%8d%ae%e9%a2%84%e8%a7%88%e8%a7%86%e5%9b%be/

3、Swift UI #Preview预览传参报错:https://fangjunyu.com/2024/10/16/swift-ui-preview%e9%a2%84%e8%a7%88%e4%bc%a0%e5%8f%82%e6%8a%a5%e9%94%99/

4、SwiftData 预览报错问题:https://fangjunyu.com/2024/11/04/swiftdata-%e9%a2%84%e8%a7%88%e6%8a%a5%e9%94%99%e9%97%ae%e9%a2%98/

5、SwiftData ModelContainer can not be created in iOS 17.4 Beta:https://forums.developer.apple.com/forums/thread/746507

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开放者。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注