Highlight
iOS 14 为
NSPersistentCloudKitContainerOptions增加databaseScope,让 Core Data store 可以同步到 CloudKit public database,并用权限查询、轮询导入和软删除策略处理公共数据集的读写边界。
核心内容
2019 年的 NSPersistentCloudKitContainer 已经把 Core Data store 同步到 CloudKit private database。这个模型适合用户自己的数据:账号必须存在,数据只属于当前用户,CloudKit push notification 可以把变化及时带回本地。(00:38)
public database 的需求来自另一类 App。开发者想预置一套所有用户都能读取的应用模板,也想让用户贡献共享数据。游戏高分榜是演讲里最直接的例子:高分需要能被所有玩家读取、排序和本地查询,存档和角色配置仍然应该留在 private database。(05:14)
iOS 14 的改变很小,但影响的是整条数据链路。你可以把某个 Core Data store 指向 public database,让设备上保留一份完整本地镜像;同时把私有数据放在另一个 store。这样,UI 仍然读 Core Data,本地 fetch、sort、order 的路径不变,CloudKit 负责把公共数据带进来。(02:28、05:58)
代价是公共数据库的语义更复杂。未登录 iCloud 的用户也能读取 public database,只有登录用户能创建记录,修改和删除还要看记录所有者、store 类型和 CloudKit zone 能力。Session 的重点就是把这些边界拆清楚,避免把 private database 的同步假设直接搬到 public database。(06:48)
详细内容
配置 public database store(02:28)
演讲里的最小改动是在已有 Core Data stack 里设置 store description 的 cloudKitContainerOptions,再把 NSPersistentCloudKitContainerOptions.databaseScope 设为 public。完整顺序是:创建 NSPersistentStoreDescription,保留 history tracking 和 remote change notifications,创建 NSPersistentCloudKitContainerOptions,设置 databaseScope,把 options 放回 store description,最后让 container 加载这些 stores。
关键点:
databaseScope是本 session 引入的新 API,用来选择 public database 或 private database。NSPersistentCloudKitContainerOptions告诉NSPersistentCloudKitContainer这个 store 要接入 CloudKit。- public store 仍然通过普通 Core Data store description 加载,不需要把 UI 层改成直接访问
CKDatabase。 - 如果 App 同时有公共数据和私有数据,可以把它们放进不同 store,让同一个 Core Data stack 管理。
在 CloudKit Dashboard 增加索引(03:30)
设置 databaseScope 后,还要配置 CloudKit schema。public database 的导入方式依赖 query,Dashboard 里每个 record type 都需要给 recordName 和 modifiedAt 增加索引。演讲里的示例有五个 record type,所以这一步要重复五次。
关键点:
- 这些索引用来支持
NSPersistentCloudKitContainer对 public database 发起查询。 - 如果 Dashboard 里看不到完整 record type,需要先做 schema initialization。
- public database 会在设备上形成完整本地镜像,所以本地查询可以继续走 Core Data。
- 每个 record type 都要配置,漏掉一个类型就会影响该类型记录的导入。
控制创建、修改和界面编辑状态(06:48)
private database 的规则很简单:未登录 iCloud 不能操作,登录后用户拥有自己的全部数据。public database 允许未登录用户读取数据;创建记录需要登录;修改记录要判断当前用户是否有权修改这个对象。过去可以先向 CloudKit 请求当前账号,再向 NSPersistentCloudKitContainer 取出 managed object 背后的 record,比对 creator UserID,但这条路径不适合每个 UI 控件都执行。
关键点:
canUpdateRecord(forManagedObjectWithID:)用一个 managed object ID 判断当前对象是否可更新。- 这个判断会考虑当前设备账号、对象所在 persistent store、store 是否由 CloudKit 支持,以及账号切换等边界情况。
canModifyObjects(in:)适合列表页这类更粗粒度的 UI,用来判断某个 store 里的对象是否可修改。- 编辑按钮、新增按钮和详情页编辑控件应该使用这些判断结果,而不是只看 App 是否登录 iCloud。
理解 public database 的导入节奏(10:08)
private database 可以依赖 push notification。收到 CloudKit 推送后,NSPersistentCloudKitContainer 创建 CKFetchRecordsZoneChangesOperation,把 changed records 拉回本地 store。public database 不能使用这条路径,它要用 CKQueryOperation 查询每个 record type,所以 posts、tags、attachments、images、many-to-many relationships 都会分别发起请求。
关键点:
- public database 使用 polling,而不是等待 push notification。
NSPersistentCloudKitContainer只会在 App 启动时轮询,或在 App 使用约 30 分钟后再次轮询。- public database 的 freshness 会和 private database 不同,不能假设每次云端变化都会马上出现在本地。
- 管理对象模型越简单,public database 需要的 query 请求越少;应把公共数据实体限制到专门的 Core Data configuration。
用软删除处理公共数据的删除语义(13:35)
private database 删除记录时,CloudKit 会留下 tombstone。后续导入可以通过 CKFetchRecordsZoneChangesOperation 看到 tombstone,并在其他设备上删除本地对象。public database 走 query,记录被删除后不会留下可查询的 tombstone,其他已经下载该对象的设备可能永远收不到删除事件。
关键点:
canDeleteRecord(forManagedObjectWithID:)可以判断这个对象的删除能否按当前 store 语义传播。- 返回
false时,演讲建议把“从界面移除”和“从 public database 删除”分开处理。 - 示例做法是把对象的
isTrashed设为true,再让 fetch request 用 predicate 过滤已 trash 的记录。 - 如果需要回收空间,可以把已 trash 记录中不再需要的字段清空;等确认用户都处理过这次更新后,再清理 public database 中的记录。
核心启发
共享模板库
做什么:为创作类 App 提供一套所有用户都能读取的模板库,用户自己的草稿继续保存在 private database。
为什么值得做:session 明确提到 application templates 和 initial data set 是 public database 的典型需求,设备还能保留本地镜像。
怎么开始:把模板相关 entity 放到 public store 的 Core Data configuration,设置 databaseScope 为 public,在 CloudKit Dashboard 给每个 record type 增加 recordName 和 modifiedAt 索引。
社区贡献的排行榜数据
做什么:记录用户贡献的高分、关卡成绩或挑战结果,让所有用户在本地按分数排序和筛选。
为什么值得做:演讲把 high scores table 当成最常见请求,理由是用户需要本地快速 fetch、sort 和 order,也需要向共享数据集贡献记录。
怎么开始:只把排行榜 entry 放到 public store;存档、角色配置和用户偏好放进 private store;新增按钮根据 iCloud 登录状态开启,编辑控件用 canUpdateRecord(forManagedObjectWithID:) 判断。
可下线的公共标签目录
做什么:为内容 App 维护一套公共标签、分类或精选集合,管理员能把过期条目从界面移除。
为什么值得做:public database 删除不会向已下载设备传播 tombstone,直接 delete 可能让不同设备看到不同结果。
怎么开始:给公共 entity 增加 isTrashed 字段;当 canDeleteRecord(forManagedObjectWithID:) 返回 false 时改为更新 isTrashed;列表 fetch request 过滤已 trash 的记录。
有明确刷新预期的公共资料库
做什么:做一个离线优先的公共资料库,例如示例数据、参考条目或精选内容,启动后可用,刷新节奏不追求秒级。
为什么值得做:NSPersistentCloudKitContainer 会在启动和约 30 分钟使用后轮询 public database,适合对 freshness 要求不高的数据。
怎么开始:把公共资料模型保持小而清晰,只把需要同步到 public database 的 entity 放进对应 configuration;界面文案避免承诺实时更新。
关联 Session
- Core Data: Sundries and maxims — 深入 Core Data 的批量写入、persistent history、fetch 裁剪和 store 变更通知。
- Data Essentials in SwiftUI — 理解 SwiftUI 视图如何依赖数据源,适合作为 Core Data UI 层的补充。
- Synchronize health data with HealthKit — 对照 HealthKit 的 sync identifier、anchored query 和多设备数据一致性。
- The Push Notifications primer — 了解 background notification 与私有数据库导入路径里的 push 机制。
- Tap into Game Center: Leaderboards, Achievements, and Multiplayer — 如果共享数据目标是游戏高分榜,可以比较 Game Center 的系统级排行榜方案。
评论
GitHub Issues · utterances