移动/游戏
在 iOS/macOS 应用实现或审查…
适用于需要使用 CloudKit、iCloud 键值存储或 iCloud Drive 在 Apple 设备间同步数据的 iOS/macOS 应用开发与代码审查。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
在 iOS/macOS 应用实现或审查 特定
解决思路
- 在 Signing & Capabilities 中启用 iCloud + CloudKit;写记录前选择 database scope 和同步 owner,并验证 capability、container、account status、schema、environment。
- 让本地修改先持久化,再入队;通过 subscriptions 或 CKSyncEngine 驱动远端工作,不要轮询。
- 成功应用变更后持久化 change tokens 或 sync-engine state。
- 测试离线编辑、部分失败、限流、token 过期、冲突、账户丢失、zone 删除、重启。
- 失败时分类 CKError,恢复受影响的 fixture 或队列项,执行文档化 retry/reset/merge 动作,重跑同一场景;部分成功后不要盲目重启完整同步。
- 需要增量 zone 变更、shares、assets、batch operations、Dashboard 流程时加载 references/cloudkit-patterns.md。
- 使用 CKContainer.default() 或 CKContainer(identifier:) 获取容器,分别访问 publicCloudDatabase、privateCloudDatabase、sharedCloudDatabase。
- 记住数据库范围:Public 所有用户,读不需 iCloud、写需要,使用 app quota;Private 当前用户,需要 iCloud,使用用户 quota;Shared 共享记录,需要 iCloud,使用 owner quota。
- 用 CKRecord(recordType:) 创建记录,以 CKRecordValue 设置键值,单记录最大 1 MB 不含 CKAsset 数据,调用 privateDB.save 保存。
- 用 CKRecord.ID(recordName:) 和 privateDB.record(for:) 按 ID 获取记录。
- 更新先 fetch,再修改,再 save;删除用 deleteRecord(withID:)。
- 在私有数据库创建自定义 record zones:CKRecordZone(zoneID:),privateDB.save(zone);用带 zoneID 的 CKRecord.ID 创建 zone 内记录。自定义 zone 支持原子提交、变更跟踪、共享;public DB 不支持;shared DB 暴露其他用户共享的 zones。
- 用 NSPredicate 和 CKQuery 查询;支持 ==、!=、<、>、<=、>=、BEGINSWITH、CONTAINS、IN、AND、NOT、BETWEEN、distanceToLocation:fromLocation:。
- CONTAINS 用于列表成员判断;self CONTAINS 用于 tokenized 全文搜索;BEGINSWITH 用于字符串前缀;不支持的 operator、key path 或字段类型会在查询执行时失败。
- 加密审查中明确字段资格:encrypted values 不能查询或排序;CKAsset 默认加密;CKRecord.Reference 不能加密,因为 CloudKit 需要服务端使用。
- 查询时创建 CKQuery(recordType:predicate:),设置 sortDescriptors,调用 privateDB.records(matching:);遍历结果 result.get()。可查询某类型全部记录:NSPredicate(value: true)。全文搜索 self CONTAINS。复合条件用 NSCompoundPredicate(andPredicateWithSubpredicates:)。
- 用 CKQuerySubscription 创建查询订阅,设置 recordType、predicate、subscriptionID、options 如 firesOnRecordCreation/firesOnRecordUpdate;设置 NotificationInfo.shouldSendContentAvailable = true 静默推送;保存到 privateDB。
- 用 CKDatabaseSubscription 订阅任意数据库变更;用 CKRecordZoneSubscription 订阅 zone 内变更;保存订阅。
- 在 AppDelegate 的 didReceiveRemoteNotification 中用 CKNotification(fromRemoteNotificationDictionary:) 处理;按 subscriptionID 过滤;用 CKSyncEngine 或 CKFetchRecordZoneChangesOperation 拉取变更;返回 .newData。
- CloudKit 启用时 APNs entitlement 由 CloudKit/Xcode 处理,无需单独显式 App ID push 设置;静默/后台处理仍需 Background Modes > Remote notifications。
- 对自定义模型数据优先用 CKSyncEngine(iOS 17+);它处理调度、瞬时重试、change tokens、数据库订阅,但不处理 app 特定保存失败;sentRecordZoneChanges.failedRecordSaves 中的 CKError.serverRecordChanged 仍需自定义冲突解决和重新调度;自动同步时机不确定;要求 CloudKit capability + Remote notifications;仅 private/shared DB。
- 实现 CKSyncEngineDelegate:创建 CKSyncEngine.Configuration(database:stateSerialization:delegate:),通常用 privateCloudDatabase,加载已保存 state。
- 在 handleEvent 中:stateUpdate 时保存 update.stateSerialization;accountChange 处理账户变更;fetchedRecordZoneChanges 时处理 modifications 的 record 和 deletions 的 recordID;sentRecordZoneChanges 时标记 savedRecords 已同步,对 failedRecordSaves 调用失败处理。
- 实现 nextRecordZoneChangeBatch:从 syncEngine.state.pendingRecordZoneChanges 过滤 context.options.zoneIDs,构造 CKSyncEngine.RecordZoneChangeBatch(pendingChanges:) 并通过闭包 recordToSend(for:) 返回记录。
- 调度变更:syncEngine.state.add(pendingRecordZoneChanges: [.saveRecord(recordID)])。
- 触发立即同步:try await syncEngine.fetchChanges() 和 try await syncEngine.sendChanges()。
- 跨启动持久化 stateSerialization;引擎需要它从正确 change token 恢复。
- SwiftData + CloudKit 用 ModelConfiguration(cloudKitDatabase: .private(...)) 和 ModelContainer;@Model 兼容模型。
- SwiftData CloudKit 实现或审查必须报告 model compatibility:不要 #Unique 或 unique constraints;关系用 optional;不要 .deny;大 Data 用 @Attribute(.externalStorage)。
- SwiftData CloudKit 必须报告 schema rollout:在非生产构建初始化 development schema;在 CloudKit Dashboard 验证;发布前 promote;生产 promotion 后只添加 schema;不要删除 model types 或修改既有 attributes。
- NSUbiquitousKeyValueStore 用于简单 key-value 同步;限制 1024 keys、总 1 MB、每值 1 MB;iCloud 不可用时本地存储。
- 写入用 kvStore.set(_, forKey:) 后 synchronize();读取用对应类型 getter,如 string(forKey:)。
- 观察 NSUbiquitousKeyValueStore.didChangeExternallyNotification;从 userInfo 读取 NSUbiquitousKeyValueStoreChangeReasonKey 和 NSUbiquitousKeyValueStoreChangedKeysKey;按 reason 处理:ServerChange 对 keys 应用远端变更;InitialSyncChange 重新加载全部设置;QuotaViolationChange 处理配额超限。
- iCloud Drive 文档级同步用 FileManager ubiquity APIs;在主线程外调用 url(forUbiquityContainerIdentifier:) 和 setUbiquitous;setUbiquitous 执行协调文件工作且可能阻塞;如果 app 正在展示文件,移动前配置 active file presenter。
- 创建 ubiquity container 的 Documents 目录,用 setUbiquitous(true, itemAt: localURL, destinationURL: cloudURL) 移动到 iCloud;url 返回 nil 表示 iCloud 不可用。
- 用 NSMetadataQuery 监控文件,scope 设为 NSMetadataQueryUbiquitousDocumentsScope 或 NSMetadataQueryUbiquitousDataScope。
- 同步前总是检查 accountStatus();监听 .CKAccountChanged。
- 处理账户状态:.available 返回;.noAccount 抛出无 iCloud 账户;.restricted 抛受限;.temporarilyUnavailable 抛暂时不可用;.couldNotDetermine 抛未知;@unknown default 抛未知。
- 处理 CKError:.networkFailure/.networkUnavailable 排队等网络恢复重试;.serverRecordChanged 三方合并;.requestRateLimited/.zoneBusy/.serviceUnavailable 按 retryAfterSeconds 重试;.quotaExceeded 通知用户并减少数据;.notAuthenticated 提示登录 iCloud;.partialFailure 用 partialErrorsByItemID 逐项检查;.changeTokenExpired 重置 token 并重新拉取全部变更;.userDeletedZone 重建 zone 并重新上传数据;default 记录错误。
- 在 handleCloudKitError 中按 code switch;限流时 delay = ckError.retryAfterSeconds ?? 3.0 后 scheduleRetry;partialFailure 遍历 partialErrorsByItemID 递归 handleCloudKitError。
- 冲突解决:.serverRecordChanged 包含 ancestorRecord、clientRecord、serverRecord 三个版本;总是合并到 serverRecord,因为它有正确 change tag。
- 对 client.changedKeys() 逐 key:若 server[key] == ancestor[key],用 client[key];若 client[key] == ancestor[key],保留 server;若两边都改,用 mergeValues(ancestor:client:server:) 自定义合并;然后保存 server。
- 避免常见错误:同步必须有 account gate,检查 accountStatus() 并把 .noAccount 建模为用户可见状态。
- 用户个人数据放 private scope;public scope 只放 app-wide 内容。
- 不用 timer polling;用 database subscriptions 或 CKSyncEngine。
- 限流后不要立即重试;尊重 retryAfterSeconds 并保留 pending work。
- 不要假设引擎解决冲突;对 failedRecordSaves 做三方合并,然后重新调度保存。
- 不要每次 fetch 都用 nil token 开始;持久化 tokens/state,仅在文档化过期路径重置。
- 审查清单:确认启用 iCloud + CloudKit capability;同步前检查 account status 并优雅处理 .noAccount;用户数据用 private DB,public 仅共享内容;在 private DB 创建自定义 zone,shared DB zones 从 shares 发现;处理 CKError.serverRecordChanged 并三方合并到 serverRecord;网络失败排队重试且尊重 retryAfterSeconds;用 CKDatabaseSubscription 或 CKSyncEngine 做推送同步,后台投递启用 Remote notifications;change tokens 持久化到磁盘,changeTokenExpired 时重置并重新获取;partialFailure 逐项检查;userDeletedZone 重建 zone 并重新同步;SwiftData CloudKit 审查报告模型兼容和 schema rollout;观察 NSUbiquitousKeyValueStore.didChangeExternallyNotification;加密审查说明 CKRecord.Reference 不能使用 encryptedValues,加密字段不能查询/排序,CKAsset 默认加密;CKSyncEngine state serialization 跨启动持久化。
- 加载 references/cloudkit-patterns.md 查看 incremental sync、CKShare、zones、CKAsset storage、batch operations、Dashboard 用法。
适用边界 · 注意事项
- 非 Apple 平台或非 iOS/macOS 应用:CloudKit 和 iCloud API 不可用。
- 只需要本地持久化、或已使用其他后端且不需要 iCloud 跨设备同步时。
- 无法启用 iCloud/CloudKit capability、没有 Apple Developer 账号,或用户不愿登录 iCloud 的场景。