ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
CKSyncEngineでiCloudに同期
CKSyncEngineを使用して、人々のCloudKitデータをiCloudに同期する方法を紹介します。同期操作のスケジューリングをシステムに任せることで、アプリのコード量を削減する方法を学びます。CloudKitの進化に合わせて強化されたパフォーマンスの恩恵を自動的に受ける方法、同期実装のテストなどをご紹介します。 このセッションを最大限に活用するには、CloudKitとCKRecordタイプに精通している必要があります。
関連する章
- 0:00 - Intro
- 0:48 - The state of sync
- 3:09 - Meet CKSyncEngine
- 9:47 - Getting started
- 13:18 - Using CKSyncEngine
- 19:25 - Testing and debugging
- 22:27 - Wrap-up
リソース
関連ビデオ
Tech Talks
-
ダウンロード
♪ ♪
こんにちは CloudKitチームの エンジニアTimです 同僚のAamerと私は CKSyncEngineという 新しいCloudKit APIについて 話していきます CKSyncEngineはデバイスとクラウド間の データ同期を支援するために 設計されています まずAppleプラットフォームでの CloudKitへの同期状況について 話します 次にCKSyncEngineがどのようなもので どのように動作するのかについて 説明しプロジェクトから CKSyncEngineを 使い始める方法について学びます セットアップが完了したら デバイス間でデータを同期化する 同期エンジンの使い方を学びます 最後にCKSyncEngine との統合を テストまたはデバッグするための ベストプラクティスについて 知識を深めていきます まずCloudKitとのシンク状況について
新しいアプリを作るとユーザーは 自分のデータが同期されることを期待します iPhoneで何かを作りMacを開くと そこにもそれがあるものだと 思います 外からだと これはまるで 魔法のようです ユーザデータは一ヶ所場所だけでなく あちこちにあります 簡単なことではないです CloudKit自体はそれほど複雑ではないが 同期全般は難しいのです 複数のデバイスをシナリオに持ち込む場合 うまくいかないことが たくさんあります だから同期コードはシンプルであれば あるほど好ましいのです 同期コードを簡素化する最善の方法は できるだけ記述を少なくすることです ありがたいことにCloudKit と 同期化の素晴らしい API の選択肢が いくつかあり これらのAPIが 多くの重要な作業を代行してくれます ローカル永続化を含むフルスタック・ ソリューションが必要な場合は NSPersistentCloudKitコンテナを 使えます 独自のローカル永続化を行いたい場合は 新しい CKSyncEngine API を 使用できます なおよりきめ細かい制御が必要だと 感じたらCKDatabaseと CKOperationsを使いましょう しかしCloudKitと同期したい場合や NSPersistentCloudKitContainerを 使用していない場合は CKSyncEngineを使用する 必要があります 同期には多くの可動部分があり CKSyncEngineのような APIを使用することで 複雑さを軽減し 高レベルのアプリの同期エクスペリエンスを 向上させます
シンクの核心はほとんどの場合 あるデバイスから変更を送信し 別のデバイスでそれをフェッチし 必要に応じてCloudKitのレコードとの間で 変換します それだけなら簡単かもしれませんが するべきことはそれだけではありません さまざまな操作やエラーシステム状態の 監視アカウント変更 リッスンプッシュ通知の処理 サブスクリプションの管理と追跡など すべてを学ぶ必要があります CKSyncEngineを使えば 書かなければならない 同期のためのコード量は ずっと少なくなって より集中できるように なるでしょう アプリに特有なことだけを 処理するだけで 残りは同期エンジンが処理してくれます 適切な同期エンジンを書くには おそらく数千行のコードが必要で テストではその倍が必要になり 実はNSPersistentCloudKitContainer CKSyncEngineにもかなり多くの テストがあるという噂を聞きました このソフトが多くのことを 処理してくれるからです CKSyncEngineにもかなり 多くのテストがある この新しいCKSyncEngine APIとは 何でしょうか?
CKSyncEngine は CloudKitデータベースと 同期するための共通ロジックを カプセル化します 便利なAPIを提供しつつ 必要に応じて 柔軟性を持たせることを 目指しています カスタム同期エンジンを書くような ほとんどのアプリのニーズを 満たすように設計されています 一般的にアプリのプライベートデータや 共有データを同期させるなら CKSyncEngineが最適です 同期エンジンで使用するデータモデルは レコードとゾーンで構成され これらはCloudKitの他の部分で 使用されるのと同じデータタイプです 既存のCloudKit APIを使用して これらのデータにアクセスできます このため既存のCloudKit同期実装が ある場合は CKSyncEngineはそれと 同期することができます この同期エンジンはFreeformアプリを含む システム全体のいくつかの アプリやサービスで使用されています もう一つの例は同期エンジンの上で 書き直された NSUbiquitousKeyValueStoreです これは 後方互換性のある使用例です 新しいOSでは同期エンジンを使用しますが 以前のバージョンとも同期します すでにカスタムCloudKit同期実装を 持っている場合は CKSyncEngineに切り替える オプションがあります このメリットが気に入れば 乗り換えを検討すべきですが 必須ではありません 時にはメンテナンスするコードが 少なくてすむのは便利です CKSyncEngineに新しい機能が 追加されるたびにメリットを受けられます プラットフォームが進化すれば 同期エンジンも進化し より簡単で効率的な 同期が可能になります CKSyncEngineのAPIサーフェスが 小さいというメリットもあります これで特定のデータモデルと ユースケースに集中できるます CKSyncEngineの使用を 検討しているがサポートされていない 特別なニーズがある場合は いつでも自分で構築することが できます ただしCKSyncEngine の 新機能によってニーズが満たされると 思われる場合 ユースケースを添えて フィードバックを提出してください 結局のところ 同期エンジンの最高の アイデアのいくつかは、あなたのような デベロッパから生まれたものなのです では同期エンジンは実際に どのように機能するのか? 一般的に同期エンジンは アプリとCloudKit サーバー間の データのパイプとして機能します アプリは同期エンジンと通信します 保存すべき変更があると アプリはそれを同期エンジンに伝えます 他のデバイスでこれらの変更を取得すると それらをあなたのアプリに提供します それでも同期エンジンが 仕事を抱えている場合 必ずしもすぐにそれを 実行するとは限りません サーバーと通信する必要がある場合は システムのタスクスケジューラと 相談します これはバックグラウンドタスク管理に OS全体で使われているのと同じ スケジューラーでデバイスがデバイスが 同期できる状態にあることを確認します デバイスの準備が整うとスケジューラーが タスクを実行し同期エンジンが サーバーと通信します これが同期エンジンの 基本的な動作の流れです 具体的にそのように 同期エンジンが変更を サーバーに送るのでしょうか まず誰かがデータに変更を加え 何かをタイプしたか スイッチを入れましたのか または何かを削除したか アプリは保留中の変更を 同期エンジンに伝え サーバーに送信することを 同期エンジンに指示します 次に、同期エンジンはタスクを スケジューラに送信します デバイスの準備ができたら スケジューラはタスクを実行 タスクが実行されると 同期エンジンは変更を サーバーに送信するプロセスを開始します そのためにアプリに次の 変更の送信を要求するします もし誰かが1つだけ変更を加えた場合 保留中の変更は1つだけかもしれません しかし誰かが新しいデータの 巨大なデータベースを インポートした場合 何百何千もの変更があるかもしれません 1回のリクエストでサーバーに 送信できる量には限りがあるからです 同期エンジンは これらの変更を 一括して要求します これで実際に必要になるまで レコードをメモリに取り込まないことにより メモリのオーバーヘッドを 減らすのにも役立ちます 次のバッチを提供した後 同期エンジンは それをサーバーに送信します サーバーは変更の成否に関する情報を含め 操作の結果を応答します リクエストが終了すると 同期エンジンは 結果をアプリにコールバックします これは作戦の成否に反応するチャンスです 保留中の変更がある場合 同期エンジンは送信するものがなくなるまで バッチを要求し続けます あるデバイスがデータを サーバーに送信すると 他のデバイスがそのデータを取得します サーバーが新しい変更を受信すると そのデータにアクセスできる他のデバイスに プッシュ通信します CKSyncEngineはアプリでこれらの プッシュ通知を自動的にリッスンします 通知を受け取ると タスクを スケジューラに投入します スケジューラタスクが実行されると 同期エンジンがサーバーからフェッチします 新しい変更を取得すると アプリに送ります これらの変更をローカルに保持し UIに表示するチャンスです これが同期エンジンを使う際の 基本的な操作の流れとなります これらのフローに共通しているのは システムのスケジューラーです 通常CKSyncEngineは何かをする前に スケジューラを参照します このように 自動的に同期されるのです
スケジューラは ネットワーク 接続性 バッテリー残量 リソースの使用状況などを監視します 同期を試みる前に デバイスが前提条件を 満たしていることを確認します スケジューラを尊重することで 同期エンジンは ユーザー体験とデバイス・リソースの適切な バランスを確保することができます 通常のコンディションであれば 同期はかなり速く通常は 数秒かそこらで完了します しかしネットワーク接続がない場合 またはデバイスのバッテリー残量が 少ない場合 同期が遅れたり 延期されたりすることがあります デバイスに大きな負荷がかかっている場合 同期メカニズムがアプリ内の 他の緊急タスクの邪魔になることは 避けたいです 同期エンジンの自動スケジューリングに 頼ることで シンクすることができます より効率的なだけでなく 使いやすくなりました 同期するタイミングを気にする 必要がなければ 他のことに集中できます 引き直して更新するUIが あるかもしれません または今すぐバックアップボタンが あれば保留中の変更を即座にサーバーに 送信できるかもしれません 手動で同期することは自動テストを 書くときにも役に立ちます イベントの順序を制御する必要がある場合 複数のデバイスに特定の同期シナリオを シミュレートするのに役立ちます 一般的には自動同期スケジューリングに 頼ることをおすすめします しかし手動で同期する有効なユースケースが あることは理解していますし 同期エンジンは必要に応じて それを行うためのAPIを備えています これからAamerがCKSyncEngineの 使い始め方について話してくれます Tim 紹介ありがとう CloudKitクライアント・チームのエンジニアの Aamerです これから CKSyncEngineについて説明します CKSyncEngineを使用する前に プロジェクトをセットアップするために 必要なことがいくつかあります これらの要件はCKSyncEngineを 使用する場合でも 独自のCloudKit実装を 構築する場合でも同じです まずCloudKitの基本的なデータ型である CKRecordとCKRecordZoneの 基本的な知識が必要です 同期エンジンのAPIは レコードとゾーンを多用するので その意味をよく理解してから使いましょう 次にXcodeでプロジェクトのCloudKit 機能を 有効にする必要があります 最後に同期エンジンはプッシュ通知に 依存していて最新の状態を保つために リモート通知機能を 有効にする必要があります これができたら 同期エンジンを初期化します まずCKSyncEngineを初期化する必要があります 同期エンジンを初期化すると 自動的にプッシュ通知と スケジューラタスクをバックグラウンドで 受信しはじめます これらの通知やタスクは いつでも発生する可能性があり それらを処理するために同期エンジンを 初期化する必要があります
アプリとCKSyncEngineの間の 主な通信手段は CKSyncEngineDelegate と 呼ばれるプロトコルです 同期エンジンを初期化する際には このプロトコルに準拠した オブジェクトを提供する必要があります 適切かつ効率的に機能するために 同期エンジンはいくつかの 内部状態を追跡します また同期エンジンの状態や 既知のバージョンを提供する 必要があります
同期操作を行っている間 デリゲートには時々 状態更新イベントという形で この状態の更新版が渡されます 同期エンジンが新しい状態を 連続化するたびにそれを ローカルに永続化する必要があります こうすることで次にプロセスを起動し 同期エンジンを初期化するときに この情報が提供されるようになります これを理解するために いくつかのコード例を使って 説明しましょう
同期エンジンを初期化するには 設定オブジェクトを指定し 同期したいデータベースと 同期エンジンの状態の 最終バージョンを 指定する必要があります デリゲート・プロトコルの関数のひとつに handle event関数があります この機能は通常の同期操作中に発生する 様々なイベントを同期エンジンが アプリに通知する方法です 例えばサーバーから 新しいデータを取得したり アカウントが変更されたときに イベントを投稿します これらのイベントのひとつに 状態更新イベントというものがあります 同期エンジンが内部状態を更新したとき またはあなた自身が状態を更新したとき 同期エンジンは状態更新イベントを ポストします このイベントに応答して この新しい連続化された バージョンの状態をローカルに 永続化しなければなりません この例では次に同期エンジンを 初期化するときに この状態のシリアライズを使用します これで土台は整ったので 同期エンジンを使った 同期について説明します
サーバーに変更を送信するには いくつかの簡単な手順があります まず保留中のレコードゾーンの変更と 保留中のデータベースの変更を 同期エンジンの状態に追加します これは同期エンジンにシンクを スケジュールするよう警告します 同期エンジンは一貫性を確保し これらの変更を重複排除します
次にデリゲートメソッドを実装 同期エンジンは次のレコードゾーン変更の バッチを取得するために デリゲートメソッドを呼び出し サーバーに送信します 最後にイベント sentDatabaseChanges と sentRecordZoneChanges を 処理します これらのイベントは変更が サーバーに送られると投稿されます
サーバーに変更を送信する例です このアプリケーションは データを編集し 新しいレコードの変更を 同期しようとしてます そのためには同期エンジンにそのレコードを 保存する必要があることを伝え 保留中のレコードゾーンの変更を 同期エンジンの状態に追加します 同期エンジンがレコードを シンクする準備のできたところで デリゲートメソッドを 呼び出します サーバーに送信する 次の変更バッチを返します レコードゾーン変更バッチを 初期化するには 保留中の変更のリストと レコードプロバイダを提供することで 初期化します 保留中の変更のリストには 保存または削除する レコードIDと実際の同期が行われるときに それらのIDをレコードにマッピングする レコードプロバイダが含まれています アプリがサーバーから変更を取得する 方法を説明していきます 同期エンジンが自動的にサーバーから 変更を取得します そうしてフェッチされたDatabaseChangesと fetchedRecordZoneChangesの イベントが投稿されます ユースケースによってはFetchChangesと didFetchChangesのイベントを リッスンしたいかもしれません 例えばこれらのイベントを ハンドリングすることは 変更をフェッチする前後にセットアップや クリーンアップのタスクを実行したい場合に 便利です 以下はアプリがサーバーから変更を フェッチする例です 同期エンジンがレコードゾーン内の変更を フェッチするとイベントを投稿します このイベントには他のデバイスによって 実行された 変更と削除が含まれます これをリッスンする際には フェッチされた変更とフェッチされた削除を チェックする必要があります 修正を受けたらデータをローカルに 永続化する必要があります 削除を受けたらローカルで データを削除してください データベースの変更をフェッチするのも よく似ていて 同じアプローチで処理できます エラーの処理は厄介です 同期エンジンがこれに役立ちます 同期エンジンはネットワークの問題 スロットリング アカウントの問題などの一時的なエラーを 自動的に処理します 同期エンジンはこれらのエラーの影響を 受けた作業を自動的に再試行します その他のエラーについては アプリが処理する必要があります これらのエラーを解決したら 必要に応じ 仕事のスケジュールを 変更する必要があります
これはレコードゾーンの変更を送信する際の エラーハンドリングの例です sentRecordZoneChangesイベントが 投稿されたら failedRecordSavesを チェックして
保存に失敗したレコードがあるかどうかを 確認する必要があります これはアプリがまだ取得していない 新しいバージョンを別のデバイスが 保存したということです コンフリクトを解決し作業を 再スケジュールしなければなりません
zoneNotFoundはまだサーバに 存在しないことを示します これを解決するにはゾーンを作成し 作業を再スケジュールする 必要があるかもしれません 同期エンジンは常に最初に ゾーンの保存を試み 次にnetworkFailure networkUnavailable serviceUnavailable そしてrequestRateLimitedは 一過性のエラーの例です このようなエラーが表示されることは ありますが それに対してアクションを起こす 必要はありません 同期エンジンはシステム状況が許す限り これらのエラーに対して 自動的に再試行します
もうひとつ同期エンジンが役立つのは アカウントの変更です iCloudアカウントの変更はデバイス上で いつでも発生する可能性があります 同期エンジンは これらをサポートします 同期エンジンは変更をリッスンし サインイン サインアウトまたは アカウントが切り替わったことを accountChange イベントで 通知します アプリはタイプに応じて変更に 対応する必要があります
同期エンジンはデバイスにアカウントが 存在するまで iCloudとの同期を開始しません
同期エンジンはいつでも初期化でき アカウントに変更があった場合は 自動的に更新されます 他のユーザーとデータを共有するのは CloudKitの重要な部分です 同期エンジンは ここでも生活を楽にしてくれます 同期エンジンは CloudKit 共有データベースで動作します アプリで使用する データベースごとに 同期エンジンを作成するだけです たとえばプライベートデータベース用に 同期エンジンを作成し 共有データベース用に別の同期エンジンを 作成することができます CloudKitによる共有の詳細については Tech Talk「Get the most out of CloudKit Sharing」をご覧ください。
これはCKSyncEngineを使用することを カバーしています ではそれを使う際のテスト方法について 説明していきましょう 自動テストは迅速な開発を行いながら コードベースの安定性を確保する 最良の方法です 同期エンジンを使用すると複数の CKSyncEngineインスタンスを使用して デバイス間のユーザーフローを シミュレートできます
アプリが遭遇する可能性のある エッジケースをシミュレートする 必要があります これを行うには同期エンジンの フローに介入して automaticallySyncを 無効に設定します 以下は2つのデバイスとサーバー間の データ競合を シミュレートしたテストケースです このテストの目的は 複数のデバイスを操作する際に ユーザーが取るであろう 完全なフローを シミュレートすることにあります 競合解決も有効です まずMySyncManagerを使って 2つのデバイスをシミュレートします この例ではMySyncManagerが ローカルデータベースと 同期エンジンを作成します デバイスAは値をAに設定し その変更をサーバーに送信
デバイスBがサーバーから変更を フェッチする前にその変更をサーバーにも 送るように要求します デバイスAが先にサーバーに保存したため デバイスBの保存は 失敗すると考えられます この結果サーバーのレコードが変更された エラーが発生し ローカルの競合解決コードが実行されます このサンプルでは競合解消が サーバーからのデータを 優先することを期待しているため デバイスBの新しい値は デバイスAからサーバーに送信された 直近の値になります
テストとデバッグの スピードアップに役立つポイントを いくつか紹介していきましょう 各デバイスでの一連の イベントを理解することは アプリケーションのフローのどこで 問題が発生しているかを 特定するのに役立ちます 開発時に可能な限りログを取ることは これらのフローをトレースし 複数のデバイス間でログを 比較するのに役立ちます CloudKitは受信した各イベントを ログに記録しますが アプリ内でイベントに関連するアクションも レコードIDとゾーンIDを ログに記録することで
同期エンジン サーバーそしてシンクしている 他のデバイスの間で どのようなデータが流れているかを デバッグできるようになります
それぞれのユーザーフローを シミュレートするテストを 書くことでコードベースが 大きくなっても安定性を 保つことができます
パズルをつなぎ合わせ謎を解くように 同期オペレーションは数回しか 行われないかもしれないし 短時間に何度も行われるかもしれない 正しいトレースができているかどうかを 確認することは複数のデバイス間で デバッグを行う際の鍵となります
これらの手順はCKSyncEngineを使用して 信頼性が高く長持ちする アプリを作成し 維持するのに 役立ちます
これでCKSyncEngineについての話を 終わります 同期エンジンのサンプルコードで アプリでの完全な動作例を 参考にしてください さらに詳しく知りたい場合は CKSyncEngineのドキュメントを確認して 同期エンジンを改善するための 提案があるならば CloudKitチームに フィードバックを送ってください 私たちは どんな作品ができるのか 楽しみにしています ご視聴ありがとうございました 素晴らしいWWDCを! ♪ ♪
-
-
12:14 - Initializing CKSyncEngine
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 } } }
-
14:13 - Sending changes to the server
func userDidEditData(recordID: CKRecord.ID) { // Tell the sync engine we need to send this data to the server. 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) } }
-
15:40 - Fetching changes from the server
func handleEvent(_ event: CKSyncEngine.Event, syncEngine: CKSyncEngine) async { switch event { case .fetchedRecordZoneChanges(let recordZoneChanges): for modifications in recordZoneChanges.modifications { // Persist the fetched modification locally } for deletions in recordZoneChanges.deletions { // Remove the deleted data locally } case .fetchedDatabaseChanges(let databaseChanges): for modifications in databaseChanges.modifications { // Persist the fetched modification locally } for deletions in databaseChanges.deletions { // Remove the deleted data locally } // Perform any setup/cleanup necessary 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 { // Merge server record into local data syncEngine.state.add(pendingRecordZoneChanges: [ .save(recordID) ]) } case .zoneNotFound: // Tried to save a record, but the zone doesn't exist yet. syncEngine.state.add(pendingDatabaseChanges: [ .save(recordID.zoneID) ]) syncEngine.state.add(pendingRecordZoneChanges: [ .save(recordID) ]) // CKSyncEngine will automatically handle these errors case .networkFailure, .networkUnavailable, .serviceUnavailable, .requestRateLimited: break // An unknown error occurred default: break } } case .accountChange(let event): switch event.changeType { // Prepare for new user case .signIn: break // Delete local data case .signOut: break // Delete local data and prepare for new user case .switchAccounts: break } } }
-
18:49 - Using CKSyncEngine with private and shared databases
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) }
-
20:00 - Testing CKSyncEngine integration
func testSyncConflict() async throws { // Create two local databases to simulate two devices. let deviceA = MySyncManager() let deviceB = MySyncManager() // Save a value from the first device to the server. deviceA.value = "A" try await deviceA.syncEngine.sendChanges() // Try to save the value from the second device before it fetches changes. // The record save should fail with a conflict that includes the current server record. // In this example, we expect the value from the server to win. deviceB.value = "B" XCTAssertThrows(try await deviceB.syncEngine.sendChanges()) XCTAssertEqual(deviceB.value, "A") }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。