CoreDataとCloudKit: 9つのアドバイスと留意点、デバッギングのガイド

`Core Data` と `CloudKit` のフレームワークを合わせて用いるプロジェクトが色々あります。以前いくつか問題点があったので、皆さんがプロジェクトでセットアップのエラーを防げるように、問題点の多くをリスト化しました。

プロジェクトでそのような問題が起きないように、アプリケーションを発行する前にこのチェックリストによく目を通しておいてください。

既存のCoreDataデーターベースのクラウド機能を設定するためには、コードの`NSPersistentContainer`ではなく、単に`NSPersistentCloudKitContainer`を使用してください。

## 1. ベーシックプロジェクト設定

### iCloudコンテナー

必ず特定の名称フォーマットの iCloud CloudKit コンテナーを作成するようにして下さい：`iCloud.あなたの_App_Bundle_ID`.

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/6f4ce039-7a65-b60f-6637-21d35b286a97.png">

### バックグラウンドモード

必ずXcodeの `Background Modes` セクションで `Remote notifications` を有効にして下さい。

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/4f5c0021-130c-ab83-65e5-a8bbcea98e1a.png">

## 2. さまざまなデータベース環境

### CloudKit データベースがローカルの CoreData データベーススキーマと同一のスキーマであることを確認してください

シミュレーターでiOSアプリケーションを実行、あるいはXcodeを使ってiPhone上のアプリケーションを起動する場合は、お使いのCloudkit データベース環境は開発モードにあります。

CoreDataとCloudKitを使用するプロジェクトを設定すると、レコード作成イベントが発生したときに初めてデータベーススキーマが作成されます。データベーススキーマをローカルに設定するだけでは、クラウドのデータベーススキーマは作成されません。

システムがクラウドサーバー上に適切なデータベーススキーマを作成するためには、すべてのプロパティを持つレコードを作成する必要があります。

たとえば、`name` 、`birthday` 、`favoriteToys` というプロパティを持つレコードタイプ`Cat` があるとします。

`.xcdatamodeld`ファイルでスキーマを定義した後も、 CloudKitデータベースにはスキーマが含まれません。

`name` プロパティを使用してシミュレータ上にレコードを作成すると、 CloudKitデータベースは、 `name` プロパティのみを含む`CD_Cat`という名前の新しいスキーマを自動的に作成します。

CloudKitデータベースのスキーマをローカルスキーマと同じにするには、 `name`、`birthday`、`favoriteToys`を含むテストレコードをローカルに作成する必要があります。

### 開発とプロダクション環境の違い

TestFlightまたはApp Storeを通じてアプリを実行すると、CloudKit データベース環境はプロダクションに設定されます。このプロパティを `.entitlements` ファイルに追加することでアプリを手動でプロダクション環境に設定することもできます:

```
<key>com.apple.developer.icloud-container-environment</key>
<string>Production</string>
```

開発データベースと本番データベース間のデータが同じではありません。

## 3. マージポリシーを設定する

Core Data には様々なマージポリシーがあります。マージポリシーは、リモートデータとローカル (メモリ内の) データの間に競合がある場合に処理します。

手動で処理することができますが、この記事ではマージポリシーを使用します。

データベースが自身に変更を書き込もうとする際には、コンフリクトが発生することがあります。（例えばオブジェクトの重複など。）

デフォルトで、`Core Data` フレームワークは例外を発生させます。

`viewContext` の `mergePolicy` プロパティーを設定することで、コンフリクトの解消を試みることができます。

```swift
self.viewContext.mergePolicy = NSMergePolicy(merge: .mergeByPropertyObjectTrumpMergePolicyType)
```

以下は一部の結合ポリシーの説明です。

| Name | Explanation |
|---|---|
| NSErrorMergePolicyType | デフォルト。`try viewContext.save()` を呼び出すと、例外が報告されます。 |
| NSMergeByPropertyStoreTrumpMergePolicyType | メモリ内の変更は外部変更を置き換えます |
| NSMergeByPropertyObjectTrumpMergePolicyType | 外部変更はメモリ内の変更を置き換えます。 |
| NSOverwriteMergePolicyType | メモリ内のオブジェクト全体が永続ストアにプッシュされます。 |
| NSRollbackMergePolicyType | 対立する変更されたオブジェクトについて、すべての状態が破棄されます |

通常、`NSMergeByPropertyObjectTrumpMergePolicy` を使用すると、`Core Data` はデータベースのデータバージョンを新しい変更で自動的に上書きします。

ここではCoreDataフレームワークのヘッダーファイルから説明します。

```swift
// This singleton policy merges conflicts between the persistent store's version of the object and the current in memory version. The merge occurs by individual property. For properties which have been changed in both the external source and in memory, the in memory changes trump the external ones.
@available(iOS 3.0, *)
public var NSMergeByPropertyObjectTrumpMergePolicy: AnyObject
```

メモリ内の変更が外部の変更を上書きするという意味です。

## 4. クラウドからの変更を自動的にマージします

Core Data がクラウドから変更を自動的に取得し、ローカルデータベースに適用できるようにする必要があります。

```swift
localStorageContext.automaticallyMergesChangesFromParent = true
```

## 5. まったく同じ値のオブジェクトを作成しないでください

データベースに保存する各オブジェクトにランダムなIDを追加し、同じオブジェクトが2つ存在しないようにしてください。

## 6. 複数の`NSManagedObjectContext`の利用は避けましょう

クラスの共有インスタンスを生成し、データベースを管理してください。複数のデータベースコンテキストを操作する場合、そのプログラムに問題が生じる場合があります。

```swift
class PersistenceController {
    static let shared = PersistenceController()
    
    let localStorageContext: NSManagedObjectContext
    
    init() {
        let container = NSPersistentCloudKitContainer(name: "[.xcdatamodeldファイルの名前]")
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                print(error.userInfo)
            }
        })
        localStorageContext = container.viewContext
        localStorageContext.automaticallyMergesChangesFromParent = true
        localStorageContext.mergePolicy = NSMergePolicy(merge: .mergeByPropertyObjectTrumpMergePolicyType)
    }
}
```

## 7. ローカルの変更がiCloudにアップロードされるかどうかを確認します

シミュレータ上のすべてのレコードタイプとすべてのプロパティをテストする必要があります。テストを行った後、`https://icloud.developer.apple.com` に移動し、作成したレコードがそこに存在することを確認します。

メニューからアプリケーションを選択し、`Development` データベースの `Records` オプションを選びます。データベースをプライベートデータベースに設定します（なぜなら、コアデータは設定をプライベートデータベースに保存するからです）。ゾーンを `com.apple.coredata.cloudkit.zone` に設定し、`Type:` を `CD_[あなたのデータ型]` に設定します。

### クエリー操作を許可してください

レコードに対する問い合わせを行う前にクエリー操作の許可を求められる場合があります。

トップメニューを選択し、`Schema` をクリックしてください

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/92462980-47e6-cb79-62ba-41b01d6eeb4b.png">

その後、タイトル `Record Type` をクリックし、`Indexes` オプションを選択してください

<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/884edf0e-bd73-859b-f3e4-226db9f43e0b.png">

データ型名 (`CD_[あなたのデータ型]`) を選択し → `Add Index` をクリックし → `recordName` を選択し → `Save Changes` をクリックして下さい

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/56518855-8144-7d08-19b1-d238cfa7d27b.png">

これで保存されたコアデータの全記録を見て、記録が適切にバックアップされているかが確認できるようになっているはずです。

#### テストアプリを走らせる際のアカウントはアップル開発者アカウントと同じものにして下さい。アクセスできるのは、ご自身の開発者アカウントのiCloudにあるプライベートデータのみです。

#### また、製品に変更を施す場合には、その前に必ずインデックスキー `recordName` を削除して下さい。このインデックスは不要です。

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/a19005a7-6923-3420-9773-4bcf99504864.png">

## 8. 制作環境に変化を加えましょう

ギアのアイコンをクリックし、`Deploy Schema to Production` をクリックすることで、開発環境から制作へと変化を加えることができます。アップストアでアプリを公開する前にそれを行う必要があります。

![スクリーンショット 0003-01-29 16.33.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/855011ab-0516-48a7-8c46-5bf319bc3833.png)

## 9. データベーススキーマに変更を加えるたびに、新しいデータベースバージョンを作成し、その新バージョンを現在のデータベースとして設定してください

Core Data ローカルスキーマに変更を加える際には、システムがデータの移行方法を把握できるよう新たなデータベースバージョンを作成する必要があります。また、プロパティの削除と追加を同時に行うことは（データを移行するコードを手作業で記述しない限り）お勧めしません。

私は通常、プロパティを削除せずに新しいプロパティを追加するだけにしています。

新しいデータベーススキーマバージョンを作成するには、データベースモデルファイルを選択し、最上部の `Editor` メニューをクリックして `Add Model Version` を選択してください。

<img width="350" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/1d1bb03d-2f2b-cd91-4f3d-63a6e34b569f.png">

`Finish` ボタンをクリックすると、データベースの新しいバージョンを作成できるようになります。

![スクリーンショット 0003-01-29 16.38.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/3906f077-db49-2863-3c66-458f1615b92c.png)

次に新しく作成したデータベースモデルのバージョンをクリックします（この例では`InkMemo 3.xcdatamodel`）。修正しましょう。

そして右側のバーで、新しいデータベースバージョンを現在のバージョンとして選択してください。

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/35372713-741a-6d02-7270-62a1e067a1e2.png">

データベースモデル内での変更が反映されるよう、コードファイルの更新を行ってください。

## 他の設定

また、 `.xcdatamodel` ファイルをクリックしてから、 `CONFIGURATIONS` セクション内の `Default` をクリックして、`Used with CloudKit` をトグルしてください

![スクリーンショット 0003-01-29 16.42.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/e5013620-3c71-e57e-9f65-84d7d4455ef7.png)

この設定が機能に大きな影響を与えることはないと思います。この設定をオンにしなくても、データは普通にCloudKitにバックアップされているようです...

## デバッグのヒント

アプリを実行するとコンソールに多くのログメッセージが表示されるはずです。これらのメッセージを必ず読むようにしてください。また、CloudKitダッシュボードで、すべてのデータベーススキーマが作成され、適切にデプロイされていることを確認してください。

ここでは、おおよその情報を得るために追加できるデバッグ引数をいくつか紹介します。

```
-com.apple.CoreData.SQLDebug 3
-com.apple.CoreData.Logging.stderr 3
-com.apple.CoreData.ConcurrencyDebug 3
-com.apple.CoreData.MigrationDebug 3
-com.apple.CoreData.CloudKitDebug 3
```

これらの引数は、`Run`モードの`Arguments Passed On Launch`セクションで追加することができます。数値が大きいほど、Xcodeコンソールでより詳細なデバッグメッセージを得ることができます。

![スクリーンショット 0003-01-29 16.47.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/328b2417-d4fa-e479-a67d-064d9102ffe4.png)

また、シミュレーターでCloudKitをテストする際は、実際のデータを作成し、CloudKit Dashboardに行ってiCloudにデータがきちんとアップロードされているか確認してください。

---

この記事が Core Data + CloudKit の問題の修正にお役に立てますように。

---

:relaxed: [Twitter @MszPro](https://twitter.com/MszPro)

:sunny: [私の公開されているQiita記事のリストをカテゴリー別にご覧いただけます。(52のiOS記事)](http://mszpro.com/ioskiji/)
<img width="150" src="https://cdn.mszmagic.com/static-web-content/AppClipImage_small.png">