iOSの標準のデータベースフレームワーク「Core Data」。

バックエンドデータベースのSQLite3の詳細を隠し、使いやすくしてくれるものですが、関連するクラスやファイルが多く、学習コストが高いフレームワークとして知られています。

ここではCore Dataの最も簡単な使用法をサンプルプログラムを作成しながら説明します。

サンプルプログラムの作成

Xcodeを立ち上げ新規プロジェクトで「Single View Application」を選択します。プロジェクトの設定で「Use Core Data」のチェックを入れるとCore Dataが最初から使用できるプロジェクトが作成されるのですが、ここではあえてチェックを入れずに作成してください。

自力でCore Dataサポートを追加する場合以下の前準備が必要となります。

  • .xcdatamodeldの作成(テーブル定義に相当)
  • NSManagedObjectのサブクラスの作成(テーブルデータレコードを格納するクラス)

実際に処理を行う場合は次の順序で処理を行います。

  • Core Dataスタックの初期化(NSManagedObjectContextの生成)
  • NSManagedObjectContextとNSManagedObjectのサブクラスを利用して、データの取得、追加、削除、更新処理を実行

.xcdatamodeldの作成

まずはじめにデータベースのテーブル構造を定義する.xcdatamodeldを作成します。

「File > New > File」を選択し「Core Data」から「Data Model」を選択し、例えば「DemoModel.xcdatamodeld」を作成します。名前は適当でかまいません。

Add entity

「Add Entity」アイコンをクリックしエンティティ(テーブル)を追加します。

Entityの名前を好きに変更し「Add Attributes」で属性(カラム)を追加していきます。

Attributes

属性の名前や型などの情報はAttributeパネルで設定します。名前と型が重要なのはもちろんですが、Optionalのチェックにも注意が必要です。非Optional(Not Null)の属性を使用する場合チェックを外しておきましょう。

NSManagedObjectのサブクラスの作成

次に作成したエンティティに対応したクラスを作成しましす。

「File > New > File」を選択し「Core Data」から「NSManagedObject subclass」を選択します。先ほど作成した「Book」エンティティを選択すると以下の2つのファイルが作成されます。

Book.swift: 独自処理を追加する

class Book: NSManagedObject {

// Insert code here to add functionality to your managed object subclass

}

Book+CoreDataProperties.swift: 属性定義が行われている

extension Book {

    @NSManaged var title: String
    @NSManaged var content: String
    @NSManaged var price: NSNumber

}

なおCore DataのOptionalのチェックを外していても、生成されるSwiftのソースコードではOptional型の属性が定義されるようです。

Swift側でも非Optionalとして扱いたい場合「String?」から「String」のように最後の「?」を手動で削除しておきましょう。

自動的に反映されない理由としてCore DataとSwiftのOptionalの意味が違うことが上げられています。詳細が気になる方は以下のリンクを参照してください。

参考: Why Xcode generated NSManagedObject Subclass contains always optional properties even if they are marked as non-optional?

一応これで前準備は完成です。

Core Dataスタックの初期化

次にCore Dataを使用する前に必要な処理を作成します。

ここではCore Data関連の処理を初期化するためのクラスとしてDataControllerを作成します。「Use Core Data」にチェックを入れていた場合AppDelegateに自動的に書き込まれる処理とほぼ同様の処理となっています。

クラスが多数登場しかなり複雑な処理に見えますが、基本はNSManagedObjectContextを初期化して返しているだけのクラスです。以下の公式ドキュメントを参照にしました。

参考: Initializing the Core Data Stack

DataController.swift

class DataController: NSObject {
    var managedObjectContext: NSManagedObjectContext
    override init() {
        //自作のxcdatamodeldファイルの名前を指定。拡張子部分はこのまま。
        guard let modelURL = NSBundle.mainBundle().URLForResource("DemoModel", withExtension:"momd") else {
            fatalError("Error loading model from bundle")
        }
        //マネージドオブジェクトモデル。
        guard let mom = NSManagedObjectModel(contentsOfURL: modelURL) else {
            fatalError("Error initializing mom from: \(modelURL)")
        }
        
        let psc = NSPersistentStoreCoordinator(managedObjectModel: mom)
        managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
        managedObjectContext.persistentStoreCoordinator = psc
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {
            let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
            let docURL = urls[urls.endIndex-1]
            let storeURL = docURL.URLByAppendingPathComponent("DataModel.sqlite")
            do {
                try psc.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: storeURL, options: nil)
            } catch {
                fatalError("Error migrating store: \(error)")
            }
        }
    }
}

NSManagedObjectContextは通常アプリケーションごとに一つのインスタンスを作成すれば十分であるため、ここではAppDelegateから取得できるようにします。

AppDelegate.swift

class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    var dataController: DataController!
    
    var managedObjectContext: NSManagedObjectContext {
        return dataController.managedObjectContext
    }
}

Core Data保存・読み込み処理の実装

いよいよCore Dataのデータを操作する処理を作成します。

ここではViewControllerに4つのボタンを設置しボタンを押した際にCore Dataの「保存」「読み込み」「削除」「バッチ削除」を実行する処理を作成します。

本来は取得したデータはテーブルビューなどのコントロールに表示するのですが、ここでは説明のためコンソール上にデバッグ出力するだけにとどめます。

最初に全体のソースコードを掲載します。

ViewController.swift

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        print("##### 起動時の読み込み #####")
        dumpData()
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    
    @IBAction func loadTapped(sender: AnyObject) {
        print("##### データ読み込み開始 #####")
        dumpData()
        print("##### データ読み込み終了 #####")
    }
    
    func dumpData() {
        let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
        let managedObjectContext = appDelegate.managedObjectContext
        let booksFetch = NSFetchRequest(entityName: "Book") //全ての書籍情報を取得
        do {
            let fetchedBooks = try managedObjectContext.executeFetchRequest(booksFetch) as! [Book]
            for book in fetchedBooks {
                print(book.title)
            }
        } catch {
            fatalError("Failed to fetch books: \(error)")
        }
    }

    @IBAction func saveTapped(sender: AnyObject) {
        print("##### データ追加開始 #####")
        let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
        let managedObjectContext = appDelegate.managedObjectContext
        let book = NSEntityDescription.insertNewObjectForEntityForName("Book", inManagedObjectContext: managedObjectContext) as! Book
        
        let dateFormatter = NSDateFormatter()
        dateFormatter.dateFormat = "yyyy/MM/dd HH:mm:ss"
        book.title = "書籍のタイトルです at " + dateFormatter.stringFromDate(NSDate())
        book.content = "書籍の中身です"
        book.price = 999
        do {
            try managedObjectContext.save()
        } catch {
            fatalError("Failure to save context: \(error)")
        }
        print("##### データ追加終了 #####")
    }

    @IBAction func deleteTapped(sender: AnyObject) {
        print("##### データ削除開始 #####")
        //iOS9以前の削除方法: フェッチして削除
        let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
        let managedObjectContext = appDelegate.managedObjectContext
        
        let fetchRequest = NSFetchRequest(entityName: "Book")
        let predicate = NSPredicate(format: "price=%d", 999) //削除するオブジェクトの検索条件
        fetchRequest.predicate = predicate
        do {
            let books = try managedObjectContext.executeFetchRequest(fetchRequest) as! [Book]
            for book in books {
                managedObjectContext.deleteObject(book)
            }
            try managedObjectContext.save()
        } catch let error as NSError {
            fatalError("Failed to delete books: \(error)")
        }
        print("##### データ削除終了 #####")
    }
    
    @IBAction func deleteBatchTapped(sender: AnyObject) {
        print("##### データバッチ削除開始 #####")
        //iOS9で導入されたNSBatchDeleteRequestを使用する場合
        let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
        let managedObjectContext = appDelegate.managedObjectContext
        
        let fetchRequest = NSFetchRequest(entityName: "Book")
        let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
        do {
            try managedObjectContext.executeRequest(deleteRequest)
            //try managedObjectContext.save() //実際はsave()を呼ばなくても反映されている模様
        } catch let error as NSError {
            fatalError("Failed to delete books: \(error)")
        }
        print("##### データバッチ削除終了 #####")
    }    
}

データを読み込む場合NSFetchRequestでエンティティ名を指定して、NSPredicateによる条件を指定してexecuteFetchRequestを実行します。エンティティ全体を取得する場合はNSPredicateの指定は不要です。

let booksFetch = NSFetchRequest(entityName: "Book") //全ての書籍情報を取得
do {
    let fetchedBooks = try managedObjectContext.executeFetchRequest(booksFetch) as! [Book]
    for book in fetchedBooks {
        print(book.title)
    }
} catch {
    fatalError("Failed to fetch books: \(error)")
}

データを新規作成する場合、insertNewObjectForEntityForNameによってオブジェクトを生成し、オブジェクトに値を設定した後NSManagedObjectContextのsave()を呼び出します。

let book = NSEntityDescription.insertNewObjectForEntityForName("Book", inManagedObjectContext: managedObjectContext) as! Book
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "yyyy/MM/dd HH:mm:ss"
book.title = "書籍のタイトルです at " + dateFormatter.stringFromDate(NSDate())
book.content = "書籍の中身です"
book.price = 999
do {
    try managedObjectContext.save()
} catch {
    fatalError("Failure to save context: \(error)")
}

データの削除はNSManagedObjectのサブクラスをNSManagedObjectContextのdeleteObject()に渡します。削除対象のオブジェクトが存在しない場合はデータを取得した後、そのオブジェクトをdeleteObject()します。


let fetchRequest = NSFetchRequest(entityName: "Book")
let predicate = NSPredicate(format: "price=%d", 999) //削除するオブジェクトの検索条件
fetchRequest.predicate = predicate
do {
    let books = try managedObjectContext.executeFetchRequest(fetchRequest) as! [Book]
    for book in books {
        managedObjectContext.deleteObject(book)
    }
    try managedObjectContext.save()
} catch let error as NSError {
    fatalError("Failed to delete books: \(error)")
}

iOS 9以降NSBatchDeleteRequestバッチ削除も実行できます。削除する件数が多い場合はこちらの方が有利です。

let fetchRequest = NSFetchRequest(entityName: "Book")
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do {
    try managedObjectContext.executeRequest(deleteRequest)
    //try managedObjectContext.save() //実際はsave()を呼ばなくても反映されている模様
} catch let error as NSError {
    fatalError("Failed to delete books: \(error)")
}

実行

Header

▲実行するとエミュレーターが起動します。

Result

▲ボタンを押すとログがコンソールに表示されます。

まとめ

Core Dataは複雑なように見えますが、基本は以下の3つの重要ポイントを押さえておけば使用することができます。

  • Core Dataテーブルの定義を行う.xcdatamodeld
  • モデルに該当するNSManagedObjectのサブクラス
  • 読み書きを行うNSManagedObjectContext

ただしUITableViewで効率良く使用するにはさらにNSFetchedResultsControllerと呼ばれる重要なクラスの使い方を覚える必要もあります。この部分は次回説明します。