axiom-cloud-sync
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCloud 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
| Aspect | CloudKit | iCloud Drive |
|---|---|---|
| Data shape | Structured records | Files/documents |
| Query support | Full query language | Filename only |
| Relationships | Native support | None (manual) |
| Conflict resolution | Record-level | File-level |
| User visibility | Hidden from user | Visible in Files app |
| Sharing | Record/database sharing | File sharing |
| Offline | Local cache required | Automatic download |
| 维度 | CloudKit | iCloud 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 for complete CKSyncEngine setup.
axiom-cloudkit-refswift
// 适用于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-refPattern 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 for NSFileCoordinator and conflict handling.
axiom-icloud-drive-refswift
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-refAnti-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:
-
Xcode → Signing & Capabilities
- ✓ iCloud capability added
- ✓ CloudKit checked (for CloudKit)
- ✓ iCloud Documents checked (for iCloud Drive)
- ✓ Container selected/created
-
Apple Developer Portal
- ✓ App ID has iCloud capability
- ✓ CloudKit container exists (for CloudKit)
-
Device
- ✓ Signed into iCloud
- ✓ iCloud Drive enabled (Settings → [Name] → iCloud)
同步功能正常工作前需完成:
-
Xcode → 签名与功能
- ✓ 添加iCloud功能
- ✓ 勾选CloudKit(若使用CloudKit)
- ✓ 勾选iCloud Documents(若使用iCloud Drive)
- ✓ 选择/创建容器
-
Apple开发者后台
- ✓ App ID已启用iCloud功能
- ✓ CloudKit容器已创建(若使用CloudKit)
-
设备端
- ✓ 已登录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
相关技能
- — Complete CloudKit API reference
axiom-cloudkit-ref - — File-based sync with NSFileCoordinator
axiom-icloud-drive-ref - — Debugging sync failures
axiom-cloud-sync-diag - — Choosing where to store data locally
axiom-storage
- —— 完整CloudKit API参考
axiom-cloudkit-ref - —— 基于NSFileCoordinator的文件同步
axiom-icloud-drive-ref - —— 同步故障调试
axiom-cloud-sync-diag - —— 本地数据存储选择指南
axiom-storage