SwiftData 教程与示例:最佳实践(七)
避免在异步任务中传递 modelContext
如果在异步任务中传递 modelContext,会遇到 Xcode 提示如下错误:
SwiftData.ModelContext: Unbinding from the main queue. This context was instantiated on the main queue but is being used off it. ModelContexts are not Sendable, consider using a ModelActor.
这是由于 ModelContext 不支持并发访问。
ModelContext 不是 Sendable,不能在异步函数中跨线程传递,否则可能导致数据竞争。
将属性设置为可选或添加默认值
如果计划使用 CloudKit 备份与同步数据,CloudKit 要求 SwiftData 中所有数据模型的数据必须设置为可选或者设置默认值。
例如,有以下实例数据模型:
@Model
class ChatMessage: Identifiable {
var id: UUID
var text: String
var timestamp: Date
var isSentByCurrentUser: Bool
init(
text: String,
timestamp: Date,
isSentByCurrentUser: Bool,
) {
self.id = UUID()
self.text = text
self.timestamp = timestamp
self.isSentByCurrentUser = isSentByCurrentUser
}
}
如果启用了 CloudKit 服务,在运行 App 会以下错误提示:
CloudKit integration requires that all attributes be optional, or have a default value set. The following attributes are marked non-optional but do not have a default value:
注意:在 CloudKit 集成的上下文中,在 init 初始化器中设置属性值并不被视为属性的默认值。CloudKit 要求默认值必须在数据模型本身中定义,而不是仅在代码的初始化器中设置。
- 数据模型层面要求:CloudKit 和 Core Data 的集成是基于数据模型(即 .xcdatamodeld 文件)的定义。当 CloudKit 在不同设备之间同步数据时,它可能会直接实例化模型对象,而不会调用您自定义的 init 方法。如果属性在模型中没有默认值,CloudKit 可能无法正确处理这些属性。
- 模型与代码分离:在数据模型中定义的默认值是持久化层的一部分,而在代码中初始化的值仅在您创建对象时有效,不能保证在数据同步或迁移过程中被正确设置。
因此,必须在属性声明时提供默认值,以满足 CloudKit 的要求。
当在初始化器中为属性赋值时,这些赋值将覆盖在属性声明时设置的默认值。
添加默认值
UUID 类型
对于 UUID 类型属性,直接在声明时提供默认值,无需在初始化器中实现:
@Model
class ChatMessage: Identifiable {
var id: UUID = UUID()
}
String 类型
对于 String 类型属性,可以在初始化时设置为空(""),并保留初始化器中的初始化逻辑:
@Model
class ChatMessage: Identifiable {
var text: String = ""
init(
text: String = ""
) {
self.text = text
}
}
Date 类型
对于 Date 类型属性,可以在初始化时设置为 Date()
。
- 如果该属性就是创建时的时间,则无需在初始化器中再保留属性(例如消息的创建时间)。
- 如果该属性为需要计算得到,则应该保留初始化器中的初始化逻辑(例如基于使用时间计算得到的结束时间):
@Model
class ChatMessage: Identifiable {
var timestamp: Date = Date()
init(
timestamp: Date = Date(),
) {
self.timestamp = timestamp
}
}
Bool 类型
对于 Bool 类型属性,可以在初始化时设置为 flase
或者 true
,并保留初始化器中的初始化逻辑:
@Model
class ChatMessage: Identifiable {
var isSentByCurrentUser: Bool = true
init(
isSentByCurrentUser: Bool,
) {
self.isSentByCurrentUser = isSentByCurrentUser
}
}
Double 类型
对于 Double 类型属性,可以在初始化时设置为 0.0
,并保留初始化器中的初始化逻辑:
var amount: Double = 0.0
Data 类型
对于 Data 类型属性,可以在初始化时设置为 Data()
,并保留初始化器中的初始化逻辑:
@Attribute var fileData: Data = Data()
这样,fileData 将默认初始化为一个空的 Data 对象(即没有内容的二进制数据)。
设置为可选
设置关系属性为可选
CloudKit 要求所有关系都是可选的,以防止在同步时缺少相关对象导致的问题。
例如,有以下实例数据模型:
@Model
class FinancialRecord: Codable {
@Relationship(deleteRule: .cascade)
var attachments: [RecordAttachment] = []
@Relationship(deleteRule: .cascade)
var comments: [Comment] = []
}
虽然已经为关系属性提供了默认值(空数组),但在 CloudKit 集成中,关系属性仍然必须被声明为可选类型。
如果启用了 CloudKit 服务,在运行 App 出现以下错误提示:
CloudKit integration requires that all relationships be optional, the following are not:
CloudKit 要求的原因是:
- 必须可选的关系: CloudKit 要求所有的关系属性必须是可选的。这是为了处理在数据同步过程中,可能出现的相关对象缺失或未能及时同步的情况。
- 防止同步冲突: 在 CloudKit 中,非可选的关系可能导致同步冲突或数据不一致,因为在同步过程中,如果相关联的对象尚未同步或不存在,非可选关系将无法被满足。
设置为可选
将关系属性声明为可选类型,即在类型后面加上问号 ?
:
@Model
class FinancialRecord: Codable {
@Relationship(deleteRule: .cascade)
var attachments: [RecordAttachment]? = []
@Relationship(deleteRule: .cascade)
var comments: [Comment]? = []
}
现在,attachments 和 comments 是可选的数组,可以为 nil 或一个数组。
仍然可以为可选的关系属性提供默认值(如空数组 []),以便在对象初始化时有一个初始值。
避免使用唯一约束
CloudKit 不支持在 SwiftData 模型中定义的唯一约束。唯一约束可能会在数据同步时导致冲突。
避免使用:@Attribute(.unique)