CoreDataをやさしく使う

iOS開発においてデータの保存を扱う時、代表的な方法として以下のようなものが上げられると思います。

- UserDefaults
- Realm
- CoreData

主な使い分け方としては、**「UserDefaultsがちょっとしたものの保存、Realmがもっと複雑なものの保存で、CoreDataはRealmと役割的に同じだけど昔からちょっと扱いにくかったからあんまり...」**という場合が多かったと思います。

ですが実際のところCoreDataはアップデートによって扱いやすくなっており、それが知られていないだけというのが現状です。
この流れをうけてtry! Swift Tokyo 2019においてDonny Walsさんが[In defence of Core Data](https://www.youtube.com/watch?v=aUoT5w4JOoM)という発表を行いました。この記事はそれをもとに、**「ではどのようにすれば、もっともっとCoreDataを扱いやすくできるか？」**という部分に注目してつくってみたCoreDataの入門記事となっています。

### サンプル

実際にこの記事の内容を用いて作ったサンプルアプリがこちらです。

- [touyou/EasyCoreDataSample: This is simple todo app with CoreData.](https://github.com/touyou/EasyCoreDataSample)

簡単なTodoアプリになっています。ぜひ参考にしてみてください。

# How to

## i. 導入

CoreDataの導入方法は２パターンあります。
まずひとつ目はプロジェクトを作る際、この画面でUse CoreDataのチェックを入れておくというもの。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10943/65d4110a-dfa6-44df-2ab9-06ae4fae6502.png)

もう一つは既存のプロジェクトに対しCoreDataを追加する方法です。これはNew FileのData Modelを追加することでできます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10943/f1ed1956-d321-40b7-6878-cf4cb33d0b9c.png)

これによってCoreDataが扱えるようになりました。簡単ですね。

## ii. モデルを定義する

続いてモデルを定義していきます。
CoreDataにおいてはEntityが保存単位、AttributeがEntityに含まれているプロパティ群といったかたちになっています。
これを◯◯.xcdatamodelというファイルで作成していきます。

まずファイルを開くとこのような画面になっているかと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10943/fc8e6630-9d54-81e1-667a-13a3aa3456eb.png)

たとえばTodoにタイトル・内容・日付の３つを入れたい場合は

0. Add Entitiesを押す
1. Entityという名前でENTITIESに項目が追加されるのでダブルクリックかEnterでTodoにリネーム
2. 選択した状態でAdd Attributeを三回押す
3. それぞれに名前とタイプを設定する

という流れになります。選択できるタイプは

- Undefined
- Integer 16
- Integer 32
- Integer 64
- Decimal
- Double
- Float
- String
- Boolean
- Date
- Binary Data
- UUID
- URI
- Transformable

です。Undefinedを設定するとNSObjectを継承したCustom Classを使用できます。
その他にもRelationshipsなどを駆使するとさらに複雑なデータ構造も定義できると思います。詳細は割愛しますが、Editor Styleを切り替えることで関係性なども視覚的に確認しながら定義していけるので興味のあるかたは公式のドキュメントなどをあたってみてください。

## iii. Managerクラスを定義する

さてモデルが定義できました。モデルはこれだけで扱えてコードを書く必要がなくなったため昔よりかなり楽になったかと思います。
しかし保存や読み出しに関しては、いくつかの工程が必要で十分に簡単とはいきません。

そこでそのもろもろをできるだけ隠蔽したManagerクラスを作ります。以下それが作成してみたクラスです。

```swift
import CoreData

class DataManager {

    static let shared: DataManager = DataManager()

    private var persistentContainer: NSPersistentContainer!

    init() {

        persistentContainer = NSPersistentContainer(name: "CoreDataSample")
        persistentContainer.loadPersistentStores { (description, error) in

            if let error = error {
                fatalError("Failed to load Core Data stack: \(error)")
            }

            print(description)
        }
    }

    func create<T: NSManagedObject>() -> T {

        let context = persistentContainer.viewContext
        let object = NSEntityDescription.insertNewObject(forEntityName: String(describing: T.self), into: context) as! T
        return object
    }

    func saveContext() {

        let context = persistentContainer.viewContext

        do {

            try context.save()
        } catch {

            print("Failed save context: \(error)")
        }
    }

    func getFetchedResultController<T: NSManagedObject>(with descriptor: [String] = []) -> NSFetchedResultsController<T> {

        let context = persistentContainer.viewContext
        let fetchRequest = NSFetchRequest<T>(entityName: String(describing: T.self))
        fetchRequest.sortDescriptors = descriptor.map { NSSortDescriptor(key: $0, ascending: true) }
        return NSFetchedResultsController<T>(fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
    }
}
```

`NSPersistentContainer`の初期化時に渡す名前をxcdatamodelの名前にあわせることでどんなプロジェクトでも使用できるようにしています。
またジェネリクスを使うことでEntityごとに一つ一つ書く必要はありません。

## iv. 実際の操作

ここまでくれば非常に直感的にCoreDataを扱えるようになっています。

まずManagerクラスにはシングルトンパターンを利用しているので

```swift
let dataManager = DataManager.shared
```

を宣言しておきます。

### 保存

保存は、まずEntityのクラスのインスタンスを用意します。新しく作る場合は例えばTodoクラスの場合

```swift
let todo: Todo = dataManager.create()
```

とします。ジェネリクスを使っているので型指定は省略しないようにしましょう。
更新の場合はすでにあるインスタンスを利用するかたちで大丈夫です。

プロパティの値を必要に応じて設定したら最後に

```swift
dataManager.saveContext()
```

を呼びます。これによって保存が完了しました。

### 読み出し

読み出しの時は`NSFetchedResultController`と`NSFetchedResultsControllerDelegate`を利用します。まず読み出したいViewControllerで

```swift
lazy var fetchedResultsController: NSFetchedResultsController<Todo> = {

    let _controller: NSFetchedResultsController<Todo> = dataManager.getFetchedResultController(with: ["date"])
    _controller.delegate = self
    return _controller
}()
```

のように宣言します。中でdataManagerを利用するので`lazy`にしておき、`getFetchedResultController`メソッドは、ソートして呼び出したい場合その基準となるプロパティ名を、そうでない場合は何も渡さずに呼び出します。その場合は

```swift
lazy var fetchedResultsController: NSFetchedResultsController<Todo> = {

    let _controller: NSFetchedResultsController<Todo> = dataManager.getFetchedResultController()
    _controller.delegate = self
    return _controller
}()
```

とできます。

データの取得は`viewWillAppear`などで行うと便利です。その場合は

```swift
override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    do {

        try fetchedResultsController.performFetch()
    } catch {

        print(error)
    }
}
```

このようにしておきます。後で`NSFetchedResultsControllerDelegate`メソッドの中でTableViewの更新など行うためここではこれだけで大丈夫です。

セクションの数やデータなどは`NSFetchedResultsController`が全てもっているのでそこから取ってくることになります。

```swift:セクション数
fetchedResultsController.sections?.count
```

`swift:要素の個数
guard let sections = fetchedResultsController.sections else { return 0 }

let sectionInfo = sections[section]
sectionInfo.numberOfObjects // これ


```swift:オブジェクトのデータ
fetchedResultsController.object(at: indexPath)
```

これらをTableViewのデータソース内で呼ぶと例えばこんな感じになります。objectは自動で型推論が働くようになっています。

```swift
extension ViewController: UITableViewDataSource {
    func numberOfSections(in tableView: UITableView) -> Int {

        return fetchedResultsController.sections?.count ?? 0
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        guard let sections = fetchedResultsController.sections else { return 0 }

        let sectionInfo = sections[section]
        return sectionInfo.numberOfObjects
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let cell: TodoTableViewCell = tableView.dequeueReusableCell(withIdentifier: String(describing: TodoTableViewCell.self), for: indexPath) as! TodoTableViewCell
        configureCell(cell, at: indexPath)
        return cell
    }

    func configureCell(_ cell: TodoTableViewCell, at indexPath: IndexPath) {

        let todo = fetchedResultsController.object(at: indexPath)
        cell.titleLabel.text = todo.title
        cell.contentLabel.text = todo.content
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy/MM/dd"
        cell.dateLabel.text = formatter.string(from: todo.date ?? Date())
    }
}
```

最後に`NSFetchedResultsControllerDelegate`でデータ操作が行われた時になにをするかを書いていきます。ここでTableViewへのもろもろを呼び出しておくと`reloadData`を行わずに表示の更新ができるだけでなく、差分のみの更新になるのでパフォーマンスの向上にもつながります。

```swift
extension ViewController: NSFetchedResultsControllerDelegate {

    func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {

        tableView.beginUpdates()
    }

    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {

        switch type {
        case .insert:
            if let indexPath = newIndexPath {
                tableView.insertRows(at: [indexPath], with: .automatic)
            }
        case .delete:
            if let indexPath = indexPath {
                tableView.deleteRows(at: [indexPath], with: .automatic)
            }
        case .update:
            if let indexPath = indexPath,
                let cell = tableView.cellForRow(at: indexPath) as? TodoTableViewCell {
                configureCell(cell, at: indexPath)
            }
        case .move:
            if let indexPath = indexPath,
                let newIndexPath = newIndexPath {
                tableView.moveRow(at: indexPath, to: newIndexPath)
            }
        @unknown default:
            fatalError("unknown fetched results change type")
        }
    }

    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {

        tableView.endUpdates()
    }
}
```

以上でTableViewへのデータの反映もできるようになりました。

# まとめ

CoreDataはまだまだ難しい部分もありますが、かなり扱いやすくなってきたんじゃないかなと思っています。特に親和性が高いのはTableViewなどになるとは思いますが、それ以外にも様々応用できそうです。