iOS開発メモ9 (XcodeでCore Dataを使う)

"Core Dataを使用しない"を選択したときのAppDelegate

// Core Data 使用しない場合のAppDelegate
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

    func application(_ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        return true
    }

    func applicationWillResignActive(_ application: UIApplication) {
    }

    func applicationDidEnterBackground(_ application: UIApplication) {
    }

    func applicationWillEnterForeground(_ application: UIApplication) {
    }

    func applicationDidBecomeActive(_ application: UIApplication) {
    }

    func applicationWillTerminate(_ application: UIApplication) {
    }
}

"Core Dataを使用する"を選択したときのAppDelegateが以下になる。手動でCore Dataを追加する場合は、以下を参考にコードを追加する。

// Core Dataを使用するときのAppDelegate

import UIKit
import CoreData

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

//
// ここまでは、Core Data を使用しないときと同じなので省略
//

    func applicationWillTerminate(_ application: UIApplication) {
        self.saveContext()
    }

    // MARK: - Core Data stack

    lazy var persistentContainer: NSPersistentContainer = {
        // "XXX"はプロジェクト名がデフォルトで指定される
        let container = NSPersistentContainer(name: "XXX")
        container.loadPersistentStores(completionHandler: {
            (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        return container
    }()

    // MARK: - Core Data Saving support

    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)")
            }
        }
    }
}

手動で、Core Dataを作成してエンティティを追加する

新規作成で、Data Modelを選択する

ss--2017-11-02-0.25.55

拡張子は*.xcdatamodeldになる

ss--2017-11-02-0.26.37

プロジェクトにファイルが作成される

ss--2017-11-02-0.26.56

*.xcdatamodeldを選択すると、Xcode上で専用画面に切り替わる。作成直後は空の状態である。

ss--2017-11-02-0.27.14

Add Entityをメニューから選択して、エンティティ(データベースのテーブル相当)を追加する。

ss--2017-11-02-1.47.52

空のエンティティが作成される。Attributeのを選択して属性(テーブルのカラム相当)を追加する。

ss--2017-11-02-1.49.22

ss--2017-11-02-1.49.37

ss--2017-11-02-1.51.20

各属性の型を選択する。下図のような型が選択できる。
ss--2017-11-02-1.51.37

ss--2017-11-02-1.52.09

注意点
ネットで検索すると、この後にCreate NSManagedObject Subclassを作成する手順になっているが、Xcode9でエンティティのクラスファイルを生成してビルドすると、エラーになる。

ss--2017-11-02-2.13.08-1

昔は、クラスファイルを生成するしかなかったらしいが、最近では選択式になって、デフォルトでは、わざわざエンティティのサブクラスファイルしなくても自動生成する。

下図がエンティティを作成した直後のデフォルト状態で、CodegenClass Definitionになっている。このままサブクラスを生成してビルドすると、同名クラスが重複する旨のコンパイルエラーが発生する。

ss--2017-11-02-1.53.09

もしエンティティのサブクラスに、メンバ変数など追加したりカスタマイズする必要がある場合は、ソースファイルを生成する必要があるので、以下のようにCodegenの値をManula/Noneを選択すると、自動生成されなくなってコンパイルが成功するようになる。

ss--2017-11-02-1.53.11

実際にサブクラスを生成する手順が以下になる。以下のメニューからCreate NSManagedObject Subclassを選択する。

ss--2017-11-02-1.55.40

データモデルとエンティティを選択して進めると、サブクラスが生成される。

ss--2017-11-02-1.56.07

ss--2017-11-02-1.56.31

ss--2017-11-02-1.57.44

生成された各ソースファイルの中身は以下になる。NSManagedObjectを継承したエンティティクラスになっており、

// Person+CoreDataClass.swift

import Foundation
import CoreData
@objc(Person)
public class Person: NSManagedObject {

}

そのサブクラスのエクステンションとして、fetchRequestメソッドの実装と、属性がメンバ変数になっている。

// Person+CoreDataProperties.swift

import Foundation
import CoreData
extension Person {
    @nonobjc public class func fetchRequest() -> NSFetchRequest<Person> {
        return NSFetchRequest<Person>(entityName: "Person")
    }
    @NSManaged public var name: String?
    @NSManaged public var age: Int16
    @NSManaged public var gender: String?
}

Core Dataなどドキュメントの保存場所について

File System Programming Guide

わかりやすい解説サイトが以下にある

今こそ復習したい、iOSアプリのディレクトリ構成

とりあえず、シミュレータの場合は以下になる。実機の場合もアプリケーションディレクトリ以下は同じはずである。

  • シミュレータのデバイスディレクトリ
~/Library/Developer/CoreSimulator/Devices/{各モデルのデバイスディレクトリ}
  • デバイス内のアプリケーションディレクトリ
    このディレクトリ配下にインストールしたアプリケーションディレクトリがある。
{デバイスディレクトリ}/data/Containers/Bundle/Application/
  • デバイス内のドキュメントディレクトリ
{デバイスディレクトリ}/data/Containers/Data/Application/

この配下にドキュメントやリソース、テンポラリファイルなどを格納するフォルダがある。

> cd data/Containers/Data/Application/
> ls
Documents/	Library/	tmp/

'modelURL'に代入しているモデルのディレクトリ'XXX.momd'の場所は、

{デバイス名}/data/Containers/Bundle/Application/{アプリケーションフォルダ}/{アプリケーション.app}/XXX.momd

*.momXXX.omoどちらもバイナリデータで、VersionInfo.plistのバイナリデータだとか、オプティマイズされたファイルだとからしい。先ほど定義したPersonエンティティの情報が格納されているCore Dataの情報ファイルなのは間違いなさそう。直接中身を見るようなものではないと思われる。

RDBMSのデータベースだと、テーブルを管理しているシステムテーブル情報のようなイメージだと思う。。

> cd {デバイス名}/data/Containers/Bundle/Application/{アプリケーションフォルダ}/XXX.app
> ls
XXX.momd
> cd XXX.momd
> ls
VersionInfo.plist	XXX.mom			XXX.omo

Core Dataを使ったデータの検索、追加、更新、削除など

Appleの公式ドキュメント

Core Dataプログラミングガイド

永続データのロード、保存などの準備

まずデータの操作の前に永続データのロード、保存などの準備を行う処理を実装する。
以下の例では、永続ストアとしてSQLITEを使用している。

SQLITEファイルの保存先は、applicationDocumentsDirectoryで取得するアプリケーションのドキュメントディレクトリを指定する。

後述するプログラム内で、content.save()を実行すると、ファイルが存在してない場合、以下のファイルが生成される。

> cd data/Containers/Data/Application/{アプリケーションフォルダ}/Documents
> ls
sample.sqlite		sample.sqlite-shm	sample.sqlite-wal

サンプルでは、AppDelegateに実装している。各アプリのクラスのどこからでもアクセスしやすい場所ならどこでもいいはず。

// AppDelegate.swift

// MARK: - Core Data stack
    // モデル管理オブジェクトを生成して返す
    lazy var managedObjectModel: NSManagedObjectModel = {
        // *.momdディレクトリのURL
        let modelURL = Bundle.main.url(forResource: "XXX", withExtension: "momd")!
        return NSManagedObjectModel(contentsOf: modelURL)!
    }()
    
    // アプリケーションのDocumentディレクトリを返す
    lazy var applicationDocumentsDirectory: NSURL = {
        let urls = FileManager.default.urls(for: .documentDirectory,
            in: .userDomainMask)
        return urls[urls.count-1] as NSURL
    }()
    
    // コーディネータを生成して返す
    lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
        // 管理オブジェクトからコーディネータを生成する
        let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
        // アプリケーションのドキュメントディレクトリに
        // SQLITEデータベースファイルを作成する。(存在していれば既存データを取得)
        let url = self.applicationDocumentsDirectory.appendingPathComponent("sample.sqlite")
        
        do {
            // コーディネータに永続ストアとして設定する
            try coordinator.addPersistentStore(ofType: NSSQLiteStoreType,
                configurationName: nil, at: url, options: nil)
        } catch {
            // エラー処理
            NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)")
            abort()
        }
        
        return coordinator
    }()
    
    // コンテキストを返す
    lazy var managedObjectContext: NSManagedObjectContext = {
        // コンテキストを生成して、コーディネータを設定して返す
        let coordinator = self.persistentStoreCoordinator
        var managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
        managedObjectContext.persistentStoreCoordinator = coordinator
        return managedObjectContext
    }()
    
    // 永続ストアをロードして、そのコンテナを返す
    lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "XXX")
        container.loadPersistentStores(completionHandler: {
            (storeDescription, error) in
            if let error = error as NSError? {
                // error
                var dict = [String: AnyObject]()
                dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data" as AnyObject?
                dict[NSLocalizedFailureReasonErrorKey] = failureReason as AnyObject?
                dict[NSUnderlyingErrorKey] = error as NSError
                let wrappedError = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)
                NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)")
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        return container
    }()

    // MARK: - Core Data Saving support
    // コンテキストを保存する
    func saveContext () {
        let context = persistentContainer.viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                // error
                let nserror = error as NSError
                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
            }
        }
    }

データの検索、追加、更新、削除

とりあえずボタンをタップしたときに検索、追加、更新、削除を行うようなイメージで各処理を実装すると以下になる。

  • 検索
    コンテキストを取得してPersonのスタティックメソッドfetchRequestでリクエストオブジェクトを生成して、フェッチしている。NSPredicateはSQLのWhere句のような検索条件のクラスになる。
func search()->[Person]{
    
    // コンテキストを取得
    let appDelegate:AppDelegate = UIApplication.shared.delegate as! AppDelegate
    let context:NSManagedObjectContext = appDelegate.managedObjectContext
    
    let fetchRequest:NSFetchRequest<Person> = Person.fetchRequest()
    // 検索条件
    let predicate = NSPredicate(format:"%K = %@","name","swift 太郎")
    fetchRequest.predicate = predicate
    
    // データをフェッチ
    let fetchData = try! context.fetch(fetchRequest)

    return fetchData
}
  • 挿入
    Personクラスをインスタンス化して値を設定し、最後のコンテキストでsave()することによって、データが追加される。SQLっぽさが隠蔽化されてなくなっている。
func insert()->Void{
    // insert
    let appDelegate:AppDelegate = UIApplication.shared.delegate as! AppDelegate
    let context:NSManagedObjectContext = appDelegate.managedObjectContext

    let person = Person(context:context)
    person.name = "swift 太郎"
    person.gender = "male"
    person.age = 20
    do{
       try context.save()
    }catch{
       print(error)
    }
    return
}
  • 更新
    search()でフェッチしたデータに対して、1件ずつ値を更新して、最後に保存している。ループする分、SQLよりも効率悪そうだが。。更新に関しては、ほかの方法があるかもしれないが別の機会とする。とりあえず単純なやり方のみ。
func update()->Void{
    
    let appDelegate:AppDelegate = UIApplication.shared.delegate as! AppDelegate
    let context:NSManagedObjectContext = appDelegate.managedObjectContext
    
    // update
    let data = search()
    for i in 0..<data.count{
       data[i].age = 19
    }
    do{
       try context.save()
    }catch{
       print(error)
    }
}
  • 削除
    search()でフェッチしたデータに対して、1件ずつ値を削除指示する。最後にコンテキストを保存してコミットしている。
func delete()->Void{
    let appDelegate:AppDelegate = UIApplication.shared.delegate as! AppDelegate
    let context:NSManagedObjectContext = appDelegate.managedObjectContext
    
    // delete
    let data = search()
    for i in 0..<data.count{
        let deleteObject = data[i] as Person
        context.delete(deleteObject)
        print("delete")
    }
    do{
        try context.save()
        print("delete commit")
    }catch{
        print(error)
    }
}