Open7

Core Data with CloudKit で端末間同期を実装する

Shun KashiwaShun Kashiwa

Svadilfariにデータの端末間同期を実装したい。

https://github.com/shumbo/Svadilfari/issues/36

こうなることを見越してSvadilfariでは最初からCore Dataを使っているので、Core Data with CloudKitを使って同じApple IDでデータを同期できるようにすることを考える。

Shun KashiwaShun Kashiwa

Core Data with CloudKit の概要については以下リンクが詳しい。

https://developer.apple.com/documentation/coredata/mirroring_a_core_data_store_with_cloudkit

Core Data with CloudKit を導入するのは、Appleのドキュメントに沿って作業をするだけでできる。

https://developer.apple.com/documentation/coredata/mirroring_a_core_data_store_with_cloudkit/setting_up_core_data_with_cloudkit

具体的には、必要なCapabilities (iCloud, Push Notifications, Background) を追加して、 NSPersistentContainerNSPersistentCloudKitContainer に書き換える。

ドキュメントには明示されていないが、 cloudKitContainerOptions も指定しないと同期がされなかった。

let description = NSPersistentStoreDescription(url: storeURL)
description.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(
  containerIdentifier: "iCloud.app.svadilfari.svadilfari"
)

Svadilfariでは、アプリ本体とApp Extension (Safariの拡張) でCore Data Storeを共有するためにCore Data StoreをApp Groupで共有されるURLに設置している。

App Extensionとの同期もCloud Kit経由でできる気もするが、2つのsqliteをネットワーク経由で同期するのは非効率な気がするので、一旦Persistent History Trackingを使ったApp <-> App Extension間の同期はそのままにしている。

加えて、viewContextautomaticallyMergesChangesFromParent を true にしておく。

https://developer.apple.com/documentation/coredata/nsmanagedobjectcontext/1845237-automaticallymergeschangesfrompa

ここまで行ってアプリを2端末で起動すると、リアルタイムでこそないものの設定が同期されるようになった。

Shun KashiwaShun Kashiwa

同期ができるようになってめでたしかと思いきや、初期データの扱いに関して問題があることが発覚する。

Svadilfariでは、初回起動時にデフォルトジェスチャーをCore Dataに挿入する処理を行っている。

Core Data with CloudKit はEventually Consistentなデータストアであり、「同期が終わった」ことを保証するAPIが存在しない。

https://stackoverflow.com/questions/60928628/how-to-wait-until-cloud-kit-data-is-being-synced-with-core-data-in-swift-in-ios1

最近では NSPersistentCloudKitContainer.eventChangedNotification というNotificationによって同期がなされたことを検知できるようであるが、以下のStack Overflowのコメントにあるように端末がオフラインの場合にもイベントが発火するようである。そもそも、「このイベントが発火するまで待つ」といった実装にはしたくないという気持ちもある。

https://stackoverflow.com/questions/59138880/nspersistentcloudkitcontainer-how-to-check-if-data-is-synced-to-cloudkit

別の観点として、このクラウド同期の機能はオプトインにすることを考えている。デフォルトでは無効化されており、ユーザーが設定で有効化した場合のみ、同期がされる。この仕様では、もし「同期が終わるまで待つ」という処理が実装できたとしても意味がなく、同期を有効化した段階で、それ以前に追加された初期データが端末の数だけ重複するようになってしまう。

Shun KashiwaShun Kashiwa

ここまでで「できること」「できないこと」がハッキリしたので、アプリとしての仕様を考え直してみる。

絶対にやりたいこと

  • (ある時点以降の) データを端末間で同期できる
  • データの同期を端末ごとにオンオフできる

できること

  • 重複データの削除
    • ジェスチャーも除外リストも内容が重複している場合には消してしまっても問題ない
Shun KashiwaShun Kashiwa

色々と話が膨れ上がってしまったが、今回は

  • デフォルトオン(既存ユーザー向けにはオフ)でiCloud Syncの設定項目を追加する
  • 同期がされたタイミングで重複データを削除するクリーンアップを行う
    • 重複データは古い方を残す

だけでよさそう。

SyuSyu

コメント失礼します。

同期がされたタイミングで重複データを削除する

というのはどのように行うか教えていだだけますか?
私のアプリでもcloudkitを使っていますが、cloudkitの値とデバイスの値が統合されてしまい、同じ値が2つ出現しています。
ご教授いただけますと幸いです。

Shun KashiwaShun Kashiwa

返信できておらずすみません。
私のアプリは、同じデータが2つあることに意味がない仕様なので、同期されたタイミングで全てのデータを比べて、同じデータが複数ある場合は1つ以外を削除するような処理を入れています。
同じデータが複数件あることに意味がある場合には、CloudKitに挿入する前にエントリごとにUUIDを振っておいて、同じUUIDのものは1つを除いて削除するようにすると良さそうです。