CloudKitをもっと使ってみた【追記あり】

#【追記(2020年3月23日)】
iOS/iPadOS 13 より、CoreData と CloudKit が連携するようになりました。これまで CoreData で構築したアプリも、一部コードに手を加えるだけで、あっという間にデバイス間でデータを共有できるようになりました。
この記事のように CloudKit 特有のコーディングをする必要がなくなりました。これまでの努力はいったいなんだったのでしょう。ありがたいことです。
結果的に、この記事を読むのは無駄です...

#はじめに
CloudKitを利用して複数のiOSデバイス間でデータを共有できるアプリを個人開発しています(趣味の延長レベルです)。
開発中、CloudKitの入門記事等を大いに参考にしていますが、Objective-Cベースのものだったり、最新のswiftに対応しているものや日本語のドキュメントが足りていないと感じた部分もありました。
このようにドキュメントが不足していると感じた部分について、自分なりに調べたことを備忘録代わりにまとめてみました。

なお、現状の使用環境は、Xcode 10.2.1、swift 5.0 です。

#参照
CloudKitでは他のレコードに対する参照をフィールドとして持つことができます。これを利用して1対1、1対多、多対多のリレーションを表現することができます。

参照元に、CKReferenceというレコードタイプを用意して、ここに参照先のID(CKRecordID)を格納します。
概念的に通常のデータベースと同様で特に変わったことはありません。


下図にあるようなリレーションを実装してみます。

![alt](https://developer.apple.com/library/archive/documentation/DataManagement/Conceptual/CloudKitQuickStart/Art/to_many_relationship_2x.png)
※Apple による CloudKit Quick Start より

##1対1リレーション
まずは、1対1対応にあるレコードを作成してみます。

```swift
// 親レコード
let artistRecord = CKRecord(recordType: "Artist")
itemRecord["name"] = "Vermeer"

// 親レコードへの参照を作る
let referenceToArtist = CKRecord.Reference(record: artistRecord, action: CKRecord_Reference_Action.deleteSelf)

// 子レコードを作成し、参照フィールドに親レコードへの参照を入れる
let artworkRecord1 = CKRecord(recordType: "Artwork")
artworkRecord1["title"] = "Girl with a Pearl Earring"
artworkRecord1["artist"] = referenceToArtist as CKRecordValue
```

参照を作る時の、action: には、親レコードを削除した時に連動して子レコードをどう処理するべきかを指定します。
.deleteSelf の場合は子レコードも削除し、.noneだと何もしません。

##1対多リレーション

上図のように、ひとつの親レコードが複数の子レコードを持っている場合、親を指定してその子全てを取得するには以下のようにします。

```swift
// 親レコードへの参照を得る
let reference = CKRecord.Reference(recordID: artist.id!, action: .deleteSelf)

// 親レコードへの参照を持つことを検索条件として設定
let predicate = NSPredicate(format: "artist == %@", reference)
let query : CKQuery = CKQuery(recordType: "artwork", predicate: predicate)

// この親レコードに関連づけられた子レコードを検索
collection.perform(query, inZoneWith: nil, completionHandler: {
    (records: [CKRecord]?, error: Error?) -> Void in
    
    if let error = error {
        print("ERROR!: \(error.localizedDescription)")
        return
    }

    // records配列に全ての子レコードが格納されている。
    for record in records! {
        print(" artwork: \(record["title"]!)")
     }
})
```

多対多のリレーションもこの応用で実装できますね。

#一括操作
特に参照関係のある複数のレコードに対し、取得、保存あるいは削除を行いたい場合、それら操作を一括で行うべきです。CloudKitには効率よく、一括でそれらを行う方法が用意されています。

あらかじめ操作対象となる複数のレコード、あるいはレコードIDの配列を作成しておき、また、個々の操作完了毎に実施したい処理と、全操作完了後に実施したい処理をクロージャとして設定しておいた上で、一括処理を開始するという流れとなります。

##取得(fetch)
まずは、一括取得の例です。対象のレコードIDの配列を用意しておきます。

```swift
// 取得対象となるレコードIDを配列にまとめておく
// recordIDs: [CKRecord.ID] 取得対象のレコードIDの配列
// ckDatabase: CKDatabase データベース
let fetchRecordsOperaton = CKFetchRecordsOperation(recordIDs: recordIDs)
    
// レコードごとにfetchが完了したときに実行する処理の設定
fetchRecordsOperaton.perRecordCompletionBlock = {
    (record: CKRecord?, recordID: CKRecord.ID?, error: Error?) -> Void in
    if let error = error {
        print("*** ERROR!!: \(error.localizedDescription)")
        // エラー処理
        return
    }
    
    if let record = record {
        print("**** SUCCESS!!")
        // 必要な処理
    }
}

// 全操作完了時に実行する処理の設定
fetchRecordsOperaton.fetchRecordsCompletionBlock = {
    (recordsAndIDs: [CKRecord.ID : CKRecord]?, error: Error?) -> Void in
    if let error = error {
        print("*** ERROR!!: \(error.localizedDescription)")
        // エラー処理
        return
    }
    
    if let recordsAndIDs = recordsAndIDs {
        print("*** SUCCESS!!: \(recordsAndIDs.count) records.")
        // 必要な処理
    }
}

fetchRecordsOperaton.database = ckDatabase // データベースの指定
fetchRecordsOperaton.start() // 実行
```

さらに、var perRecordProgressBlock: ((CKRecord.ID, Double) -> Void) を使って、操作の進行状況を取得することもできます。

##保存(save)と削除(delete)
複数のレコードの一括保存と削除は同時に行うことができます。
保存すべきレコードの配列と、削除したいレコードのIDの配列を用意します。

```swift
// 操作対象となるレコードを配列にまとめておく
// recordsToSave: [CKRecord] ←変更対象のレコードの配列
// recordIDsToDelete: [CKRecord.recordID] ←削除対象のレコードのIDの配列
// ckDatabase: CKDatabase データベース

let modifyRecordOperation = CKModifyRecordsOperation(recordsToSave: recordsToSave, recordIDsToDelete: nil)
    
// レコードごとに処理が完了したときに実行する処理の設定
modifyRecordOperation.perRecordCompletionBlock = {
    (record: CKRecord?, error: Error?) -> Void in
    if let error = error {
        print("*** ERROR!!: \(error.localizedDescription)")
        // エラー処理
        return
    }
    print("*** SUCCESS!!:")
    // 必要な処理
}
    
// 全操作完了時に実行する処理の設定
modifyRecordOperation.modifyRecordsCompletionBlock = {
    (records: [CKRecord]?, recordIDs: [CKRecord.ID]?, error: Error?) -> Void in
    if let error = error {
        print("*** ERROR!!: \(error.localizedDescription)")
        // エラー処理
        return
    }
    print("*** SUCCESS!!")
    // 必要な処理
}

modifyRecordOperation.database = database // データベースの指定
modifyRecordOperation.start() // 実行
```

#最後に

mBaaSといえば、まずはFirebase等が選択肢にあがると思いますが、iOS端末のみ対応すればよいなどの限定的な条件下では、認証を考えなくてよいなどのメリットがあり、CloudKitが気軽に使えるのではないかと思います。
逆にいうと、条件が限られるのであまり利用されておらず、ドキュメントも少ないのかなぁと思います。

#参考:
- [CloudKit Quick Start](https://developer.apple.com/library/archive/documentation/DataManagement/Conceptual/CloudKitQuickStart/Introduction/Introduction.html)
- [CloudKitを使ってみた Swift版](https://qiita.com/tfutada/items/83b30311a5fbc3fad212)
- [続 CloudKitを使ってみた Swift版 - サブスクリプション編](https://qiita.com/tfutada/items/e7b1ebf71b276516594b)
- [CloudKitを使ってみた Swift4版](https://qiita.com/NaoSekig/items/25b6640fcdd7450581fa)
- [mBaaSとしてのApple CloudKit の利用シーンと利用制限についての考察](https://qiita.com/n-funaki/items/da38b09e5f79a3e3cfce)