NSPersistentCloudKitContainerを導入し、既存のデータをiCloudにアップロードする際のコツ

# NSPersistentCloudKitContainerとは？
[NSPersistentCloudKitContainer](https://developer.apple.com/documentation/coredata/nspersistentcloudkitcontainer)はCoreDataで使用する[NSPersistentContainer](https://developer.apple.com/documentation/coredata/nspersistentcontainer)をCloudKitに対応させたクラスです。
*CoreDataについては[この記事](https://qiita.com/da1ssk/items/3b1b9c11106717a5a935)がわかりやすいです。

従来、CoreDataで扱っているデータをCloudKitにアップロードするためには色々とコードを書く必要がありました。しかし、AppleがWWDC2019で発表したNSPersistentCloudKitContainerを使えば、それが簡単に実現できるようになりました。

WWDC2019で発表している動画は[こちら](https://developer.apple.com/videos/play/wwdc2019/202/)。日本語字幕もあるのでぜひ見てください。

簡単に言うと、これまでNSPersistentContainerを使っていた部分をNSPersistentCloudKitContainerに書き直すだけで簡単に(Apple製品間での)クラウド同期が実現できると言うのがこの発表の趣旨でした。iOS13以上、macOS10.15以上など多少の制限はありますがこれは非常に便利です。

# 使ってみた
私はこれまで自作アプリでCoreDataを使用していたので、早速これを導入してクラウド同期を実現させて見ることにしました。

今までのコード

```Swift:AppDelegate.swift
lazy var persistentContainer: NSPersistentContainer = {
    let container = NSPersistentContainer(name: "Container Name")
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
        if let error = error as NSError? {
            fatalError("Unresolved error \(error), \(error.userInfo)")
        }
    })
    return container
}()
```

対応後のコード

```Swift:AppDelegate.swift

lazy var persistentContainer: NSPersistentCloudKitContainer = {
    let container = NSPersistentCloudKitContainer(name: "Container Name")
    //ここはなくても動きますが一応……
    let defaultStoreURL = try! FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appendingPathComponent("name.sqlite")
        let description = NSPersistentStoreDescription(url: defaultStoreURL)
        description.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "コンテナ名")
        container.persistentStoreDescriptions = [description]

    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
        if let error = error as NSError? {
            fatalError("Unresolved error \(error), \(error.userInfo)")
        }
    })
    return container
}()
```

コードで変更する部分はこれだけです。

あとはiCloud、Push Notification、BackGround ModesのRemote notificationをSigning & Capabilitiesに追加すれば晴れてクラウド同期対応は完了です

なお、上記の動画ではCloudKitのContainerを選ぶ際に"Use default container"を選んでいますが、現行のXCode(11.4)にはそのチェックボックスはありません。私は自分のBundle Identifierをそのままコンテナ名にして新しくコンテナを作りました。このコンテナ名はNSPersistentCloudKitContainerOptionsのcontainerIdentifierで指定しています。


(2021/01/15 追記)
CloudKit Dashboardでレコードタイプを追加しないとうまく同期されないかもしれません。手順は[CloudKit Dashboard](https://icloud.developer.apple.com/dashboard/)に移動して作成したコンテナを選び、Scheme > RecordTypes > New Typeを押して```CD_エンティティ名```の名前でレコードタイプを作るだけです。フィールドにはCoreData EntityのAttributesをそのまま入れましょう。型はいくつかマッチしない部分があるのでそこは UUID→String、 Bool→Int のように近い型を指定します。

このまま起動すると、同じAppleIDでログインしているデバイス同士でクラウド同期が使えるようになっているはずです。やったぜ。

# 問題（本題）
使っているうちに、問題に気付きます。NSPersistentCloudKitContainer導入後に作成したデータは同期されますが、それ以前に作成したデータは同期されていません。これの解決方法はいくつかあるのですが、今回割と単純な解決方法を発見したので以下に記述します。

1. xcdetamodelのバージョンを一つ上げ、Boolean型のisUploaded属性を追加します。Optionalのチェックは外し、デフォルト値をNOにしておきます。
2. データを読み込む箇所のコードを編集します。データが全て読み込まれたタイミングで、全てのisUploadedをTrueにする処理を追加します。context.saveも忘れずに追加してください。

これだけです。要は既存のデータを新しいデータにしてしまえば、それらも自動的にアップロードされるというわけです。

# 感想
手法がゴリ押しな感じがするので、もっといい解決方法を知っている人は教えてください。