cloudkit-sync
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCloudKit and iCloud Sync
CloudKit与iCloud同步
Sync data across devices using CloudKit, iCloud key-value storage, and iCloud
Drive. Covers container setup, record CRUD, queries, subscriptions, CKSyncEngine,
SwiftData integration, conflict resolution, and error handling. Targets iOS 26+
with Swift 6.2; older availability noted where relevant.
借助CloudKit、iCloud键值存储和iCloud Drive实现跨设备数据同步。内容涵盖容器配置、记录CRUD、查询、订阅、CKSyncEngine、SwiftData集成、冲突解决和错误处理。目标平台为iOS 26+,使用Swift 6.2;相关旧版本兼容信息会在对应位置标注。
Contents
目录
Container and Database Setup
容器与数据库配置
Enable iCloud + CloudKit in Signing & Capabilities. A container provides
three databases:
| Database | Scope | Requires iCloud | Storage Quota |
|---|---|---|---|
| Public | All users | Read: No, Write: Yes | App quota |
| Private | Current user | Yes | User quota |
| Shared | Shared records | Yes | Owner quota |
swift
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在Signing & Capabilities中启用iCloud + CloudKit功能。一个容器包含三个数据库:
| 数据库 | 作用范围 | 是否需要iCloud | 存储配额 |
|---|---|---|---|
| Public(公共) | 所有用户 | 读取:无需,写入:需要 | 应用配额 |
| Private(私有) | 当前用户 | 需要 | 用户配额 |
| Shared(共享) | 共享记录 | 需要 | 所有者配额 |
swift
import CloudKit
let container = CKContainer.default()
// 或指定容器:CKContainer(identifier: "iCloud.com.example.app")
let publicDB = container.publicCloudDatabase
let privateDB = container.privateCloudDatabase
let sharedDB = container.sharedCloudDatabaseCKRecord CRUD
CKRecord增删改查
Records are key-value pairs. Max 1 MB per record (excluding CKAsset data).
swift
// 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)记录为键值对结构。单条记录最大1MB(不包含CKAsset数据)。
swift
// 创建记录
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)
// 通过ID获取记录
let recordID = CKRecord.ID(recordName: "unique-id-123")
let fetched = try await privateDB.record(for: recordID)
// 更新记录 -- 先获取,修改后保存
fetched["title"] = "Updated Title" as CKRecordValue
let updated = try await privateDB.save(fetched)
// 删除记录
try await privateDB.deleteRecord(withID: recordID)Custom Record Zones (Private/Shared Only)
自定义记录区(仅私有/共享数据库可用)
Custom zones support atomic commits, change tracking, and sharing.
swift
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)自定义记录区支持原子提交、变更追踪和共享功能。
swift
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
CKQuery查询
Query records with NSPredicate. Supported: , , , , , ,
, , , , , ,
.
==!=<><=>=BEGINSWITHCONTAINSINANDNOTBETWEENdistanceToLocation:fromLocation:swift
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")
])使用NSPredicate查询记录。支持的操作符:, , , , , , , , , , , , 。
==!=<><=>=BEGINSWITHCONTAINSINANDNOTBETWEENdistanceToLocation:fromLocation:swift
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 ?? "")
}
// 获取某类型的所有记录
let allQuery = CKQuery(recordType: "Note", predicate: NSPredicate(value: true))
// 对字符串字段进行全文搜索
let searchQuery = CKQuery(
recordType: "Note",
predicate: NSPredicate(format: "self CONTAINS %@", "roadmap")
)
// 复合谓词
let compound = NSCompoundPredicate(andPredicateWithSubpredicates: [
NSPredicate(format: "createdAt > %@", cutoffDate as NSDate),
NSPredicate(format: "tags CONTAINS %@", "work")
])CKSubscription
CKSubscription订阅
Subscriptions trigger push notifications when records change server-side.
CloudKit auto-enables APNs -- no explicit push entitlement needed.
swift
// 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)Handle in AppDelegate:
swift
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
}当服务器端记录发生变更时,订阅会触发推送通知。CloudKit会自动启用APNs -- 无需显式配置推送权限。
swift
// 查询订阅 -- 匹配的记录变更时触发
let subscription = CKQuerySubscription(
recordType: "Note",
predicate: NSPredicate(format: "tags CONTAINS %@", "urgent"),
subscriptionID: "urgent-notes",
options: [.firesOnRecordCreation, .firesOnRecordUpdate]
)
let notifInfo = CKSubscription.NotificationInfo()
notifInfo.shouldSendContentAvailable = true // 静默推送
subscription.notificationInfo = notifInfo
try await privateDB.save(subscription)
// 数据库订阅 -- 数据库发生任何变更时触发
let dbSub = CKDatabaseSubscription(subscriptionID: "private-db-changes")
dbSub.notificationInfo = notifInfo
try await privateDB.save(dbSub)
// 记录区订阅 -- 指定记录区内发生变更时触发
let zoneSub = CKRecordZoneSubscription(
zoneID: CKRecordZone.ID(zoneName: "NotesZone"),
subscriptionID: "notes-zone-changes"
)
zoneSub.notificationInfo = notifInfo
try await privateDB.save(zoneSub)在AppDelegate中处理通知:
swift
func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any]
) async -> UIBackgroundFetchResult {
let notification = CKNotification(fromRemoteNotificationDictionary: userInfo)
guard notification?.subscriptionID == "private-db-changes" else { return .noData }
// 使用CKSyncEngine或CKFetchRecordZoneChangesOperation获取变更
return .newData
}CKSyncEngine (iOS 17+)
CKSyncEngine(iOS 17+)
CKSyncEngineswift
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) {
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
) -> CKSyncEngine.RecordZoneChangeBatch? {
let pending = syncEngine.state.pendingRecordZoneChanges
return CKSyncEngine.RecordZoneChangeBatch(
pendingChanges: Array(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()Key point: persist across launches; the engine needs it
to resume from the correct change token.
stateSerializationCKSyncEngineswift
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) {
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
) -> CKSyncEngine.RecordZoneChangeBatch? {
let pending = syncEngine.state.pendingRecordZoneChanges
return CKSyncEngine.RecordZoneChangeBatch(
pendingChanges: Array(pending)
) { recordID in self.recordToSend(for: recordID) }
}
}
// 调度变更
let zoneID = CKRecordZone.ID(zoneName: "NotesZone")
let recordID = CKRecord.ID(recordName: noteID, zoneID: zoneID)
syncEngine.state.add(pendingRecordZoneChanges: [.saveRecord(recordID)])
// 触发立即同步(下拉刷新)
try await syncEngine.fetchChanges()
try await syncEngine.sendChanges()关键点:跨启动会话持久化;引擎需要它从正确的变更令牌位置恢复同步。
stateSerializationSwiftData + CloudKit
SwiftData + CloudKit
ModelConfigurationswift
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)CloudKit model rules: use optionals for all non-String properties; avoid
; keep models flat; use for large data;
avoid complex relationship graphs.
#Unique@Attribute(.externalStorage)ModelConfigurationswift
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)CloudKit模型规则:所有非String属性使用可选类型;避免使用;保持模型扁平化;对大数据使用;避免复杂的关系图。
#Unique@Attribute(.externalStorage)NSUbiquitousKeyValueStore
NSUbiquitousKeyValueStore键值存储
Simple key-value sync. Max 1024 keys, 1 MB total, 1 MB per value. Stores
locally when iCloud is unavailable.
swift
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
}
}轻量级键值同步方案。最多支持1024个键,总存储1MB,单值最大1MB。当iCloud不可用时,数据会存储在本地。
swift
let kvStore = NSUbiquitousKeyValueStore.default
// 写入数据
kvStore.set("dark", forKey: "theme")
kvStore.set(14.0, forKey: "fontSize")
kvStore.set(true, forKey: "notificationsEnabled")
kvStore.synchronize()
// 读取数据
let theme = kvStore.string(forKey: "theme") ?? "system"
// 监听外部变更
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 File Sync
iCloud Drive文件同步
Use ubiquity APIs for document-level sync.
FileManagerswift
guard let ubiquityURL = FileManager.default.url(
forUbiquityContainerIdentifier: "iCloud.com.example.app"
) else { return } // iCloud not available
let docsURL = ubiquityURL.appendingPathComponent("Documents")
let cloudURL = docsURL.appendingPathComponent("report.pdf")
try FileManager.default.setUbiquitous(true, itemAt: localURL, destinationURL: cloudURL)
// Monitor iCloud files
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: "%K LIKE '*.pdf'", NSMetadataItemFSNameKey)
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
NotificationCenter.default.addObserver(
forName: .NSMetadataQueryDidFinishGathering, object: query, queue: .main
) { _ in
query.disableUpdates()
for item in query.results as? [NSMetadataItem] ?? [] {
let name = item.value(forAttribute: NSMetadataItemFSNameKey) as? String
let status = item.value(
forAttribute: NSMetadataUbiquitousItemDownloadingStatusKey) as? String
}
query.enableUpdates()
}
query.start()使用的ubiquity API实现文档级同步。
FileManagerswift
guard let ubiquityURL = FileManager.default.url(
forUbiquityContainerIdentifier: "iCloud.com.example.app"
) else { return } // iCloud不可用
let docsURL = ubiquityURL.appendingPathComponent("Documents")
let cloudURL = docsURL.appendingPathComponent("report.pdf")
try FileManager.default.setUbiquitous(true, itemAt: localURL, destinationURL: cloudURL)
// 监听iCloud文件
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: "%K LIKE '*.pdf'", NSMetadataItemFSNameKey)
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
NotificationCenter.default.addObserver(
forName: .NSMetadataQueryDidFinishGathering, object: query, queue: .main
) { _ in
query.disableUpdates()
for item in query.results as? [NSMetadataItem] ?? [] {
let name = item.value(forAttribute: NSMetadataItemFSNameKey) as? String
let status = item.value(
forAttribute: NSMetadataUbiquitousItemDownloadingStatusKey) as? String
}
query.enableUpdates()
}
query.start()Account Status and Error Handling
账户状态与错误处理
Always check account status before sync. Listen for .
.CKAccountChangedswift
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
}
}执行同步前务必检查账户状态。监听通知。
.CKAccountChangedswift
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 Handling
CKError错误处理
| Error Code | Strategy |
|---|---|
| Queue for retry when network returns |
| Three-way merge (see Conflict Resolution) |
| Retry after |
| Notify user; reduce data usage |
| Prompt iCloud sign-in |
| Inspect |
| Reset token, refetch all changes |
| Recreate zone and re-upload data |
swift
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)
}
}| 错误码 | 处理策略 |
|---|---|
| 网络恢复后重新排队执行 |
| 三方合并(见冲突解决章节) |
| 等待 |
| 通知用户;减少数据占用 |
| 提示用户登录iCloud |
| 通过 |
| 重置令牌,重新获取所有变更 |
| 重新创建记录区并重新上传数据 |
swift
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)
}
}Conflict Resolution
冲突解决
When saving a record that changed server-side, CloudKit returns
with three record versions. Always merge into
-- it has the correct change tag.
.serverRecordChangedserverRecordswift
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) }
}当保存的记录在服务器端已发生变更时,CloudKit会返回错误,并提供三个版本的记录。务必将变更合并到中 -- 它包含正确的变更标记。
.serverRecordChangedserverRecordswift
func resolveConflict(_ error: CKError) {
guard error.code == .serverRecordChanged,
let ancestor = error.ancestorRecord,
let client = error.clientRecord,
let server = error.serverRecord
else { return }
// 将客户端变更合并到服务器记录
for key in client.changedKeys() {
if server[key] == ancestor[key] {
server[key] = client[key] // 服务器记录未变更,使用客户端值
} else if client[key] == ancestor[key] {
// 客户端记录未变更,保留服务器值(已存在)
} else {
server[key] = mergeValues( // 双方均变更,执行自定义合并
ancestor: ancestor[key], client: client[key], server: server[key])
}
}
Task { try await CKContainer.default().privateCloudDatabase.save(server) }
}Common Mistakes
常见误区
DON'T: Perform sync operations without checking account status.
DO: Check first; handle .
CKContainer.accountStatus().noAccountswift
// WRONG
try await privateDB.save(record)
// CORRECT
guard try await CKContainer.default().accountStatus() == .available
else { throw SyncError.noiCloudAccount }
try await privateDB.save(record)DON'T: Ignore errors.
DO: Implement three-way merge using ancestor, client, and server records.
.serverRecordChangedDON'T: Store user-specific data in the public database.
DO: Use private database for personal data; public only for app-wide content.
DON'T: Assume data is available immediately after save.
DO: Update local cache optimistically and reconcile on fetch.
DON'T: Poll for changes on a timer.
DO: Use or for push-based sync.
CKDatabaseSubscriptionCKSyncEngineswift
// WRONG
Timer.scheduledTimer(withTimeInterval: 30, repeats: true) { _ in fetchAll() }
// CORRECT
let sub = CKDatabaseSubscription(subscriptionID: "db-changes")
sub.notificationInfo = CKSubscription.NotificationInfo()
sub.notificationInfo?.shouldSendContentAvailable = true
try await privateDB.save(sub)DON'T: Retry immediately on rate limiting.
DO: Use to wait the required duration.
CKError.retryAfterSecondsDON'T: Merge conflict changes into .
DO: Always merge into -- it has the correct change tag.
clientRecordserverRecordDON'T: Pass nil change token on every fetch.
DO: Persist change tokens to disk and supply them on subsequent fetches.
错误做法:未检查账户状态就执行同步操作。
正确做法:先检查;处理情况。
CKContainer.accountStatus().noAccountswift
// 错误示例
try await privateDB.save(record)
// 正确示例
guard try await CKContainer.default().accountStatus() == .available
else { throw SyncError.noiCloudAccount }
try await privateDB.save(record)错误做法:忽略错误。
正确做法:使用祖先、客户端和服务器记录实现三方合并。
.serverRecordChanged错误做法:在公共数据库中存储用户特定数据。
正确做法:使用私有数据库存储个人数据;仅将公共数据库用于应用级共享内容。
错误做法:假设保存后数据会立即可用。
正确做法:先乐观更新本地缓存,再在获取时进行协调。
错误做法:通过定时器轮询变更。
正确做法:使用或实现基于推送的同步。
CKDatabaseSubscriptionCKSyncEngineswift
// 错误示例
Timer.scheduledTimer(withTimeInterval: 30, repeats: true) { _ in fetchAll() }
// 正确示例
let sub = CKDatabaseSubscription(subscriptionID: "db-changes")
sub.notificationInfo = CKSubscription.NotificationInfo()
sub.notificationInfo?.shouldSendContentAvailable = true
try await privateDB.save(sub)错误做法:遇到速率限制时立即重试。
正确做法:使用等待指定时长后重试。
CKError.retryAfterSeconds错误做法:将冲突变更合并到中。
正确做法:务必合并到中 -- 它包含正确的变更标记。
clientRecordserverRecord错误做法:每次获取时都传递nil变更令牌。
正确做法:将变更令牌持久化到磁盘,并在后续获取时提供。
Review Checklist
评审检查清单
- iCloud + CloudKit capability enabled in Signing & Capabilities
- Account status checked before sync; handled gracefully
.noAccount - Private database used for user data; public only for shared content
- handled with three-way merge into
CKError.serverRecordChangedserverRecord - Network failures queued for retry; respected
retryAfterSeconds - or
CKDatabaseSubscriptionused for push-based syncCKSyncEngine - Change tokens persisted to disk; resets and refetches
changeTokenExpired - errors inspected per-item via
.partialFailurepartialErrorsByItemID - handled by recreating zone and resyncing
.userDeletedZone - SwiftData CloudKit models use optionals, no ,
#Uniquefor large data.externalStorage - observed
NSUbiquitousKeyValueStore.didChangeExternallyNotification - Sensitive data uses on CKRecord (not plain fields)
encryptedValues - state serialization persisted across launches (iOS 17+)
CKSyncEngine
- 在Signing & Capabilities中启用了iCloud + CloudKit功能
- 同步前检查账户状态;情况已妥善处理
.noAccount - 用户数据使用私有数据库;公共数据库仅用于共享内容
- 错误已通过三方合并到
CKError.serverRecordChanged处理serverRecord - 网络故障已排队等待重试;已被遵守
retryAfterSeconds - 使用或
CKDatabaseSubscription实现基于推送的同步CKSyncEngine - 变更令牌已持久化到磁盘;时会重置并重新获取
changeTokenExpired - 错误已通过
.partialFailure逐个检查partialErrorsByItemID - 错误已通过重新创建记录区并重新同步处理
.userDeletedZone - SwiftData CloudKit模型使用可选属性,无,大数据使用
#Unique.externalStorage - 已监听通知
NSUbiquitousKeyValueStore.didChangeExternallyNotification - 敏感数据使用CKRecord的(而非普通字段)
encryptedValues - 的stateSerialization已跨启动会话持久化(iOS 17+)
CKSyncEngine
References
参考资料
- See for CKFetchRecordZoneChangesOperation incremental sync, CKShare collaboration, record zone management, CKAsset file storage, batch operations, and CloudKit Dashboard usage.
references/cloudkit-patterns.md - CloudKit Framework
- CKContainer
- CKRecord
- CKQuery
- CKSubscription
- CKSyncEngine
- CKShare
- CKError
- NSUbiquitousKeyValueStore
- CKAsset
- 查看了解CKFetchRecordZoneChangesOperation增量同步、CKShare协作、记录区管理、CKAsset文件存储、批量操作和CloudKit Dashboard使用方法。
references/cloudkit-patterns.md - CloudKit Framework
- CKContainer
- CKRecord
- CKQuery
- CKSubscription
- CKSyncEngine
- CKShare
- CKError
- NSUbiquitousKeyValueStore
- CKAsset