CS193p - Lecture 13

iTunes U スタンフォード大学のiOSアプリ開発講義のLecure 13(Core Data)の講義メモです。

Core Data

Core Dataはストレージの基盤でSQLデータベースの上で成り立っている。

Xcodeのツールを使ってビジュアルにオブジェクトとデータベースをマッピングさせることが出来る。そしてそれがNSManagedObjectのサブクラスになる。

■Xcodeのビジュアルマップ(Data Model)で行われるマップ

Entity -> クラス
Attribute -> プロパティ
Relationship -> データベースの中で他のオブジェクトをポイントするプロパティ

・Attributesで設定したもの

Integer -> NSNumber
String -> NSString
Boolean -> NSNumber
Binary data -> NSData
Date -> NSDate

■コードからアクセスする方法

NSManagedObjectContextのインスタンスを作成してそのインスタンスにメッセージを送ることでアクセスする。
・インスタンスを作成する2つの方法

  • UIMangedDocument(iOS 5の新クラス)を作成してそのmanagedObjectContextプロパティを使う
  • プロジェクト作成時に"Use Core Data"にチェックを入れる

AppDelegateがmanagedObjectContextプロパティを持っているのでそれを使う

UIManagedDocument

UIManagedDocumentは、UIDocumentのサブクラスで、Core Dataデータベースを保持する枠組みである。

■UIManageDocumentの作成

UIManagedDocument *document =

[[UIManagedDocument alloc] initWithFileURL:(URL *)url];

このURLはアプリのサンドボックス内のDocumentsディレクトリやCachesディレクトリなどを指定する。
このインスタンスを作成した状態ではまだファイル操作は一切行っていない状態。

・documentの作成

ファイルの存在チェック

[[NSFileManager defaultManager] fileExistsAtPath:[url path]]

存在していれば、documentをオープン

- (void)openWithCompletionHandler:(void (^)(BOOL success))completionHandler;

存在していなければ、作成

- (void)saveToURL:(NSURL *)url

forSaveOperation:(UIDocumentSaveOperation)operation

competionHandler:(void (^)(BOOL success))completionHandler;

 

ファイルのオープンと作成は非同期に行われるので、blockで完了時の処理を引数として渡す。

<例>

self.document = [[UIManagedDocument alloc] initWithFileURL:(URL *)url];
if ([[NSFileManager defaultManager] fileExistsAtPath:[url path]]) {
    [document openWithCompletionHandler:^(BOOL success) {
        if (success) [self documentIsReady];
        if (!success) NSLog(@“couldn’t open document at %@”, url);

    }];

} else {
    [document saveToURL:url forSaveOperation:UIDocumentSaveForCreating
      completionHandler:^(BOOL success) {
        if (success) [self documentIsReady];
        if (!success) NSLog(@“couldn’t create document at %@”, url);
    }];

}

※documentを操作する処理は、block内で呼び出されているdocumentIsReadyメソッド内で行う
- (void)documentIsReady
{
    if (self.document.documentState == UIDocumentStateNormal) {
        NSManagedObjectContext *context =

self.document.managedObjectContext;

//Core Data contextを使って処理

    }
}

 

NSNotificationCenter

NSNotificationは、delegate と違ってラジオ局のようにNSNotificationCenterがブロードキャストで複数の受信者に一斉に通知を送る。
使用コストが高いので乱用は避ける。

■使用方法

1. NSNotificationCenter に通知をもらうように登録
2. 通知を受け取った際のアクションをメソッドで実装

3. 使い終わったらNSNotificationCenterの登録を削除

<例>

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center addObserver:self
               selector:@selector(contextChanged:)
                   name:NSManagedObjectContextObjectsDidChangeNotification
                 object:self.document.managedObjectContext];
}

 

- (void)viewWillDisappear:(BOOL)animated
{

[center removeObserver:self

name:NSManagedObjectContextObjectsDidChangeNotification

object:self.document.managedObjectContext];

[super viewWillDisappear:animated];

}

 

- (void)contextChanged:(NSNotification *)notification
{

/* notification.userInfoは次のキーを持ったNSDictionary
NSInsertedObjectsKey //挿入されたオブジェクトの配列
NSUpdatedObjectsKey //Attributesが更新されたオブジェクトの配列NSDeletedObjectsKey //削除されたオブジェクトの配列

*/

}

 

UIManagedDocument

■documentの保存

documentの保存は非同期で自動的に行われる。
明示的に行うには次のようにする。

[self.document saveToURL:self.document.fileURL

forSaveOperation:UIDocumentSaveForOverwriting completionHandler:^(BOOL success) {

if (!success) NSLog(@“failed to save document %@”, self.document.localizedName);

}];

 

<比較>
UIDocumentSaveForOverwritingはファイルが存在していなければエラー
UIDocumentSaveForCreatingはファイルが存在していればエラー

■documentのクローズ

UIManagedDocumentへのstrongポインタがなくなれば自動的に非同期で閉じられる。
明示的に行うには次のようにする。

[self.document closeWithCompletionHandler:^(BOOL success) {

if (!success) NSLog(@“failed to close document %@”, self.document.localizedName);

}];

 

■同一documentへの異なるポインタ

同一documentへ異なるポインタを持つと、異なるNSMagedObjectContextを持つ。しかし、一方で起こった変更は自動的には他方へ反映されない。そのため、一方を変更したら他方ではrefetchしないといけない。
コンフリクトが発生したら自分で解決する。
同時に書き込むケースはレアかもしれないが、複数の参照者がいるケースは多々ある。

Core Data

■データベースへオブジェクトを挿入

NSManagedObject *photo =

[NSEntityDescription insertNewObjectForEntityForName:@“Photo”

inManagedObjectContext:(NSManagedObjectContext *)context];

 

■MSmanagedObjectインスタンスのAttributesへアクセス

NSKeyValueObservingプロトコルの次のメソッドを使う

- (id)valueForKey:(NSString *)key;
- (void)setValue:(id)value forKey:(NSString *)key;

※valueForKeyPath:/setValue:forKeyPath:を使って、ドットで関係をたどって行ける
(例)_contents.description

■Core Dataへのデータ書き込み時の注意点

変更した内容はメモリ上にあり、SQLデータベースには保存されていない。
UIManagedDocumentは自動保存機能があるが、多くの変更を行った時は明示的に保存するのが良い。

■よりエレガントなアクセス

valueForKey:/setValueForKey:はキー名を文字列で指定することになるので、これをさけるために、NSManagedObjectのサブクラスを作ってそのプロパティでアクセスする。

・NSManagedObjectのサブクラスの作成

XcodeでCore Dataのグラフィカルマップ図でEntitiesの中から作成したいエンティティを選んでメニューから Editor > Create NSmanagedObject Subclassを選択する。

※選択時に表示されるscalarプロパティのオプションは、数字をプリミティブな型にするかNSNumberオブジェクトにするかの選択であるが、NSDateは1970年からの時間に変換されてしまうので、このオプションは外しておくのが良い
→これで出来たファイルには、AttributesとRelationshipsがプロパティとして定義されている。しかし、各エンティティの関係は順番に評価されて行くので、最初に評価されたエンティティは他のエンティティへの正確なポインタがわからないので、MSManagedObjectへのポインタとしてプロパティ定義されてしまっている。
これを解決するために、再度メニューからサブクラスの作成を行う。

・実装ファイル

実装ファイルには@sysnthesizeに代わりに@dynamicが定義されているが、これは、@synthesizeを行っていないことでコンパイラにwarningを出させないようにしている。@synthesizeでsetter/getterを定義してないので、実行時には任意のメソッドを実行することが出来て、この場合はNSManagedObjectがvalueForKey:やSetValueForKey:を実行する。

・ドット表記でのアクセスの方法

Photo *photo = [NSEntityDescription

insertNewObjectForEntityForName:@“Photo” inManagedObj...];

NSString *myThumbnail = photo.thumbnailURL;
photo.thumbnailData = [FlickrFetcher

urlForPhoto:photoDictionary format:FlickrPhotoFormat...];

photo.whoTook = ...; //Photographerオブジェクトを示す
photo.whoTook.name = @“CS193p Instructor”; //連結して関係をたどれる

 

これで作成されたサブクラスに独自のメソッドを追加したい場合は、カテゴリーを使う。直接このサブクラスにコードを追加してしまうと、データベースに変更を加えて、サブクラスを再作成した時に無くなってしまい、再度追加することになる。

Objective-C カテゴリ

カテゴリーはサブクラスを作成せずにメソッドやプロパティをクラスに追加する
■構文

@interface Photo (AddOn)
- (UIImage *)image;
@property (readonly) BOOL isOld;
@end

 

■ファイル名の付け方
通常、クラス名+拡張の目的をファイル名とする。

■実装ファイルの記述例

@implementation Photo (AddOn)
-(UIImage*)image //imageisnotanattributeinthedatabase,but photoURL is
{
    NSData *imageData = [NSData dataWithContentsOfURL:self.photoURL];
    return [UIImage imageWithData:imageData];
}
-(BOOL)isOld //whetherthisphotowasuploadedmorethanadayago
{
    return [self.uploadDate timeIntervalSinceNow] < -24*60*60;
}
@end

 

※カテゴリーにはインスタンスの追加は出来ないので、@synthesizeは記述出来ない

■注意点

既にクラスに存在しているものをカテゴリーで上書きしてしまう危険がある。
既存のメソッドを書き換えてしまうともとのクラスのコンセプトから逸脱するので、上書きは一切せず、追加だけにする。

■NSManagedObjectのサブクラスのカテゴリ記述例
・データ作成

@implementation Photo (Create)
+ (Photo *)photoWithFlickrData:(NSDictionary *)flickrData
        inManagedObjectContext:(NSManagedObjectContext *)context
{
    Photo *photo = ...; //DB内に既にFlickrのphotoデータがないかチェック
    if (!photo) {
        photo = [NSEntityDescription

insertNewObjectForEntityForName:@“Photo”

                                              inManagedObjectContext:context];
        //photoをFlickrのデータで初期化
        //おそらく他のデータベースオブジェクトも作成する(Photographerなど)
    }
    return photo;
}
@end

 

・削除

[self.document.managedObjectContext deleteObject:photo];

※注意点:削除したあとにphotoへのstrongポインタを持ったままにしない

・削除の準備

@implementation Photo (Deletion)
- (void)prepareForDeletion
{

//リレーションのポインタは自動的にnilにセットされるので自分で実装する

//必要はない。

//しかし、例えばリンク先のPhotographerが削除しようとしている

//Photoの数を属性に持っていた場合は、ここでその数をデクリメントする。//(例えば、self.whoTook.photoCount--)

}
@end

 

Core Data

■データ検索

NSFetchRequestをNSManagedObjectContextの中で実行させることでデータの検索を行う。

1. 取得対象のEntityを指定
2. NSPredicateで取得対象データの制限を行う

※指定は任意で、デフォルトは全データ

3. NSSortDescriptorでデータの並び順を指定
4. 取得するオブジェクトの数(一度に取得する数、最大数)を指定

※指定は任意で、デフォルトは全データ

・NSFetchRequestの作成

//Entityの種類を指定
//  ※一度のqueryで指定するのは1つのentityのみ
NSFetchRequest *request =

[NSFetchRequest fetchRequestWithEntityName:@“Photo”];

//一度に20づつデータ取得
request.fetchBatchSize = 20;
//最大取得数は100
request.fetchLimit = 100;
//ソート順の指定
request.sortDescriptors = [NSArray arrayWithObject:sortDescriptor];

//取得対象データの制限を設定

request.predicate = ...;

 

・NSSortDescriptor
データはNSManagedObjectのNSArrayで返ってくるので、次のメソッドで並び順を指定する。

NSSortDescriptor *sortDescriptor =

[NSSortDescriptor

sortDescriptorWithKey:@“thumbnailURL”

ascending:YES

selector:@selector(localizedCaseInsensitiveCompare:)];

並び順は複数のキーで指定することがあるので、上記のをメソッドをリストで与える。

■NSPredicate

・フォーマット

NSString *serverName = @“flickr-5”;
NSPredicate *predicate =
    [NSPredicate predicateWithFormat:@“thumbnailURL contains %@”,

serverName];

 

・条件指定例

@“uniqueId = %@”, [flickrInfo objectForKey:@“id”] //ユニーク指定
@“name contains[c] %@”, (NSString *) //大文字小文字区別無しで部分文字列指定
@“viewed > %@”, (NSDate *) //日時条件指定
@“whoTook.name = %@”, (NSString *) //Photo検索 (Photographerのnameで)
@“any photos.title contains %@”, (NSString *) //Photographerからphotoを検索
 

・複合条件の指定

(a) NSPredicateの文字列の中でAND,ORで条件をつなげる

@“(name = %@) OR (title = %@)”

 
(b) NSCompoundPredicate

NSArray *array = [NSArray arrayWithObjects:predicate1, predicate2, nil];

NSPredicate *predicate =

[NSCompoundPredicate andPredicateWithSubpredicates:array];

 

■検索実装例

//Photographersの全部に対して
NSFetchRequest *request =

[NSFetchRequest fetchRequestWithEntityName:@“Photographer”];

// ...直近の24時間に写真を取った人
NSDate *yesterday =

[NSDate dateWithTimeIntervalSinceNow:-24*60*60];

request.predicate =

[NSPredicate predicateWithFormat:@“any photos.uploadDate > %@”,

yesterday];

// ... Photographerの名前でソートして

NSSortDescriptor *sortByName =

[NSSortDescriptor sortDescriptorWithKey:@“name” ascending:YES];

request.sortDescriptors = [NSArray arrayWithObject:sortByName];

//検索実行
NSManagedObjectContext *moc = self.document.managedObjectContext;

NSError *error;
NSArray *photographers =

[moc executeFetchRequest:request error:&error];

 

<前の記事

CS193p - Lecture 12    - CS193p - Lecture 14 次の記事>