時計仕掛けのiPhoneアプリ(6) – CoreDataを使ってみる

練習で作っている時計アプリも、そろそろ形になってきました。

前回は NSUserDefaults を使い、アプリケーションの設定を保存しました。
しかし、この方法ではデータの検索をしたり、データの相互関係(リレーション)を実現するには、少し力不足…

でも、そこは大丈夫。iPhoneはさすがスマートフォンと言うだけあって、標準でSQLiteを使う事ができちゃうんです!!(Androidもそうらしいですよ)

しかも、普段SQLを使っていない人でもある程度扱えるよう、ORマッパー的な機能が用意されています。

Appleはこれらの、バックエンドのストレージ、それらを操作するコンテキスト、データ構造を扱うモデルを、CoreData(wikipedia)というフレームワークに纏めて提供しています。

今日はこれを扱っていこうと思います。

Core Dataコトハジメ

Core Dataの手っ取り早いサンプルを見たいのなら、Xcodeで「Navigation Based Application」を作れば、Core Dataを使った簡単なアプリケーションが出来上がります。
※現段階(SDK4.0)で自動生成されるアプリケーションスケルトンでは、前回やったアプリ終了時の処理にiOS4.0以降への対応ができていないので、そこは自分で終了時コードを追記してやる必要があります。

私は自動生成されたアプリケーションのスケルトンをぼーっと眺めてみたのですが、なんだかよくわからないのですよね。
あ、それって私の理解が足りないだけですか? ソウデスカ…

理解が足りないかもしれませんが、私なりにCore Dataの初期化に必要な最低限の処理をまとめてみました。

Core Dataの基本

Core Dataでデータの永続化をするには、次のような手順で処理を作成します。

  1. XcodeでData Model(モデル構造)を作成する。
  2. Core Dataの初期化コードを書き、NSManagedObjectContext のインスタンスを作成する。
  3. NSManagedObjectContext のインスタンスを通じて、データのCRUDを行う。
  4. CRUD操作結果をファイルに書き出す。
CRUDとは
「作成(Create)」「読み出し(Read)」「更新(Update)」「削除(Delete)」をそれぞれ頭文字で表したもの。

XcodeでData Modelを作成する

Data Modelというのは、Core Dataフレームワークに、モデルのデータ構造を伝えるためのファイルです。

Data Modelは、Xcode上で 新規ファイル → iPhone OS → Resource → Data Model と辿ることで作成することが出来ます。

「Navigation Based Application」でアプリを作成した場合、自動生成されるファイルの中に 「Hogehoge.xcdatamodeld」というファイルが見当たるはずです。これがData Modelのファイルになります。
ちなみに、Hogehogeは適当です。作成したプロジェクト名に合わせて読みかえてくださいね。

拡張子は [.xcdatamodeld] になっていますが、上記の新規ファイルから辿って作成した場合、標準では [.xcdatamodel] という拡張子になります。
両者の違いは、 [.xcdatamodeld] は [.xcdatamodel] のディレクトリのようなもので、[.xcdatamodeld] の中に複数の [.xcdatamodel] を入れることができます。

Data Modelの作成は次のようなグラフィカルな画面でモデルを作成することができます。

Data Model作成画面

NSManagedObjectContext のインスタンスを作成する

Data Modelを作成したら、今度はこのモデルを扱う為のコードを書きます。

Core Dataを使う為には最低限、次のインスタンスを作成する必要があります。

NSManagedObjectModel
機能:Data Modelで作成したモデル情報を読み込み、プログラムコード上で利用できるようにします。
NSPersistentStoreCoordinator
機能:[NSPersistentStore]クラスを使い、ファイルの読み書きを行ないます。
NSManagedObjectContext
機能:データベースの1レコードに相当する[NSPersistentObject]クラスのを使い、データの追加・削除を行ないます。

では、実際に上記のクラスのインスタンス化をしてみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
static NSString *const MODEL_NAME = @"Hogehoge";
static NSString *const DB_NAME = @"hoge.sqlite"

// アプリケーションのDocumentsディレクトリへのパスを返す
- (NSString *)applicationDocumentsDirectory
{
    return [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
}

// NSManagedObjectContextをインスタンス化して返す
- (NSManagedObjectContext *)managedObjectContext
{
    NSURL *modelURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:MODEL_NAME ofType:@"momd"]];
    NSURL *storeURL = [NSURL fileURLWithPath:[[self applicationDocumentsDirectory] stringByAppendingPathComponent:DB_NAME]];

    NSManagedObjectModel *managedObjectModel;
    NSPersistentStoreCoordinator *persistentStoreCoordinator;
    NSManagedObjectContext *managedObjectContext;
    NSError *error = nil;
   
    // NSManagedObjectModel をインスタンス化
    managedObjectModel = [[[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL] autorelease];

    // NSPersistentStoreCoordinator をインスタンス化
    persistentStoreCoordinator = [[[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:managedObjectModel] autorelease];
    if (![persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }    

    // NSManagedObjectContext をインスタンス化
    if (persistentStoreCoordinator != nil) {
        managedObjectContext = [[[NSManagedObjectContext alloc] init] autorelease];
        [managedObjectContext setPersistentStoreCoordinator:persistentStoreCoordinator];
    }

    return managedObjectContext;
}

modelURL を作成する際に 「ofType:@”momd”」と書いていますが、これはData Modelを[.xcdatamodeld]形式で作成した場合です。
[.xcdatamodel]の場合は「ofType:@”mom”」と書けば良い様です。

NSPersistentStoreCoordinatorのインスタンスに「addPersistentStoreWithType:」メッセージを使い、実際にデータを保存するストレージを設定しています。
今回はSQLiteを使っていますが、SQLiteの他にも、バイナリファイルへの書き出しや、XMLファイルへの書き出しもサポートしています。あまり使うことはないと思いますが…

データのCRUDを行う

NSManagedObjectContext を作成できれば、あとは NSManagedObjectContext を通じてデータのCRUDが実現できます。

データベースに入っているモデル情報の1レコード(配列で言えば、配列の1要素)ごとに、[NSManagedObject] のインスタンスとして作成されます。
[NSManagedObject] は NSMutableDictionary に似たインタフェースを持っていて、値の取り出し、書き込みをすることができます。

データの新規作成

新規作成は、[NSEntityDescription] クラスの [insertNewObjectForEntityForName:] メッセージを利用します。

1
2
3
4
5
6
7
8
9
10
// insert用の新しい NSManagedObject を作成する。
NSManagedObject* newObject;
newObject = [NSEntityDescription insertNewObjectForEntityForName:@"ENTITY_NAME" inManagedObjectContext:self.managedObjectContext];

// 初期データを登録する
// setValue には基本的にオブジェクトしか入らない為、
// 整数やBOOLを入れるときには、NSNumber でラップする
[newObject setValue:[NSNumber numberWithBool:NO] forKey:@"on"];
[newObject setValue:[NSDate date] forKey:@"timeStamp"];
[newObject setValue:@"hello!!" forKey:@"message"];

ここで、NSManagedObjectContext のインスタンスは self に入っているものとします。

“ENTITY_NAME”とは、DBで言うテーブル名のようなものです。

NSManagedObjectContextは「モデル情報」「読み書きするストレージ」の情報を持っているため、あとはモデル情報の中の、どのテーブル(エンティティ)かを指定すれば、新規作成に必要な項目はすべて揃う訳ですね。

データの取り出し

データの取り出しは少し面倒です。次の手順で行ないます。

  1. [NSFetchRequest]インスタンスを作成する。
  2. [NSFetchRequest]のインスタンスにDBから検索する条件を設定する。
  3. [NSFetchRequest]のインスタンスを検索条件に、[NSFetchedResultsController]のインスタンスを作成する。
  4. [NSFetchedResultsController]のインスタンスに[performFetch:]メッセージを送り、検索結果を取得する。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
    // DBから読み取るためのリクエストを作成
    NSFetchRequest *fetchRequest = [[[NSFetchRequest alloc] init] autorelease];
   
    // 取得するエンティティを設定
    NSEntityDescription *entityDescription;
    entityDescription = [NSEntityDescription entityForName:@"ENTITY_NAME" inManagedObjectContext:self.managedObjectContext];
    [fetchRequest setEntity:entityDescription];
   
    // ソート条件配列を作成
    NSSortDescriptor *desc;
    desc = [[[NSSortDescriptor alloc] initWithKey:@"timeStamp" ascending:YES] autorelease];
   
    NSArray *sortDescriptors;
    sortDescriptors = [[[NSArray alloc] initWithObjects:desc, nil] autorelease];
    [fetchRequest setSortDescriptors:sortDescriptors];
   
    // 取得条件の設定
    NSPredicate *pred;
    pred = [NSPredicate predicateWithFormat:@"key = %@", key];
    [fetchRequest setPredicate:pred];
   
    // 取得最大数の設定
    [fetchRequest setFetchBatchSize:1];
   
    // データ取得用コントローラを作成
    NSFetchedResultsController *resultsController;
    resultsController = [[[NSFetchedResultsController alloc]
                          initWithFetchRequest:fetchRequest
                          managedObjectContext:[self managedObjectContext]
                          sectionNameKeyPath:nil
                          cacheName:entityName] autorelease];  
   
    // DBから値を取得する
    NSError *error;
    if (![resultsController performFetch:&error]) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }

    // 取得結果は[fetchedObjects]プロパティに入っている
    NSArray *result = resultsController.fetchedObjects;

[NSPredicate]による検索条件の設定や[setFetchBatchSize:]メッセージの設定はオプションです。これを設定しない場合、検索の絞り込みは行われません。

データの更新

データの更新は特にコードはありません。

データの新規作成時に値の初期値を設定するコードを記載しましたが、あれと同じように [setValue:forKey] メッセージを使い、直接 NSManagedObject のインスタンスを書き換えれば良いです。

データの削除

値の削除は、[NSManagedObjectContext]のインスタンスに[deleteObject:]メッセージを送り、
引数として削除したい[NSManagedObject]のインスタンスを渡すだけです。

1
2
3
4
5
6
7
8
    // self.deletableObjは削除したいオブジェクトとします
    NSManagedObject *obj = [self.deletableObj];
    if (! obj) {
        return;
    }

    // 値を取り除く
    [self.managedObjectContext deleteObject:obj];

保存を忘れずに

さて、上記の方法でデータのCRUDを実現できましたが、上記のコードだけではファイルへの書き出しは行われません。

ファイルへの書き出しを行うためには、[NSManagedObjectContext] のインスタンスに [save] メッセージを送ります。

下記のように、AppDelegateに書いておけばよいでしょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)applicationDidEnterBackground:(UIApplication *)application {
    NSError *error = nil;
    NSManagedObjectContext *context = self.managedObjectContext;
    if (context != nil) {
        if ([context hasChanges] && ![context save:&error]) {
            NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
            abort();
        }
    }
}

- (void)applicationWillTerminate:(UIApplication *)application {
    NSError *error = nil;
    NSManagedObjectContext *context = self.managedObjectContext;
    if (context != nil) {
        if ([context hasChanges] && ![context save:&error]) {
            NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
            abort();
        }
    }
}

今回もサンプルです

前回のNSUserDefaultsをCoreDataに置き換えてみたものです。

時計アプリ6

Core Dataの使いどころは?

私はNSUserDefaultsを置き換える目的でCore Dataを利用しましたが、本来、Core DataはSQLを使えることもあって、リレーションを駆使した複雑な構造を記述するのに適しています。

私の時計アプリのような小規模なアプリケーションの場合、Core Dataの出番は有りません。実際のところ、コード量が増えるだけで、特別メリットはありませんでした。

しかし、中規模〜大規模アプリケーションではCore Dataの持つ意義は非常にあると思います。モデルデータを通して、各コントローラ、ビュー間でデータの相互利用を可能にしたり、複雑なデータ構造も比較的簡単に記述出来るメリットは大きいと思います。

「Navigation Based Application」の初期コードでCore Dataが使われているのも、そのような理由なのかもしれません。

今度、機会があればナビゲーションベースのアプリケーションを作ってみたいと思いましたね。

このエントリーを含むはてなブックマーク Buzzurlにブックマーク livedoorクリップ Yahoo!ブックマークに登録

関連記事

タグ

トラックバック&コメント

この投稿のトラックバックURL:

トラックバック

コメント

  1. みんと より:

    xcdatamodelの中身ってどうやってみるんですか?
    ターミナルとかでSQL文たたくと見れたりしますか?
    出来ればバカな子でも分かるように教えてください


コメントをどうぞ

このページの先頭へ