LoginSignup
68
51

More than 3 years have passed since last update.

CoreDataをやさしく使う

Last updated at Posted at 2019-05-04

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

  • UserDefaults
  • Realm
  • CoreData

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

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

サンプル

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

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

How to

i. 導入

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

image.png

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

image.png

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

ii. モデルを定義する

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

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

image.png

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

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

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

  • 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クラスを作ります。以下それが作成してみたクラスです。

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クラスにはシングルトンパターンを利用しているので

let dataManager = DataManager.shared

を宣言しておきます。

保存

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

let todo: Todo = dataManager.create()

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

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

dataManager.saveContext()

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

読み出し

読み出しの時はNSFetchedResultControllerNSFetchedResultsControllerDelegateを利用します。まず読み出したいViewControllerで

lazy var fetchedResultsController: NSFetchedResultsController<Todo> = {

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

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

lazy var fetchedResultsController: NSFetchedResultsController<Todo> = {

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

とできます。

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

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

    do {

        try fetchedResultsController.performFetch()
    } catch {

        print(error)
    }
}

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

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

セクション数
fetchedResultsController.sections?.count
要素の個数
guard let sections = fetchedResultsController.sections else { return 0 }

let sectionInfo = sections[section]
sectionInfo.numberOfObjects // これ
オブジェクトのデータ
fetchedResultsController.object(at: indexPath)

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

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を行わずに表示の更新ができるだけでなく、差分のみの更新になるのでパフォーマンスの向上にもつながります。

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などになるとは思いますが、それ以外にも様々応用できそうです。

68
51
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
68
51