2010-12-04

記事の表示機能を作る (MacBloggerGlass)

右のスクリーンショットは MacBloggerGlass のプロトタイプ #0 だが、アプリウィンドウの上部が記事一覧、下部が一覧で選んだ記事内容の表示となっている。これを見る限り、一応記事表示らしいことはできるが、前にも書いたように、これはフィードから切り出した/データをそのまま、WebView に流し込んでいるだけだ。言わば HTML の断片で、正しい HTML にするためには、いろいろと補わなければならない。

というより、GAE 版のようにテンプレートを用意し、このデータを流し込むようにするべきだ。さらに言えば、テンプレートの HTML 的な構造は GAE 版と同じにしておきたい。そうすればスタイルシートがそのまま使える。

GAE 版の Mac 用(記事表示画面)テンプレートの構造(の概略)は以下のようになっている。この div.content の部分に、フィードから切り出した記事のデータを(多少、記事タイトル等の付加情報を付けて)流し込んでいる。

html
+-- head
+-- body
    +-- header
    +-- section
    |   +-- div.content
    |       ...
    +-- footer

Objective-C でこれ(テンプレートの利用)をやるには、どうすれば良いか? 手軽な方法は、テンプレートを div.content で 2 つに分けてしまうものだろう。そして、前半分、記事内容、後半分の 3 つを連結し、一時ファイルに保存するか、または文字列で直接、WebView にわたす。

CSS や JavaScript もアプリの中に抱えることになる。HTML 中からそこへのリンクをどうするか。「file:///」でアクセスすることになるから問題はパス。一時ファイルに書き出すなら、その場所から MacBloggerGlass.app の中のファイルにリンクすることになる。つまり、CSS 等へのリンクは実行時に生成して、HTML に書き込まなければならない。

いっそのこと、CSS 等の付属ファイルも一時ファイルの置き場所にまとめて書き出すか。それなら、リンクは相対表記になりあらかじめテンプレートに書き込んでおける。そうなると一時ファイルというか実行時の環境だな。

そういえば、今、気付いたが、内部リンクをどうすれば良いのか。WebView に HTML としてわたしてしまったらただのリンク、クリックすれば(WebView は)ブラウザとして開くだろう。アプリには手の出しようがない。となると、記事内容はファイルとして書き出すしかないか。そうすれば内部リンクはローカルファイルの参照に書き換えられる。書き出す場所は ~/Library/MacBloggerGlass あたりに。

ウェブアプリなら簡単に実現できること(内部リンクはアプリへのリクエストに書き換える)がローカルなアプリには難しい。そういうものがあるとはね。ちょっと意外だった。

関連記事

追記@2010-12-08

↑のリストに「内部リンクを置き換える #1 (Blogger Glass)」を追加。

2010-12-03

Google App Engine SDK がアップデート (1.3.8 → 1.4.0)

2010-12-02 付けで Python 版 Google App Engine SDK がバージョンアップしている。iMac で作業をしていると、Google アプリのアップデータが起動して、GoogleAppEngineLauncher.app が更新されたと告げられた。そのままアップデートを実行。Java 版のことはわからないが、公式サイトからダウンロード可能な SDK のバージョンは Python 版と同じ 1.4.0 になっている。

で、何が変わったかを調べようと公式ドキュメントのページから Release Note を開いた。以下に、ざっくりと超直訳してみる。誤解、曲解、いろいろふくまれている可能性がある。訳の品質に期待しすぎないように。

  • Always On 機能はアプリが 3 つのインスタンスを常時実行状態のまま保持することのできる機能だ(課金対象)。アプリの遅延を著しく削減することができる。
  • 開発者は Warmup リクエストを有効にできるようになった。アプリの app.yaml 中でハンドラを指定すると、App Engine はアプリの新しいインスタンスがユーザからのリクエストを受け付け始める前に、その初期化のために Warmup Request を送ろうとする。これにより、エンドユーザがアプリの初期化にともない感じる遅延を削減できる。
  • Channle API がすべてのユーザで利用できるようになった。
  • Task Queue が公式にリリースされ、もはや実験的な機能ではなくなった。'labs' を使った API のインポートパスは非推奨となった。Task Queue で使うストレージはアプリ全般のストレージ割り当て量として計量され、課金の対象となる。
  • Task Queue と Cron リクエストのデッドラインが 10 分に引き上げられた。それらのリクエスト中であっても、データストアと API のデッドラインは以前から変更されていない。
  • Task Queue に対して、開発者は queue.yaml でタスクの retry_parameters を指定できる。
  • 課金を有効にしているアプリでは Task Queue API で待ち行列(queue)を 100 個まで使える。
  • データストアに対して種別、名前空間および実体のプロパティを問い合わせるメタデータクエリが使えるようになった。
  • URLFetch ではレスポンスのサイズとして 32 MB まで許されるようになった。リクエストのサイズは引き続き 1MB までとなっている。
  • イメージ API に対するリクエストとレスポンスのサイズが 32BM に増やされた。Memcach のバッチ操作の合計サイズが 32 MB に増やされた。Memcache の個別のオブジェクトに対する 1MB の制限は引き続き適用される。
  • 送信メールへの添付のサイズが 1MB から 10MB に増やされた。受信メールのサイズ制限は引き続き 10MB となっている。
  • データストア上のバッチ方式による get/put/delete の操作に対する大きさと数量の制限は取り除かれた。個別の実体は引き続き 1MB に制限されているが、データストア全体に対するデッドラインに余裕があれば、バッチ方式で望むだけ多くの実体を同時に get/pub/delete 処理することができる。
  • クエリー結果をもとに反復する場合、データストアサービスは非同期に結果を先読みするようになった。これにより遅延を 10 - 15 % 削減できる場合がある。
  • 管理コンソールの Blacklist ページは拒絶された訪問者の上位の一覧を表示する。
  • 画像のサムネイルの自動生成サービスは 1600px までの任意の切り取りサイズをサポートする。
  • 管理コンソールに表示される全般的なインスタンス遅延の平均値は、インスタンスごとの QPS に応じた平均値になった。
  • アプリのあるバージョンをアップロードした開発者は appcfg.py download_app コマンドを使ってそのバージョンのコードをダウンロードできる。この機能はアプリごとに管理コンソールの Permissions タブで無効にできる。一度無効にすると、この機能を再度有効にすることはできない。
  • 独自ドメインで Google Appes を使っているユーザに対して、カスタム管理コンソールのページが機能していなかった問題を修正。
  • Python 実行環境では、リクエストハンドラが DeadlineExceededError を起こした場合は、インスタンスは強制終了された後に再開される。これは、Django を使っているときに SystemErrors が周期的に起きる問題と関連して修正されるべきだ。
    http://code.google.com/p/googleappengine/issues/detail?id=772
  • webapp.template とピュア Django を混在させたときに起きる Django のバージョンの不一致を避けるため、Django の初期化を appengine_config.py に移動できるようになった。
    http://code.google.com/p/googleappengine/issues/detail?id=1758
  • SSL 上の OpenId の問題を修正。
    http://code.google.com/p/googleappengine/issues/detail?id=3393
  • dev_appserver で login/logout のためのコードが Python 2.6 で動かない問題を修正。
    http://code.google.com/p/googleappengine/issues/detail?id=3566
  • dev_appserver で get_serving_url が透明で長さの足りない(cropped) PNG に対して機能しない問題を修正。
    http://code.google.com/p/googleappengine/issues/detail?id=3887
  • DatastoreFileSub の問題を修正。
    http://code.google.com/p/googleappengine/issues/detail?id=3895

Blogger Glass に関係する変更としては、Task Queue が公式に GAE のサービスの一部としてリリースされたことぐらいのようだ。API が変わったとは書いていないから、今までのコードはそのまま動くってことだろう。とりあえず、taskqueue の import から labs のネームスペースを削除しておいた。appspot に配備ずみ。

関連リンク

関連記事

2010-12-02

設定変更を通知する - 環境設定パネルを作る #4 (MacBloggerGlass)

今回の記事の内容は、環境設定パネルで Blog ID を保存したときに(「Save」ボタンを押す)、アプリウィンドウに表示されているブログ一覧が変更された ID のものに自動的に変わるようにするための実装について。このような動作を実装する意図については、前回の記事(→「次は NSNotification に取り組む」)を参照してもらいたい。

変更点

実装の手順は以下の 4 つ。

  • (a) 通知の名前を文字列定数として定義する。
  • (b) 通知を送るオブジェクトで、通知を送るコードを追加する。
  • (c) 通知を受けるオブジェクトで、通知を処理するメソッド(通知ハンドラ)を定義する。
  • (d) 通知を受けるオブジェクトで、監視の登録のためのコードを追加する。
  • (e) 通知を受けるオブジェクトで、監視の登録解除のためのコードを追加する。

(a) はどこで定義しても構わないが、(b) と同じく通知を送るオブジェクトと一緒に定義する方が良いだろう。通知を受けるオブジェクトでは、送るオブジェクト(のクラス)のインタフェース部を #import することになる。

MacBloggerGlass の場合、通知を送るのが PreferenceController で、受けるのが AppController になる。

(b) の通知を送るタイミングは、環境設定パネルで「Save」ボタンが押されたとき。つまり、ボタンに対応するアクションメソッド内になる。

監視の登録 (d) については、少なくとも最初に環境設定パネルが開くより前でなければならない。確実なのは AppController が作られたときだ。AppController はアプリの MainMenu.xib にインスタンスとして登録されているから、アプリが起動した後は NIB がロードされる中で生成(というか復元)される。このタイミングは awakeFromNib で拾うことができる。一般にアプリの起動時に生成(初期化)されるオブジェクトなら init で登録すれば良い(ヒレガス本 Chapter 14 の例はこちらになっている)。また、登録の解除 (e) は dealloc で行う(これはヒレガス本の例と同じ)

実装

PreferenceController
インタフェース部: PreferenceController.h

追加したのは 1 行。クラスのインタフェースにふくまれるものではないので @interface {...} の外側に置かなければならない。こういう定数が増えたら、定数だけを独立させて別ファイルにすべきかも。

extern NSString * const MBGBlogIDChangeNotification;
実装部: PreferenceController.m

ヒレガス本からそのまま引き写し。

- (IBAction)saveSettings:(id)sender
{
    [...snip...]
    // send notification
    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
    NSDictionary *userInfo = [NSDictionary dictionaryWithObject:selectedBlog.blogID forKey:@"blogID"];
    [nc postNotificationName:MBGBlogIDChangeNotification
                      object:self
                    userInfo:userInfo];
    NSLog(@"Post notification with userInfo (%@)", userInfo);

    [self close];
}
AppController
実装部: AppController.m

通知ハンドラ handleBlogIDChange: は AppController.m だけから参照できる AppController のプライベートメソッドとした(ファイルローカルメソッドと呼ぶべきか?)。これについては荻原(2.0)本の「CHAPTER 09 カテゴリ」を参照のこと。

fetchPosts は GData クライアントライブラリを使って記事のフィードをリクエストするもの。これまで getFeed: というアクションメソッドの一部だったものを今回独立させた。これは、getFeed: と通知ハンドラの両方から呼べるようにするため。もっとも、今回の変更にあわせて、メインウィンドウから Blog ID を入力するフィールドと記事一覧を取得するためのボタンを取り除いたため、getFeed: は無用になってしまったが。

@interface AppController (PrivateMethods)
- (GDataServiceBase *)bloggerService;
- (void)fetchPosts;
- (void)renderHTML:(NSString *)content withBaseURL:(NSString *)baseURL;
- (void)handleBlogIDChange:(NSNotification *)note;
@end

@implementation AppController
[...snip...]
- (void)awakeFromNib
{
    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
    [nc addObserver:self selector:@selector(handleBlogIDChange:) name:MBGBlogIDChangeNotification object:nil];
}
@end

@implementation AppController (PrivateMethods)
[...snip...]
- (void)handleBlogIDChange:(NSNotification *)note
{
    NSLog(@"Received notification: %@", note);
    blogID = [[note userInfo] objectForKey:@"blogID"];

    [posts removeAllObjects];
    [self fetchPosts];
}
@end

プロトタイプ #0

右のスクリーンショットが今の MacBloggerGlass でこのブログを表示させた様子だ。NSSplitView を使い、上半分で記事一覧、下半分でその内容を表示させている。記事の表示には WebView に対して、取得した記事のフィードのコンテンツをただ文字列として流し込んでいるだけ。つまり、HTML としてきちんとしたドキュメントの構造になっていないし、もちろんスタイルシートや JavaScript も付いていない。ま、GData クライアントライブラリを使った RSS リーダのサンプルアプリといったところか。

今回のプロトタイプでは Blog ID の選択を環境設定に押し込めている。これは、ブログは高々 1 つしか見ることはないし、Blog ID を切り替える頻度は高くない、と想定したから。実際に動かしてみて思うことは、複数のブログを作っているなら他のブログも同時に見たいと思うかもしれない、ということ。つまりは、ブラウザのようにタブやウィンドウで複数のコンテンツを同時に開ける方が良いかもしれない。

ま、それは 1 つのブログをきちんと表示できるようになってから考えよう。せめて、GAE 版と同等の表示ができるようにしてからだ。

おまけ

予告(→「次は NSNotification に取り組む」)では「通知」に関連するドキュメントも探すと書いたが、時間切れで本当に探すだけになってしまった。まだ読めていない。リンクだけは「関連リンク」のところに挙げておく。

また、「通知」に関して書かれたドキュメントを検索していて便利そうなものを見つけた。Devpedia シリーズとでも呼べば良いのかな(URL の一部になっている)。右のスクリーンショットはそれを Safari で開いたところだ。

何が便利かと言うと、たとえば「Cocoa Core Competencies」なら、Cocoa アプリを作るために必要になるさまざまなトピックを一覧できること。また、それぞれについての簡単な説明(使い方をふくむ)が付いていて、より詳しいドキュメントへのリンクもある。ヒレガス本のように、アプリの作り方を順を追って説明するようなタイプのものではないが、実際にアプリを作るときにドキュメントを参照する出発点として役に立ちそう。

検索して見つかったのは以下の 3 つ。

参考文献

詳解 Objective-C 2.0
荻原 剛志
ソフトバンククリエイティブ ( 2008-05-28 )
ISBN: 9784797346800
Cocoa Programming for Mac OS X
Aaron Hillegass
Addison-Wesley Professional ( 2008-05-15 )
ISBN: 9780321503619

関連リンク

関連記事

2010-12-01

次は NSNotification に取り組む - 環境設定パネルを作る #3 (MacBloggerGlass)

今回は明日の作業の予告だけ。

MacBloggerGlass の環境設定パネルについて「設定の保存にまつわる問題」で挙げた 3 つの問題は、NSTableView の選択を Cocoa Bindings でモデルと結びつける方法がわかったことですべて解決できた。明日は、本筋の環境パネルの実装に戻ることにする。

以前に挙げた「ざっくり手順」(→「表示するブログの選択 - 環境設定パネルを作る (MacBloggerGlass)」を参照)で言えば、残りは「User Defaults の値を参照するように、メインンアプリのコードを変更する」になる。

とは言っても、単純にメインウィンドウのアクション(右のスクリーンショットの Read ボタン)に組み込むことは難しくない。アクションメソッドの冒頭でテキストフィールドから読み取っている Blog ID を User Defaults から読むだけ。

今ある動作確認用の一時コードは以下のようになっている(Blog ID を入力するフィールドは削除する予定だから、以下はあくまで環境設定パネルの動作確認用)。

- (IBAction)getFeed:(id)sender
{
    blogID = [blogIdField stringValue];
    if ([blogID length] == 0) {
        NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
        blogID = [defaults objectForKey:MBGSelectedBlogID];
    }
    NSLog(@"Blog ID: %@", blogID);
    [...snip...]
}

これだけで「メインアプリのコードを変更する」ことが完了なら、もう終わっていることになる。もちろん、これだけではない。環境設定パネルで表示するブログを選択(変更)し「Save」ボタンを押したときに何が起きるべきかを考えてみよう。

ユーザは表示するブログを変更するために環境設定パネルを開く。その中でブログを変更したなら、アプリウィンドウに戻ってきたときに何を期待するだろうか? もう一度「Read」ボタンを押そうと思うだろうか? ユーザが環境設定パネルで「Save」ボタンを押したときの期待は (a) パネル内の変更が User Defaults として保存されること、(b) その変更がアプリに直ちに反映されること、の 2 つのはずだ。

そこで NSNotification の出番となる(ヒレガス本の Chapter 14 の内容)。これは、言わばアプリ内のオブジェクト間通信で、あらかじめ取り決めた名前を媒介にして、あるオブジェクトから別のオブジェクトに情報を送る仕組みだ。

MacBloggerGlass の場合で言えば、PreferenceController から saveSettings: の中で通知を送り、それを AppController が受け取ることになる。通知には任意のオブジェクトを同封できるため、新しい Blog ID を (NSString) として付ければ良い。受け取った側では、送られてきた Blog ID が表示中のものと変わっていたら、記事一覧を新たに取得する、となる。

明日は、コードの実装に加えて、関連するドキュメントも探すつもり。

関連記事

twitter より (2010-11-30)

Powered by twtr2src.

2010-11-30

Cocoa Bindings の使い方 #3 - 選択を結びつける

前回の記事で(→「設定の保存にまつわる問題 - 環境設定パネルを作る (MacBloggerGlass)」)、環境設定パネルの表示状態を保存・復元するためには、表部分の選択もモデルとして保持しなければならない、と書いた。

そこで、今回の内容は、NSTableView で選択されるべき行をモデルに格納する方法。もちろん、両者を結びつけるには Cocoa Bindings を使う。

まず、考えるべきは表の選択(に関する情報)を、Cocoa Bindings ではどう扱えば良いのか? 表 (NSTableView) とモデルを結びつけているのは NSArrayController だ。ならば、選択のための結びつきも NSArrayController で用意されているはず。

NSArrayController の Selection Indexes

NSArrayController の「Bindings」パネルを見てみると、それらしい項目が見つかる。それが「Selection Indexes」だ。右のスクリーンショットは「Selection Indexes」を開いたところ。この例では結びつける先は「File's Owner」(この NIB をロードするオブジェクト)の selectionIndex というプロパティになっている。

さて、これで選択のための結びつきを作れることはわかった。次の問題は選択を表現するデータの型は何かということ。NSTableView で選択された行は selectedRow メソッドで取り出すことができるが、この型は NSInteger で、これはつまり int だ。では、上記の結びつきの先となるプロパティは int (あるいは NSInteger) で良いのか?

Interface Builder 上で Selection Indexes のところにマウスを持っていき、しばらくじっとしているとツールチップが表示される。そこには NSIndexSet のインスタンスが選択された行を指定すると書かれている。もっと詳しく知りたければ「Cocoa Bindings Reference」の NSArrayController > Controller Content Parameters Bindings > selectionIndexes を見ること。

サンプルコード

NSTableView の選択を Cocoa Bindings を使ってモデルと結びつける実例として、MacBloggerGlass の環境設定パネルを実現するコードからの抜粋を示す。

まず、モデルとしての NSIndexSet は PreferenceController オブジェクトに抱えさせている。プロパティ名は先のスクリーンショットにも出ていたように selectionIndex だ。

PreferenceController の実装で selectionIndex にアクセスするのは(解放する dealloc を除いて) 3 ヶ所。

  • プロパティとしてのアクセサ(readonly)
  • windowDidLoad 内でインスタンスの確保と初期化(空選択として)
  • ブログ一覧を取得した際に実行される blogListTicket:finishedWithFeed:error で User Defaults に保存された選択の復元

NSIndexSet にはインスタンス生成用のクラスメソッドが用意されていて、空選択の場合には +indexSet を用いる。

保存された選択の復元を行うコードを以下に示す。

処理の内容は、(a) User Defaults から「選択された Blog ID」を取り出し、(b) ブログ一覧の各項目の ID と比較し、(c) 一致するものが見つかれば、(d) それを選択する、となっている。

- (void)blogListTicket:(GDataServiceTicket *)ticket
      finishedWithFeed:(GDataFeedBlog *)aFeed
                 error:(NSError *)error
{
    [...snip...]
    // selectedIndex
    NSString *blogID = [self selectedBlogID]; // ........................ (a)
    int index = 0;
    EntryBlog *entry;
    for (entry in feed.entries) {
        if ([entry.blogID compare:blogID] == NSOrderedSame) break; // ... (b)
        index++;
    }
    if (index < [feed.entries count]) { // ........................... (c)
        [self willChangeValueForKey:@"selectionIndex"];
        [selectionIndex initWithIndex:index]; // ........................ (d)
        [self didChangeValueForKey:@"selectionIndex"];
    } else {
        [selectionIndex init]; // empty NSIndexSet
    }
}

(d) の前後にある willChangeValueForKey: と didChangeValueForKey は KVO の通知を送るためのお作法の 1 つ(→「表示するブログの選択 - 環境設定パネルを作る (MacBloggerGlass)」を参照)。

今さらなんだけど、NSString って == で(文字列としての)比較はできないのな。Ruby や Python に慣れていると、つい == で比較してしまうよ。

関連リンク

関連記事

2010-11-29

設定の保存にまつわる問題 - 環境設定パネルを作る #2 (MacBloggerGlass)

設定を User Defaults に書き込むことは難しくない。設定値が NSString であるならなおさらだ。PreferenceController の実装部に以下のようなコードを埋め込むだけだ。

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSString *accountID = [accountField stringValue];
    [defaults setObject:accountID forKey:@"AccountID"];

また参照する側 (AppController) では、以下のようにすれば良い。

    blogID = [blogIdField stringValue];
    if ([blogID length] == 0) {
        NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
        blogID = [defaults objectForKey:@"blogID"];
    }

今回の記事の内容は、設定を保存するコードではなく、設定の保存を中心としたユーザ体験上の問題について。

UI が提供するユーザ体験は、実際に動かしてみると、デザイン段階での「あら」があちこちに浮かび上がってくる。環境設定パネルでもアプリ本体とつないで見ると、いくつかの不備が見えてきた。今日、「Save」ボタンの機能を実装してみて、(そして動かしてみて)以下の 3 点について気付いた。

  • パスワードを User Defaults に保存するのはマズい。
  • 「Save」ボタンを押した後のフィードバックがない。
  • ブログのリストと選択の状態が保存、復元されない。

設定を保存すると ~/Library/Preferences に置かれるファイルに書き込まれる。テキスト形式は言うに及ばず、バイナリ形式にしても、そこにパスワードを平文で書き込むのはマズい。パスワードを保存する場合には、キーチェーンと連携させるのが Mac アプリの作法だろう。とりあえず、パスワードは保存の対象から外しておく。キーチェーンとの連携は棚上げ。

アプリを使う際にさまざまな処理の結果がユーザに対して適切にフィードバックされるかどうかはユーザ体験に大きく影響する。「ボタンを押す」というユーザのアクションに対してユーザの目に見える反応が何も起こらないと、ユーザに不安を感じさせることになる。ありがちなフィードバックとしては「完了しましたダイアログ」を出すことだが、これは使いどころを間違えると、ただわずらわしいだけになる。

今回の環境設定パネルの場合、「Save」が押されたらパネル自体を消すというのが良いように思う。現状ではこのパネルの役割はアプリのメインウィンドウで表示するブログの選択のみだ。そこで「Save」ボタンが押されたのだから、保存処理が完了したのなら何も仕事は残っていない。だから、パネルを消せば処理が(成功のうちに)完了したことは十分に伝わるし、(わずらわしいダイアログのような)余分な UI も増えない。

今、悩んでいるのは、最後の環境設定パネルを(設定を保存し)一度閉じて再び開いたときに、元の状態を復元させる仕組みだ。必要な情報はブログ(タイトル)の一覧と選択された項目へのインデックス。ここで、問題が 2 つほど出てくる。

1 つは、「ブログ(タイトル)の一覧の保存」に関係する。前回の記事(→「表示するブログの選択」)で書いたように、環境設定パネル(右図)の表部分 (NSTableView) は、Cocoa Bindings によって(NSObjectController と NSArrayController を介して)モデルのプロパティに結びついている。表に出てくる項目は EntryBlog のプロパティであり、これを保存することは EntryBlog を保存することになる。Cocoa Bindings で結びついている以上、ブログのタイトルだけを NSStrings を抱えた NSMutableArray として保存しても意味はない。

そう、1 つ目の問題は環境設定パネルにとってのモデルである FeedBlog と EntryBlog の保存(永続化)の仕組みが必要だ、というものだ。

2 つ目の問題は、選択の状態をどうやって復元するか、というもの。何度も書いているようにパネルの表(ビュー)は Cocoa Bindings によってモデルと結びついている。だから、その選択の状態も Cocoa Bindings でどこかの何かと結びつけなければならない。Interface Builder で NSTableView の Bindings パネルを開くと Selection Indexes という「結びつき」を設定する項目がある。これを使えば選択の状態を Cocoa Bindings による結びつきで伝えられそうなんだが、問題はそれを「どこ」の「何」というプロパティにするか。

保存すべき情報という意味でこれもモデルの一部と言える。FeedBlog の一部にすることもできるが、抽象化の障壁に穴を空けてしまうようでイヤな感じだ。別のモデルオブジェクトとして独立させた方が良い。と言っても、情報自体は整数値 1 つなので、実際には PreferenceController に int のプロパティを増やすことになるか。

次回は表項目の「選択」のための結びつきを Cocoa Bindings で作ることを試してみよう。

まあ、ユーザ体験うんぬん以前に、ウィンドウの UI を考えるときに状態の保存と復元について考えていないのがそもそも悪い。経験不足だと切って捨ててしまえばそれまでだけど、Cocoa Bindings を使ってモデルとビューを同期させることに注意が引きつけられていたせいもあるだろう。

関連記事

2010-11-28

表示するブログの選択 - 環境設定パネルを作る #1 (MacBloggerGlass)

GAE 版 Blogger Glass では表示するブログの Blog ID を config.yaml という設定ファイルで指定するようになっている。また、実行時にユーザが(Google アカウント)でログインすることで、ユーザごとに Blog ID を指定できるように「Settings」画面も用意した(→「GAE アプリでユーザごとの設定を可能にする #5」)。

Mac 版では表示するブログの選択は「環境設定パネル(Preferences)」を開いて行う。GAE 版のように Blog ID を直接指定するのはメンドウなので、Google アカウントの ID をパスワードを入力することで、そのアカウントで作ったブログの一覧から選べるようにしたい。

今回は、そんな「環境設定パネル」でブログ一覧を取得し、表示するところまでを作り込む。

環境設定パネルを追加する

Cocoa アプリに対する「環境設定パネル」の設置手順は「ヒレガス本」の Chapter 12 〜 14 の内容となる。

ざっくり手順を並べると以下のようになる。

  1. 「環境設定パネル」を制御するコントローラを、NSWindowController を継承して作る。
  2. 適当な場所(AppContoller みたいなものを作っていればそこ)に「環境設定パネル」を表示させるアクションを作る。
  3. Interface Builder でメインメニューのメニュー項目(「アプリ名」>「Preferences...」)に「環境設定パネル」を表示させるアクションを接続する。
  4. 「環境設定パネル」用の NIB ファイルを作る(実際には XIB ファイル)。この NIB の File's Owner は「環境設定パネル」を制御するコントローラにする。
  5. Interface Builder でパネルを作る。
  6. 「環境設定パネル」を制御するコントローラに、パネルで使うアクション等を作り込む。
  7. 「環境設定パネル」の設定を User Defaults に書き込むようにする。
  8. User Defaults の値を参照するように、メインのアプリウィンドウのコードを変更する。

今回の記事の内容は、このうちの 6. までとなっている。7. と 8. は次回に持ち越し。

デザイン (意匠)

環境設定パネルの外観と簡単な使い方を以下に示す。

MacBloggerGlass ウィンドウデザイン
環境設定パネル
Google Account の ID (E-mailアドレス) とパスワードを入力し「Get Blog List」を押すと、下部の Blog for viewing にその Google アカウントで Blogger に作ったブログのタイトル一覧が現れる。アプリで表示させたいブログを選択し、「Save」ボタンを押して保存する。パネルを閉じるには左上のクローズボタンを押すか、ESC キーを叩く。

デザイン (設計)

Google アカウントの ID とパスワードを入力させて、ブログの一覧を取得するという機能は、GData ライブラリのパッケージにふくまれている Blogger 用サンプルにほぼそのままの形で作り込まれている。それをのまま流用しても良かったが(実際、GData を使った Google Data API へのアクセスの部分は流用している)、ここでは Cocoa Bindings を使ってみたい。つまり、「環境設定パネル」のコントローラ(PreferenceController クラスとした)を NSTableView のデータソースとするのではなく、モデルとなるオブジェクトを NSObjectController (あるいはその派生クラスたち)を介して、NSTableView と結びつけるようにしたい、ということ。

後述するように、モデルは、ブログごとの情報を抱えるオブジェクトと、その配列を抱えるオブジェクトの二段構えになっている。これは、Google Data API を経由してユーザのブログ一覧を取得した際に、GData ライブラリが生成するオブジェクトの構成をそのまま写し取ったものだ。

(GData ライブラリによるブログ一覧)
+---------------+
| GDataFeedBlog |
|---------------|            +----------------+
|- entries    --|----------->| GDataEntryBlog |
|  ...          | 1        * |----------------|
|               |            |  ...           |
+---------------+            +----------------+

GDataEntryBlog は個々のブログの ID やらタイトルやらといった情報を保持している。

この二段構えのモデル構造は以前の記事(→「Cocoa Bindings の使い方」)でダミーとして作ったモデルの構造と同じだ。あのサンプルアプリでは、二段構えモデルに対応するため、Cocoa Bindings のためのコントローラも NSObjectController と NSArrayController の二段構えとした。その構成をそのまま、今回の「環境設定パネル」でも用いる。

実装

モデル

モデルクラスは 2 つ。FeedBlog クラスは GData ライブラリが提供する GDataFeedBlog に対応するもので、EntryBlog クラスは同じく GDataEntryBlog に対応するものだ。どちらも対応する GData ライブラリのクラスを内部に抱える(has-a の関係)とともに、ビューで表示するための情報を抽出するためのクラスとして作った。抽出した情報(例: ブログのタイトル)は、主にビューから Cocoa Bindings の結びつきを介してアクセスするために用いる。

どちらのクラスにも、対応する GData ライブラリのオブジェクトをセットするためのメソッドを用意していて、そこで情報の抽出も行っている。

FeedBlog
//
//  FeedBlog.h
//  MacBloggerGlass

#import <Cocoa/Cocoa.h>
@class GDataFeedBlog;

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

@property (readonly) NSMutableArray *entries;
@property (readwrite, retain) GDataFeedBlog *gDataFeed;

@end
//
//  FeedBlog.m
//  MacBloggerGlass

#import "GData/GDataBlogger.h"
#import "FeedBlog.h"
#import "EntryBlog.h"

@implementation FeedBlog
- (id)init
{
    [super init];
    entries = [[NSMutableArray alloc] init];
    return self;
}

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

@synthesize entries;
@synthesize gDataFeed;
- (void)setGDataFeed:(GDataFeedBlog *)aFeed
{
    if (aFeed == gDataFeed) return;
    [gDataFeed release];
    gDataFeed = [aFeed retain];

    [entries removeAllObjects];

    EntryBlog *blog;
    GDataEntryBlog *entry;
    for (entry in [gDataFeed entries]) {
        blog = [[EntryBlog alloc] init];
        blog.gDataEntry = entry;        
        [entries addObject:blog];
    }
    NSLog(@"%d blogs was found.", [entries count]);
}
@end

setGDataFeed: で GDataFeedBlog の抱える配列から GDataEntryBlog の要素を取り出している for (var in array) { ... } という書き方が Objective-C 2.0 から導入された高速列挙 (fast enumeration) だ。Ruby や Python ではお馴染みのもの。配列の添字でループし、objectAtIndex: で配列にアクセスするよりもスッキリと書ける。array の部分には配列(NSArray)以外の集合(NSSet)や辞書(NSDictionary)も指定できる。詳しくは荻原(2.0)本 CHAPTER 08 の p.202 〜 206 あたりを参照のこと。

EntryBlog
//
//  EntryBlog.h
//  MacBloggerGlass

#import <Cocoa/Cocoa.h>
@class GDataEntryBlog;

@interface EntryBlog : NSObject {
    NSString *title;
    NSString *blogID;
    GDataEntryBlog *gDataEntry;
}

@property (readonly) NSString *title, *blogID;
@property (readwrite, retain) GDataEntryBlog *gDataEntry;

@end
//
//  EntryBlog.m
//  MacBloggerGlass

#import "GData/GDataBlogger.h"
#import "EntryBlog.h"

@interface EntryBlog (PrivateMethods)
- (NSString *)extractBlogID:(NSString *)identifier;
@end


@implementation EntryBlog
- (void)dealloc
{
    [gDataEntry release];
    [blogID release];
    [title release];
    [super dealloc];
}

@synthesize title, blogID;
@synthesize gDataEntry;
- (void)setGDataEntry:(GDataEntryBlog *)aEntry
{
    if (aEntry == gDataEntry) return;
    [gDataEntry release];
    gDataEntry = [aEntry retain];

    title = [[[gDataEntry title] stringValue] retain];
    blogID = [[self extractBlogID:[gDataEntry identifier]] retain];
}
@end

@implementation EntryBlog (PrivateMethods)

#pragma mark -
#pragma mark PrivateMethods
- (NSString *)extractBlogID:(NSString *)identifier
{
    if (!identifier || [identifier length] == 0) return nil;

    // GDataEntryBlog.identifier is in the format
    // "tag:blogger.com,1999:user-<user id>.blog-<blog id>"
    NSRange rangeBlog = [identifier rangeOfString:@"blog-"];
    int startPos = rangeBlog.location + rangeBlog.length;

    return [identifier substringFromIndex:startPos];
}

@end

EntryBlog.m の方で @interface EntryBlog (PrivateMethods) ... @end として宣言しているのは、このファイルからのみ参照するためのメソッドの定義だ。C でいう static 関数のようなもの。カテゴリの使い方の 1 つ。これも詳しくは荻原(2.0)本 CHAPTER 09 の p.227 〜 228 を参照のこと。

コントローラ

先に述べたように Cocoa Bindings のためのコントローラには NSObjectController と NSArrayController の 2 つを組み合わせて使う。左のスクリーンショットで Feed Observer となっているものが NSObjectController (のインスタンス)、Array Observer となっているものが NSArrayController (のインスタンス) だ。インスタンス名を Observer としているのは、どちらも KVO (Key-Value Observing) のため(だけ)に使っているからだ。

PreferenceController

環境設定パネルを表示するためのクラスで、NSWindowController を継承している。

GData を使ってブログ一覧を取得する部分は以前の記事(→「記事一覧を取得する」)で示したものとほとんど同じなので省略する。

以下に示すのは、ブログ一覧を取得するために GData ライブラリのサービスを呼び出した際に指定するコールバックメソッドだ。注意してほしいのは 102 と 104 行目。これは、KVO で キーに対応する変更通知を発行するための「作法」の 1 つ。103 行目で、feed が保持するオブジェクト(前述の FeedBlog のインスタンス)が抱える配列の内容が変更される。そのことを、PreferenceController に(「feed」というキーで)結びついているオブジェクト(今回の場合、それは Feed Observer という名前の NSObjectController のインスタンス)に通知している。

- (void)blogListTicket:(GDataServiceTicket *)ticket
      finishedWithFeed:(GDataFeedBlog *)aFeed
                 error:(NSError *)error
{
    if (error) {
        NSLog(@"ERROR in fetching the blog list: %@", error);
        return;
    }
    [self willChangeValueForKey:@"feed"];
    feed.gDataFeed = aFeed;
    [self didChangeValueForKey:@"feed"];
}

監視するオブジェクトの変更通知を受け取った Feed Observer (NSObjectController) は、今度は自分自身に(「entries」というキーで)結びついているオブジェクト(Array Observer という名前の NSArrayController のインスタンス)に変更通知を送る。それを受け取った Array Observer は、NSTableView (の中の NSTableColumn) に変更通知を送る。NSTableView は KVC によって結びついたオブジェクトのプロパティ(EntryBlog の title)を取得して、表示を更新する。このようにして、モデルの変更が Cocoa Bindings によって結びついたビューに伝播してゆくのだ。

次の展開

ブログ一覧を取得して表示できるようになっているが、まだ肝心の「Save」ボタンが機能しない。当然、メインウィンドウ(アプリ本体)とも連携していない。現段階では環境設定のためのパネルが単独で動いているだけ。

なので、次は「Save」ボタン(に対応するアクション)の実装と、アプリ本体とのつなぎ部分を実装することになる。

参考文献

詳解 Objective-C 2.0
荻原 剛志
ソフトバンククリエイティブ ( 2008-05-28 )
ISBN: 9784797346800
Cocoa Programming for Mac OS X
Aaron Hillegass
Addison-Wesley Professional ( 2008-05-15 )
ISBN: 9780321503619

関連記事