SwiftData 教程与示例:最佳实践(七)

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)