2010-12-18

iBook で Apple 提供の開発者向けドキュメントが読める

昨夜(2010-12-17)のこと、iPod touch で App Store を開くと iBook のアップデートが配布されていることに気付いた。他にもいろいろアップデートが出ていたのでまとめてアップデートした。で、ふと iBook を開き、さらに iBookStore を開いてみたところ、iOS 関連の開発者向けドキュメントが配信されていることに気付いた(右のスクリーンショットは iPad で iBookStore を開いたもの)。

そう、ようやく Apple がやってくれた。ずっと、これが欲しかった。もともと PDF 版が配布されているから iPad に取り込んで読むことはできたし、さらに iPad がネットにつながるなら Safari でウェブ版を開けば iPad に最適化された状態で読むこともできた(→「iPad で Apple 提供の開発者向けドキュメントを読む」)。実際、このウェブ版は、iPad というデバイスの特性とあいまって、「ADC のドキュメントを読むならコレだ!」と思わせるぐらい読みやすいものになっていた。iMac で Xcode を開いているときにも、そばに立てた iPad でガイド系の文書(のウェブ版)を開いて参照していた(Xcode のドキュメントブラウザは主にクラスリファレンスを参照するのに使っている)。

とはいえ、ウェブ版のドキュメント(というよりブラウザで長い文書を読むこと)には不満がある。スクロールして読む読書体験と、ページをめくって読む読書体験を比較すれば、後者の方が圧倒的に心地良い。画面上を文字が移動するのは「読む」ことを前提にした場合は気持の良いものじゃない(せめて、iPad 上の Safari に「Page Up」と「Page Down」があれば……)。もっとも、これはわたしがデジタル以前の世界で育った世代からかもしれない。情報の入れ物としての「本」というカタチに慣れてしまっていることがそう思わせるのだろう。

また、いくら Wi-Fi 接続だろうとネット上のリソースを開くのには「遅れ」が生じる。アドレスバーが水色で塗られていくのを眺めていても楽しくない。

だから、iPad で ADC ドキュメントを読むようになってからずっと思っていた。いや、むしろ願っていた。Apple が ebook として提供してくれるようになることを。

iPad で読んでみる

いくつかスクリーンショットを撮ったので貼ってみる。すべて、iPad のスクリーンショット機能(電源ボタン + ホームボタンの同時押し)で撮影したもの。ここに並ぶんでいるのはサムネイル。実際の見え方は画像をクリックして Picasa ウェブアルバムにアップしたものを見てほしい。

iPad の iBook で ADC ドキュメントを読む
iBook の本棚に並んだ iOS 開発者向けドキュメントたち
早速、ダウンロードしてみた。最上段の 5 つが iOS 開発者向けドキュメントだ。O'Reilly の ebook と比べると表紙が文字だけで少しさびしい。
iOS Application Programming Guide を開いてみた (#1)
文字はもちろん、写真もクッキリ。
iOS Application Programming Guide を開いてみた (#2)
図もはっきり見える。図中の文字も十分に判別できる。

もう、なんて言うかさ、iPad はこのために作られたんじゃないか、って思えるぐらい。Apple は開発者向けに ADC ドキュメントを(iOS 用のものだけじゃなく)丸ごと収録した iPad を販売するべきだよ。有料の Dev program (iOS 向けと Mac 向けのものがある)に参加している場合はちょっと安くしてさ。

iPod touch で読んでみる

画素数ではともかく、画面が小さいから iPad ほどの見やすさは得られないとわかっているけど、せっかくなので iPod touch でも試してみた。以下は iPod touch のスクリーンショット機能で撮ったもの。残念ながらスクショでは iPod touch の画面での見た目は再現できないから参考程度に見てほしい。

iPod touch の iBook で ADC ドキュメントを読む
iOS Application Programming Guide を開いてみた (#1)
スクショだとやけに文字が大きく見えるが、実機で見るとこれより小さいと読みづらくなるサイズ。ま、見えやすさには個人差が出ると思うが。
iOS Application Programming Guide を開いてみた (#2)
iPad のスクショと同じ図。実機で見ると、図中の文字の判別はかなり厳しい。
図だけを表示(横向き)
横向きで、図だけを表示させると少し大きくなる。ただ、やはり実機上では図中の文字は読みづらい。

iBook では図をダブルタップすると図だけを表示することができる。この状態ではピンチオープン、クローズによる拡大縮小も可能。iPod touch でも、ダブルタップからピンチオープンで拡大すれば図中の文字も読み取れる。

他のドキュメントも欲しい

iBook 向けに提供されているのはまだたった 5 つだけ。ADC にはそれこそ山ほどのドキュメントがある。iOS 向けだけじゃなくて、Mac 向けの方のものも iPad で読みたい。

Apple さん、どんどん配信してください。お願いします。

関連リンク

関連記事

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

関連リンク

関連記事

twitter より (2010-12-16)

  • 18:34  Safari の Command + Ctrl + D を知らなかったよ。単語をダブルクリックで選択状態にして、Command + Ctrl + D。単語のそばに小さい辞書ウィンドウが開く。ナニコレ、便利すぎる。→ http://bit.ly/ethdtl
Powered by twtr2src.

2010-12-16

クラスクラスタの導入 - オフライン機能の実現 #4 (MacBloggerGlass)

FeedManager を実装していて、どうにも FeedBlog と FeedBlogPost の取り扱いがメンドウになってきたので、両者に共通の基底クラスを作ることにした。ついでに、これらをクラスクラスタとしてまとめることにした。

クラスクラスタとは

荻原(2.0)本から引用する。

(「詳解 Objective-C 2.0」p.245 より)
クラスクラスタ (class cluster) とは、同じインタフェースを持ち、同じ機能を提供する複数のクラスの集合体です。インタフェースを表す抽象クラスのみを公開し、これをそのクラスクラスタのパブリッククラス (public class) と呼びます。個々の具体的なクラスはパブリッククラスのインタフェースによって抽象化され、クラスタの内部に隠蔽されます。

Cocoa (Foundation フレームワーク)で提供されている具体的なクラスタには、NSString や NSArray などがある。

公式ドキュメントにも独立した項目として解説されているが(→「Cocoa Core Competencies: Class Cluster」等)、言語および実行環境として特別なサポートの仕組みが提供されているわけではない。設計上の慣習 (convention) と見るべき。デザインパターンよりはもっと広い概念だ。

ずい分、昔のことだけど、C++ で可変 (mutable) オブジェクトとして生成したものを、不変 (immutable) オブジェクトとして返すなんてコードを書いていた。あれもクラスクラスタの一種だと言える。

実例: Feed クラス

MacBloggerGlass のモデルクラスは、これまで FeedBlog と FeedBlogPost として作ってきた。前者は Google アカウントごとのブログの一覧を表し、GData ライブラリのクラス、GDataFeedBlog に対応する。後者は、ブログごとの記事のフィードを表現するもので、GData ライブラリでは GDataFeedBlogPost に対応する。FeedBlog は環境設定パネルでブログの一覧を表示するために使い、FeedBlogPost はメインウィンドウで記事の一覧とその内容表示に使っている。

GData ライブラリのフィードを表現するクラスたちには共通の基底クラス (GDataFeedBase) があり、ほとんどの機能はこのクラス(あるいはさらにその基底クラス)で定義されていて、GDataFeedBlog と GDataFeedBlogPost の違いはわずかなものだ。当然、そのラッパーと言うべき FeedBlog と FeedBlogPost にも大きな差はなかった。

以下に、Feed クラスタの公開クラス Feed のインタフェース部を示す。

@interface Feed : NSObject {
    NSMutableArray *entries;
    NSMutableDictionary *pairOfLinks;
    GDataFeedBase *gDataFeed;
}

@property (readwrite, retain) NSMutableArray *entries;
@property (readonly) NSMutableDictionary *pairOfLinks;

// class factories
+ (Feed *)feedWithGDataFeedBlog:(GDataFeedBlog *)gfeed;
+ (Feed *)feedWithGDataFeedBlogPost:(GDataFeedBlogPost *)gfeed;
+ (Feed *)feedWithXMLText:(NSString *)xmltext feedClass:(Class)classOfGData;

// initializer
- (id)initWithGDataFeed:(GDataFeedBase *)gfeed; // desiginated initializer

// element manipulation
// subclass must be override the following 2 methods.
+ (Class)classForEntries;
+ (Entry *)entryWithGDataEntry:(GDataEntryBase *)gentry;

// for KVC compliance (required)
- (void)insertObject:(Entry *)entry inEntriesAtIndex:(NSUInteger)index;
- (void)removeObjectFromEntriesAtIndex:(NSUInteger)index;
- (void)replaceObjectInEntriesAtIndex:(NSUInteger)index withObject:(Entry *)entry;
// for KVC compliance (optional)
- (int)countOfEntries;
- (Entry *)objectInEntriesAtIndex:(NSUInteger)index;

// utilities
- (NSString *)XMLText;
- (NSUInteger)findEntry:(NSString *)identifier;

// for subclasses
- (void)setGDataFeed:(GDataFeedBase *)gfeed;
@end

継承関係にあるクラス群をクラスクラスタとして機能させるには、状況に合わせて具体的な派生クラスを生成するためのファクトリーメソッドが必要になる。上記の例では 3 つのファクトリーメソッドを Feed クラスのクラスメソッドとして定義している。対応する GDataFeedBase オブジェクトを指定して生成するものが 2 つと、もう 1 つは XML テキスト(フィードの生データ)から生成するものだ。XML から GDataFeedBase オブジェクトを作り、それに対応する Feed クラスを生成することを想定している。このため、どの GDataFeedBase クラスを作るかを引数でわたすようになっている。

また、Feed クラスが要素として抱えるオブジェクトの方も同様の理由でクラスクラスタとしている(Entry)。ただ、こちらの方は Feed よりもさらに派生クラスの差異が目立たないため、ほとんど Entry 単独のクラスのようになってしまった。まあ、クラスクラスタ化してしまえば、後から派生クラスを基底クラスにまとめたとしても(あるいは派生クラスを増やしたとしても)、それを使う側のコードに影響はほとんどない。派生クラスが不要だと思えば、削除してしまえば良い。

実はこのコード、まだ実装が完了していないので、動くかどうかは未確認。正確には、Feed クラスタの実装は完了しているが、それを使う FeedManager の方がまだできていない。明日にはできると思うのだけど……。

参考文献

詳解 Objective-C 2.0
荻原 剛志
ソフトバンククリエイティブ ( 2008-05-28 )
ISBN: 9784797346800

クラスクラスタの説明は「CHAPTER 10 抽象クラスとクラスクラスタ」の「10-02 クラスクラスタ」にある。あと、(インスタンス)オブジェクトの所属するクラスを調べる方法については「07-01 NSObject クラス」に書かれている。

関連リンク

関連記事

2010-12-15

FeedManager の実装 - オフライン機能の実現 #3 (MacBloggerGlass)

前回の記事で書いたように(→「オフライン機能の実現 #2」)、フィードデータの保存、読み込みは、FeedManager と呼ぶ管理用のクラスを作り、ネットワークからのデータの取得と合わせて、その詳細を隠すことに決めた。

今日は、その FeedManager を作っている。残念ながら FeedManager はまだ完成していない。今回は、この実装の過程で気付いた注意点を 2 点ほど書き留めておく。

シングルトン・パターン

FeedManager にはシングルトン・パターンを使う。Cocoa でシングルトン・パターンを使うには少し注意が必要だ。簡単に言うと、NSObject で定義ずみのいくつかのメソッドを上書きしなければならない。また、シングルトン・パターンには付きもののファクトリーメソッドの定義にも Cocoa 独特の「お作法」がある。詳しくは公式ドキュメントの Cocoa Fundamentals Guide: Cocoa Objects: Creating a Singleton Instance を参照のこと。

この「Creating a Singleton Instance」には、シングルトン・パターンを実装するための実例が載っていて、そのまま流用できる。以下が、FeedManager 用に書き直したもの。sharedManager の戻り値以外は、実例そのままになっている。

+ (FeedManager *)sharedManager {
    if (sharedFeedManager == nil) {
        sharedFeedManager = [[super allocWithZone:NULL] init];
    }
    return sharedFeedManager;
}

+ (id)allocWithZone:(NSZone *)zone {
    return [[self sharedManager] retain];
}

- (id)copyWithZone:(NSZone *)zone {
    return self;
}

- (id)retain {
    return self;
}

- (NSUInteger)retainCount {
    return NSUIntegerMax;
}

- (void)release {
    // do nothing
}

- (id)autorelease {
    return self;
}

コールバック方式への対応

GData ライブラリではフィードデータの取得は非同期に実行される。このため、アプリ(のコントローラ)は、コールバックのメソッドを呼び出してもらうことでその完了を知る。データ取得が非同期に実行されるということは、取得したデータからオブジェクトを生成して返す GetFeedBlog のような関数を定義できないことを意味する。

@implementation FeedManager
- (void)trigger {
    [...snip...]
    FeedBlog *feedBlog = GetFeedBlog(account, password);
    [...snip...]
}
@end

非同期方式の場合は、取得開始のトリガーとなるコードと、取得したデータを受け取るコードの間で、引数と戻り値を使った直接的な情報の交換(通信)ができない。これを解決するための方法は 2 つある。1 つは、トリガーと受け取りのコードを同一のオブジェクトのメソッドとして実装し、オブジェクトのインスタンス変数を介して情報のやり取りを行うもの。以下のような感じのコードになる。

@interface FeedManger : NSObject {
    FeedBlog *feedBlog;
}
@end

@implementation FeedManager
- (void)trigger {
    Service *service = [...snip...]
    [service fetch];
    [...snip...]
}

- (void)callback:(id)dataObject {
    feedBlog = [[FeedBlog alloc] initWithDataObject:dataObject];
}
@end

GData ライブラリの GDataServiceBase クラスでは、上記とは別の方法をサポートしている。それは、サービス(フィードデータの取得)がコールバックに対して送り返すデータの一部として、クライアント(サービスを利用するコード)がユーザデータとして任意のオブジェクトを添付する仕組みだ。具体的には GDataServiceBase のインスタンスメソッド setServiceUserData だ。

GDataServiceBase に setServiceUserData でわたされたオブジェクトは、その後の同サービスオブジェクトに対するフィード取得のリクエストを処理する中で、コールバックに引数としてわたすオブジェクト(GDataServiceTicket)に封入される。コールバックメソッドでは、GDataServiceTicket の userData として、トリガーコードが添付したオブジェクトを受け取ることができる。おおよそ、こういう感じ(↓)。

@implementation FeedManager
- (void)trigger {
    GDataServiceBase *service = [...snip...]
    id userData = /* an arbitrary object */;
    [service setServiceUserData:userData];
    [service fetchFeedWithURL:[...snip...]]n;
}

- (void)callback:(GDataServiceTicket *)ticket [...snip...]] {
    id userData = [ticket userData];
    [...snip...]
}

この userData として、フィードデータ(から生成したオブジェクト)を受け取るオブジェクト(レシーバー)を添付すれば、callback の中でレシーバーにフィードデータをわたすことができる(レシーバーのメソッド経由で)。

FeedManager ではフィードデータから生成されるオブジェクトを保持するインスタンス変数を別の目的(オブジェクトが生成ずみかどうかの判別)で使っているため、GDataServiceBase (のサブクラス)に userData を添付する方法を採用している。

関連リンク

関連記事

2010-12-14

オフライン機能の実現 #2 (MacBloggerGlass)

GData オブジェクトの保存については実現の目処が立った(→「GData オブジェクトを保存する」)ので、今回はオフライン機能全般について検討してみた。

ユーザ体験

前にも書いたように、MacBloggerGlass はドキュメントアーキテクチャに基づいたアプリではない。言わば(Blogger で作ったブログ専用の)ブログブラウザだが、アプリの分類では特殊用途の RSS リーダだ。そして、ユーザ体験から言えば Mail.app と言ったところ(ただし閲覧専用)。

Mail.app (を始めとするメーラのほとんど)がそうであるように、このアプリの場合もユーザが明示的に保存を指示することはない。Mac (や PC) のアプリでは、こういうタイプは少数派だが、iOS アプリではむしろ主流となっている。データを保存し読み込むというよりは、(アプリの終了によって)中断した状態を復元する方式だ。

現状の UI は、Google アカウントを切り替えることを想定したものではないが(わたし自身、複数のアカウントは使っていないし)、オフライン機能のためのデータ保存はアカウントごとに行う。これは、アカウントを切り替えたとしてもデータを上書きしないことを意味する。この方式を取った場合、アカウントごとにデータを削除する手段も用意すべきだが、それは将来の検討課題としておく。

一方で、ブログを切り替える UI を持つのだから、データの保存をブログごとに独立して行うのは当然。ただし、こちらもデータの削除は将来の検討課題に。

データの保存はユーザに意識させないが、データの更新はユーザに明示的な指示を出してもらう。メニューの「Reload」をそれに使う。アプリの動きとしては、(User Defaults に保管されている)ブログ ID に対応するフィードデータが存在した場合、データの取得は行わなず、保存されたデータから GData オブジェクトを復元し、表示する。もちろん、データが保存されていないければ、特にユーザからの指示がなくとも取得する。

デザイン(設計)

保存先は ~/Library/BloggerGlass とする。この中にアカウント名でフォルダを作る。そこにさらに Blog ID でフォルダを作り、データファイルはこの Blog ID によるフォルダに置く。ファイル名は feedblogpost.xml とする。将来的にフィードデータを分割保存することがあるかもしれない。その場合は feedblogpost-20101214.xml のように日付をファイル名に取り込む。

また、アカウントごとのブログ一覧のフィードも、アカウント名のフォルダ直下に置く。ファイル名は feedblog.xml とする。

データファイルの形式は XML のテキスト。言わゆる human readable な形にしておく。

「ユーザ体験」のところで書いたように、ユーザに保存を指示させない方式だとすると、そのトリガーをどうするか? まずは、フィードの取得が完了し(GData オブジェクトが作られ)た直後に実行される、GData ライブラリのコールバックが良いだろう。ユーザへのレスポンスの速さが気になるなら別スレッドで非同期に保存すれば良い。ただ、今の段階では同期的に保存する方式で。

では読み込みのトリガーは? これはブログの切り替えになる。つまり、環境設定パネルでブログが切り替えられた通知のハンドラで行うことになる。今は、このタイミングでフィードをネットワーク経由で取得に行っている。保存が実現でき後は、ここでまず保存されたデータの有無を確認し、あればそれを読み込み、なければネットワークにアクセスする、となる。

データの保存、読み込みはアプリコントローラとは独立させる。FeedManager クラス(仮名)を作り、ローカルのデータとネットワーク経由の取得の違いを隠す。アプリコントローラは FeedManager にアカウント名と Blog ID をわたして、FeedBlogPost オブジェクトを受け取ることになる。

実装に関する考慮点

~/Library に保存用のフォルダを作る際には、パスを直接指定するのではなく(そもそもユーザ ID を知らずにユーザのホームディレクトリを直接指定できない)、Cocoa に用意された API 経由でライブラリ用フォルダを取得する。それが NSSearchPathForDirectoriesInDomains 関数だ。

NSSearchPathForDirectoriesInDomains 関数は 3 つの引数を取る。1 つ目がフォルダの種類、2 つ目がフォルダのドメイン(のマスク)、そして 3 つ目がチルダ(~)を展開するか否か。

ドメインマスクには NSUserDomainMask、NSLocalDomainMask、NSNetworkDomainMask、NSSystemDomainMask、NSAllDomainMask のうちから 1 つを指定する。今回の実装ではユーザのライブラリフォルダ(のパス)を知りたいのだから、NSUserDomainMask で良い。

戻り値は配列になる。ユーザドメイン(のマスク)を指定すれば要素は 1 つだけだ。

以下に簡単なサンプルプログラムを示す。

int main (int argc, const char *argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    
    NSArray *paths =
    NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,
                                        NSUserDomainMask,
                                        YES);
    
    NSString *path;
    for (path in paths) {
        NSLog(@"Path: %@", path);
    }
    
    [pool drain];
    return 0;
}

上記のプログラムの実行結果は以下の通り(コンソールへの出力)。

2010-12-14 22:35:48.525 ExpApp[11456:903] Path: /Users/mnbi/Library

関連リンク

関連記事

2010-12-13

GData オブジェクトを保存する - オフライン機能の実現 #1 (MacBloggerGlass)

今回から、オフライン機能を実現するための作業を開始する。まずは、フィードから構築したオブジェクトの保存と復元について実験する。

GData オブジェクトの保存と復元

MacBloggerGlass では内部データとして GData ライブラリのオブジェクト(以下では GData オブジェクトと呼ぶ)をそのままの形で保持している。モデルオブジェクトで覆うことで、表示のために加工が必要なデータ(ID や日付け)はキャッシュしているものの、記事のタイトルや内容は GData オブジェクトのデータをそのまま使っている。このためオフライン機能を実現するためには、GData オブジェクトの保存と復元が必要だ。

GData オブジェクト自体には Cocoa アプリでそのまま保存、復元できるための仕組み(NSCoding プロトコルの実装)はないが、その代わりに NSXMLDocument として参照することができるように作られている。

NSXMLDocument は NSCoding プロトコルこそ実装していないものの、NSData 化するためのメソッドが用意されており、これを抱えるクラスで NSCoding を実装することは難しくない。

GData オブジェクト (GDataObject とその派生クラスたち)を NSData 化するための手順は以下のようになる:

  1. GData オブジェクトから NSXMLDocument オブジェクトを取り出す。 (XMLDocument メソッド)
  2. 取り出した NSXMLDocument を NSData 化する。 (XMLData メソッド)

また、NSData から GData オブジェクトを復元する手順は以下の通り:

  1. NSData から NSXMLDocument オブジェクトを作る。 (initWithData:options:error: メソッド)
  2. NSXMLDocument からルート要素を取り出す。 (rootElement メソッド)
  3. 取り出したルート要素で GData オブジェクトを初期化する。 (initWithXMLElement:parent: メソッド)

実験的な実装

環境設定パネルのモデルとなっている FeedBlog は内部にブログ一覧のフィード(GDataFeedBlog)を抱えるオブジェクトで、初期化に GDataFeedBlog のオブジェクトを指定する。今回は、このクラスを使って GDataFeedBlog の保存と復元を作り込むことにする。

インタフェース部では NSCodig プロトコルを実装することを宣言する。

@interface FeedBlog : NSObject <NSCoding> {
    NSMutableArray *entries;
    GDataFeedBlog *gDataFeed;
}

実装部では、NSCoding プロトコルの 2 つのメソッドを実装する。

// for conformance to NSCoding protocol
- (id)initWithCoder:(NSCoder *)aDecoder {
    [super init];
    NSData *data = [aDecoder decodeDataObject];
    NSXMLDocument *xmldocument =
    [[NSXMLDocument alloc] initWithData:data
                                options:NSXMLDocumentTidyXML
                                  error:NULL];
    NSXMLElement *xmlroot = [xmldocument rootElement];
    GDataFeedBlog *aFeed =
    [[GDataFeedBlog alloc] initWithXMLElement:xmlroot parent:nil];
    return [self initWithGDataFeed:aFeed];
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    NSData *data = [[gDataFeed XMLDocument] XMLData];
    [aCoder encodeDataObject:data];
}

NSCoding プロトコルを実装したオブジェクトは、保存時に NSKeyedArchive を使って NSData 化する。逆に、NSData オブジェクトからの復元には NSKeyedUnarchive を使う。

以下は環境設定パネルのコントローラクラス(PreferenceController)の実装の一部で、FeedBlog を NSData 化したものを User Defaults に保存している。本来、ここに保存するようなデータではないが、とりあえずこれで FeedBlog の保存と復元を確認することができる。

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    [...snip...]
    // feed data
    NSData *data = [NSKeyedArchiver archivedDataWithRootObject:self.feed];
    [defaults setObject:data forKey:BGKeyFeedBlogData];

復元時は User Defaults から NSData オブジェクトを取り出し、NSKeyedUnarchive にかけている。

    // load feed blog data
    NSData *data = [self getUserDefaults:BGKeyFeedBlogData];
    @try {
        self.feed = [NSKeyedUnarchiver unarchiveObjectWithData:data];        
    }
    @catch (NSException * e) {
        NSLog(@"Exception: %@", e);
        self.feed = [[[FeedBlog alloc] init] autorelease];
    }
    @finally {
    }

次の一手

保存と復元の仕組みはわかったので、次はアプリとして、どこに、どのような形式でデータを書き出すかを考えなくてはならない。

MacBloggerGlass は、Cocoa のドキュメントアーキテクチャを使ったアプリではない。フィードを XML として書き出すことを選んだとしても Finder からそれをダブルクリックしてアプリを開くというタイプのものではない。ユーザ体験から見たアプリのタイプとしては、Mail.app に近いものを想定している。~/Library の下にアプリ専用のフォルダを作って、そこにブログごとのフィードデータを置くというような方式になるか。

書き出しの形式に関しては、まずは XML (のテキスト)で良いだろう。パフォーマンス上の問題が出てこない限り、人が読める形にしておくとデバッグをふくめて、何かと便利だから。ただ、データをブログごとに分けるのは当然として、同じブログのすべての記事を 1 つにまとめるのか、それとも一定の数で分割するのかについては検討する必要がある。

参考文献

Cocoa Programming for Mac OS X
Aaron Pablo Hillegass
Addison-Wesley Professional ( 2008-05-15 )
ISBN: 9780321503619

Chapter 10 Archiving では NSCoder を使ったオブジェクトの保存と復元のサンプルが載っている。

Objective-C逆引きハンドブック
林 晃
シーアンドアール研究所 ( 2010-02-26 )
ISBN: 9784863540514

CHAPTER 10 に XML の操作に関する項目が載っている。NSXMLDocument の NSData 化は 187、ルート要素の取り出しは 188。

関連リンク

関連記事

2010-12-12

キーチェーンサービスを使ってパスワードを保存する

以前の記事で書いたように(→「設定の保存にまつわる問題」)、ユーザのパスワード、それもアプリとは異なる別のサービス用のアカウントのものを、アプリの設定(User Defaults)にそのまま保存するのは望ましくない。たとえ、アプリの設定(~/Library/Preferences に plist ファイルとして保存される)がバイナリ形式で、かつファイルモードが 0600 であったとしても。

パスワードを保存するなら、やはり「キーチェーン」を使うべきだ。

(「Keychain Services Programming Guide: Introduction」より)
Keychain Services provides secure storage of passwords, keys, certificates, and notes for one or more users. A user can unlock a keychain with a single password, and any Keychain Services–aware application can then use that keychain to store and retrieve passwords.

今回は、このキーチェーンサービスについて調べてみた。

アプリへの組み込み

アプリからキーチェーンサービスにアクセスするために必要な関数は以下の 2 つ。

  • SecKeychainAddGenericPassword (キーチェーンにパスワードを追加)
  • SecKeychainFindGenericPassword (キーチェーンからパスワードを取得)

これをアプリ内で使う場合の注意点は、(a) security/security.h を import すること、(b) Security.framework をリンクすること、の 2 点だ。(a) はソースに書くだけ。一方、(b) のためには Xcode のプロジェクトウィンドウで以下のような操作を行う。

  1. 「グループとファイル」の「Frameworks」グループ上でポップアップメニューを出し「追加」>「既存のフレームワーク...」を選ぶ。
  2. 現れたシートから Security.framework を選び「追加」ボタンを押す。

上記を実行すれば「Frameworks」グループ内に Security.framework が追加されるが、実際にはどこでポップアップメニューを開いても大差はない。それよりも重要なのは「ターゲット」のビルドフェーズ「バイナリをライブラリにリンク」に Security.framework が追加されていることだ。

サンプルアプリ

この 2 つの関数を使って、簡単なサンプルアプリを作ってみた。右のスクリーンショットがそのアプリウィンドウだ。Save Password でアカウントとパスワードを指定して「Save」ボタンを押すと、パスワードがキーチェーンに保存される。次に、Load Password でアカウントを入力し「Load」ボタンを押すと、下のパスワードフィールドに保存したパスワードが表示される。もちろん、アカウントが間違っていればパスワードの取得に失敗する。

デフォルトのキーチェーンがロックされていれば、保存時にロックを解除するかというダイアログが出てくるし、同様に取得時にはキーチェーンへのアクセスを許可するかというダイアログも出てくる。また、実際にパスワードが保存できていることは「キーチェーンアクセス.app」を起動すれば確認できる。

以下に、アプリのソースの一部(AppController.{h,m})を示す。

#import <Cocoa/Cocoa.h>


@interface AppController : NSObject {
    NSTextField *accountFieldForSave;
    NSTextField *passwordFieldForSave;
    NSTextField *accountFieldForLoad;
    NSTextField *passwordFieldForLoad;
}

@property (assign) IBOutlet NSTextField *accountFieldForSave;
@property (assign) IBOutlet NSTextField *passwordFieldForSave;
@property (assign) IBOutlet NSTextField *accountFieldForLoad;
@property (assign) IBOutlet NSTextField *passwordFieldForLoad;

- (IBAction)load:(id)sender;
- (IBAction)save:(id)sender;

@end
#import <Security/Security.h>
#import "AppController.h"

NSString * const KCTServiceName = @"KeyChainTest App";

@implementation AppController

@synthesize accountFieldForSave, passwordFieldForSave;
@synthesize accountFieldForLoad, passwordFieldForLoad;

- (IBAction)load:(id)sender {
    NSString *account = [accountFieldForLoad stringValue];

    const char *serviceName = [KCTServiceName UTF8String];
    const char *accountName = [account UTF8String];

    void *passwordData = nil;
    SecKeychainItemRef itemRef = nil;
    UInt32 passwordLength;
    
    OSStatus status;
    status = SecKeychainFindGenericPassword(NULL,
                                            strlen(serviceName),
                                            serviceName,
                                            strlen(accountName),
                                            accountName,
                                            &passwordLength,
                                            &passwordData,
                                            &itemRef);

    NSLog(@"Load password: status = %d", status);
    if (status == noErr) {
        NSString *password =
        [[NSString alloc] initWithBytes:passwordData
                                 length:passwordLength
                               encoding:NSUTF8StringEncoding];
        [passwordFieldForLoad setStringValue:password];

        status = SecKeychainItemFreeContent(NULL,
                                            passwordData);
        [password release];
    } else {
        [passwordFieldForLoad setStringValue:@""];
    }

}

- (IBAction)save:(id)sender {
    NSString *account = [accountFieldForSave stringValue];
    NSString *password = [passwordFieldForSave stringValue];
    // !!!:
    const char *serviceName = [KCTServiceName UTF8String];
    const char *accountName = [account UTF8String];
    const char *passwordData = [password UTF8String];
    OSStatus status;
    status = SecKeychainAddGenericPassword(NULL,
                                           strlen(serviceName),
                                           serviceName,
                                           strlen(accountName),
                                           accountName,
                                           strlen(passwordData),
                                           passwordData,
                                           NULL);
    NSLog(@"Save password: status = %d", status);
}

@end

Cocoa のオブジェクトにくるまれていないサービスのため、使うのが少し面倒(NSString を直接わたせなかったり)だが、実質的に 2 つの関数を呼び出すだけでパスワードのような機密性の高い情報を安全に保管できる。やってみると思ったよりも簡単だった。

実は、この 2 つの関数だけではパスワードを変更することができない(同じアカウントで Add を呼ぶとエラーになる)。実際のアプリ(MacBloggerGlass)に組み込むときは、もちろん変更もできるようにしなければならない。

関連リンク

関連記事