跳到正文
返回博客
·约 1 分钟读完

用 CloudKit 同步 SwiftData

让 SwiftData 通过 CloudKit 同步:所需能力、ModelConfiguration、CloudKit 对模型的要求、把架构部署到生产环境,以及只同步到私有数据库。

要让 SwiftData 通过 iCloud 同步,需要给 App 添加两项能力(带 CloudKit 和容器的 iCloud,以及勾选了远程通知的后台模式),让模型符合 CloudKit 的要求,再运行 App 创建开发架构(schema)。发布前,要在 CloudKit Console 里把这个架构部署到生产环境,因为 App Store 上的版本只能访问生产环境。大部分工作都在模型上:不能有唯一性约束,关系必须是可选的,每个属性都要有值。想在浏览器里对照这些规则检查一份模型文件,可以用我们的 SwiftData 与 CloudKit 检查工具。

Apple 的文章说,SwiftData 使用 Core Data 的 NSPersistentCloudKitContainer 来完成 CloudKit 同步,并通过读取 App 的授权(entitlements)来推断 CloudKit 配置。

添加所需能力

Apple 列出了 SwiftData 做 iCloud 同步所需的两项能力:

  1. iCloud。 打开 CloudKit,并创建或选择一个容器。Apple 把容器描述为 CloudKit 用来在 iCloud 服务器上隔离你 App 的数据库的对象。这项能力需要有管理员权限的 Apple Developer 账号。
  2. 后台模式(Background Modes)。 勾选"Remote notifications"选项。系统会以静默方式递送这些通知,SwiftData 用它们得知其他设备上发生的变化并加以应用。

在 ModelConfiguration 里选择 CloudKit 数据库

ModelConfiguration(iOS 17、macOS 14)有一个类型为 ModelConfiguration.CloudKitDatabase 的 cloudKitDatabase 参数,它有三种取值:

  • 默认值。 SwiftData 读取 Entitlements.plist,选用找到的第一个容器标识符。不传这个参数时就是这种行为。
  • .private("iCloud.com.example.Trips")。 指定一个具体的容器。Apple 建议在 App 使用多个 CloudKit 容器时这样做。
  • .none。 关闭 SwiftData 的同步,覆盖它本来会找到的任何容器。Apple 给出的用途是:App 已经以自己的方式使用 CloudKit,架构可能互相冲突。
import SwiftUI
import SwiftData

@main
struct TripsApp: App {
    let modelContainer: ModelContainer

    init() {
        let config = ModelConfiguration(cloudKitDatabase: .private("iCloud.com.example.Trips"))
        do {
            modelContainer = try ModelContainer(for: Trip.self, Stay.self, configurations: config)
        } catch {
            fatalError(error.localizedDescription)
        }
    }

    var body: some Scene {
        WindowGroup { Text("Trips") }
            .modelContainer(modelContainer)
    }
}

func localOnlyContainer() throws -> ModelContainer {
    let config = ModelConfiguration(cloudKitDatabase: .none)
    return try ModelContainer(for: Trip.self, Stay.self, configurations: config)
}

func defaultContainer() throws -> ModelContainer {
    try ModelContainer(for: Trip.self, Stay.self)
}

Apple 还提醒:如果你的 App 已经有生产环境的 CloudKit 架构,只能指定之前由 SwiftData 或 Core Data 管理过的容器,其他容器都不兼容。

让模型符合 CloudKit 的要求

SwiftData 的宏生成的架构在很多情况下本来就兼容 CloudKit,但有少数功能无法带过去。下表把 Apple 列出的内容和另外一条规则放在一起。

模型中的写法CloudKit 的要求
@Attribute(.unique)不要使用。CloudKit 无法保证唯一性,因为变化是并发地、在合适的时机同步的。
关系(relationship)每个关系都必须是可选的。iCloud 服务器不保证以原子方式处理关系的变化。
反向关系(inverse)只有 SwiftData 能从架构中可靠地推断出反向关系时才会替你设置。否则要在保存前显式设置,因为 CloudKit 处理变化的顺序是不确定的。
删除规则 .deny不支持,因为变化不会立即同步。请改用其他规则,比如 .cascade 或 .nullify。
属性(attribute)给每个属性一个默认值,或者设为可选(见下文)。

最后一行不在 Apple 的 SwiftData 页面上。我们是在用 SwiftData 模型加载一个开启了 CloudKit 的 Core Data 存储时看到的:它报错说"CloudKit integration requires that all attributes be optional, or have a default value set."。在我们的测试里,只在 init() 内部给的默认值不算数,所以请把默认值直接写在属性上。

@Model
final class Trip {
    var name: String = ""
    var startDate: Date = Date()
    var notes: String?

    @Relationship(deleteRule: .cascade, inverse: \Stay.trip)
    var stays: [Stay]?

    init(name: String) {
        self.name = name
    }
}

@Model
final class Stay {
    var place: String = ""
    var trip: Trip?

    init(place: String) {
        self.place = place
    }
}

如果你要迁移的是已有的 Core Data 应用,可以用我们的 Core Data 转 SwiftData 工具 生成模型,再用上面的检查工具按这份清单核对。Apple 关于 Core Data 与 CloudKit 的文章还要求所有关系都有反向关系,并且同一个配置(configuration)里的实体不能与另一个配置里的实体建立关系。

创建开发架构

开发期间,CloudKit 在开发环境里有一份草稿架构,可以随意重写。Apple 的文章展示了如何在调试构建里创建它:先建一个 ModelConfiguration,把 NSPersistentStoreDescription 指向它的 url,设置容器标识符,同步加载存储,用你的 SwiftData 类生成一个托管对象模型,然后在 NSPersistentCloudKitContainer 上调用 initializeCloudKitSchema()。最后在 SwiftData 自己的容器加载之前把这个存储移除,避免两者同时同步。

import CoreData

#if DEBUG
func initializeSchema() throws {
    let config = ModelConfiguration()
    try autoreleasepool {
        let desc = NSPersistentStoreDescription(url: config.url)
        desc.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.com.example.Trips")
        desc.shouldAddStoreAsynchronously = false
        if let mom = NSManagedObjectModel.makeManagedObjectModel(for: [Trip.self, Stay.self]) {
            let container = NSPersistentCloudKitContainer(name: "Trips", managedObjectModel: mom)
            container.persistentStoreDescriptions = [desc]
            container.loadPersistentStores { _, err in
                if let err { fatalError(err.localizedDescription) }
            }
            try container.initializeCloudKitSchema()
            if let store = container.persistentStoreCoordinator.persistentStores.first {
                try container.persistentStoreCoordinator.remove(store)
            }
        }
    }
}
#endif

像 Apple 那样用 #if DEBUG 包起来,确保它不会在发布版本里运行。之后打开 CloudKit Console 查看你的记录类型。如果看不到,Apple 说你可能需要为它们开启查询支持。

把架构部署到生产环境

App Store 上的 App 只能访问生产环境,Apple 说你必须在发布之前把开发架构部署过去。在 CloudKit Console 中:

  1. 在 icloud.developer.apple.com 登录,打开 CloudKit Database 应用。
  2. 选择你的容器。
  3. 选择 Deploy Schema Changes。
  4. 检查待部署的变更,点击 Deploy。

部署会复制记录类型、字段和索引,但不会复制记录。你需要有编辑生产环境的权限:个人开发者本人就有,团队里则由管理员来做这一步,或者授予你该权限。

此后架构只能增量变化。你可以添加记录类型和字段,但不能删除或修改已经在生产环境里的记录类型和字段。Apple 给出的后续改动策略有:把用户迁移到新容器里的新存储;逐步添加字段,这样旧版本 App 仍能看到每条记录(但不是每个字段);或者保留一个 version 属性,只获取当前版本的 App 看得懂的记录。设计第一版生产架构时要想到这一点:现在起的名字就改不了了。

只同步到私有数据库

看看上面的取值:默认值、.private(_:) 和 .none。SwiftData 的 CloudKitDatabase 没有公共或共享数据库的选项。Apple 的 Core Data 设置文章讲的是 SwiftData 所依托的机制,其中提到本地存储"由 CloudKit 私有数据库支持"。实际上,这意味着它在同一个人自己的设备之间、通过其自己的 iCloud 账号同步,设计功能时请以此为前提。Apple 的 SwiftData 页面没有谈到在不同人之间共享记录,所以我们不写这部分。

来源

以下页面均阅读于 2026 年 11 月 1 日。

Apple 的要求可能变化,请以上述页面的最新内容为准。

作者

独立开发者,在 Obelisk Club 做本地优先的 Mac 与 iOS 效率应用。 关于作者 →