2010-12-17

FeedManager が完成 - オフライン機能の実現 #5 (MacBloggerGlass)

ようやく、FeedManager の実装とそれを使うコントローラ(AppController、PreferenceController)部分の修正が完了した。FeedManager は必要に応じてフィードを GData API を通じて入手し、ローカルに保存する。これで、取得ずみのフィードが存在すれば、ネットワークにアクセスすることなく記事を表示することができるようになった。つまりは、オフライン機能が実現できたことになる。

XML テキストの保存と復元

ファイルへの書き出し

最初は、XML テキストの保存に NSString の writeToFile 系のメソッドを使っていた。問題なくファイルに書き込めていたのだけれど、気になったのはファイルの許可情報。保存先が ~/Library の下だということもあり、所有者以外の読み取り許可を付けたくなかった。ところが、NSString の writeToFile だとファイル作成時に許可情報を指定することができない。デフォルトだと 0644 で作られてしまう(グループと全ユーザに対して読み取り許可が付いている)。

作った後に許可を変更することも考えたが、何かまずいことが起きてアプリがクラッシュした際に、許可を変更できないままのファイルが残る(という可能性がある)ことが気持ち悪い。結局、NSString から NSData に変換し、NSFileManager の createFileAtPath:contents:attributes: を使うことにした。該当する部分のコード(FeedManager のメソッド)を以下に示す。

- (void)writeFileAtPath:(NSString *)path contents:(NSString *)text {
    // always overwrites a existing file
    NSData *data = [text dataUsingEncoding:NSUTF8StringEncoding];
    if (! data) {
        NSLog(@"Some data might be lost in converting into the UTF-8 encoding.");
        return;
    }

    NSMutableDictionary *attrs = [NSMutableDictionary dictionary];
    [attrs setObject:[NSNumber numberWithInt:0600]
              forKey:NSFilePosixPermissions];

    BOOL result = [fileManager createFileAtPath:path contents:data attributes:attrs];
    if (! result) {
        NSLog(@"Fail to write a file: %@", path);
    }    
}

実はこの部分はちょっと気になっている。というのも、Feed オブジェクトから XML データをテキストとして抽出する際に、NSData から NSString に変換している。ファイルに書き出す際に、それを FeedManager で NSData に戻していることになる。どう考えてもムダだ。途中で NSString を利用しているわけでもないし。

気が向いたら修正しておこう。

XML テキストからの復元

ファイルから XML テキストを読み込み、Feed オブジェクトを復元しているコードを以下に示す。まずは、FeedManager のメソッドから。ここでは指定されたファイルの内容を NSString として取り出し、Feed クラスのファクトリーメソッドにわたしている。

- (Feed *)restoreFeedFromFile:(NSString *)path feedClass:(Class)classOfGData {
    Feed *feed;
    NSString *xmltext =
    [NSString stringWithContentsOfFile:path
                              encoding:NSUTF8StringEncoding
                                 error:NULL];
    feed = [Feed feedWithXMLText:xmltext feedClass:classOfGData];
    return feed;
}

次は、実際に Feed オブジェクトを生成している部分。こちらは Feed クラスのクラスメソッドだ。

+ (Feed *)feedWithXMLText:(NSString *)xmltext feedClass:(Class)classOfGData {
    NSXMLDocument *xmldocument =
    [[NSXMLDocument alloc] initWithXMLString:xmltext
                                     options:0
                                       error:NULL];
    GDataFeedBase *gfeed =
    [[classOfGData alloc] initWithXMLElement:[xmldocument rootElement]
                                      parent:nil];
    
    Feed *feed;
    if (classOfGData == [GDataFeedBlog class]) {
        feed = [[[FeedBlog alloc] initWithGDataFeed:gfeed] autorelease];
    } else if (classOfGData == [GDataFeedBlogPost class]) {
        feed = [[[FeedBlogPost alloc] initWithGDataFeed:gfeed] autorelease];
    } else {
        NSLog(@"Unexpected class was specified: %@", classOfGData);
        feed = nil;
    }

    [xmldocument release];
    [gfeed release];
    return feed;
}

ここで引っかかったのは、引数としてわたってきた NSString から NSXMLDocument を生成する部分。より正確には NSXMLDocument の initWithXMLString:options:error に対する第 2 引数 options の値だ。始めこれに NSXMLDocumentTidyXML という定数を指定していた。リファレンスによれば構造的に誤りをふくむ XML を正しい XML に修正する、と書いてあった。とくに XML の修正機能が必要だったわけではなく、ただなんとなく選んだだけだ。ところが、この指定で記事内容(HTML で書かれた部分)の pre 要素の改行がすべて取り除かれてしまった。

たしかにリファレンスには「pretty-print で付けられた余分の空白等を取り除く」と書かれていた。ここを見落としたのだ。

ともあれ、この値として 0 (余分な動作は何もしない) を指定して、期待した通りの結果を得ることができた。

Feed オブジェクトをキャッシュする

メモリ中に生成した Feed オブジェクトをキャッシュしておくための仕組みを FeedManager に組み込んだ。といっても、実体は NSMutableDictionary だ。

この仕組みは、ブログ一覧 (FeedBlog) の方はアカウントをキーとしてキャッシュし、ブログ記事 (FeedBlogPost) の方はアカウントと Blog ID をキーとしてキャッシュする。環境設定パネルでブログが切り替えられたとき(またはアカウントが変更されたとき)に、すでに生成ずみの Feed オブジェクトがあれば、ネットワークに取りに行くこともなく、さらにはディスクから読み直すこともせずに、メモリ中のオブジェクトを使うことができるようにするためのものだ。

以下にインタフェース部だけを示す。実装は、単なる NSMutableDictionary へのアクセスで、ほぼ自明だから。Blog ID とアカウントの 2 つをキーとして指定する方は、単に文字列として連結しているだけだ。

// FeedCache is made to encapsulate the implementation detail
// to manage cache for Feed instances.
@interface FeedCache : NSObject
{
    NSMutableDictionary *blogFeeds;
    NSMutableDictionary *postsFeeds;
}

// manage cache for Feed instances
- (Feed *)feedBlogForAccount:(NSString *)account;
- (void)setFeedBlog:(Feed *)feed account:(NSString *)account;

- (Feed *)feedBlogPostForBlogID:(NSString *)blogID account:(NSString *)account;
- (void)setFeedBlogPost:(Feed *)feed blogID:(NSString *)blogID account:(NSString *)account;

- (NSString *)keyForBlogID:(NSString *)blogID account:(NSString *)account;

@end

プロトタイプ #0 の完成まで、もう少し

始めに挙げた機能のうち、残るはラベルによる検索だけとなった(→「欲しいのはオフライン機能」参照)。

必要になる仕組みとしては、おおよそ「ラベルの列挙」と「指定されたラベルが付いた記事の抽出」の 2 つ。前者は記事を表現した Entry オブジェクト (実体は GDataEntryBlogPost) からラベルをかき集めれば良い。後者については、GAE 版では GData API にクエリーをかけたが、Mac 版ではフィードはすべてローカルに保存してある(メモリ中にオブジェクトとして、あるいはディスク中に XML テキストとして)から、GData API をたたく必要はない。GData ライブラリにメモリ中のオブジェクトに対してフィルタをかけられればそれを使うし、できないならその部分を自前で作り込むことになる。

実装よりも「見せ方」をどうするかが悩みどころ。プロトタイプなのだから、凝った UI を作るつもりはないのだけれど……。

参考文献

詳解 Objective-C 2.0
荻原 剛志
ソフトバンククリエイティブ ( 2008-05-28 )
ISBN: 9784797346800
Objective-C逆引きハンドブック
林 晃
シーアンドアール研究所 ( 2010-02-26 )
ISBN: 9784863540514

関連リンク

関連記事

0 件のコメント:

コメントを投稿