关于 cloudkit
面向开发者的同步技能,介绍如何通过 CloudKit、iCloud 键值存储和 iCloud Drive 跨设备同步应用数据。内容包括记录操作、推送驱动同步、SwiftData 集成、冲突处理和常见错误恢复。
主要功能
- 01CloudKit 容器与数据库配置
- 02记录增删改查、查询和订阅
- 03CKSyncEngine 状态与变更令牌管理
- 04SwiftData 与 CloudKit 集成
- 05iCloud 键值存储和 Drive 文件同步
- 06错误恢复与冲突合并
适用场景
- 01在用户的多台设备间同步应用记录
- 02根据 CloudKit 变更通知刷新数据
- 03解决同一记录上的并发编辑
- 04排查同步故障、账户丢失或令牌过期
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill cloudkit安装后在 Claude Code 中输入/cloudkit确认已加载。
附带文件evals/evals.jsonreferences/cloudkit-patterns.md
详细介绍
CloudKit
使用 CloudKit、iCloud 键值存储和 iCloud Drive 在不同设备间同步数据。涵盖容器设置、记录 CRUD、查询、订阅、CKSyncEngine、 SwiftData 集成、冲突解决和错误处理。
目录
- 容器和数据库设置
- 工作流
- CKRecord CRUD
- CKQuery
- CKSubscription
- CKSyncEngine(iOS 17+)
- SwiftData + CloudKit
- NSUbiquitousKeyValueStore
- iCloud Drive 文件同步
- 账户状态和错误处理
- 冲突解决
- 常见错误
- 检查清单
- 参考资料
工作流
- 选择数据库范围和同步负责人;写入记录前,验证功能、容器、账户状态、架构和环境。
- 确保本地更改持久化,将其加入队列,然后通过订阅或
CKSyncEngine驱动远程操作,而不是轮询。 - 成功应用更改后,持久化更改令牌或同步引擎状态。
- 测试离线编辑、部分失败、速率限制、令牌过期、冲突、账户丢失、区域删除和重新启动。
- 失败时,对
CKError分类,恢复受影响的测试数据或队列项,执行文档说明的重试、重置或合并操作,然后重新运行相同场景。部分成功后,切勿盲目重启完整同步。
加载 references/cloudkit-patterns.md,了解增量区域更改、共享、资源、批量操作和 Dashboard 流程。
容器和数据库设置
在 Signing & Capabilities 中启用 iCloud + CloudKit。一个容器提供三个数据库:
| 数据库 | 范围 | 是否需要 iCloud | 存储配额 |
|---|---|---|---|
| 公共 | 所有用户 | 读取:否,写入:是 | App 配额 |
| 私有 | 当前用户 | 是 | 用户配额 |
| 共享 | 共享记录 | 是 | 所有者配额 |
import CloudKit
let container = CKContainer.default()
// Or named: CKContainer(identifier: "iCloud.com.example.app")
let publicDB = container.publicCloudDatabase
let privateDB = container.privateCloudDatabase
let sharedDB = container.sharedCloudDatabase
CKRecord CRUD
记录是键值对。每条记录最大为 1 MB(不包括 CKAsset 数据)。
// CREATE
let record = CKRecord(recordType: "Note")
record["title"] = "Meeting Notes" as CKRecordValue
record["body"] = "Discussed Q3 roadmap" as CKRecordValue
record["createdAt"] = Date() as CKRecordValue
record["tags"] = ["work", "planning"] as CKRecordValue
let saved = try await privateDB.save(record)
// FETCH by ID
let recordID = CKRecord.ID(recordName: "unique-id-123")
let fetched = try await privateDB.record(for: recordID)
// UPDATE -- fetch first, modify, then save
fetched["title"] = "Updated Title" as CKRecordValue
let updated = try await privateDB.save(fetched)
// DELETE
try await privateDB.deleteRecord(withID: recordID)
自定义记录区域
App 在私有数据库中创建自定义区域。共享数据库会公开其他用户与当前用户共享的区域。自定义区域支持原子提交、更改跟踪和共享;公共数据库不支持自定义区域。
let zoneID = CKRecordZone.ID(zoneName: "NotesZone")
let zone = CKRecordZone(zoneID: zoneID)
try await privateDB.save(zone)
let recordID = CKRecord.ID(recordName: UUID().uuidString, zoneID: zoneID)
let record = CKRecord(recordType: "Note", recordID: recordID)
CKQuery
使用 NSPredicate 查询记录。支持:==、!=、<、>、<=、>=、
BEGINSWITH、CONTAINS、IN、AND、NOT、BETWEEN、
distanceToLocation:fromLocation:。
除使用 self CONTAINS 进行分词全文搜索外,CONTAINS 用于测试列表成员关系。BEGINSWITH 是字符串前缀运算符;查询执行时,不支持的运算符、键路径或字段类型会导致失败。
每次进行加密审查时,都要明确说明字段是否符合条件:加密值无法查询或排序;CKAsset 默认加密;CKRecord.Reference 无法加密,因为 CloudKit 需要在服务器端使用它。
let predicate = NSPredicate(format: "title BEGINSWITH %@", "Meeting")
let query = CKQuery(recordType: "Note", predicate: predicate)
query.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: false)]
let (results, _) = try await privateDB.records(matching: query)
for (_, result) in results {
let record = try result.get()
print(record["title"] as? String ?? "")
}
// Fetch all records of a type
let allQuery = CKQuery(recordType: "Note", predicate: NSPredicate(value: true))
// Full-text search across string fields
let searchQuery = CKQuery(
recordType: "Note",
predicate: NSPredicate(format: "self CONTAINS %@", "roadmap")
)
// Compound predicate
let compound = NSCompoundPredicate(andPredicateWithSubpredicates: [
NSPredicate(format: "createdAt > %@", cutoffDate as NSDate),
NSPredicate(format: "tags CONTAINS %@", "work")
])
CKSubscription
当服务器端记录发生变化时,订阅会触发推送通知。启用 CloudKit 后,CloudKit/Xcode 会处理 APNs 权限;无需单独显式配置 App ID 推送。静默或后台处理仍需要在 Background Modes 中启用 Remote notifications。
// Query subscription -- fires when matching records change
let subscription = CKQuerySubscription(
recordType: "Note",
predicate: NSPredicate(format: "tags CONTAINS %@", "urgent"),
subscriptionID: "urgent-notes",
options: [.firesOnRecordCreation, .firesOnRecordUpdate]
)
let notifInfo = CKSubscription.NotificationInfo()
notifInfo.shouldSendContentAvailable = true // silent push
subscription.notificationInfo = notifInfo
try await privateDB.save(subscription)
// Database subscription -- fires on any database change
let dbSub = CKDatabaseSubscription(subscriptionID: "private-db-changes")
dbSub.notificationInfo = notifInfo
try await privateDB.save(dbSub)
// Record zone subscription -- fires on changes within a zone
let zoneSub = CKRecordZoneSubscription(
zoneID: CKRecordZone.ID(zoneName: "NotesZone"),
subscriptionID: "notes-zone-changes"
)
zoneSub.notificationInfo = notifInfo
try await privateDB.save(zoneSub)
在 AppDelegate 中处理:
func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any]
) async -> UIBackgroundFetchResult {
let notification = CKNotification(fromRemoteNotificationDictionary: userInfo)
guard notification?.subscriptionID == "private-db-changes" else { return .noData }
// Fetch changes using CKSyncEngine or CKFetchRecordZoneChangesOperation
return .newData
}
CKSyncEngine(iOS 17+)
CKSyncEngine 是自定义模型数据的推荐同步方式。它负责处理调度、临时重试、更改令牌和数据库订阅,但不处理特定于应用的保存失败:sentRecordZoneChanges.failedRecordSaves 中的 CKError.serverRecordChanged 仍需自定义冲突解决并重新调度。自动同步时间无法确定。需要启用 CloudKit 功能和远程通知;仅支持私有数据库和共享数据库。
import CloudKit
final class SyncManager: CKSyncEngineDelegate {
let syncEngine: CKSyncEngine
init(container: CKContainer = .default()) {
let config = CKSyncEngine.Configuration(
database: container.privateCloudDatabase,
stateSerialization: Self.loadState(),
delegate: self
)
self.syncEngine = CKSyncEngine(config)
}
func handleEvent(_ event: CKSyncEngine.Event, syncEngine: CKSyncEngine) async {
switch event {
case .stateUpdate(let update):
Self.saveState(update.stateSerialization)
case .accountChange(let change):
handleAccountChange(change)
case .fetchedRecordZoneChanges(let changes):
for mod in changes.modifications { processRemoteRecord(mod.record) }
for del in changes.deletions { processRemoteDeletion(del.recordID) }
case .sentRecordZoneChanges(let sent):
for saved in sent.savedRecords { markSynced(saved) }
for fail in sent.failedRecordSaves { handleSaveFailure(fail) }
default: break
}
}
func nextRecordZoneChangeBatch(
_ context: CKSyncEngine.SendChangesContext,
syncEngine: CKSyncEngine
) async -> CKSyncEngine.RecordZoneChangeBatch? {
let pending = syncEngine.state.pendingRecordZoneChanges
.filter { context.options.zoneIDs.contains($0) }
return await CKSyncEngine.RecordZoneChangeBatch(
pendingChanges: pending
) { recordID in self.recordToSend(for: recordID) }
}
}
// Schedule changes
let zoneID = CKRecordZone.ID(zoneName: "NotesZone")
let recordID = CKRecord.ID(recordName: noteID, zoneID: zoneID)
syncEngine.state.add(pendingRecordZoneChanges: [.saveRecord(recordID)])
// Trigger immediate sync (pull-to-refresh)
try await syncEngine.fetchChanges()
try await syncEngine.sendChanges()
要点:在应用每次启动之间持久化 stateSerialization;引擎需要它才能从正确的更改令牌处恢复。
SwiftData + CloudKit
ModelConfiguration 支持 CloudKit 同步。在所有 SwiftData CloudKit 实现或审查中,始终报告两项结论:
- 模型兼容性:不得使用
#Unique或唯一性约束;关系必须为可选;不得使用.deny;大型Data应使用外部存储。 - 架构发布:在非生产构建中初始化开发架构,在 CloudKit Dashboard 中验证,并在发布前提升架构;提升到生产环境后只能添加架构,不要删除模型类型或更改现有属性。
import SwiftData
@Model
class Note {
var title: String
var body: String?
var createdAt: Date?
@Attribute(.externalStorage) var imageData: Data?
init(title: String, body: String? = nil) {
self.title = title
self.body = body
self.createdAt = Date()
}
}
let config = ModelConfiguration(
"Notes",
cloudKitDatabase: .private("iCloud.com.example.app")
)
let container = try ModelContainer(for: Note.self, configurations: config)
NSUbiquitousKeyValueStore
简单的键值同步。最多支持 1024 个键,总容量为 1 MB,每个值最大为 1 MB。iCloud 不可用时会在本地存储。
let kvStore = NSUbiquitousKeyValueStore.default
// Write
kvStore.set("dark", forKey: "theme")
kvStore.set(14.0, forKey: "fontSize")
kvStore.set(true, forKey: "notificationsEnabled")
kvStore.synchronize()
// Read
let theme = kvStore.string(forKey: "theme") ?? "system"
// Observe external changes
NotificationCenter.default.addObserver(
forName: NSUbiquitousKeyValueStore.didChangeExternallyNotification,
object: kvStore, queue: .main
) { notification in
guard let userInfo = notification.userInfo,
let reason = userInfo[NSUbiquitousKeyValueStoreChangeReasonKey] as? Int,
let keys = userInfo[NSUbiquitousKeyValueStoreChangedKeysKey] as? [String]
else { return }
switch reason {
case NSUbiquitousKeyValueStoreServerChange:
for key in keys { applyRemoteChange(key: key) }
case NSUbiquitousKeyValueStoreInitialSyncChange:
reloadAllSettings()
case NSUbiquitousKeyValueStoreQuotaViolationChange:
handleQuotaExceeded()
default: break
}
}
iCloud Drive 文件同步
使用 FileManager 的 ubiquitous API 进行文档级同步。请在主线程之外调用
url(forUbiquityContainerIdentifier:) 和 setUbiquitous;setUbiquitous 会执行协调文件操作,可能造成阻塞。如果应用正在展示该文件,请在移动文件前配置一个活动的文件协调器。
Task.detached {
guard let ubiquityURL = FileManager.default.url(
forUbiquityContainerIdentifier: "iCloud.com.example.app"
) else { return } // iCloud not available
let docsURL = ubiquityURL.appendingPathComponent("Documents")
try FileManager.default.createDirectory(at: docsURL, withIntermediateDirectories: true)
let cloudURL = docsURL.appendingPathComponent("report.pdf")
try FileManager.default.setUbiquitous(true, itemAt: localURL, destinationURL: cloudURL)
}
使用 NSMetadataQuery 监控文件,范围设为
NSMetadataQueryUbiquitousDocumentsScope 或
NSMetadataQueryUbiquitousDataScope。
账户状态与错误处理
同步前始终检查账户状态。监听 .CKAccountChanged。
func checkiCloudStatus() async throws -> CKAccountStatus {
let status = try await CKContainer.default().accountStatus()
switch status {
case .available: return status
case .noAccount: throw SyncError.noiCloudAccount
case .restricted: throw SyncError.restricted
case .temporarilyUnavailable: throw SyncError.temporarilyUnavailable
case .couldNotDetermine: throw SyncError.unknown
@unknown default: throw SyncError.unknown
}
}
CKError 处理
| 错误代码 | 策略 |
|---|---|
.networkFailure、.networkUnavailable | 网络恢复后将操作加入重试队列 |
.serverRecordChanged | 执行三方合并(参见冲突解决) |
.requestRateLimited、.zoneBusy、.serviceUnavailable | 在 retryAfterSeconds 指定的时间后重试 |
.quotaExceeded | 通知用户;减少数据用量 |
.notAuthenticated | 提示用户登录 iCloud |
.partialFailure | 按项检查 partialErrorsByItemID |
.changeTokenExpired | 重置令牌,重新获取所有更改 |
.userDeletedZone | 重新创建区域并重新上传数据 |
func handleCloudKitError(_ error: Error) {
guard let ckError = error as? CKError else { return }
switch ckError.code {
case .networkFailure, .networkUnavailable:
scheduleRetryWhenOnline()
case .serverRecordChanged:
resolveConflict(ckError)
case .requestRateLimited, .zoneBusy, .serviceUnavailable:
let delay = ckError.retryAfterSeconds ?? 3.0
scheduleRetry(after: delay)
case .quotaExceeded:
notifyUserStorageFull()
case .partialFailure:
if let partial = ckError.partialErrorsByItemID {
for (_, itemError) in partial { handleCloudKitError(itemError) }
}
case .changeTokenExpired:
resetChangeToken()
case .userDeletedZone:
recreateZoneAndResync()
default: logError(ckError)
}
}
冲突解决
保存服务器端已更改的记录时,CloudKit 会返回包含三个记录版本的 .serverRecordChanged。始终合并到 serverRecord 中——它具有正确的更改标签。
func resolveConflict(_ error: CKError) {
guard error.code == .serverRecordChanged,
let ancestor = error.ancestorRecord,
let client = error.clientRecord,
let server = error.serverRecord
else { return }
// Merge client changes into server record
for key in client.changedKeys() {
if server[key] == ancestor[key] {
server[key] = client[key] // Server unchanged, use client
} else if client[key] == ancestor[key] {
// Client unchanged, keep server (already there)
} else {
server[key] = mergeValues( // Both changed, custom merge
ancestor: ancestor[key], client: client[key], server: server[key])
}
}
Task { try await CKContainer.default().privateCloudDatabase.save(server) }
}
常见错误
| 错误 | 修正方法 |
|---|---|
| 未检查账户状态就进行同步 | 检查 accountStatus(),并将模型 .noAccount 作为用户可见状态处理。 |
| 在公共数据库中存储个人数据 | 用户数据使用私有范围;公共范围用于整个应用共享的内容。 |
| 使用定时器轮询 | 使用数据库订阅或 CKSyncEngine。 |
| 受到限流后立即重试 | 遵循 retryAfterSeconds,并保留待处理的工作。 |
| 认为引擎会解决冲突 | 对 failedRecordSaves 执行三方合并,然后重新调度保存操作。 |
| 每次获取数据都从空令牌开始 | 持久化令牌/状态;仅在文档规定的过期情况下重置。 |
审查清单
- 已在 Signing & Capabilities 中启用 iCloud + CloudKit 功能
- 同步前已检查账户状态;妥善处理
.noAccount - 用户数据使用私有数据库;仅共享内容使用公共数据库
- 已在私有数据库中创建自定义记录区;从共享中发现共享数据库的记录区
- 使用三方合并将
CKError.serverRecordChanged合并到serverRecord - 将网络故障加入重试队列;遵循
retryAfterSeconds - 使用
CKDatabaseSubscription或CKSyncEngine实现基于推送的同步;已启用远程通知,以便在后台投递 - 将更改令牌持久化到磁盘;
changeTokenExpired会重置令牌并重新获取数据 - 通过
partialErrorsByItemID检查.partialFailure错误中的每个项目 - 通过重建记录区并重新同步来处理
.userDeletedZone - SwiftData CloudKit 审查报告了模型兼容性和架构推出情况:已初始化/验证开发架构,发布前已提升架构,且生产环境只进行增量式更改
- 已监听
NSUbiquitousKeyValueStore.didChangeExternallyNotification - 加密审查指出,
CKRecord.Reference不能使用encryptedValues,因为 CloudKit 需要在服务器端使用它;不得对加密字段进行查询/排序;CKAsset默认已加密 - 已跨启动持久化
CKSyncEngine状态序列化(iOS 17+)
参考资料
- 增量同步、CKShare、记录区、CKAsset 存储、批量操作和 Dashboard 用法,请参阅 references/cloudkit-patterns.md。
- CloudKit 框架
- CKContainer
- CKRecord
- CKQuery
- CKSubscription
- CKSyncEngine
- CKShare
- CKError
- NSUbiquitousKeyValueStore
- SwiftData CloudKit 同步