
cloudkit
熱門在 iOS/macOS App 中实作、审查或改善 CloudKit 与 iCloud 同步功能。适用于使用 CKContainer、CKRecord、CKQuery、CKSubscription、CKSyncEngine、CKShare、NSUbiquitousKeyValueStore 或 iCloud Drive 档案协调;透过 ModelConfiguration 与 cloudKitDatabase 同步 SwiftData 模型;处理用于冲突解决、网络连线失败或配额限制的 CKError 错误码;或在执行同步操作前检查 iCloud 账号状态等情境。
在 iOS/macOS App 中实作、审查或改善 CloudKit 与 iCloud 同步功能。适用于使用 CKContainer、CKRecord、CKQuery、CKSubscription、CKSyncEngine、CKShare、NSUbiquitousKeyValueStore 或 iCloud Drive 档案协调;透过 ModelConfiguration 与 cloudKitDatabase 同步 SwiftData 模型;处理用于冲突解决、网络连线失败或配额限制的 CKError 错误码;或在执行同步操作前检查 iCloud 账号状态等情境。
CloudKit
使用 CloudKit、iCloud 键值储存(key-value storage)与 iCloud Drive 在多台装置间同步资料。涵盖容器(container)设定、Record CRUD、Query、Subscription、CKSyncEngine、SwiftData 整合、冲突解决与错误处理。
目录
- 容器与数据库设定
- 工作流程
- CKRecord CRUD
- CKQuery
- CKSubscription
- CKSyncEngine (iOS 17+)
- SwiftData + CloudKit
- NSUbiquitousKeyValueStore
- iCloud Drive 档案同步
- 账号状态与错误处理
- 冲突解决
- 常见错误
- 审查检核表
- 参考资料
工作流程
- 选择数据库作用域(scope)与同步拥有者;在写入记录前,先确认 Capability、Container、账号状态、Schema 以及环境配置。
- 将本地变更持久化并加入队列,接着由 Subscription 或
CKSyncEngine驱动远端同步作业,而非采用轮询(polling)。 - 成功套用后,持久化保存变更标记(change token)或 sync-engine 的状态。
- 测试离线编辑、部分失败、速率限制(rate limiting)、Token 过期、冲突、账号注销/丢失、Zone 删除以及重新启动等情境。
- 发生失败时,先对
CKError进行分类,还原受影响的测试资料或队列项目,执行文件所载的重试/重设/合并动作,并重新执行相同情境。切勿在部分成功后盲目重启完整同步。
载入 references/cloudkit-patterns.md 以了解增量 Zone 变更、共享(shares)、资产(assets)、批次操作及 Dashboard 操作步骤。
容器与数据库设定
在 Signing & Capabilities 中启用 iCloud + CloudKit。容器会提供三种数据库:
| 数据库 | 作用域 (Scope) | 需要 iCloud | 储存空间配额 |
|---|---|---|---|
| Public | 所有用户 | 读取:否,写入:是 | App 配额 |
| Private | 当前用户 | 是 | 用户配额 |
| Shared | 共享记录 | 是 | 拥有者配额 |
import CloudKit
let container = CKContainer.default()
// 或指定名称:CKContainer(identifier: "iCloud.com.example.app")
let publicDB = container.publicCloudDatabase
let privateDB = container.privateCloudDatabase
let sharedDB = container.sharedCloudDatabase
CKRecord CRUD
记录(Record)是以键值对形式呈现。每笔记录最大 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)
// 依据 ID 读取 (FETCH)
let recordID = CKRecord.ID(recordName: "unique-id-123")
let fetched = try await privateDB.record(for: recordID)
// 更新 (UPDATE) -- 先获取、修改,再保存
fetched["title"] = "Updated Title" as CKRecordValue
let updated = try await privateDB.save(fetched)
// 删除 (DELETE)
try await privateDB.deleteRecord(withID: recordID)
自订 Record Zone
App 会在 Private 数据库中建立自订 Zone。Shared 数据库则展现其他用户共享给当前用户的 Zone。自订 Zone 支持原子提交(atomic commits)、变更追踪以及共享功能;Public 数据库不支持自订 Zone。
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 为字串前缀比对运算符;若使用不支持的运算符、Key Path 或栏位类型,查询执行时将直接报错。
在每次进行加密审查时,须明确指出栏位的适用条件:经过加密的值无法被查询或排序;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 ?? "")
}
// 取得特定类型的所有记录
let allQuery = CKQuery(recordType: "Note", predicate: NSPredicate(value: true))
// 针对字串栏位进行全文检索
let searchQuery = CKQuery(
recordType: "Note",
predicate: NSPredicate(format: "self CONTAINS %@", "roadmap")
)
// 复合条件 Predicate
let compound = NSCompoundPredicate(andPredicateWithSubpredicates: [
NSPredicate(format: "createdAt > %@", cutoffDate as NSDate),
NSPredicate(format: "tags CONTAINS %@", "work")
])
CKSubscription
Subscription 会在伺服端记录发生变更时触发推送通知(push notifications)。当启用 CloudKit 时,CloudKit/Xcode 会自动处理 APNs Entitlement,无需另外进行独立的 App ID 推送设定。不过静默/背景处理仍须开启 Background Modes > Remote notifications。
// Query subscription -- 当符合条件的记录变更时触发
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 -- 当数据库有任何变更时触发
let dbSub = CKDatabaseSubscription(subscriptionID: "private-db-changes")
dbSub.notificationInfo = notifInfo
try await privateDB.save(dbSub)
// Record zone subscription -- 当指定 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 }
// 使用 CKSyncEngine 或 CKFetchRecordZoneChangesOperation 抓取变更
return .newData
}
CKSyncEngine (iOS 17+)
对于自订模型资料,CKSyncEngine 是官方推荐的同步方案。它负责处理排程、瞬时重试(transient retries)、变更标记(change tokens)以及数据库 Subscription;但不包含 App 特定逻辑的保存失败:来自 sentRecordZoneChanges.failedRecordSaves 的 CKError.serverRecordChanged 仍需自订冲突解决逻辑并重新排程。自动同步的时机是不确定的。必须具备 CloudKit Capability 与 Remote notifications;且仅适用于 Private / Shared 数据库。
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) }
}
}
// 排程变更
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()
关键重点:跨启动保存 stateSerialization;Engine 需要依赖它从正确的变更标记(change token)继续同步。
SwiftData + CloudKit
ModelConfiguration 支持 CloudKit 同步。在每一次 SwiftData CloudKit 的实作或审查中,务必提出两项评估判定:
- 模型相容性 (Model compatibility):不得使用
#Unique或唯一性限制条件、关联(relationship)须设为可选(optional)、不使用.deny规则,且大型Data须设为外置储存(external storage)。 - Schema 发布 (Schema rollout):在非正式环境建置(nonproduction builds)中初始化开发阶段 Schema,于 CloudKit Dashboard 进行验证并在上架前推进部署(promote);在正式上线部署后,仅能新增 Schema,绝不能删除模型类型或修改现有属性。
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
简易的键值(key-value)同步。最多 1024 个 key,总容量上限 1 MB,单值上限 1 MB。当 iCloud 不可用时会在本地端进行保存。
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:
reloa
<!-- truncated for translation batch; full body continues in source -->





