【Swift】Core Dataの基本的な使い方

はじめに

Core Dataの基本的な使い方について端的にまとめます。
初心者の方向け、もしくは備忘録(主に自分の)です。

環境はXcode 11.3.1 / Swift5。

Core Data の使用準備

新規プロジェクト作成時

Xcodeにて新規にプロジェクトを作る際、以下の画面で「Use Core Data」をチェックON。

既存プロジェクトにCore Dataを加える場合

プロジェクト作成時に「Use Core Data」をチェックしなかった場合、もしくはデータモデルを別途作りたい場合は、メニューの File > New > File… を選択し、「Choose a template for your new file:」の画面で「Data Model」を選択する。

Persistent Containerの初期化

AppDelegate.swiftを開いて、AppDelegateクラス内に以下を記述。
(名前が異なる場合は、それ相当のファイルとクラス)

プロジェクト作成時に「Use Core Data」をチェックした場合は自動で記述される。
“LearnCoreData” の部分は適宜変更のこと。

また、「Use CloudKit」にチェックした場合は、NSPersistentContainerではなくCloud Kitと自動連携が可能なNSPersistentCloudKitContainerとして作成される。

NSPersistentCloudKitContainerでも別途Cloud Kitとの連携設定を行わない限り普通のNSPersistentContainerとして動作するので、基本的にはチェック推奨。

lazy var persistentContainer: NSPersistentCloudKitContainer = {
    let container = NSPersistentCloudKitContainer(name: "LearnCoreData")
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
        if let error = error as NSError? {
            fatalError("Unresolved error \(error), \(error.userInfo)")
        }
    })
    return container
}()

Persistent Containerを使いたい場所に渡す

上で用意したPersistent Containerを使用したい場所(ViewControllerやViewModel、Modelなど)に渡す。

ここでは例として、CoreDataModelというクラスで使う前提。

import UIKit
import CoreData

class CoreDataModel {
    private static var persistentContainer: NSPersistentCloudKitContainer! = (UIApplication.shared.delegate as? AppDelegate)?.persistentContainer
}

保存ロジックを書く

プロジェクト作成時に「Use Core Data」をチェックしている場合、AppDelegateに以下の保存ロジックが記述されている。

func saveContext () {
    let context = persistentContainer.viewContext
    if context.hasChanges {
        do {
            try context.save()
        } catch {
            let nserror = error as NSError
            fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
        }
    }
}

これを使ってももちろん問題ないが、Extensionで少し便利に。

import CoreData

extension NSPersistentCloudKitContainer {
    
    /// viewContextで保存
    func saveContext() {
        saveContext(context: viewContext)
    }

    /// 指定したcontextで保存
    /// マルチスレッド環境でのバックグラウンドコンテキストを使う場合など
    func saveContext(context: NSManagedObjectContext) {
        
        // 変更がなければ何もしない
        guard context.hasChanges else {
            return
        }
        
        do {
            try context.save()
        }
        catch let error as NSError {
            print("Error: \(error), \(error.userInfo)")
        }
    }
}

データモデルの設計

エンティティを作成し、属性を設定する

自動もしくは手動で追加した .xcdatamodeld ファイルを開く。

画面下部にある「Add Entity」をクリックしてモデルを追加し、名前変更、属性追加を行う。

以下は例として、PersonCareerの2つを作成する場合。

エンティティのリレーションを結ぶ

一人の人が複数のキャリアを持つということを例として、PersonとCareerを 1 : N に関連付ける設定を加える。

先程の .xcdatamodeld ファイルの編集画面にて、「Relationships」の欄からまずはPersonに「careers」を追加。

続いてCareerに「person」を追加する。

エンティティに対応するNSManagedObject派生クラスを作る

モデル編集画面からPersonとCareerの2つのエンティティを選択し、画面右ペインの「Codegen」を「Manula/None」に変更する。

※Codegenを「Class Definition」にしている場合、裏で自動的にエンティティと同名の派生クラスが生成されます。特にカスタマイズを加える必要がない場合はこれでも十分です。

続けて、メニューの Editor > Create NSManagedObject Subclass… を選択し、以降表示される画面を進める。

結果、1つのモデルに対し2つずつのファイルが生成される。

データの登録

NSManagedObjectのインスタンス作成

RDBで言うINSERTを行うには、NSManagedObject(実際にはその派生クラス)のインスタンスの生成を行う。

インスタンス生成にはNSEntityDescriptionを使用する。

以下は、上述のPersonとCareerを生成する場合の例。
CoreDataModelクラスのメソッドとして追加。

static func newPersion() -> Person {
    let context = persistentContainer.viewContext
    let person = NSEntityDescription.insertNewObject(forEntityName: "Person", into: context) as! Person
    return person
}

static func newCareer(into person: Person) -> Career {
    let context = persistentContainer.viewContext
    let career = NSEntityDescription.insertNewObject(forEntityName: "Career", into: context) as! Career
    person.addToCareers(career)
    return career
}

インスタンスの生成とINSERTを分けたい場合は以下のように行う。

// INSERTせずにインスタンスだけ生成
let entity = NSEntityDescription.entity(forEntityName: "Person", in: context)!
let person = Person(entity: entity, insertInto: nil)

// これで初めてINSERT
context.insert(person)

NSManagedObjectインスタンスの保存

保存処理を呼んで変更を確定させる。
RDBで言うコミット。

CoreDataModelクラスに以下を追加。

static func save() {
    persistentContainer.saveContext()
}

データの作成から保存まで

実際に作成から保存までを行うと、以下のような感じになる。

// 日付変換用
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy/MM/dd"
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.timeZone = TimeZone(identifier: "Asia/Tokyo")

// Person作成
let person = CoreDataModel.newPersion()
person.name = "コアデータ太郎"
person.birthday = dateFormatter.date(from: "1980/12/31")

// Career ①
var career = CoreDataModel.newCareer(into: person)
career.company = "ABC社"
career.startDate = dateFormatter.date(from: "2002/04/01")
career.endDate = dateFormatter.date(from: "2006/06/30")

// Career ②
career = CoreDataModel.newCareer(into: person)
career.company = "DEF社"
career.startDate = dateFormatter.date(from: "2006/08/01")
career.endDate = dateFormatter.date(from: "2012/12/31")

// Career ③
career = CoreDataModel.newCareer(into: person)
career.company = "GHI社"
career.startDate = dateFormatter.date(from: "2012/01/06")
career.endDate = nil

// 保存
CoreDataModel.save()

データの削除

NSManagedObjectインスタンスを削除

RDBで言うDELETEを行うには、NSManagedObjectContextのdeleteメソッドを使用する。

static func delete(person: Person) {
    let context = persistentContainer.viewContext
    context.delete(person)
}

static func delete(career: Career) {
    let context = persistentContainer.viewContext
    context.delete(career)
}

deleteを呼んだ後も保存されるまでは確定されない。

確定前なら、削除したものは「context.deletedObjects」で拾うことが可能。

データの取得

全件取得

保存したデータを取得するにはNSFetchRequestを使用する。

CoreDataModelクラスに以下のメソッドを追加。

static func getPersons() -> [Person] {
    
    let context = persistentContainer.viewContext
    let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Person")
    
    do {
        let persons = try context.fetch(request) as! [Person]
        return persons
    }
    catch {
        fatalError()
    }
}

デフォルトだとリレーションが結ばれているサブエンティティのデータも引っ張ってくるため、それを避ける場合は「request.includesSubentities = false」を追加する。

条件で絞り込み

条件で絞り込みを行いたい場合はNSPredicateを使用する。

CoreDataModelクラスに追加したメソッドを少し改造。

static func getPersons(onlyAfter birthday: Date) -> [Person] {
    let predicate = NSPredicate(format: "birthday >= %@", birthday as NSDate)
    return getPersons(with: predicate)
}

static func getPersons(with predicate: NSPredicate?) -> [Person] {

    let context = persistentContainer.viewContext
    let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Person")
    request.predicate = predicate
    
    do {
        let persons = try context.fetch(request) as! [Person]
        return persons
    }
    catch {
        fatalError()
    }
}

サブエンティティのデータを条件に絞り込みたい場合はSUBQUERYを使用する。

static func getPersons(onlyAfterStartDate date: Date) -> [Person] {
    let predicate = NSPredicate(format: "SUBQUERY(careers, $c, $c.startDate >= %@).@count > 0", date as NSDate)
    return getPersons(with: predicate)
}

サブエンティティ(ここではCareer)のデータだけ、特定の親(Person)にぶら下がるものだけ取得したい場合は、以下の通り。

static func getCareers(only person: Person) -> [Career] {
    
    let context = persistentContainer.viewContext
    let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Career")
    request.predicate = NSPredicate(format: "person == %@", person)
    
    do {
        let careers = try context.fetch(request) as! [Career]
        return careers
    }
    catch {
        fatalError()
    }
}

ソート

データをソートした状態で取得したい場合は、NSSortDescriptorを使う。

上記getCareersメソッドに、startDateで並び替える記述を加える。

static func getCareers(with predicate: NSPredicate?) -> [Career] {
    
    let context = persistentContainer.viewContext
    let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Career")
    request.predicate = predicate

    // ソートを追加
    request.sortDescriptors = [
        NSSortDescriptor(key: "startDate", ascending: true)
    ]
    
    do {
        let careers = try context.fetch(request) as! [Career]
        return careers
    }
    catch {
        fatalError()
    }
}

参考リンク

Apple公式
https://developer.apple.com/documentation/coredata

Qiita – 「NSPredicate 全構文解説」
https://qiita.com/yusuga/items/8fd531ebd8f5e72bb97b