mcpField
A
axiom-cloud-sync

axiom-cloud-sync

github.com/charleswiltgen云与 DevOps1.2k184 次安装今天更新
代码仓库

关于 axiom-cloud-sync

为 Apple 平台开发者提供 iCloud 同步设计指导的技能,帮助根据结构化数据或文件选择 CloudKit 或 iCloud Drive,并规划离线优先和冲突处理。

主要功能

  1. 01区分 CloudKit 与 iCloud Drive
  2. 02离线优先同步模式
  3. 03冲突解决策略
  4. 04SwiftData 与 CKSyncEngine 模式
  5. 05大数据集分批同步建议

适用场景

  1. 01为记录或文档选择同步技术
  2. 02设计断网时仍可使用的同步流程
  3. 03处理多设备编辑冲突
  4. 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

方面CloudKitiCloud 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)
}

权限配置检查清单

同步正常工作前,请确认:

  1. Xcode → Signing & Capabilities

    • ✓ 已添加 iCloud 功能
    • ✓ 已勾选 CloudKit(用于 CloudKit)
    • ✓ 已勾选 iCloud Documents(用于 iCloud Drive)
    • ✓ 已选择/创建容器
  2. Apple Developer Portal

    • ✓ App ID 已启用 iCloud 功能
    • ✓ CloudKit 容器已存在(用于 CloudKit)
  3. CloudKit Console(提交 App Store 前)

    • ✓ 架构已部署到生产环境(记录类型、字段、索引)
    • ✓ 在 Xcode 方案中使用生产环境进行测试,确认查询正常工作
  4. 设备

    • ✓ 已登录 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 — 选择本地数据的存储位置

评论

支持 Markdown