Xcode
CoreData
Swift

iOSアプリでデータベース(CoreData)を使う時に必要な知識をまとめてみた - Swift編 -

More than 1 year has passed since last update.

CoreData を扱う機会があったので、 CRUD操作やマイグレーション、エンティティ同士の関連付け、大規模なアプリで必要になるであろうマルチスレッド時のプラクティス、その他シミュレータのデータベースの中身を見るにはどうすればいいかなど、必要となる知識を一通りまとめてみました。

※ Xcode7.1.1 で動かします。

CoreDataに関連するクラス

※ 実際に開発者が操作する必要があるのは、★がついているクラスです。あとのクラスはほとんど触る機会はないと思います。

クラス名 説明
NSManagedObjectModel 管理オブジェクトモデル。データベースのスキーマ情報を表す
NSPersistentStore データベース情報を表す
NSPersistentStoreCoordinator アプリケーション内のオブジェクトとデータベースの間のやり取りを行う
NSEntityDescription データベースのテーブル(エンティティ)情報を表す
★ NSManagedObject 管理オブジェクト。テーブルのレコードを表すクラス
★ NSManagedObjectContext 管理オブジェクトコンテキスト。NSManagedObject 群を管理するクラス

図でまとめると以下の様な関係性になります。

スクリーンショット 2015-11-28 11.08.21.png

CoreDataのデータが更新されるタイミング

データのインスタンスは管理オブジェクトで表されますが、この管理オブジェクトを変更しても、すぐにデータベース(保存ファイル)が更新されるわけではありません。後述しますが、管理オブジェクトは管理オブジェクトコンテキストによって管理されており、この管理オブジェクトコンテキストがデータベースへの更新を行います。オブジェクトを更新した段階では、管理オブジェクトコンテキストの中では値が変わっていますが、コンテキストがデータベースへの更新を行わない限り、データベースの値は変わりません。

CoreDataのプロジェクトを作成する

1. 「Use Core Data」にチェックを入れてプロジェクトを作成する

プロジェクト作成時に「Use Core Data」にチェックを入れると、AppDelegate.swift にCoreData関連のコードがデフォルトで作成されます。上記のクラス説明を読んでいれば、何となくCoreDataを操作するためのフレームワークが導入されたんだなということがわかると思います。

2. エンティティを作成する

エンティティは、DBのテーブルをクラスで表す時の言葉です。CoreDataのエンティティを定義するファイル「プロジェクト名.xcdatamodeld」を開いて、Userエンティティを作成してみます。

  • .xcdatamodeld をクリック
  • 下に表示される +「Add Entity」をクリックし、生成されたEntityをダブルクリックして名前を「User」に変更する。

スクリーンショット 2015-11-20 11.19.59.png

  • Attributes の + を押して、以下のカラムを追加してみる。

スクリーンショット 2015-11-20 11.20.45.png

3. NSManagedObjectクラスを作成する

テーブルに紐づくクラスを作成します。

新規ファイル作成 → iOS → Core Data → NSManagedObject subclass → 対象のデータモデルにチェックを入れてNext → 対象のエンティティにチェックを入れてNext → create

すると、こんな感じのファイルが自動生成されます。

User.swift

import Foundation
import CoreData


class User: NSManagedObject {

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

}

User+CoreDataProperties.swift

import Foundation
import CoreData

extension User {

    @NSManaged var id: NSNumber?
    @NSManaged var name: String?

}

エンティティ名+CoreDataProperties.swiftの方はプロパティ情報が定義されたクラス、エンティティ名.swiftは独自メソッドの実装をするクラスのようです。

これで準備完了です。今度は実際にテーブルにレコードを作成してみます。

CRUD 操作

新規追加

ViewController で User レコードを新規追加してみます。まず、CoreDataのクラスを扱うために、CoreData ライブラリを import します。

import CoreData

エンティティの保存には、NSEntityDescriptioninsertNewObjectForEntityForNameメソッドでエンティティのインスタンスを生成し、それに対して値を設定していきます。変更をデータベースにコミットするには、NSManagedObjectContext のインスタンスを保存する必要があります。

let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate

let user = NSEntityDescription.insertNewObjectForEntityForName("User", inManagedObjectContext: appDelegate.managedObjectContext) as! User
user.id = 1
user.name = "tanaka"

// コミット
appDelegate.saveContext()

検索

NSFetchRequestクラスを使用します。条件を指定する場合は、NSPredicateクラスを使用します。

let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let fetchRequest = NSFetchRequest(entityName: "User")
let predicate = NSPredicate(format: "id = %d", 1)
fetchRequest.predicate = predicate

// 検索
do {
  let users = try appDelegate.managedObjectContext.executeFetchRequest(fetchRequest) as! [User]
  for user in users {
    print("\(user.id) \(user.name)")
  }
} catch let error as NSError {
  print(error)
}

条件に合致するオブジェクトだけが検索で用いたコンテキストに取り込まれます。

検索条件指定パターン例

// 件数を指定
fetchRequest.fetchLimit = 2

// オフセットを指定
fetchRequest.fetchOffset = 0

// 並び順を指定
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "id", ascending: false)]

// 複数指定
let predicate = NSPredicate(format: "id = %d AND name = %@", 1, "tanaka")

// 配列で複数指定(Andの場合)
let predicate = NSCompoundPredicate(type:.AndPredicateType, subpredicates: [
  NSPredicate(format: "id = %d", 1),
  NSPredicate(format: "name = %@", "tanaka")
])

更新

検索と新規追加の合わせ技です。

let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let fetchRequest = NSFetchRequest(entityName: "User")
let predicate = NSPredicate(format: "id = %d", 1)
fetchRequest.predicate = predicate

do {
  let users = try appDelegate.managedObjectContext.executeFetchRequest(fetchRequest) as! [User]
  for user in users {
    user.name = "sato"
  }
} catch let error as NSError {
  print(error)
}

// コミット
appDelegate.saveContext()

削除

NSManagedObjectContext のdeleteObjectメソッドを使用します。

let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let fetchRequest = NSFetchRequest(entityName: "User")
let predicate = NSPredicate(format: "id = %d", 1)
fetchRequest.predicate = predicate

do {
  let users = try appDelegate.managedObjectContext.executeFetchRequest(fetchRequest) as! [User]
  for user in users {
    appDelegate.managedObjectContext.deleteObject(user)
  }
} catch let error as NSError {
  print(error)
}

// コミット
appDelegate.saveContext()

これで CRUD 操作ができるようになりました。

NSManagedObject補足

オブジェクトID

オブジェクトを一意に識別するためのもの。生成した時点では一時IDを割り当て、 永続ストアに保存する際に初めて、永続IDを割り当てます。一時IDかどうかは以下のメソッドが真偽値を返すのでそれでわかります。

user.objectID.temporaryID

フォールト

CoreDataフレームワークが持っているメモリ節約の機能。フォールト状態とは、検索したエンティティのプロパティ値をデータベースからまだ読み込んでいない状態のことを指します。実際にプロパティにアクセスした時などにフォールトが発動し、データベースにアクセスするようになっています。

フォールト状態のオブジェクトをコンソールに出力してみると、以下のようにdata: <fault> と表示されます。(以下は Group というエンティティのインスタンスを出力した例です)

entity: Group; id: 0xd000000000040000 <x-coredata://72D8B660-CA5E-4F71-B5F3-4022A56F0C00/Group/p1> ; data: <fault>

awakeFromInsert

インスタンス生成時に呼ばれるメソッド。初期値の設定などに使われます。

override func awakeFromInsert() {
    super.awakeFromInsert()
    self.name = "default name"
}

NSFetchedResultsController

NSFetchedResultsController は、フェッチしたデータを NSIndexPath で取り出すことができるので、 テーブルのセルにデータを表示するときに便利です。

1. NSFetchedResultsControllerDelegate を実装する

NSFetchedResultsController のインスタンスはクラス内で使うので、インスタンス変数も宣言しておきましょう。

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, NSFetchedResultsControllerDelegate {
  var fetchedResultsController: NSFetchedResultsController!
}

2. NSFetchRequest を作成する

NSFetchedResultsController を使う場合、NSFetchRequest にソート条件を必ずセットする必要があります。

let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let fetchRequest = NSFetchRequest(entityName: "User")
let predicate = NSPredicate(format: "id = %d", 1)
fetchRequest.predicate = predicate

let fetchSort = NSSortDescriptor(key: "name", ascending: true)
fetchRequest.sortDescriptors = [fetchSort]

3. NSFetchedResultsController を初期化し、delegateをセットする

let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: appDelegate.managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)
fetchedResultsController.delegate = self

sectionNameyKeyPath: フェッチしてくる ManagedObject のプロパティを指定すると、そのプロパティでテーブルのセクションを作成する。nil だとセクション分けしない。

cacheName: 文字列を渡すと、その名前でキャッシュを生成する。nil だとキャッシュしない。

4. performFetch: でフェッチする

do {
  try fetchedResultsController.performFetch()
} catch let error as NSError {
  print(error)
}

5. フェッチしたデータを元にテーブルのセクション情報を作成する

// セクション数
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
  return self.fetchedResultsController.sections?.count ?? 0
}

// 特定のセクションに表示するセル数
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  let sectionInfo = self.fetchedResultsController.sections![section]
  return sectionInfo.numberOfObjects
}

6. テーブルのセルにデータを反映する

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
  let cell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "Cell")
  let user = fetchedResultsController.objectAtIndexPath(indexPath) as! User
  cell.textLabel?.text = user.name
  return cell
}

実行すると、テーブルのセルに User の name が表示されるはずです。

セル情報の追加・更新・削除はここでやると少し長くなるので割愛します。Qiita の他の記事で紹介されていると思うので、探してみて下さい。

型について

Intは3種類

範囲
Integer 16 -32768〜32767
Integer 32 -2147483648〜2147483647
Integer 64 -9223372036854775808〜9223372036854775807

通知

NSManagedObjectContextDidSaveNotification

saveContext()の呼び出し時に、新規追加、変更、削除のいずれかがある場合に呼ばれます。

let notificationCenter = NSNotificationCenter.defaultCenter()
notificationCenter.addObserver(self, selector: Selector("didObserveNotificationManagedObjectContextDidSave:"), name: NSManagedObjectContextDidSaveNotification, object: managedObjectContext)

...

func didObserveNotificationManagedObjectContextDidSave(notification: NSNotification) {
    print("didObserveNotificationManagedObjectContextDidSave")
}

値の検証

エンティティの値を設定するときに、不正な値が入らないようにバリデーションを設定することができます。ここでは user.id の数値を 1〜10 までに設定してみましょう。

スクリーンショット_2015-11-28_12_52_41.png

これで id を 11 に設定して保存しようとすると、エラーが起こります。エラーを catch してみると、以下の情報が見れます。

do {
  // 保存処理
  ...

} catch let error as NSError {
  error.code // エラーコード。このコードから何のエラーになったのかわかる。この場合はバリデーションの数値の上限を超えたエラーなので 1610 となり、NSValidationNumberTooLargeError と一致する。
  error.userInfo["NSValidationErrorKey"] // 何のプロパティがエラーになったのか
  error.userInfo["NSValidationErrorValue"] // 何の値がエラーになったのか
}

スキーマの変更

管理オブジェクトのプロパティ情報などは、バージョニングされて管理されています。バージョン1の時はこのプロパティがあり、バージョン2の時は... という感じで、管理オブジェクトの情報はすべてバージョンで管理されています。

管理オブジェクトのカラムを追加して新しいバージョンが作成された時、すでに普及しているアプリ内でも新しいバージョンを読み込まないとアプリ内で以下のエラーが発生します。

The model used to open the store is incompatible with the one used to create the store

「今のバージョンとスキーマ情報が矛盾してますよ」というエラーです。よって、アプリ内で使用するバージョンを移行してあげる必要があります。このことをマイグレーションといいます。

スキーマ情報を変えるときは、まず新しいバージョンのモデルファイルを作成し、それに対してスキーマ情報の変更を与え、さらにそれを現在のバージョンに設定する必要があります。バージョンを変更するときに、今のデータをどう移行するか、という処理をする必要があるのですが、シンプルなものだと CoreData が自動的にやってくれます。複雑なものだと自分でマイグレーションファイルを作成して実装する必要があるのですが、たいていの場合は自動マイグレーションで済むはずなので、今回は自動マイグレーションの場合を例にあげたいと思います。以下その手順です。

1. バージョンファイルを新規作成

.xcdatamodeld を選択した状態で、Editor > Add Model Version…を選択し、新しいモデルバージョンを作成します。たいてい名前の最後に v1, v2 ... といったバージョン番号をつけることが多いようです。

2. 新規作成した xcdatamodeld を選択して、スキーマを変更

.xcdatamodeld が階層構造になっているので、下にあるファイルを展開すると、指定した名前の .xcdatamodelファイルが追加されているので、選択してスキーマを変更します。(エンティティやカラムなどを追加・変更・削除したり)。

スクリーンショット 2015-11-26 0.57.24.png

3. 変更したら NSManagedObject のクラスを作成・変更

修正後、.xcdatamodel を選択した状態で、Editor > Create NSManagedObject Subclassを選択します。変更を与えたエンティティのファイル名を選択してクラスを作成・変更します。

4. 現在のバージョンを変更

.xcdatamodeld を選択した状態で、File Inspector の Core Data Model の Model Version の Current を新規作成したバージョンに変更します。

5. マイグレーションを実行する

以下のような簡単な変更なら、CoreData が自動でマイグレーションしてくれます。

  • カラムの追加・削除・名前変更
  • エンティティの追加・削除・名前変更
  • 必須入力を任意に変更する
  • 任意入力を必須に変更し、デフォルト値を設定する
  • 関連の追加・削除・変更

自動マイグレーションを実行するためには、AppDelegate.swift で、NSPersistentStoreCoordinator をインスタンス化する際にオプションを指定します。

// 自動マイグレーションを行うためのオプション
let options = [
  NSInferMappingModelAutomaticallyOption: true,
  NSMigratePersistentStoresAutomaticallyOption: true
]

// デフォルトだと options :nil なので、options を渡す
try coordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: url, options: options)
  • NSInferMappingModelAutomaticallyOption: 自動マイグレーションを行うかどうか
  • NSMigratePersistentStoresAutomaticallyOption: mapping model を自動的に作成するかどうか(mapping model はマイグレーションをするためのファイルです。手動だとこれを自分で作成して実装する必要があります。)

あとは実行するだけです。

※ 補足

ローカルで開発していると、モデルのプロパティなどを頻繁に更新することがあると思いますが、すでにデータベースに保存データがあると、その度にフェッチに失敗します。
その場合は一旦永続ファイルを削除しましょう。

エンティティの関連付け

2つのエンティティを関連付けすると、一方のエンティティから他のエンティティを参照することができるようになります。

「プロジェクト名.xcdatamodeld」を開くと、Relationshipsに以下の項目が表示されています。

  • Relationship: 参照するときの名前
  • Destination: 関連付けるエンティティ名
  • Inverse: 逆関連。関連付けされるクラスから見て、関連付けするクラスを何の名前で参照するかを設定します。AppleはすべてのRelationshipに Inverse を設定することを推奨しています。 

関連には、1対1(To-One Relationships)、1対多(To-Many Relationships)、多対多(Many-To-Many Relationships)の3種類がありますが、ここでは、ユーザがある1つのグループに所属しているのを想定し、Group エンティティと User エンティティを1対多で関連付ける例でご紹介します。.xcdatamodeldでエンティティを以下のように設定します。

User エンティティの設定

スクリーンショット_2015-11-28_12_10_39.png

Relationship: group
Destination: Group
inverse: users

大事なのは、右側の Data Model Inspector で Relationship の Type が To Oneであることです。こうなっていると user.group で ユーザが所属するグループを参照できるようになります。(実際に参照するには、user.group = Group のインスタンス をセットしてあげましょう)

Group エンティティの設定

スクリーンショット_2015-11-28_12_10_27.png

Relationship: users
Destination: User
inverse: group

グループに紐づくユーザは複数いるので、Relationship の Type を To Many に設定しましょう。group.users で特定のグループに所属する全ユーザを参照できるようになります。対多のエンティティを参照する際は、NSSet で返されます。NSSet は NSArray と違って並び順が固定でないことに注意して下さい。

NSPredicate と関連

検索の条件文に関連するエンティティの属性を指定したい場合は、以下のように書くことができます。

let fetchRequest = NSFetchRequest(entityName: "User")
let predicate = NSPredicate(format: "group.name = %@", "グループA")

let fetchRequest = NSFetchRequest(entityName: "Group")
let predicate = NSPredicate(format: "ANY users.name = %@", "sato") // To-Many の場合は ANY をつける

削除ルール (Delete Rules)

関連元を削除した場合に、関連先のレコードをどうするかを設定できます。ちょうど上のキャプチャでピンクで囲った Type の上に Delete Rule の項目が見えると思います。

  • No Action: 自分が消えても関係先オブジェクトには何もしない
  • Nullify: 自分が消えたら関係先オブジェクトが持っている自分を null にする
  • Cascade: 自分が消えたら関係先オブジェクトも一緒に削除する
  • Deny: 関連先が残っていたら自分を削除できなくする

Core Dataをマルチスレッドで使用する方法

CoreDataは大量のデータを扱うことがあるので、時にはバックグラウンドで処理を行いたい、ということもあると思います。しかし、NSManagedObject、NSManagedObjectContext、 NSPersistentStoreCoordinator はスレッドセーフではありません。なので、バックグラウンド処理などでメインスレッドとは別のスレッドでも CoreData を扱う場合、スレッド間でそれらオブジェクトの整合性を保つためにはどうすればいいのかを知る必要があります。ただ、NSPersistentStoreCoordinator に関しては、各スレッドの NSManagedObjectContext がアクセスしたときに他からのアクセスをロックするようになっているので、開発者が意識的にスレッド間のコントロールをする必要はありません。

実装前の注意点として、もしバックグラウンドでメインスレッドのコンテキストを参照しても、デフォルトだと実行時に何もエラーにならないのですが、散発的にアプリがクラッシュする原因になります。ローカルでCoreDataのマルチスレッド実装を行うときは、スキーマの編集の「Run」→「Arguments」で、以下の引数を追加しましょう。こうすると、実装が間違っている時にエラーにしてくれます。

-com.apple.CoreData.ConcurrencyDebug 1

NSManagedObjectの扱い方

NSManagedObject には objectID というプロパティがあり、これが唯一のスレッドセーフのプロパティになっています。他のプロパティは更新処理によって変わってしまう可能性があるため、各スレッドで同じオブジェクトを参照する際は、objectIDでフェッチするようにしましょう。

ObjectID でフェッチする際は、self を指定します。

let objectID = user.objectID
let predicate = NSPredicate(format: "self = %@", objectID)

NSManagedObjectContext の扱い方

各スレッドでそれぞれ NSManagedObjectContext を作成し、それぞれの変更をコンテキスト間でうまく更新していきます。やり方は2つあります。

1. NSManagedObjectContext に親子関係を作る方法(推奨)

メインスレッドで生成される NSManagedObjectContext を親、メインスレッド以外で生成される NSManagedObjectContext を子にすると、子の NSManagedObjectContext が更新されると同時に親の NSManagedObjectContext も更新されます。子の NSManagedObjectContext は、更新時に親の NSManagedObjectContext に通知するだけで、NSPersistentStoreCoordinator へ直接アクセスすることはありません。なので、変更内容を永続化するには、親のコンテキストで再度保存処理を行う必要があります。(永続化できるのはルートコンテキストのみ)

NSManagedObjectContext は parentContextプロパティを持っており、これが nil なら自分がルートコンテキスト、他のコンテキストが入っていれば そのコンテキストが親となります。

まず、子の ManagedObjectContext を生成します。init時に.PrivateQueueConcurrencyType を指定し、生成したコンテキストのperformBlock:(非同期)またはperformBlockAndWait:(同期)メソッドのブロック内に専用スレッドで実行したい処理を書きます。ちなみに Type は全部で3つ指定できます。

Type 説明
.MainQueueConcurrencyType 生成した ManagedObjectContext をメインスレッドのみで使用します
.PrivateQueueConcurrencyType 生成した ManagedObjectContext を専用のスレッドで使用します。
メインスレッドから参照するとエラーになります。
.ConfinementConcurrencyType コンテキストを生成したスレッドでのみ使用可能。後方互換のために残されているが、現在は非推奨
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate

// バックグラウンド用のコンテキストを作成
let tempContext: NSManagedObjectContext = NSManagedObjectContext.init(concurrencyType: .PrivateQueueConcurrencyType)

// 親のコンテキストにはメインスレッドのコンテキストをセット
tempContext.parentContext = self.appDelegate.managedObjectContext

// このコンテキストの専用スレッドで、実行したい処理をブロック内に書く
tempContext.performBlock {
  print(NSThread.isMainThread()) // false

  // ユーザを作成する
  let user = NSEntityDescription.insertNewObjectForEntityForName("User", inManagedObjectContext: tempContext) as! User

  user.id = 1
  user.name = "sato"

  do {
    try tempContext.save()
  } catch let error as NSError {
    print(error)
  }
}

これで親の ManagedObjectContext も自動的に更新されます。しかし、 Persistent Store への更新でメインスレッドの処理がちょっとの間妨げられるので、実際の構成としては、下記のようにバックグラウンドで書き込みする用のコンテキストをメインスレッドの親のコンテキストに設定しておくといいかもしれません。

let writeContext: NSManagedObjectContext = NSManagedObjectContext.init(concurrencyType: .PrivateQueueConcurrencyType)

let mainContext: NSManagedObjectContext = NSManagedObjectContext.init(concurrencyType: .MainQueueConcurrencyType)
mainContext.parentContext = writeContext

let backgroundContext: NSManagedObjectContext = NSManagedObjectContext.init(concurrencyType: .PrivateQueueConcurrencyType)
backgroundContext.parentContext = mainContext

子のコンテキストで保存されたことを通知で受け取りたければ、NSManagedObjectContextDidSaveNotification をリッスンすれば良いですが、NSFetchedResultsController ならデリゲートを通じてビューコントローラでコールバックとして受け取ることも可能です。

2. 通知で各スレッド間の更新差分をマージする(古いやり方)

指定したコンテキストを save した時に、新規追加・変更・削除のいずれかがあればNSManagedObjectContextDidSaveNotificationのイベントが発火するのを利用して、コンテキスト間のマージ作業を行います。マージすると、バックグラウンドのコンテキストで変更された値が、メインコンテキストの方にも反映されます。マージにはmergeChangesFromContextDidSaveNotificationメソッドを使用します。

ManagedObjectContext はスレッドごとに作成して、それらを1つの PersistentStoreCoordinator に紐付けます。通知監視とマージ処理は同じスレッドで行うようにしましょう。

// バックグラウンドのコンテキストの変更をリッスンする
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("didChangeTempContext:"), name: NSManagedObjectContextDidSaveNotification, object: tempContext)

// バックグラウンドの処理
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
  // 更新処理
  ... 

  do {
    try tempContext.save()
  } catch let error as NSError {
    print(error)
  }
}

// 最後にリッスンを解除
NSNotificationCenter.defaultCenter().removeObserver(self)

...

// 通知があると呼ばれるメソッド
func didChangeTempContext(notification: NSNotification) {
  // バックグラウンドの変更をメインスレッドに反映する
  self.appDelegate.managedObjectContext.mergeChangesFromContextDidSaveNotification(notification)
}

コンテキストの新規追加、更新、削除などの情報は、NSNotification に含まれています。

※ Google Analytics を使用していて、addObserver で通知を監視するときに、object: に nil を指定すると、Google Analytics のオブジェクトが飛んで来る時があり、マージがうまく行かずアプリがクラッシュすることがあるようです。監視対象のコンテキストはきちんと指定するようにしましょう。

バックグラウンドの処理を終えた後にメインスレッドに戻って処理をする

バックグラウンドで取得したデータをもとに、メインスレッドでUIを更新したい場合などは以下のように書きます。NSManagedObject はスレッドセーフではないので、他のスレッドに直接渡せないことは、上記で説明済だと思います(厳密には渡せるのですが、ObjectID以外のプロパティを参照するとエラーになります)。なので、一度メインスレッドでバックグラウンドでフェッチした NSManagedOject の ObjectID を取得して、再度フェッチするようにします。

var users: [User] = []

temContext.performBlock {
  var temp_users = []

  do {
    // フェッチなどの処理
    let temp_users = ...
  } catch {
  }

  // メインスレッドに移行
  mainContext.performBlockAndWait {
    let objectIds = temp_users.map{ $0.objectID }
    for objectId in objectIds {
      do {
        let user = try self.appDelegate.managedObjectContext.existingObjectWithID(objectId) as! User
        users += [user]
      } catch {              
      }
    }
    // 実行したい処理
    ...
  }
}

★ 複数スレッドにおけるチェックポイントおさらい

  • UIの更新は MainQueueConcurrencyType のコンテキストでやる
  • コンテキストを複数のスレッドで使用していないか
  • コンテキストを経由する処理は、performBlock もしくは performBlockAndWait メソッドのブロック内で行う
  • 管理オブジェクトのスレッド間の受け渡しは、管理オブジェクトIDで行う。

シミュレータのデータベースの中身を閲覧する

CoreData のデータベースは sqlite なので、まず sqlite のクライアントソフトをインストールします。私は無料で使用できるSQLite Free - Datumを使用しています。

そして、インストールしたソフトで .sqlite ファイルを開けばテーブルを見ることができます。通常 .sqlite ファイルは、アプリの /documents ディレクトリに保存されます。そこまでのパスは、

let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)

で知ることができますが、プロジェクトやシミュレータごとにパスが変わるので、毎回確認するのも面倒だと思います。そこで SimPholders2 というアプリをお勧めします。これを使うと、シミュレータのディレクトリの保存場所が一発でわかります。メニューバーにアイコンが追加され、それをクリックすると現在起動中のシミュレータが緑の○で表示され、そのシミュレータを使用しているプロジェクトの一覧が表示されます。

名称未設定2 + 名称未設定.png

選択すると、そのシミュレータのディレクトリまで移動した状態でFinderが起動してくれます。そのディレクトリのDocumentsディレクトリの直下に .sqlite ファイルがあるはずです。

スクリーンショット 2015-11-28 13.35.45.png

あとはこのファイルをインストールしたソフトで開いてみましょう。

CoreDataの処理を監視する

Xcode に付属している Instruments を使用することで、フェッチやsaveに要した時間などを見ることができます。

Xcode → Open Developer Tool → Instruments → Core Data

CoreData のデバッグ

  • 発行するSQLをコンソールに出す
-com.apple.CoreData.SQLDebug 1

※ ただし、NSPredicate に指定した値は ? で出力されるようです。



以上が CoreData の概要となります。最後に、CoreDataを実際にアプリで実装しようとすると、フェッチする時のメソッドを使い勝手がいいように自分でラップしたり、マルチスレッド時のコンテキストの扱いなどいろいろ考えることが多くなります。そういったことを考慮して作られた「MagicalRecord」というライブラリがあるので、こちらを使用することをお勧めします。

https://github.com/magicalpanda/MagicalRecord