HOME » Natsu note » 古い投稿 » NSFetchedResultsController : sectionNameKeyPath設定時の注意

NSFetchedResultsController : sectionNameKeyPath設定時の注意 2010/03/11/|古い投稿

この記事は情報が古い可能性があります。参照する際にはご注意ください。

Core DataをUITableViewと組み合わせて使う場合、NSFetchedResultsControllerを使うと非常に便利。
NSFetchedResultsControllerとは、一言で言えば、Core Data(の永続ストア)からフェッチしたデータを、UITableViewに表示しやすいようにsectionとrowで管理してくれるものだ。fetchRequestだけを使ってあとは自力でやるより、遥かに楽になるのでぜひ活用したい。

が、はまりどころがいくつかある。セクション情報あたりの挙動だ。
sectionNameKeyPathを指定して、セクション情報を生成する場合、以下のことは結構重要。

まず、FetchedResultsControllerの生成は以下のメソッドを使う。

- (id)initWithFetchRequest:(NSFetchRequest *)fetchRequest
      managedObjectContext:(NSManagedObjectContext *)context
        sectionNameKeyPath:(NSString *)sectionNameKeyPath
                 cacheName:(NSString *)name

引数のうち、fetchRequestとcontextはよいとして(これは実際にフェッチしたいデータとそのコンテキストを指している)、残りの二つ、sectionNameKeyPathとcacheNameの設定にはいくつか注意事項がある。

セクション名の指定

Core Dataのデータをフェッチして、そのままUITableViewに表示する際、フェッチするエンティティがもつ一つのString型属性をそのままセクション名にすることができる。
例えば日付ごとにセクション分けを行うことを考える。
エンティティが、NSString *dateString という属性をもっているとして、この属性を使ってセクション分けを行う。その場合、sectionNameKeyPathに渡すのは@“dateString”となる。

なお、sectionNameKeyPathにnilを渡すとセクション情報をもたない(セクション数1の)テーブルが生成される。

ソートデスクリプタとの関係

fetchRequestに設定したソートデスクリプタ(sortDescriptors)の第一条件キーと異なるキーをsectionNameKeyPathに指定できるのは、どちらでソートしても同じデータの集合ができる場合のみ。

今、実際に作成している家計簿アプリのプロトタイプを例にあげてみる。

Transactionというエンティティが、

NSDate *date;
NSString *dateString;

という日付に関する二つの属性をもつ。

dateStringは、一時的属性であり永続ストアには保存されない。getterで、

- (NSString *) dateString {
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setLocale:[NSLocale currentLocale]];
    [dateFormatter setDateStyle:NSDateFormatterShortStyle];
    NSString *ret = [dateFormatter stringFromDate:self.date];
    [dateFormatter release];
    return ret;
}

という処理をして、currentLocaleにあわせた書式の文字列を返している。

この場合、dateでソートしても、dateStringでソートしても、同じ集合ができあがるため、以下のように

  • ソートデスクリプタの第一条件:date
  • sectionNameKeyPath:dateString

とすることが可能。

NSSortDescriptor *descriptor = [[NSSortDescriptor alloc] initWithKey:@"date" ascending:NO];
NSArray *sds = [[NSArray alloc] initWithObject:descriptor];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];

MyAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *managedObjectContext = [appDelegate managedObjectContext];

NSEntityDescription *entity = [NSEntityDescription entityForName:@“Transaction” inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
[fetchRequest setSortDescriptors:sds];

NSFetchedResultsController *aFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
                                    managedObjectContext:managedObjectContext
                                      sectionNameKeyPath:@"dateString"
                                               cacheName:@"transactionSortedByDate"];

つまり、ソート自体はdateを使うけれど、セクション名はdateStringで作るということ。

逆に、設定できない例は、上記エンティティが金額を示す

NSNumber *amount;

という属性をもっているとして、ソートデスクリプタの第一条件を

NSSortDescriptor *descriptor = [[NSSortDescriptor alloc] initWithKey:@"amount" ascending:NO];

とした場合。

amountが同じオブジェクトの集合とdateStringが同じオブジェクトの集合は異なるため、このような設定をするとperformFetchでやむなくエラーとなる。

キャッシュ

もう一つ注意したいのがキャッシュ。initWithFetchRequestの最後の引数cacheNameだ。
キャッシュ名を指定すると、セクション情報が先に計算されキャッシュされる。
さらに、フェッチ実行時に同名のキャッシュを探しにいき、fetchRequestの条件が一致している場合には、そのキャッシュからセクション情報を引っ張ってくる。
これによって、セクション情報を生成する際のオーバーヘッドを軽減できるという優れもの。

もし、同名のキャッシュが存在していても、fetchRequestが異なる場合、キャッシュを削除して再計算を行う。
つまり、、、
ソート条件などの異なるfetchRequestを利用して再度fetchedResultsControllerを生成し直す場合には、異なるキャッシュ名を指定するべきである。そうしないと、結果的に何度もキャッシュをクリアしては書き直すを繰り返してあまり恩恵を受けられない。

なお、この「キャッシュ」はメモリ上に存在するのではなく、ディスクに保存されている。したがって、次回起動時も有効となる。

sectionNameKeyPathに一時的属性を指定している場合の注意

もし、sectionNameKeyPathに一時的な属性を指定している場合、キャッシュが存在していると再計算されないため注意が必要である。

上の例をそのまま使って説明しよう。
家計簿のセクション表示は日付にしたいのだが、その日付フォーマットはiPhoneの設定(設定→一般→言語環境→地域)に依存させたい。
そのため、dateStringは永続ストアに保存せず、あえてその場で計算をしている。
にもかかわらず、途中で地域を変更した場合に(そうやって使う人は滅多にいないだろうけれど・・・)、セクション名に二つの日付フォーマットが混在してしまってうまくセクション分けができなくなってしまった。

この理由がまさにキャッシュだ。

fetchRequestの条件はまったくかわっていないため、キャッシュに保存されているセクション情報がそのまま利用されてしまったのだ。

キャッシュの削除

上記のような問題を回避するために、強制的にセクション情報をすべて再計算させたいときは、キャッシュを削除する必要がある。

キャッシュのクリアは以下のくらすメソッドでできる、

+ (void)deleteCacheWithName:(NSString *)name

なお、もしすべてのキャッシュをクリアしたければ、nameにnilを渡せばよい。

セクション情報がうまく更新できなくて困っている場合にはキャッシュをチェック。