关于 axiom-cloud-sync
为 Apple 平台开发者提供 iCloud 同步设计指导的技能,帮助根据结构化数据或文件选择 CloudKit 或 iCloud Drive,并规划离线优先和冲突处理。
主要功能
- 01区分 CloudKit 与 iCloud Drive
- 02离线优先同步模式
- 03冲突解决策略
- 04SwiftData 与 CKSyncEngine 模式
- 05大数据集分批同步建议
适用场景
- 01为记录或文档选择同步技术
- 02设计断网时仍可使用的同步流程
- 03处理多设备编辑冲突
- 04规划大量记录的初始同步
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-cloud-sync安装后在 Claude Code 中输入/axiom-cloud-sync确认已加载。
详细介绍
云同步
概览
核心原则:根据数据形态选择合适的同步技术,然后采用离线优先模式,妥善处理网络故障。
两种截然不同的同步方式:
- CloudKit — 结构化数据(包含字段和关系的记录)
- iCloud Drive — 基于文件的数据(文档、图像及任意文件格式)
快速决策树
What needs syncing?
├─ Structured data (records, relationships)?
│ ├─ Using SwiftData? → SwiftData + CloudKit (easiest, iOS 17+)
│ ├─ Need shared/public database? → CKSyncEngine or raw CloudKit
│ └─ Custom persistence (GRDB, SQLite)? → CKSyncEngine (iOS 17+)
│
├─ Documents/files users expect in Files app?
│ └─ iCloud Drive (UIDocument or FileManager)
│
├─ Large binary blobs (images, videos)?
│ ├─ Associated with structured data? → CKAsset in CloudKit
│ └─ Standalone files? → iCloud Drive
│
└─ App settings/preferences?
└─ NSUbiquitousKeyValueStore (simple key-value, 1MB limit)
CloudKit 与 iCloud Drive
| 方面 | CloudKit | iCloud Drive |
|---|---|---|
| 数据形态 | 结构化记录 | 文件/文档 |
| 查询支持 | 完整的查询语言 | 仅文件名 |
| 关系 | 原生支持 | 无(需手动处理) |
| 冲突解决 | 记录级别 | 文件级别 |
| 用户可见性 | 对用户隐藏 | 在“文件”应用中可见 |
| 共享 | 记录/数据库共享 | 文件共享 |
| 离线 | 需要本地缓存 | 自动下载 |
危险信号
如果出现以下任何一种情况,请停止并重新考虑:
- ❌ “在 CloudKit 中存储 JSON 文件” — 工具选错了。文件请使用 iCloud Drive
- ❌ “在 iCloud Drive 中手动构建关系” — 工具选错了。请使用 CloudKit
- ❌ “假设同步是即时的” — 网络可能中断。请采用离线优先设计
- ❌ “跳过冲突处理” — 多台设备之间一定会发生冲突
- ❌ “将 CloudKit 用于用户文档” — 用户看不到这些文档。请使用 iCloud Drive
- ❌ “仅在应用启动时同步” — 用户期望持续同步
离线优先模式
强制要求:所有同步代码都必须优先支持离线使用。
// ✅ CORRECT: Offline-first architecture
class OfflineFirstSync {
private let localStore: LocalDatabase // GRDB, SwiftData, Core Data
private let syncEngine: CKSyncEngine
// Write to LOCAL first, sync to cloud in background
func save(_ item: Item) async throws {
// 1. Save locally (instant)
try await localStore.save(item)
// 2. Queue for sync (non-blocking)
syncEngine.state.add(pendingRecordZoneChanges: [
.saveRecord(item.recordID)
])
}
// Read from LOCAL (instant)
func fetch() async throws -> [Item] {
return try await localStore.fetchAll()
}
}
// ❌ WRONG: Cloud-first (blocks on network)
func save(_ item: Item) async throws {
// Fails when offline, slow on bad network
try await cloudKit.save(item)
try await localStore.save(item)
}
冲突解决策略
两台设备在同步前编辑了相同数据时,就会发生冲突。
策略 1:最后写入者胜出(最简单)
// Server always has latest, client accepts it
func resolveConflict(local: CKRecord, server: CKRecord) -> CKRecord {
return server // Accept server version
}
适用情况:数据不重要,用户不会注意到覆盖
策略 2:合并(最常见)
// Combine changes from both versions
func resolveConflict(local: CKRecord, server: CKRecord) -> CKRecord {
let merged = server.copy() as! CKRecord
// For each field, apply custom merge logic
merged["notes"] = mergeText(
local["notes"] as? String,
server["notes"] as? String
)
merged["tags"] = mergeSets(
local["tags"] as? [String] ?? [],
server["tags"] as? [String] ?? []
)
return merged
}
适用情况:两个版本都包含有价值的更改
策略 3:由用户选择
// Present conflict to user
func resolveConflict(local: CKRecord, server: CKRecord) async -> CKRecord {
let choice = await presentConflictUI(local: local, server: server)
return choice == .keepLocal ? local : server
}
适用情况:数据很重要,必须由用户决定
常见模式
模式 1:SwiftData + CloudKit(新应用推荐)
import SwiftData
// Automatic CloudKit sync with zero configuration
@Model
class Note {
var title: String
var content: String
var createdAt: Date
init(title: String, content: String) {
self.title = title
self.content = content
self.createdAt = Date()
}
}
// Container automatically syncs if CloudKit entitlement present
let container = try ModelContainer(for: Note.self)
限制:
- 仅支持私有数据库(不支持公共/共享数据库)
- 自动同步(对同步时机的控制较少)
- 不支持自定义冲突解决
- CloudKit 同步不支持
@Attribute(.unique)— 使用 CloudKit 时请移除
模式 2:CKSyncEngine(自定义持久化)
// For GRDB, SQLite, or custom databases
class MySyncManager: CKSyncEngineDelegate {
private let engine: CKSyncEngine
private let database: GRDBDatabase
func handleEvent(_ event: CKSyncEngine.Event) async {
switch event {
case .stateUpdate(let update):
// Persist sync state
await saveSyncState(update.stateSerialization)
case .fetchedDatabaseChanges(let changes):
// Apply changes to local DB
for zone in changes.modifications {
await handleZoneChanges(zone)
}
case .sentRecordZoneChanges(let sent):
// Mark records as synced
for saved in sent.savedRecords {
await markSynced(saved.recordID)
}
}
}
}
完整的 CKSyncEngine 配置请参见 axiom-cloudkit-ref。
模式 3:iCloud Drive 文档
import UIKit
class MyDocument: UIDocument {
var content: Data?
override func contents(forType typeName: String) throws -> Any {
return content ?? Data()
}
override func load(fromContents contents: Any, ofType typeName: String?) throws {
content = contents as? Data
}
}
// Save to iCloud Drive (visible in Files app)
let url = FileManager.default.url(forUbiquityContainerIdentifier: nil)?
.appendingPathComponent("Documents")
.appendingPathComponent("MyFile.txt")
let doc = MyDocument(fileURL: url!)
doc.content = "Hello".data(using: .utf8)
doc.save(to: url!, for: .forCreating)
NSFileCoordinator 和冲突处理请参见 axiom-icloud-drive-ref。
反模式
1. 忽略同步状态
// ❌ WRONG: No awareness of pending changes
var items: [Item] = [] // Are these synced? Pending? Conflicted?
// ✅ CORRECT: Track sync state
struct SyncableItem {
let item: Item
let syncState: SyncState // .synced, .pending, .conflict
}
2. 同步时阻塞 UI
// ❌ WRONG: UI blocks until sync completes
func viewDidLoad() async {
items = try await cloudKit.fetchAll() // Spinner forever on airplane
tableView.reloadData()
}
// ✅ CORRECT: Show local data immediately
func viewDidLoad() {
items = localStore.fetchAll() // Instant
tableView.reloadData()
Task {
await syncEngine.fetchChanges() // Background update
}
}
3. CloudKit 架构未部署到生产环境
CloudKit 的开发环境和生产环境使用不同的架构。App Store 中的应用只能访问生产环境。如果你在开发环境添加了记录类型、字段或索引,却从未部署它们,那么生产环境中的查询会返回空结果,且不会报错。
❌ Works in Xcode/TestFlight (Development) → empty results in App Store (Production)
Queries silently return zero results — no CKError, no crash, no clue.
✅ Before every App Store submission:
1. CloudKit Console → Select container
2. "Deploy Schema Changes" → Review changes → Deploy
3. Test with Production environment in Xcode scheme settings
跳过此步骤的时间成本:3–7 天(审核拒绝周期 + 调试“为什么在 TestFlight 中正常,但在生产环境中不行?”)。这是首次提交应用的开发者最常遇到的 CloudKit 陷阱。
4. 没有重试逻辑
// ❌ WRONG: Single attempt
try await cloudKit.save(record)
// ✅ CORRECT: Exponential backoff
func saveWithRetry(_ record: CKRecord, attempts: Int = 3) async throws {
for attempt in 0..<attempts {
do {
try await cloudKit.save(record)
return
} catch let error as CKError where error.isRetryable {
let delay = pow(2.0, Double(attempt))
try await Task.sleep(for: .seconds(delay))
}
}
throw SyncError.maxRetriesExceeded
}
同步状态指示器
始终向用户显示同步状态:
enum SyncState {
case synced // ✓ (checkmark)
case pending // ↻ (arrows)
case conflict // ⚠ (warning)
case offline // ☁ with X
}
// In SwiftUI
HStack {
Text(item.title)
Spacer()
SyncIndicator(state: item.syncState)
}
权限配置检查清单
同步正常工作前,请确认:
-
Xcode → Signing & Capabilities
- ✓ 已添加 iCloud 功能
- ✓ 已勾选 CloudKit(用于 CloudKit)
- ✓ 已勾选 iCloud Documents(用于 iCloud Drive)
- ✓ 已选择/创建容器
-
Apple Developer Portal
- ✓ App ID 已启用 iCloud 功能
- ✓ CloudKit 容器已存在(用于 CloudKit)
-
CloudKit Console(提交 App Store 前)
- ✓ 架构已部署到生产环境(记录类型、字段、索引)
- ✓ 在 Xcode 方案中使用生产环境进行测试,确认查询正常工作
-
设备
- ✓ 已登录 iCloud
- ✓ 已启用 iCloud Drive(设置 → [姓名] → iCloud)
大型数据集同步
同步 10,000 条以上记录时,简单粗暴的做法会导致超时和启动速度变慢。
初始同步策略
// ❌ WRONG: Fetch everything at once
let allRecords = try await database.fetchAll()
syncEngine.state.add(pendingRecordZoneChanges: allRecords.map { .saveRecord($0.recordID) })
// ✅ CORRECT: Batch initial sync
func performInitialSync(batchSize: Int = 200) async throws {
var cursor: CKQueryOperation.Cursor? = nil
repeat {
let (results, nextCursor) = try await database.records(
matching: query,
resultsLimit: batchSize,
desiredKeys: nil,
continuationCursor: cursor
)
// Process batch
try await localStore.saveBatch(results.compactMap { try? $0.1.get() })
cursor = nextCursor
} while cursor != nil
}
增量同步(初始同步后)
CKSyncEngine 会自动处理增量同步——它只会获取上次同步令牌之后的更改。请确保持久化 stateSerialization,这样引擎就不会在下次启动时重新获取所有内容。
性能指南
| 数据集大小 | 策略 | 备注 |
|---|---|---|
| 少于 1,000 条记录 | 默认 CKSyncEngine | 开箱即用 |
| 1,000–10,000 条 | 分批进行初始同步 | 每批 200 条记录,并显示进度界面 |
| 10,000 条以上 | 分页 + 后台处理 | 使用 BGProcessingTask 进行初始同步 |
| 100,000 条以上 | 服务端筛选 | 只同步用户需要的内容,其余内容延迟加载 |
关键要点:初始同步是瓶颈。完成初始同步后,CKSyncEngine 的增量处理方式会高效处理大型数据集,因为它只会获取差异。
高压场景
场景 1:“第一版先跳过冲突处理”
情况:迫于截止期限,决定在没有冲突解决机制的情况下发布。
风险:用户一定会在多台设备上编辑数据,数据也一定会在无提示的情况下丢失。
应对:“实现最低限度的冲突处理只需 2 小时。数据在无提示的情况下丢失会让用户失望,并招致 1 星评价。”
场景 2:“应用启动时同步就够了”
情况:避免连续同步带来的复杂性。
风险:用户希望更改能在几秒内显示,而不是等到下次启动时。
应对:使用 CKSyncEngine 或 SwiftData,它们可以自动处理连续同步。
相关技能
axiom-cloudkit-ref— 完整的 CloudKit API 参考axiom-icloud-drive-ref— 使用 NSFileCoordinator 进行基于文件的同步axiom-cloud-sync-diag— 调试同步故障axiom-storage— 选择本地数据的存储位置