本文作为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