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.