WWDC Quick Look 💓 By SwiftGGTeam
Sync to iCloud with CKSyncEngine

Sync to iCloud with CKSyncEngine

元の動画を見る

ハイライト

CKSyncEngine は、Apple が CloudKit 用に開発した高レベルの同期エンジンです。開発者はアプリケーション固有のデータ変換ロジックを処理するだけでよく、システムはプッシュ監視、タスクのスケジューリング、ネットワークの再試行、バッチ送信、競合解決を自動的に処理し、当初は数千行のコードが必要だったカスタム同期の実装を数百行に削減します。

主要内容

ユーザーは、データがすべてのデバイス間で同期されることを期待しています。 iPhone でメモを取っておけば、Mac を開いたときにそのメモが表示されるはずです。この経験は自然なことのように思えますが、実装するのは複雑です。

CloudKit 自体は複雑ではありませんが、同期には多くの詳細が含まれます。ネットワーク ステータスの監視、バッテリー管理、プッシュ通知処理、サブスクリプション管理、ステータス追跡、エラー回復、アカウント切り替えなどです。完全な同期エンジンには数千行のコードが必要になる場合があり、テスト コードは 2 倍の長さになります。 AppleはNSPersistentCloudKitContainerのテストコードが7万行を超えていることを社内で明らかにした。

WWDC23 で発表された CKSyncEngine は、これらの共通ロジックをカプセル化し、アプリケーション固有の部分のみを考慮できるようにします。

Apple プラットフォームには 3 つのレベルの CloudKit 同期ソリューションがあります。

  • NSPersistentCloudKitContainer: ローカル永続性を含む、コア データを直接使用するアプリケーションに適したフルスタック ソリューション
  • CKSyncEngine: ローカル永続同期エンジンが付属しており、カスタマイズされたストレージ ソリューションを必要とするアプリケーションに適しています
  • CKDatabase + CKOperations: 完全な手動制御。CKSyncEngine がニーズを満たせない場合にのみ使用されます。

すでに NSPersistentCloudKitContainer を使用している場合は、そのまま使用し続けてください。独自のローカル ストレージ ソリューション (SQLite、ファイル システムなど) がある場合は、CKSyncEngine が最良の選択です。

CKSyncEngine は、Freeform や NSUbiquitousKeyValueStore など、いくつかのシステム アプリケーションで採用されています。標準の CKRecord および CKRecordZone を使用し、既存の CloudKit と相互運用できます。プラットフォームが進化するにつれて、CKSyncEngine は自動的にパフォーマンスの最適化と新機能を獲得します。

同期エンジンのワークフロー

(05:11) CKSyncEngine の中核はデータ パイプラインです。アプリケーションはレコードとゾーンを使用して、サーバーとの対話を担当するエンジンと通信します。

変更を送信するプロセス:

  1. ユーザーがデータを変更する (テキストの入力、スイッチの切り替え、オブジェクトの削除)
  2. アプリケーションは同期エンジンに変更内容を送信するように指示します。
  3. 同期エンジンがタスクをシステム スケジューラに送信します。
  4. デバイスの準備が完了すると、スケジューラがタスクを実行します。
  5. 同期エンジンはアプリケーションから変更されたデータをバッチで要求します
  6. サーバーにデータを送信する
  7. サーバーは結果を返し、同期エンジンがアプリケーションにコールバックします。

(06:45) バッチ処理が重要な設計です。ユーザーが大量のデータをインポートする場合、数百または数千の変更が発生する可能性があります。同期エンジンはリクエストをバッチ処理して、すべてのレコードを一度にメモリにロードすることを回避します。アプリケーションは、エンジンによって要求された場合にのみ、次のデータ バッチを提供します。

変更を受け取るプロセス:

  1. サーバーは、新しい変更を受信した後、プッシュ通知を送信します。
  2. CKSyncEngine はプッシュ通知を自動的に監視します
  3. 通知を受け取った後、タスクをスケジューラに送信します
  4. スケジューラの実行後、同期エンジンはサーバーから変更を取得します。
  5. 変更はアプリケーションに渡され、ローカルに保存されます。

(08:00) システム スケジューラがすべての基礎です。ネットワーク接続、バッテリー電力、リソースの使用状況、その他の状況を監視して、適切なタイミングでの同期を確保します。同期は通常の状況では数秒以内に完了しますが、ネットワークが切断されている場合やバッテリー残量が少ない場合には遅延する可能性があります。このロジックを自分で処理する必要はありません。

もちろん、プルダウンの更新ボタンや即時バックアップ ボタンなど、即時同期が必要なシナリオもあります。 CKSyncEngine は手動同期用の API を提供しますが、Apple は最初に自動スケジュールに依存することを推奨します。

初期化と構成

(12:14) CKSyncEngine を使用するには、CKRecord と CKRecordZone を理解すること、Xcode で CloudKit とリモート プッシュ機能を有効にすることなど、いくつかの前提条件が必要です。

初期化コード:

actor MySyncManager: CKSyncEngineDelegate {

    init(container: CKContainer, localPersistence: MyLocalPersistence) {
        let configuration = CKSyncEngine.Configuration(
            database: container.privateCloudDatabase,
            stateSerialization: localPersistence.lastKnownSyncEngineState,
            delegate: self
        )
        self.syncEngine = CKSyncEngine(configuration)
    }

    func handleEvent(_ event: CKSyncEngine.Event, syncEngine: CKSyncEngine) async {
        switch event {
        case .stateUpdate(let stateUpdate):
            self.localPersistence.lastKnownSyncEngineState = stateUpdate.stateSerialization
        }
    }
}

キーポイント:

  • データベース、最後に保存された状態のシリアル化、初期化中のデリゲートを提供します
  • 状態のシリアル化はローカルに保持され、次回の起動時に復元される必要があります。
  • 代表者handleEventメインの通信インターフェイスです
  • プッシュを監視してタスクをスケジュールできるように、アプリケーションの起動後、できるだけ早く同期エンジンを初期化することをお勧めします。

変更をサーバーに送信

(14:13) 変更の送信は、送信する変更のマーク付け、バッチ プロビジョニング メソッドの実装、送信結果の処理の 3 つのステップに分かれています。

func userDidEditData(recordID: CKRecord.ID) {
    // 告诉 sync engine 需要发送这条数据
    self.syncEngine.state.add(pendingRecordZoneChanges: [ .save(recordID) ])
}

func nextRecordZoneChangeBatch(
    _ context: CKSyncEngine.SendChangesContext,
    syncEngine: CKSyncEngine
) async -> CKSyncEngine.RecordZoneChangeBatch? {

    let changes = syncEngine.state.pendingRecordZoneChanges.filter {
        context.options.zoneIDs.contains($0.recordID.zoneID)
    }

    return await CKSyncEngine.RecordZoneChangeBatch(pendingChanges: changes) { recordID in
        self.recordToSave(for: recordID)
    }
}

キーポイント:

  • pendingRecordZoneChangesどのレコードを同期する必要があるかをエンジンに指示します
  • 同期エンジンは重複を自動的に削除し、一貫性を維持します -nextRecordZoneChangeBatchエンジンが送信の準備ができたときに呼び出されます -RecordZoneChangeBatch保留中の変更とレコードプロバイダーを使用して初期化されると、プロバイダーは実際に必要な場合にのみレコードをロードします。

サーバーの変更を取得する

(15:40) 変更の取得は自動的に行われます。同期エンジンが送信しますfetchedRecordZoneChangesそしてfetchedDatabaseChangesイベント:

func handleEvent(_ event: CKSyncEngine.Event, syncEngine: CKSyncEngine) async {
    switch event {

    case .fetchedRecordZoneChanges(let recordZoneChanges):
        for modifications in recordZoneChanges.modifications {
            // 将修改持久化到本地
        }
        for deletions in recordZoneChanges.deletions {
            // 从本地删除数据
        }

    case .fetchedDatabaseChanges(let databaseChanges):
        for modifications in databaseChanges.modifications {
            // 处理 database 级别的修改
        }
        for deletions in databaseChanges.deletions {
            // 处理 database 级别的删除
        }

    case .willFetchChanges, .didFetchChanges:
        // 在获取前后执行设置/清理工作
        break

    case .sentRecordZoneChanges(let sentChanges):
        for failedSave in sentChanges.failedRecordSaves {
            let recordID = failedSave.record.recordID

            switch failedSave.error.code {

            case .serverRecordChanged:
                if let serverRecord = failedSave.error.serverRecord {
                    // 合并服务器记录到本地数据
                    syncEngine.state.add(pendingRecordZoneChanges: [ .save(recordID) ])
                }

            case .zoneNotFound:
                // Zone 不存在,先创建 zone 再重试
                syncEngine.state.add(pendingDatabaseChanges: [ .save(recordID.zoneID) ])
                syncEngine.state.add(pendingRecordZoneChanges: [ .save(recordID) ])

            // 以下错误 sync engine 会自动重试
            case .networkFailure, .networkUnavailable, .serviceUnavailable, .requestRateLimited:
                break

            default:
                break
            }
        }

    case .accountChange(let event):
        switch event.changeType {
        case .signIn:
            // 准备新用户数据
            break
        case .signOut:
            // 清除本地数据
            break
        case .switchAccounts:
            // 清除数据并准备新用户
            break
        }
    }
}

キーポイント:

  • fetchedRecordZoneChanges2 つの配列 (変更と削除) が含まれます。 -serverRecordChangedこれは最も一般的な競合であり、独自のマージ戦略を適用する必要があります -zoneNotFoundまずゾーンを作成してから、レコードを再度保存してみる必要があります。
  • ネットワークエラー (networkFailure、serviceUnavailable など) は同期エンジンによって自動的に処理されます。 -accountChangeイベント処理アカウントのログイン、ログアウト、3 つの状況の切り替え

プライベートデータベースと共有データベースをサポート

(18:49) データベースごとに独立した同期エンジンを作成できます。

let databases = [ container.privateCloudDatabase, container.sharedCloudDatabase ]

let syncEngines = databases.map {
    var configuration = CKSyncEngine.Configuration(
        database: $0,
        stateSerialization: lastKnownSyncEngineState($0.databaseScope),
        delegate: self
    )
    return CKSyncEngine(configuration)
}

キーポイント:

  • 各データベースには個別の同期エンジン インスタンスが必要です
  • 各インスタンスには独立した状態シリアル化ストレージが必要です
  • CloudKit 共有シナリオ用の共有データベース

### テスト

(20:00) 複数の同期エンジン インスタンスを使用して、マルチデバイス シナリオをシミュレートできます。

func testSyncConflict() async throws {
    // 创建两个本地数据库模拟两台设备
    let deviceA = MySyncManager()
    let deviceB = MySyncManager()

    // 设备 A 先保存到服务器
    deviceA.value = "A"
    try await deviceA.syncEngine.sendChanges()

    // 设备 B 在获取变更前也尝试保存
    deviceB.value = "B"
    XCTAssertThrows(try await deviceB.syncEngine.sendChanges())
    XCTAssertEqual(deviceB.value, "A")
}

キーポイント:

  • 複数の同期マネージャーを作成して複数のデバイスをシミュレートします
  • 設定automaticallySync = false同期タイミングを正確に制御可能
  • 競合解決ロジックをテストするときは、ローカル データが期待どおりに更新されていることを確認してください
  • レコード ID とゾーン ID のログ記録は、マルチデバイス同期の問題のデバッグに役立ちます

詳細

エラー処理戦略

CKSyncEngine は一時的なエラーを自動的に処理します。

  • networkFailure/networkUnavailable: ネットワークの問題、自動的に再試行します
  • serviceUnavailable: サービスは利用できません。自動的に再試行します。
  • requestRateLimited: リクエスト頻度制限、自動再試行
  • アカウントの問題: アカウントの問題、自動的に処理されます

アプリケーションで処理する必要があるエラー:

  • serverRecordChanged: サーバー レコードが変更されたため、意思決定マージ戦略を適用する必要があります。
  • zoneNotFound: ゾーンが存在しないため、最初に作成する必要があります
  • 不明なエラー: アプリケーションが処理方法を決定する必要がある不明なエラー

ステータス管理

同期エンジンの内部状態が渡されますstateSerialization保存。このステータスには次のものが含まれます。

  • 既知のサーバー変更トークン
  • 送信される変更のキュー
  • 購読情報
  • その他の内部追跡データ

ステータスが更新されるたびに、同期エンジンはstateUpdateイベント。この状態を維持する必要があります。そうしないと、同期エンジンは次回起動したときに再び完全に同期されます。

デバッグのヒント

  1. イベント シーケンスの記録:handleEvent各イベントのタイプとタイムスタンプを記録します。
  2. レコード レコード ID とゾーン ID: データ フローの追跡に役立ちます
  3. マルチデバイスのログを比較: タイムスタンプを使用して、異なるデバイスでの操作を調整します
  4. プッシュ通知コンソールを使用: プッシュが正常に届くかどうかを確認します

重要ポイント

1.既存のローカル ストレージ アプリケーションに iCloud 同期を追加

  • 対処方法: もともとローカルのメモ、ToDo、習慣追跡などのみを保存していたアプリケーションにクロスデバイス同期を追加します。
  • 実行する価値がある理由: CKSyncEngine は、同期の複雑さを最小限に抑えます。データ変換のみを行う必要があり、システム アプリケーションと同じレベルの同期エクスペリエンスを得ることができます。
  • 開始方法: CKRecord 変換ロジックを定義し、CKSyncEngine を初期化し、データ変更時に保留中の変更をマークします。

2.コラボレーションをサポートするドキュメントエディタを作成

  • 機能: Freeform に似た、複数人で共同作業できるホワイトボードまたはドキュメント アプリケーション
  • 実行する価値がある理由: CKSyncEngine は共有データベースをサポートしており、CloudKit 共有と組み合わせることで、複数のユーザー間のリアルタイムのコラボレーションを実現できます。
  • 開始方法: プライベート データベースを使用して個人データを保存し、共有データベースを使用して共同作業データを保存し、データベースごとに独立した同期エンジンを作成します。

3.オフラインファーストのフォトアルバム/コレクションアプリケーションを作成

  • やるべきこと: 画像コレクション、リンクコレクション、音楽コレクションおよびその他のアプリケーション、オフライン閲覧と自動同期をサポート
  • 価値がある理由: 同期エンジンの自動スケジュールにより、ネットワーク復旧時のシームレスな同期が保証され、バッチ処理は大量のメディア メタデータに適しています。
  • 開始方法: SwiftData または Core Data を使用してローカルに保存し、CKSyncEngine でレコードを CloudKit に同期し、メディア ファイルに CKAsset を使用します。

4.カスタム同期スキームを書き換えます

  • やるべきこと: 自分で管理している元の CloudKit 同期コードを CKSyncEngine に移行します。
  • 実行する価値がある理由: メンテナンス コストを削減し、システム レベルの最適化を自動的に取得し、テスト カバレッジを向上します。
  • 開始方法: 段階的な置き換え: まず、CKSyncEngine を既存の実装と共存させ、完全に切り替える前にデータの整合性を検証します。

関連セッション

コメント

GitHub Issues · utterances