この画像は、とあるアプリのWeb閲覧履歴の画面です。CoreDataを使っており、Attribute=timeStamp, type=Date という事にしてアクセス日時順に表示しています。Safariの履歴表示と同じような感じです。
今回ご紹介する方法は、画像のように日付毎にセクション分けしてセクションタイトルを日付にするという方法です。CoreDataのEntityにそのまんまセクション分類用のAttributeを追加すれば簡単ですが、ここで紹介する方法はちょっと違っており、冒頭で紹介した「timeStamp」を元にして分類します。timeStampは時分秒まで細かく保存されているのでそのままは使えないので工夫が必要です。ではどうするかというと…Appleのサンプルプロジェクト Custom Section Titles with NSFetchedResultsController を使います!やっほー、Apple最高!
ポイント
CoreDataのEntityにセクション分類用のAttributeを追加して、Transientにチェックを入れます。「あれ?おまえセクション分類用のAttribute追加しないって言ったじゃん?」と思った方。ぐぬぬ…。Transientにチェックを入れたAttributeは永続ストアに保存せずに動的に生成するので、無駄にストレージを圧迫しないですみますし何よりモデルのバージョンを上げなくても対応できるというメリットが有ります。なにより後から内容を変更したくなった際も動的であるがゆえに自由自在です。後述しますが、動作速度に関しても気になる速度低下は感じられません。
あと多分、Safariの履歴も同じ仕組みでやってそうです。「今日の朝」「今日の昼」なんていうセクションの分け方とか、まさに動的生成の賜物といえるのではないでしょうか。
モデルクラスの対応
NSManagedObjectを継承したモデル専用のクラスは作ってますよね。どういった内容にするかはサンプルプロジェクトの APLEvent.h, APLEvent.m を参考にしてください。sectionIdentifierプロパティを取得できるようにします。
Appleのサンプルをそのままコピペすると「月毎」になってしまって今回の目的である「日毎」にならないので、sectionIdentifierメソッドは以下のようにしています。これで、timeStampが「2013-12-19 11:23:20 +00:00」だった場合にsectionIdentifierが返す値が「20131219」になります。
- (NSString *) sectionIdentifier {
[self willAccessValueForKey:@"sectionIdentifier"];
NSString *tmp = [self primitiveSectionIdentifier];
[self didAccessValueForKey:@"sectionIdentifier"];
if(!tmp) {
NSCalendar *calendar = [NSCalendar currentCalendar];
NSDateComponents *components =
[calendar components:(NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit) fromDate:[self created]];
tmp = [NSString stringWithFormat:@"%ld", (long int)(components.year * 10000) + (components.month * 100) + components.day];
[self setPrimitiveSectionIdentifier:tmp];
}
return tmp;
}
Appleのサンプルにはないのですが、sectionIdentifierの文字列から日付を生成するメソッドも追加しておきました。日付のパースと生成はモデル側のクラスでやろうという魂胆です。
+ (NSDate *) dateFromSectionIdentifier:(NSString *)sectionIdentifier {
NSInteger numericSection = [sectionIdentifier integerValue];
NSInteger year = numericSection / 10000;
NSInteger month = (numericSection - (year * 10000)) / 100;
NSInteger day = numericSection - (year * 10000) - (month * 100);
NSDateComponents *dateComponents = [[NSDateComponents alloc] init];
dateComponents.year = year;
dateComponents.month = month;
dateComponents.day = day;
return [[NSCalendar currentCalendar] dateFromComponents:dateComponents];
}
コントローラの対応
ここまでできたらあとは簡単です。NSFetchedResultsControllerのsectionNameKeyPathにsectionIdentifierを指定すると、いい感じに分類してくれます。もちろんtimeStampでソートしておく必要があります。詳しくは APLMasterViewController.m をご参考ください。ちなみにsectionIdentifierでソートしようとすると例外で落ちますのでご注意を。sectionIdentifierが永続ストレージに存在しないのが原因です。
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:@"sectionIdentifier"
cacheName:nil];
セクションヘッダの表示部分はこのようになりました。
- (NSString *) tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> theSection = [self.fetchedResultsController.sections objectAtIndex:section];
static NSDateFormatter *formatter = nil;
if(!formatter) {
formatter = [[NSDateFormatter alloc] init];
[formatter setCalendar:[NSCalendar currentCalendar]];
NSString *formatTemplate = [NSDateFormatter dateFormatFromTemplate:@"yMMMdEEE" options:0 locale:[NSLocale currentLocale]];
[formatter setDateFormat:formatTemplate];
}
NSDate *date = [APLEvent dateFromSectionIdentifier:theSection.name];
return [formatter stringFromDate:date];
}
速度面はどうなのか?
実際のところ、このようなやり方をしていたら速度低下がすごくなりそうに思いましたので検証してみました。1万件のデータ(timeStampについても千差万別)をテーブルに表示してみたところ、100件の時と比べて体感で0.2〜0.5秒弱くらい遅くなる感じの速度で表示できました。特に意識しないと気になりません。iPod touch4thでの検証です。テーブルに1万件とかそもそも表示する必要はないので、fetchLimitで制限しておけば安心でしょう。
ただし、 fetchBatchSizeに小さい値を指定するととても遅くなります。
「-com.apple.CoreData.SQLDebug 1」でログを見ているとすぐわかるのですが、sectionIdentifierを生成するためにtimeStampを全て取得しており、fetchBatchSizeが指定されているとその分繰り返してリクエストするのでリクエスト数が無駄に多くなります。fetchBatchSizeは指定しない方がいいです。
結局この方法を採用するかどうかは、動的生成のメリットを取るか、fetchBatchSizeや動作速度を取るのかのトレードオフになるでしょうか。