cloudkit

cloudkit

热门

在 iOS/macOS 应用中实现、审查或优化 CloudKit 与 iCloud 数据同步。适用于以下场景:使用 CKContainer、CKRecord、CKQuery、CKSubscription、CKSyncEngine、CKShare、NSUbiquitousKeyValueStore 或 iCloud Drive 文件协调;通过配置 cloudKitDatabase 的 ModelConfiguration 来同步 SwiftData 模型;处理 CKError 错误码以解决冲突、应对网络失败或配额限制;以及在执行同步操作前检查 iCloud 账户状态。

957Star
48Fork
更新于 2026/7/31
SKILL.md
只读
名称
cloudkit
描述

在 iOS/macOS 应用中实现、审查或优化 CloudKit 与 iCloud 数据同步。适用于以下场景:使用 CKContainer、CKRecord、CKQuery、CKSubscription、CKSyncEngine、CKShare、NSUbiquitousKeyValueStore 或 iCloud Drive 文件协调;通过配置 cloudKitDatabase 的 ModelConfiguration 来同步 SwiftData 模型;处理 CKError 错误码以解决冲突、应对网络失败或配额限制;以及在执行同步操作前检查 iCloud 账户状态。

CloudKit

利用 CloudKit、iCloud 键值存储(Key-Value Storage)和 iCloud Drive 实现多设备间的数据同步。涵盖容器配置、Record 增删改查(CRUD)、查询、订阅、CKSyncEngine、SwiftData 集成、冲突解决以及错误处理。

Contents

工作流程

  1. 确定数据库作用域(Scope)和同步所有者;在写入记录前,务必校验 Capability 配置、容器、账户状态、Schema 架构以及运行环境。
  2. 先将本地修改持久化并入队,随后由订阅(Subscriptions)或 CKSyncEngine 驱动远端同步,避免轮询(Polling)。
  3. 成功应用更新后,持久化保存 Change Token 或 Sync Engine 的状态。
  4. 测试各种边角场景:离线编辑、部分失败、频控限流(Rate Limiting)、Token 过期、冲突、账户登出/切换、Zone 被删除以及应用重启。
  5. 发生失败时,对 CKError 进行分类归因,恢复受影响的数据项或队列任务,按文档规范执行重试/重置/合并,然后重新运行该场景。切勿在部分成功后盲目重新发起全量同步。

如需了解增量 Zone 变更、共享(Shares)、Asset 资源、批量操作以及 Dashboard 操作流程,请查阅 references/cloudkit-patterns.md

容器与数据库配置

在 Signing & Capabilities 中启用 iCloud 并勾选 CloudKit。一个容器(Container)提供三种数据库:

数据库 作用域 (Scope) 是否需要 iCloud 登录 存储配额 (Quota)
Public 所有用户 读取:否,写入:是 应用配额
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 本质上是键值对。单条 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 by ID)
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

应用可以在 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 检索记录。支持的运算符包括:==!=<><=>=BEGINSWITHCONTAINSINANDNOTBETWEEN 以及 distanceToLocation:fromLocation:

CONTAINS 用于检查列表成员资格,但配合 self 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 ?? "")
}

// 获取某个 Type 下的所有记录
let allQuery = CKQuery(recordType: "Note", predicate: NSPredicate(value: true))

// 跨字符串字段进行全文检索
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 订阅

当服务端记录发生变更时,订阅会触发推送通知(Push Notification)。开启 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 订阅 (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 是官方推荐的同步方案。它会自动处理调度、临时失败重试、Change Token 以及数据库订阅,但不包括具体业务的保存失败逻辑:例如从 sentRecordZoneChanges.failedRecordSaves 返回的 CKError.serverRecordChanged 仍需要自行编写冲突解决逻辑并重新发起调度。自动同步的时机是不确定的(Indeterminate)。需开启 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;Sync Engine 需要依靠它从正确的 Change Token 处恢复同步。

SwiftData + CloudKit

ModelConfiguration 原生支持 CloudKit 同步。在进行 SwiftData CloudKit 的实现或代码审查时,务必输出两项评估结论:

  • 模型兼容性(Model compatibility):不能包含 #Unique 或唯一性约束,关系(Relationship)必须设为可选(Optional),删除策略不能为 .deny,大体积的 Data 必须开启外置存储(External Storage)。
  • Schema 部署上线(Schema rollout):非生产环境构建时初始化 Development 阶段的 Schema,并在 CloudKit Dashboard 中进行验证;发布前将其部署至 Production(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

轻量级键值同步。最多支持 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 -->