通过 CloudKit 和 Core Data 构建数据共享 app
Build apps that share data through CloudKit and Core Data
2021年6月9日
一句话判断
NSPersistentCloudKitContainer 终于支持了多用户数据共享——这意味着协作类 app(共享笔记、共享列表)可以不用自建后端了,但共享粒度的控制还需要仔细设计。
这场 Session 讲了什么
Core Data 与 CloudKit 的集成在 iOS 14 已经可以做个人数据同步,但多用户之间的数据共享一直是个痛点。iOS 15 引入了 NSPersistentCloudKitContainer 对 CloudKit Share 的完整支持,让用户可以把 Core Data 中的特定记录共享给其他用户。
Session 用一个共享购物清单 app 做演示。用户创建的购物清单可以共享给家人,每个人都能添加和勾选商品。关键是:共享的不是整个数据库,而是用户选择的特定 NSManagedObject 及其关联对象。这通过 CloudKit 的 CKShare 记录实现。
技术细节上,NSPersistentCloudKitContainer 现在会自动管理三种数据库区域:Private Database(用户私有数据)、Shared Database(别人共享给你的数据)和 Public Database(全局公开数据)。每个区域有独立的同步队列和冲突解决策略。
值得深挖的点
共享记录的权限模型
CloudKit Share 支持两种权限:只读(read)和读写(read/write)。当你在 Core Data 中标记一个对象为可共享时,NSPersistentCloudKitContainer 会在 CloudKit 中创建对应的 CKShare 记录。接受邀请的用户会获得一个 CKShareMetadata,系统会根据这个 metadata 把共享数据注入到接收者的 Core Data stack 中。
需要注意的一点是:共享是递归的。如果你共享了一个 ShoppingList 对象,它的所有关联 Item 对象也会被自动共享。但这意味着如果你不小心把一个跟用户隐私相关的对象关联到了共享对象上,那个数据也会暴露给其他用户。Session 建议在数据模型设计阶段就明确哪些实体是「可共享」的,用独立的 @objc 关系来管理共享边界。
冲突解决策略的差异
三种数据库区域的冲突解决方式不同。Private Database 使用 last-write-wins,这是最简单的策略。Shared Database 也使用 last-write-wins,但冲突的判断依据是 CKRecord 的 lastModifiedUserRecordID——如果两个用户同时修改了同一个共享记录,最后写入的那个胜出。如果你的业务场景需要更细粒度的冲突解决(比如协同编辑),需要在应用层自己实现 Operational Transformation 或 CRDT。
代码片段
// 配置支持共享的 Core Data Stack
import CoreData
class CoreDataStack {
lazy var container: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(name: "ShoppingList")
guard let description = container.persistentStoreDescriptions.first else {
fatalError("无法加载 Store Description")
}
// 启用共享数据库支持
description.cloudKitContainerOptions =
NSPersistentCloudKitContainerOptions(
containerIdentifier: "iCloud.com.example.ShoppingList"
)
// 允许自动初始化共享 schema
description.cloudKitContainerOptions?.databaseScope = .private
container.loadPersistentStores { _, error in
if let error = error {
fatalError("Core Data 加载失败: \(error)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
return container
}()
}
// 共享一个购物清单给其他用户
import CloudKit
func shareShoppingList(_ list: ShoppingList, from viewController: UIViewController) {
guard let share = list.share else {
// 首次共享需要创建 CKShare
let share = CKShare(rootRecord: list.ckRecord)
share[CKShare.SystemFieldKey.title] = "购物清单: \(list.name ?? "")"
share[CKShare.SystemFieldKey.shareType] = "com.example.shopping-list"
let controller = UICloudSharingController(
share: share,
container: CKContainer.default()
)
controller.delegate = self
controller.availablePermissions = [.allowReadWrite, .allowPrivate]
viewController.present(controller, animated: true)
return
}
// 已有共享记录,直接展示分享界面
let controller = UICloudSharingController(
share: share,
container: CKContainer.default()
)
viewController.present(controller, animated: true)
}
// 接受共享数据
func acceptShare(metadata: CKShare.Metadata) {
let container = CKContainer.default()
let operation = CKAcceptSharesOperation(shareMetadatas: [metadata])
operation.perShareCompletionBlock = { metadata, share, error in
if let share = share {
// NSPersistentCloudKitContainer 会自动将共享数据
// 同步到本地 Core Data store
print("已接受共享: \(share.recordID.recordName)")
}
}
container.add(operation)
}
最佳实践
在数据模型设计时就把「可共享」和「私有」实体分开。不要用一个巨大的实体树来管理所有数据。把共享边界用独立的关系(relationship)隔开,避免用户不小心把私人数据通过共享链路暴露出去。
CloudKit 的 Share 有一个 100MB 的总大小限制(所有共享记录加起来)。如果你的 app 会产生大量数据(图片、视频),建议把二进制数据存在 CloudKit Asset 中,而不是直接作为 CKRecord 的字段。Asset 不计入共享大小限制。
还有什么值得关注
NSPersistentCloudKitContainer现在支持自定义 schema 初始化,可以不用提前部署 CloudKit schema 到生产环境。- 共享邀请支持链接分享和直接邀请两种方式,可以配合
UICloudSharingController实现。 - 调试时可以用 CloudKit Dashboard 的 Logs 模块查看共享操作的详细日志,排查同步问题。