2010-12-23

ラベルによる記事の抽出 - ラベル検索の実装 #3 (MacBloggerGlass)

前回(→「ドロワー (NSDrawer) の使い方」)に続いて、ラベルの付いた記事を抽出する仕組みを作り込む。

Feed にクエリを指定して Entry を絞り込む

GAE 版では、GData API への問い合わせにクエリーを指定することで、取得するフィードデータの段階で記事を絞り込んでいた。Cocoa 版ではフィードデータをローカルストレージに保存しているため、この方式は使えない。

記事の抽出は MVC のうち、M (モデル) でも C (コントローラ) でも可能だが、今回は M (モデル) で行うことにした。この場合、記事の抽出というよりも絞り込みと言うべきだろう。

具体的には、Feed クラスに対して絞り込みのための「何か」を付与することで、Feed のプロパティ entries (配列)に収められる要素(Entry のインスタンス)を制限する。Feed が抱える要素(Entry)をフィルタにかけるようなイメージだ。

Feed に付与する「何か」をクエリー(Query)と呼ぶ。その実体は、Entry クラスの述語メソッドと引数となるオブジェクトを組み合わせたものだ。Feed が抱える Entry のインスタンスそれぞれに対して、述語メソッドを(組み合わされた引数とともに)適用し、結果が真 (BOOL の YES) のものだけを残すわけだ。

今回は、ラベルによる絞り込みなので、Entry クラスには - (BOOL)hasLabel:(NSString *)label というメソッドを定義しておく。以下にその定義を示す。

- (BOOL)hasLabel:(NSString *)aLabel {
    NSString *label;
    for (label in self.labels) {
        if ([label isEqualToString:aLabel]) {
            return YES;
        }
    }
    return NO;
}

NSInvocation を使った Query の実現

Query の定義を以下に示す。これを見てわかるように、Query の実体は NSInvocation そのものだ。Entry のインスタンスに適用するために特殊化された NSInvocation と言っても良い。

NSInvocation は言わば、オブジェクトに対するメソッド呼び出し自体を独立したオブジェクトとして扱えるようにするためのものだ。そこには、メソッドの情報(セレクタとシグナチャ)、引数、ターゲットとなるオブジェクトが収められる。もともと、分散処理を実現するための仕組みの一つのようだが、今回のような目的にも利用できる。

単にラベル(文字列)による要素の絞り込みを実現するだけなら、NSInvocation のような仕組みは必要ない。ただラベル(の配列)を Feed クラスにわたすだけで良い。単純な方法をとらなかったのは、他の種類のクエリーもサポートするためだ。たとえば、投稿日時による絞り込み等。Entry で適切な述語を定義してやれば、そういった絞り込みも可能になる。

@interface Query : NSObject {
    NSInvocation *invocation;
}

+ (Query *)queryWithPredicate:(SEL)predicate argument:(id)object;
- (id)initWithInvocation:(NSInvocation *)anInvocation;

- (BOOL)applyTo:(Entry *)target;

@end

@implementation Query

+ (Query *)queryWithPredicate:(SEL)predicate argument:(id)object {
    NSMethodSignature *signature =
    [Entry instanceMethodSignatureForSelector:predicate];

    NSInvocation *anInvocation =
    [NSInvocation invocationWithMethodSignature:signature];
    [anInvocation setSelector:predicate];
    [anInvocation setArgument:&object atIndex:2];

    Query *query = [[Query alloc] initWithInvocation:anInvocation];
    return [query autorelease];
}

- (id)initWithInvocation:(NSInvocation *)anInvocation {
    [super init];
    invocation = [anInvocation retain];
    return self;
}

- (void)dealloc {
    [invocation release];
    [super dealloc];
}

- (BOOL)applyTo:(Entry *)target {
    [invocation invokeWithTarget:target];

    BOOL result;
    [invocation getReturnValue:&result];
    return result;
}
@end

コントローラ(AppController)では、ドロワー内のテーブルビューで選択が変わったときに、Feed に対して Query を付与している。ラベルが複数選択された場合は Query も複数付けられることになる。

- (void)tableViewSelectionDidChange:(NSNotification *)notification {
    id object = [notification object];
    if (object == postTable) {
        [...snip...]
    } else if (object == labelsTable) {
        NSIndexSet *indexes = [labelsTable selectedRowIndexes];
        NSArray *selectedLabels = [feed.labels objectsAtIndexes:indexes];
        NSString *label;
        NSMutableArray *queries =
        [NSMutableArray arrayWithCapacity:[indexes count]];
        for (label in selectedLabels) {
            Query *query =
            [Query queryWithPredicate:@selector(hasLabel:) argument:label];
            [queries addObject:query];
        }
        [feed setValue:queries forKey:@"queries"];
    }
}

Feed での絞り込みは entries プロパティに対するアクセサを上書きすることで実現している。これを見てわかるように、複数の Query が付けられた場合、AND 結合による絞り込みとなる。

- (NSArray *)entries {
    if ([queries count] == 0) return entries;
    NSMutableArray *filteredEntries = [NSMutableArray array];
    Entry *entry;
    Query *query;
    for (entry in entries) {
        BOOL result = YES;
        for (query in queries) {
            result = [query applyTo:entry];
            if (! result) break;
        }
        if (result) {
            [filteredEntries addObject:entry];
        }
    }
    return filteredEntries;
}

ちなみに今回は、この絞り込みは Feed クラスではなく、Feed から派生させた FilteredFeed クラスに実装している。そして記事フィードを表現する FeedBlogPost クラスをこの FilteredFeed クラスから派生するように変更した。というのも、ブログ一覧(のフィード)を表現する FeedBlog クラスには絞り込みは必要ない。絞り込みの実装がそちらに影響を及ぼすことのないようにしておきたかったのだ。

Cocoa 版 BloggerGlass の完成

ラベル検索が実現できたことで、Mac 用アプリとして作り始めたときの目標(→「欲しいのはオフライン機能」)を達成できた。

MacBloggerGlass プロトタイプ #0
ラベル検索
スクリーンショットだけではわかりにくいが、左のドロワーで選択したラベルの付いた記事だけが一覧に現れている。

これで Cocoa Touch 版、すなわち iOS デバイス向けのアプリとして作り直すための基礎ができた。Cocoa 版との大きな違いは、GUI の部品が異なること、Cocoa Bindings が使えないことなどだろうか。UI としては、Cocoa 版よりもむしろ iPhone 向けに最適化した GAE 版に近いものになるかもしれない。Cocoa Bindings を使わないとすると、コントローラの実装も変わる(コードが増える)はず。

一方で、モデルと言うべき GData オブジェクトのアダプタ(Entry と Feed)クラスおよび FeedManager はほぼそのまま流用できるだろう。

Cocoa Touch 版にとりかかるのは 2011 年になってからになるか。2010 年の残りはいくつか気になっている細部をつめる作業をしよう。

関連リンク

関連記事

0 件のコメント:

コメントを投稿