SwiftUI プログラムへの Core Data データベース機能の追加

本記事では、

- Core Data データベースを既存の SwiftUI アプリケーションに追加する方法を紹介します。
- `NSMergePolicy` を使用したコアデータデータベースのマージ競合の管理方法についても説明します。

## Core Dataモデルファイルとクラス定義の作成

モデルファイルとクラス定義用のコードファイルの作成の方法はこちらの通りです。これに関しては過去記事の1つで紹介済みです。

<details><summary>詳細に関してはこちらをクリックしてください</summary><div>

最初に、データモデルファイルを作成します。キーボードのcommand-Nキーを押して、`Data Model` を検索します。

<img width="753" alt="Screen Shot 2020-05-27 at 2.37.09 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/9ee239d9-82a6-fd94-c453-5cee988d35b7.png">

新しい `Data Model` にあなたのアプリの名前をつけてください。例えば、ここでは私のアプリの名前は `CoreDataDemo` なので、私は `CoreDataDemo.xcdatamodeld` を作りました。

#### データベース構造の作成

To-Doアイテムごとに次のプロパティを格納するTo-Doアプリを作成するとします: 

- To-Doタスク名
- タスクの担当者
- タスクの期限

##### 1. `.xcdatamodeld` ファイルを開き、`Add Entity` をクリックします。

<img width="1512" alt="Screen Shot 2020-05-27 at 2.42.46 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/b6d8e322-df60-d8cf-acfc-3fea63c77c43.png">

##### 作成した新規エンティティ・アイテムに `TodoItem` という名前を付けます。

作成した新しいアイテムをダブルクリックし、名前を付けるだけです。この例では、 `TodoItem` という名前を付けます。

##### 新しいプロパティを追加する

`Properties` セクションのプラスアイコンをクリックして、新しいプロパティを追加します。

<img width="1512" alt="Screen Shot 2020-05-27 at 2.44.30 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/61024a4d-a218-783a-3bd6-34f011f45aa7.png">

ここでは、To-Doアプリに次のプロパティが必要です。

| プロパティ名  | タイプ   |
|---|---|
| todoTaskName   | String  |
| personName  | String  |
| taskDueTime   | Date  |

タイプを設定するには、プロパティ名の横にあるドロップダウンメニューをクリックします:

<img width="672" alt="Screen Shot 2020-05-27 at 2.47.09 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/3687488d-867a-bd23-3601-f78bf4daa965.png">

このような感じになります。

<img width="670" alt="Screen Shot 2020-05-27 at 2.48.28 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/f23e43f1-e648-c37a-8142-009fac842439.png">

##### Entity クラスのコードファイルを生成

1. 作成した新しいエンティティ項目を選択します。右側のパネルで最後のセクションのアイコンを選択します :

<img width="1512" alt="Screen Shot 2020-05-27 at 3.05.23 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/8c6d3648-da3c-1a81-36a4-c6b4d34ba75b.png">

2. `Codegen` 設定を `Manual/ None` に変更

以前の設定:

<img width="254" alt="Screen Shot 2020-05-27 at 3.08.57 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/f1087650-bced-51d1-32e5-bdad66dc0d26.png">

新しい設定:

<img width="258" alt="Screen Shot 2020-05-27 at 3.10.38 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/1dcc598b-8828-f596-fa51-e0926eeca84b.png">

3. クラス定義ファイルを生成

最上部のメニューで `Editor` をクリックし、`Create NSManagedObject subclass...` をクリックします

こうしたサブクラスを生成することで、`TodoItem` を新規オブジェクトの作成に直接使用できるようになります。

<img width="961" alt="Screen Shot 2020-05-27 at 3.11.37 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/4346c51e-142e-6ee2-7747-df2ed8da5199.png">

ファイルナビゲーターに以下のファイルが表示されます :

<img width="272" alt="Screen Shot 2020-05-27 at 3.13.31 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/07d67d8d-03b1-7f59-de98-ea9e85fefd83.png">

</div></details>

## データベースマネージャーを作成する

現在、コアデータベースに関連する関数を含む新規ファイルを作成できるようになっています。

このファイルで、`NSPersistentCloudKitContainer` をご利用のデータベース名で初期化することになります。

```swift
import CoreData

struct PersistenceController {
    static let shared = PersistenceController()

    let container: NSPersistentCloudKitContainer

    init() {
        container = NSPersistentCloudKitContainer(name: "CoreDataDemo")
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                print(error.localizedDescription)
            }
        })
    }
}
```

その後、メインのSwiftUIアプリファイル（`@main`というコードから始まるファイル）で、`PersistenceController` の共有インスタンスを初期化できます。

```swift
@main
struct SimpleTODOApp: App {
    let persistenceController = PersistenceController.shared

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
```

`SceneDelegate` を使っている場合には、`SceneDelegate.swift` ファイルにおいて、コードを `scene` 関数に追加してください。`SwiftUI` と `CoreData` フレームワークのインポートをお忘れなく。

```swift
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    let context = PersistenceController.shared.container.viewContext
    let contentView = ContentView()
    if let windowScene = scene as? UIWindowScene {
        let window = UIWindow(windowScene: windowScene)
        window.rootViewController = UIHostingController(rootView: contentView)
        self.window = window
        window.makeKeyAndVisible()
    }
}
```

## `.environment` を追加

これで、`PersistenceController.shared` がすでに初期化されているので、ルートSwiftUIビューの環境変数を設定する必要があります。

まず、コンテンツビューを初期化するファイルにこの行を追加します。

```swift
ContentView()
    .environment(\.managedObjectContext, persistenceController.container.viewContext)
```

そして、この変数をコンテンツビュー `ContentView` の最上部に追加します：

```swift
@Environment(\.managedObjectContext) private var viewContext
```

これで、コンテンツビューがデータベースにアクセスできるようになります。

## FetchRequestを追加する

データベースからコンテンツをフェッチするには、`FetchRequest` を作成します。

```swift
@FetchRequest(
    sortDescriptors: [NSSortDescriptor(keyPath: \TodoItem.taskDueTime, ascending: true)],
    animation: .default)
private var items: FetchedResults<TodoItem>
```

上記のコード例では、`TodoItem.dueTime` に基づいて項目をソートしています。それを自分のプロパティ名に変更する必要があります。

`keyPath` 変数のフォーマットを\[データベースエンティティの名前].[選択したエンティティの変数名]にする必要があります。

これで、アイテムをテーブルに表示できます。データベース内の項目が変更されると、テーブルは自動的に更新されます。

## その他のSwiftUIビューの環境の準備

初期化する各ビューに対してこのステップを繰り返して環境を準備することが重要です。

例えば、`ToDoItemCreate` というビューがあるなら、`ToDoItemCreate` ビューを初期化する際にはこの行を追加する必要があります。

```swift
ToDoItemCreate(isShown: $showTodoCreateView)
    .environment(\.managedObjectContext, self.viewContext)
```

また、変数 `viewContext` を `ToDoItemCreate` ビューに追加する必要があります。

```swift
@Environment(\.managedObjectContext) private var viewContext
```

## データベースのコンフリクトへの対応

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

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

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

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

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

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

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

---

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

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