axiom-cloud-sync

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Cloud Sync

云同步

Overview

概述

Core principle: Choose the right sync technology for the data shape, then implement offline-first patterns that handle network failures gracefully.
Two fundamentally different sync approaches:
  • CloudKit — Structured data (records with fields and relationships)
  • iCloud Drive — File-based data (documents, images, any file format)
核心原则:根据数据类型选择合适的同步技术,然后实现可优雅处理网络故障的离线优先模式。
两种本质不同的同步方式:
  • CloudKit —— 结构化数据(带字段和关联关系的记录)
  • iCloud Drive —— 基于文件的数据(文档、图片、任何文件格式)

Quick Decision Tree

快速决策树

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)
需要同步的内容是什么?

├─ 结构化数据(记录、关联关系)?
│  ├─ 使用SwiftData?→ SwiftData + CloudKit(最简单,iOS 17+)
│  ├─ 需要共享/公开数据库?→ CKSyncEngine 或原生CloudKit
│  └─ 自定义持久化(GRDB、SQLite)?→ CKSyncEngine(iOS 17+)
├─ 用户期望在“文件”应用中看到的文档/文件?
│  └─ iCloud Drive(使用UIDocument或FileManager)
├─ 大二进制对象(图片、视频)?
│  ├─ 与结构化数据关联?→ CloudKit中的CKAsset
│  └─ 独立文件?→ iCloud Drive
└─ 应用设置/偏好?
   └─ NSUbiquitousKeyValueStore(简单键值对,1MB限制)

CloudKit vs iCloud Drive

CloudKit vs iCloud Drive

AspectCloudKitiCloud Drive
Data shapeStructured recordsFiles/documents
Query supportFull query languageFilename only
RelationshipsNative supportNone (manual)
Conflict resolutionRecord-levelFile-level
User visibilityHidden from userVisible in Files app
SharingRecord/database sharingFile sharing
OfflineLocal cache requiredAutomatic download
维度CloudKitiCloud Drive
数据类型结构化记录文件/文档
查询支持完整查询语言仅支持文件名查询
关联关系原生支持无(需手动实现)
冲突解决记录级别文件级别
用户可见性对用户隐藏在“文件”应用中可见
共享功能记录/数据库共享文件共享
离线处理需要本地缓存自动下载

Red Flags

注意警示

If ANY of these appear, STOP and reconsider:
  • ❌ "Store JSON files in CloudKit" — Wrong tool. Use iCloud Drive for files
  • ❌ "Build relationships manually in iCloud Drive" — Wrong tool. Use CloudKit
  • ❌ "Assume sync is instant" — Network fails. Design offline-first
  • ❌ "Skip conflict handling" — Conflicts WILL happen on multiple devices
  • ❌ "Use CloudKit for user documents" — Users can't see them. Use iCloud Drive
  • ❌ "Sync on app launch only" — Users expect continuous sync
如果出现以下任何一种情况,请立即停止并重新考虑:
  • ❌ "在CloudKit中存储JSON文件" —— 工具选择错误。应使用iCloud Drive存储文件
  • ❌ "在iCloud Drive中手动构建关联关系" —— 工具选择错误。应使用CloudKit
  • ❌ "假设同步是即时的" —— 网络会出现故障。需按离线优先设计
  • ❌ "跳过冲突处理" —— 多设备场景下必然会出现冲突
  • ❌ "使用CloudKit存储用户文档" —— 用户无法查看这些文档。应使用iCloud Drive
  • ❌ "仅在应用启动时同步" —— 用户期望持续同步

Offline-First Pattern

离线优先模式

MANDATORY: All sync code must work offline first.
swift
// ✅ 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)
}
强制要求:所有同步代码必须优先支持离线工作。
swift
// ✅ 正确:离线优先架构
class OfflineFirstSync {
    private let localStore: LocalDatabase  // GRDB、SwiftData、Core Data
    private let syncEngine: CKSyncEngine

    // 先写入本地,在后台同步到云端
    func save(_ item: Item) async throws {
        // 1. 本地保存(即时完成)
        try await localStore.save(item)

        // 2. 加入同步队列(非阻塞)
        syncEngine.state.add(pendingRecordZoneChanges: [
            .saveRecord(item.recordID)
        ])
    }

    // 从本地读取(即时完成)
    func fetch() async throws -> [Item] {
        return try await localStore.fetchAll()
    }
}

// ❌ 错误:云端优先(会被网络阻塞)
func save(_ item: Item) async throws {
    // 离线时会失败,网络差时速度慢
    try await cloudKit.save(item)
    try await localStore.save(item)
}

Conflict Resolution Strategies

冲突解决策略

Conflicts occur when two devices edit the same data before syncing.
当两个设备在同步前编辑了同一数据时,就会出现冲突。

Strategy 1: Last-Writer-Wins (Simplest)

策略1:最后写入者获胜(最简单)

swift
// Server always has latest, client accepts it
func resolveConflict(local: CKRecord, server: CKRecord) -> CKRecord {
    return server  // Accept server version
}
Use when: Data is non-critical, user won't notice overwrites
swift
// 服务器始终保留最新版本,客户端接受该版本
func resolveConflict(local: CKRecord, server: CKRecord) -> CKRecord {
    return server  // 接受服务器版本
}
适用场景:数据非关键,用户不会注意到覆盖情况

Strategy 2: Merge (Most Common)

策略2:合并(最常用)

swift
// 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
}
Use when: Both versions contain valuable changes
swift
// 合并两个版本的更改
func resolveConflict(local: CKRecord, server: CKRecord) -> CKRecord {
    let merged = server.copy() as! CKRecord

    // 对每个字段应用自定义合并逻辑
    merged["notes"] = mergeText(
        local["notes"] as? String,
        server["notes"] as? String
    )
    merged["tags"] = mergeSets(
        local["tags"] as? [String] ?? [],
        server["tags"] as? [String] ?? []
    )

    return merged
}
适用场景:两个版本都包含有价值的更改

Strategy 3: User Choice

策略3:用户选择

swift
// 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
}
Use when: Data is critical, user must decide
swift
// 将冲突呈现给用户
func resolveConflict(local: CKRecord, server: CKRecord) async -> CKRecord {
    let choice = await presentConflictUI(local: local, server: server)
    return choice == .keepLocal ? local : server
}
适用场景:数据至关重要,必须由用户决定

Common Patterns

常见模式

Pattern 1: SwiftData + CloudKit (Recommended for New Apps)

模式1:SwiftData + CloudKit(推荐用于新应用)

swift
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)
Limitations:
  • Private database only (no public/shared)
  • Automatic sync (less control over timing)
  • No custom conflict resolution
swift
import SwiftData

// 零配置自动CloudKit同步
@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()
    }
}

// 如果存在CloudKit权限,容器会自动同步
let container = try ModelContainer(for: Note.self)
局限性
  • 仅支持私有数据库(无公开/共享数据库)
  • 自动同步(对同步时机的控制较少)
  • 不支持自定义冲突解决

Pattern 2: CKSyncEngine (Custom Persistence)

模式2:CKSyncEngine(自定义持久化)

swift
// 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)
            }
        }
    }
}
See
axiom-cloudkit-ref
for complete CKSyncEngine setup.
swift
// 适用于GRDB、SQLite或自定义数据库
class MySyncManager: CKSyncEngineDelegate {
    private let engine: CKSyncEngine
    private let database: GRDBDatabase

    func handleEvent(_ event: CKSyncEngine.Event) async {
        switch event {
        case .stateUpdate(let update):
            // 持久化同步状态
            await saveSyncState(update.stateSerialization)

        case .fetchedDatabaseChanges(let changes):
            // 将更改应用到本地数据库
            for zone in changes.modifications {
                await handleZoneChanges(zone)
            }

        case .sentRecordZoneChanges(let sent):
            // 将标记记录为已同步
            for saved in sent.savedRecords {
                await markSynced(saved.recordID)
            }
        }
    }
}
完整CKSyncEngine设置请参考
axiom-cloudkit-ref

Pattern 3: iCloud Drive Documents

模式3:iCloud Drive文档

swift
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)
See
axiom-icloud-drive-ref
for NSFileCoordinator and conflict handling.
swift
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
    }
}

// 保存到iCloud Drive(在“文件”应用中可见)
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

Anti-Patterns

反模式

1. Ignoring Sync State

1. 忽略同步状态

swift
// ❌ 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
}
swift
// ❌ 错误:未感知待处理更改
var items: [Item] = []  // 这些是已同步的?待处理的?冲突的?

// ✅ 正确:跟踪同步状态
struct SyncableItem {
    let item: Item
    let syncState: SyncState  // .synced, .pending, .conflict
}

2. Blocking UI on Sync

2. 同步时阻塞UI

swift
// ❌ 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
    }
}
swift
// ❌ 错误:UI会阻塞直到同步完成
func viewDidLoad() async {
    items = try await cloudKit.fetchAll()  // 飞行模式下会一直显示加载动画
    tableView.reloadData()
}

// ✅ 正确:立即显示本地数据
func viewDidLoad() {
    items = localStore.fetchAll()  // 即时完成
    tableView.reloadData()

    Task {
        await syncEngine.fetchChanges()  // 后台更新
    }
}

3. No Retry Logic

3. 无重试逻辑

swift
// ❌ 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
}
swift
// ❌ 错误:仅尝试一次
try await cloudKit.save(record)

// ✅ 正确:指数退避重试
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
}

Sync State Indicators

同步状态指示器

Always show users the sync state:
swift
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)
}
始终向用户显示同步状态:
swift
enum SyncState {
    case synced       // ✓(对勾)
    case pending      // ↻(箭头)
    case conflict     // ⚠(警告)
    case offline      // ☁带叉号
}

// 在SwiftUI中
HStack {
    Text(item.title)
    Spacer()
    SyncIndicator(state: item.syncState)
}

Entitlement Checklist

权限检查清单

Before sync will work:
  1. Xcode → Signing & Capabilities
    • ✓ iCloud capability added
    • ✓ CloudKit checked (for CloudKit)
    • ✓ iCloud Documents checked (for iCloud Drive)
    • ✓ Container selected/created
  2. Apple Developer Portal
    • ✓ App ID has iCloud capability
    • ✓ CloudKit container exists (for CloudKit)
  3. Device
    • ✓ Signed into iCloud
    • ✓ iCloud Drive enabled (Settings → [Name] → iCloud)
同步功能正常工作前需完成:
  1. Xcode → 签名与功能
    • ✓ 添加iCloud功能
    • ✓ 勾选CloudKit(若使用CloudKit)
    • ✓ 勾选iCloud Documents(若使用iCloud Drive)
    • ✓ 选择/创建容器
  2. Apple开发者后台
    • ✓ App ID已启用iCloud功能
    • ✓ CloudKit容器已创建(若使用CloudKit)
  3. 设备端
    • ✓ 已登录iCloud
    • ✓ 已启用iCloud Drive(设置 → [用户名] → iCloud)

Pressure Scenarios

压力场景

Scenario 1: "Just skip conflict handling for v1"

场景1:“v1版本先跳过冲突处理”

Situation: Deadline pressure to ship without conflict resolution.
Risk: Users WILL edit on multiple devices. Data WILL be lost silently.
Response: "Minimum viable conflict handling takes 2 hours. Silent data loss costs users and generates 1-star reviews."
情况:受交付期限压力,想先发布不带冲突处理的版本。
风险:用户必然会在多设备上编辑数据。数据会无声丢失。
应对:“最小可行的冲突处理仅需2小时。无声数据丢失会流失用户并产生一星差评。”

Scenario 2: "Sync on app launch is enough"

场景2:“仅在应用启动时同步就够了”

Situation: Avoiding continuous sync complexity.
Risk: Users expect changes to appear within seconds, not on next launch.
Response: Use CKSyncEngine or SwiftData which handle continuous sync automatically.
情况:想避免持续同步的复杂性。
风险:用户期望更改能在几秒内同步,而不是等到下次启动应用。
应对:使用CKSyncEngine或SwiftData,它们会自动处理持续同步。

Related Skills

相关技能

  • axiom-cloudkit-ref
    — Complete CloudKit API reference
  • axiom-icloud-drive-ref
    — File-based sync with NSFileCoordinator
  • axiom-cloud-sync-diag
    — Debugging sync failures
  • axiom-storage
    — Choosing where to store data locally
  • axiom-cloudkit-ref
    —— 完整CloudKit API参考
  • axiom-icloud-drive-ref
    —— 基于NSFileCoordinator的文件同步
  • axiom-cloud-sync-diag
    —— 同步故障调试
  • axiom-storage
    —— 本地数据存储选择指南