ページ

2009年12月7日月曜日

Cocoaの日々: NSTableView にカスタムビューを表示する (9) カスタムセルでCoreDataを扱う

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

(前回)Cocoaの日々: メモリ対決:NSTableView+カスタムセル vs NSCollectionView

前回の結果を受けてユーザインタフェイスには NSTableView+カスタムセルの組み合わせを使うことにする。今回は NSTableView+カスタムセルと CoreData の組み合わせについて検証する。


NSTableView+カスタムセルと CoreDataの組み合わせ

※ここでいうカスタムセルとは、1つのセルの中に複数の情報を表示する特別なセルを意味する。

通常のセルを使う場合 NSTableView と CoreData の相性は非常に良くて、連結はほとんど Bindings設定だけで済む。ただカスタムセルを使う場合は少し工夫が必要となる。前回までの検証では CoreData を使わないケースでカスタムセルとデータをバインドしていた。

Cocoaの日々: NSTableView にカスタムビューを表示する (5)カスタムセルへ bindings経由でモデルオブジェクトを渡す

しかし同じ方法が CoreData の場合は使えない。理由は NSTableColumn がセルへデータを渡す時に使うメソッドが NSCopying プロトコルへの準拠を要求する為。

- (void)setObjectValue:(id <NSCopying>)object

Mac Dev Center: NSCell Class Reference

CoreData を使う場合、データ1件を NSManagedObject として扱うが、NSManagedObject 自体は NSCopying プロトコルに準拠していない。また推奨もされていないようだ。
Mac Dev Center: Core Data Programming Guide: Managed Object Accessor Methods


仮にサブクラス化して NSCopying プロトコルを実装したとしても、今度は仲介する NSArrayController で問題が起こる。Interface Builder を使った設定ではデータは NSManagedObject固定でサブクラスを指定することができない(コードで書けばあるいはできるかもしれないが不明)。

さてどうするか。


NSManagedObjectID を使う

いろいろ考えたがうまい方法が思いつかない。そこでネットを調べてみると以前紹介した StackOverflow の記事にその答えが載っていた。

Core Data, NSTableColumn bindings and custom NSCell - Stack Overflow

紹介されていたのは NSManagedObject をバインドするのではなく、その ID である NSManagedObjectID をカスタムセルとバインドする方法。質問者も私と同じ問題に悩んでいたようだ。

Mac Dev Center: NSManagedObjectID Class Reference

おー、なるほど。その手があったか。NSManagedObjectID が手に入れば後は -[NSManagedObjectContext objectWithID:] で目的の NSManagedObject を取り出して表示できる。

Mac Dev Center: NSManagedObjectContext Class Reference

NSManageObjectID は NSCopying プロトコル準拠なので先ほどの問題もクリア。

さっそく試してみよう。


サンプルプログラム

新規にプロジェクト(CustomCellWithCoreData)を作り実装していく。主要クラスのコードは前回までのサンプルからコピーした。

データモデルを定義する。内容はこれまでの検証で使ってきた Homepage と同じ。


InterfaceBuilderを立ち上げ、NSTableViewを配置する。カラムは1列としておく。

NSArrayController を一つ設置する。このクラスがカスタムセルとCoreDataの仲介を行う。

NSArrayController の Attributes はこんな感じ。

Mode を "Entity" に Entity Name に 先ほど定義したデータモデルのエンティティ名 "Homepage" を指定する。

次の図は NSTableColumn の bindings設定。

Controller Key に "arrangedObjects"、Model Key Path に "objectID" を指定する。こうすると -[NSManagedObject objectID] の結果(すなわち NSManagedObjectID)がカスタムセルへ渡される。


UI の調整が済んだら次はコーディング。まずは制御部分から。

CustomCellWithCoredata_AppDelegate.m
- (void)awakeFromNib
{
 NSManagedObjectContext* moc = [self managedObjectContext];

 [self createTestRecord];
 
 [array_controller setManagedObjectContext:moc];

 CustomCell* cell = [[[CustomCell alloc] init] autorelease];
 [cell setEditable:NO];
 cell.managedObjectContext = moc;
 [table_column setDataCell:cell];
}

最初に全体のオブジェクト間の調整を行う。
・NSArrayController と NSManagedObjectContext の紐づけ
・NSTableColumn と CustomCell の紐付け
・CustomCell と NSmanagedObjectContext の紐付け

-[createTestRecord] ではテストデータを10件作成している(結果は CoreData を通じて SQLiteデータベースへ格納される)。


次にカスタムセルの描画コード。ほとんどが以前のコードの流用。Homepageインスタンスの取得箇所だけ今回手を入れてある。

CustomCell.m
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
 NSManagedObjectID* moid = (NSManagedObjectID*)[self objectValue];
 Homepage* homepage = (Homepage*)[self.managedObjectContext objectWithID:moid];

 NSImage* image = [[[NSImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForImageResource:homepage.image]] autorelease];
 
 [controlView lockFocus];
 NSPoint p1 = cellFrame.origin;
 p1.x += 5;
 p1.y += 5 + [image size].height;
 [image compositeToPoint:p1 operation:NSCompositeSourceOver];

 NSPoint p2 = cellFrame.origin;
 p2.x += 100;
 p2.y += 20;
 NSDictionary* attrs = [NSDictionary dictionary];
 [homepage.title drawAtPoint:p2 withAttributes:attrs];
 [controlView unlockFocus];
}
NSManagedObjectID があれば NSManagedObjectContext を通じて Homepage が簡単に取得できる。


実行

さて実行してみよう。


出た。特に問題なし。


ソースコード

github からどうぞ
CustomCellWithCoredata at 20091207 from xcatsan's SampleCode - GitHub


- - - -
機能的には問題なさそうだ。となると次はメモリの利用状況か(続く)。