Repository navigation
ユーザー辞書をiCloud (CloudKit) で複数のMacと同期する - #547
Merged
Merged
Conversation
旧ブランチから同期クラス以外を持ち込む。実際の変換日時がわからず仮に割り当てた日時の呼び名をsyntheticからplaceholderに改める。
UserDictSyncer (窓口) が start() でセッションを作り、stop() で捨てる。 セッションは取得→反映→送信を1周として1本のループで直列に回す。 開始時・定期取得・変換のあとの送信の予約・今すぐ同期は1周の依頼になり、 実行中の依頼は「もう1周」にまとまる。CKSyncEngine は automaticallySync = false で包み、 他のゾーンのイベントを捨ててセッションに渡す。イベントのコールバックでは集めるだけにする。 取得が途中で失敗した周でも、受け取ったレコードは反映し、届いた同期状態も保存する (CKSyncEngine がメモリ上の change token を進めるため)。送信と lastSyncedAt・ 最初からの取得の完了の印の保存、エラーの消去だけを行わずに周を終える (runIteration で確認)。 送信の失敗の扱いは後で足す。アカウント変更・ゾーン削除はログだけ出す。 結合テストのテストプランが選ぶテストの指定を、クラス名だけの形に直した。
最初からの取得が成功して反映し終えた周の送信の前に、iCloudにない既存の変換候補に仮の日時を割り当てて台帳に足す。 初回同期の段階と完了の判定をUserDictInitialSyncに切り出す
アカウントのサインアウト・切り替え、他の端末でのゾーンの削除、送信時のzoneNotFoundは、 CKSyncEngineのイベントの中では印をつけるだけにして、ループがawaitの後に見て自分で止まる。 ループの中からはstop()を呼べない (自分のTaskの終わりを待ってしまう) ので、stop()と共通の後始末を 先に呼ばれたほうで1回だけ行い、窓口が同期状態を破棄して停止の理由を知らせる。 最初からの取得中に届いたゾーンの削除の履歴は無視し、encryptedDataResetならゾーンを作り直して同期を続ける。 インスタンスのdeleteAllFromiCloud()は自分のゾーンを消す (結合テストで本物のゾーンを消さないため)。
…チ提供の停止、静的削除での停止待ちなど)
CloudKitの日時 (TIMESTAMP) はミリ秒で表現されるというCloudKit Web Servicesの古いドキュメントを根拠に、 iCloudを経由しても同じ値に戻るよう日時をミリ秒に丸めていた。 結合テストの仕組みで、ミリ秒より細かい端数をもつDateを暗号化したフィールドに保存し、 CKDatabaseで取得し直して確かめたところ、丸められずにDoubleの精度のまま同じ値が返ってきた。 (1700000000.123456717、-0.001500010、現在時刻など6つの値で、取得した値と保存した値が一致し、 ミリ秒に丸めた値とは一致しなかった) 暗号化したフィールドは値を暗号化したバイト列として保存するため、サーバーのTIMESTAMPの表現の影響を受けないと思われる。 根拠にしていたコメントが誤りとわかり、丸めなくても同じ値に戻るので、丸めをやめてコードを簡単にする。 丸めずに保存した同期情報がiCloudから同じ値で返ってくることを、結合テストに残す。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #528
このPRは #530 を作り直したものです。
同じApple Accountでサインインした複数のMacの間で、ユーザー辞書をiCloudで同期できるようにします。変換・単語登録・削除した結果が、他のMacにも反映されます。
同期には CloudKit の CKSyncEngine を使用します。
マージまでにやること
リリース作業
UserDictCandidateだけ)com.apple.developer.icloud-container-environment = Productionが入っているかcodesign -d --entitlements -で確かめる。必要なら Makefile にチェックを足す動作環境
CKSyncEngineを使うため)使い方
設定画面の「iCloud」タブに、ユーザー辞書の同期の項目を追加。
同期の状態(取得中・反映中・送信中・最終同期日時・エラー)は設定画面に表示されます。取得中は受け取った件数、反映中は反映した件数、送信中は残りの件数を表示します。「今すぐ同期」ボタンで、すぐに取得・送信することもできます。
他のMacの変更を取り込むと、通知センターに「追加 N件、更新 N件、削除 N件」のように表示されます。初回同期で取り込んだときは表示しません。
iCloudからユーザー辞書を削除
「iCloudからユーザー辞書を削除」ボタンで、iCloud 上のユーザー辞書を丸ごと削除できます。iCloud に送ってはいけない単語を送ってしまったときなどに使います。
同期を無効にしただけでは、iCloud 上のデータもユーザー辞書も削除されません。
設計
データの持ち方
UserDictに、1つの変換候補を1つのレコード(レコード型UserDictCandidate)として保存しますlastUsedAt)、最後に削除した日時(deletedAt)ですencryptedValuesで保存します。iCloud の「高度なデータ保護」が有効なら、エンドツーエンドで暗号化されますskk-jisyo.utf8だけで、変換候補ごとの日時は iCloud のレコードにだけ持ちます。UserDefaults には CKSyncEngine の状態と最終同期日時などだけを保存しますマージと並び順
lastUsedAtの新しい順に先頭へ並べます。見出しの順も同様ですdeletedAtがlastUsedAtより新しければ削除、古ければ残ります。Mac A で削除した後に、まだ同期していない Mac B で同じ変換候補を変換した場合は残りますDateが同じ値で返ってくることを結合テストで確かめています同期の進め方
同期は「取得 → ユーザー辞書への反映 → 送信」を1周として、常に1本だけ直列に回します。
automaticallySync = falseで使い、取得と送信は自分で呼びます。CKSyncEngine のコールバックでは結果を集めるだけにし、処理は周の中で await の後に行います送信と取得のタイミング
初回同期の「すべて同期」
今あるユーザー辞書には変換した日時の情報がないため、ファイル内の順序から 1970年1月1日より前の仮の日時 を割り当てて送信します。同期を始めた後の実際の変換は必ずそれより新しいので、常に優先されます。
まず iCloud にあるレコードをすべて取得してこのMacに反映し、そのあとで iCloud にない変換候補だけを送信します。
既存の変換候補を全部送り終えたら初回同期の完了とします。通信エラーなどで送り残しがあれば次の周で送ります。完了する前に終了・停止した場合は、次に始めたときに取得からやり直します。
大量の変更を受け取るとき
初回同期などで数千件以上の変更を受け取ると、反映に時間がかかります。入力メソッドなのでメインスレッドを止めないように、100件ごとに
Task.yield()で他の処理に譲りながら反映します。反映中に変換・確定しても、その操作は失われません。受け取った後にこのMacで編集した変換候補は、受け取った古い内容で上書きしません。送信の失敗
serverRecordChanged): サーバーの版とマージし、このMacのほうが新しければ送り直しますunknownItem: 変更タグを捨てて送り直します同期を自動で止めるとき
次の場合は同期を自動で停止し、通知センターと設定画面に理由を表示します。もう一度有効にすると、初回同期からやり直します。
「高度なデータ保護」の鍵のリセット(
encryptedDataReset)でゾーンが消えた場合は、同期を止めずにゾーンを作り直します。entitlementの扱い
#515 と同じく、iCloud の entitlement はリリースビルド用の
macSKK/macSKK-iCloud.entitlementsだけに追加しました。Developer Program に参加していない環境でも、今まで通りビルドできます。com.apple.developer.icloud-services:CloudKitcom.apple.developer.icloud-container-identifiers:iCloud.net.mtgto.inputmethod.macSKKCloudKit の環境(Development / Production)を決める
com.apple.developer.icloud-container-environmentは entitlement ファイルには書かず、script/export-options.plistのiCloudContainerEnvironmentでProductionを指定しています。makeでエクスポートするときに Xcode がこの entitlement を付けて署名し直すので、リリースの pkg は Production 環境につながります。entitlement ファイルに書くと、Local.xcconfigで iCloud 版の entitlement を使う開発用のビルドまで Production に固定されてしまうためです。entitlement のないビルドでは
UserDictSyncer.isAvailableがfalseになり、設定画面のトグルが無効になるだけで他の動作は変わりません。entitlement なしでCKContainerを作るとクラッシュするため、事前に確認しています。開発時の注意
make dev-pkgは CloudKit の Development 環境、pkg(Release・Developer ID 署名)は Production 環境につながります。両者のデータは同期されないので、2台で確認するときは同じ種類のビルドを使ってくださいテスト
UserDictSyncerIntegrationTests): 1プロセスの中でUserDictSyncerを2つ作って2台のMacを再現し、本物の CloudKit (Development 環境) で送信・受信・削除・競合・初回同期・停止と再開などを確かめますUserDictTest-<UNIX秒>-<UUID>を使い、終わったら消します。本番のゾーンUserDictには触りませんCloudKitIntegration.xctestplan(環境変数MACSKK_CLOUDKIT_TESTS=1)のときだけ動きます。通常のmacSKK.xctestplanではスキップしますxcodebuild test -project macSKK.xcodeproj -scheme macSKK -testPlan CloudKitIntegration -destination 'platform=macOS'既知の制限
skk-jisyo.utf8を直接編集すると、このMacではすぐ反映されますが、iCloud には送信されません。同期の日時をローカルに持たないので、どこが編集されたか判断できないためです。追加した変換候補は、そのあと変換すれば送信されますserverRecordChangedで一度拒否されます。そのときはサーバーの版とマージして送り直すので、結果は正しく、通信が1往復増えるだけです。変更タグを保存すれば解消できますが、取得した件数に比例してメモリを使うため、初期リリースでは対応していませんskk-jisyo.utf8を読み込めないと同期を始められません。例えば、外部エディタで UTF-8 以外の文字コード (EUC-JP など) で保存した場合や、ファイルを読む権限がない場合です。このとき設定画面の同期の状態は「待機中」のまま (最終同期日時が更新されない、または「まだ同期していません」) で、同期できていないことは表示されません。ユーザー辞書を読み込めなかったことは、通知センターと設定画面の「辞書」タブに表示されます。ファイルを直して読み込めるようになれば、そのまま同期が始まります。他のMacの変更が何日も届かず、最終同期日時も進まない場合は、「辞書」タブでユーザー辞書の状態を確認してください確認したこと
make dev-pkgのビルド(Development 環境)により次を確認