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のチェックを入れておくというもの。
もう一つは既存のプロジェクトに対しCoreDataを追加する方法です。これはNew FileのData Modelを追加することでできます。
これによってCoreDataが扱えるようになりました。簡単ですね。
ii. モデルを定義する
続いてモデルを定義していきます。
CoreDataにおいてはEntityが保存単位、AttributeがEntityに含まれているプロパティ群といったかたちになっています。
これを◯◯.xcdatamodelというファイルで作成していきます。
まずファイルを開くとこのような画面になっているかと思います。
たとえばTodoにタイトル・内容・日付の3つを入れたい場合は
- Add Entitiesを押す
- Entityという名前でENTITIESに項目が追加されるのでダブルクリックかEnterでTodoにリネーム
- 選択した状態でAdd Attributeを三回押す
- それぞれに名前とタイプを設定する
という流れになります。選択できるタイプは
- 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()
を呼びます。これによって保存が完了しました。
読み出し
読み出しの時はNSFetchedResultController
とNSFetchedResultsControllerDelegate
を利用します。まず読み出したい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などになるとは思いますが、それ以外にも様々応用できそうです。