ラベル 2. アプリの断片 の投稿を表示しています。 すべての投稿を表示
ラベル 2. アプリの断片 の投稿を表示しています。 すべての投稿を表示

2010-12-27

NSTableView でソートしたとき、選択された要素をモデル配列から正しく取り出す方法

NSTableView では、見出し行をクリックするとカラムの内容によって行をソートすることができる。ところが、これはモデル(となっている NSMutableArray)をソートしているわけではないから、ソートしたビュー上の並び順とモデル内の要素の並び順は一致していない。

たとえば、もともと「りんご」「みかん」「ぶどう」の順にモデル配列の中に収められているとして、これをビュー上でソートして「ぶどう」「みかん」「りんご」の順に表示されているとする。ここでビュー上で「りんご」を選択すると、そのインデックスは(0 から数えるので) 2 となる。このインデックスを使い、モデル配列から objectAtIndex: で要素を取り出せば、それは「りんご」ではなく「ぶどう」になってしまう。

つまり、ビューでソートした状態でビュー上の選択(のインデックス)に合わせてモデルの要素を取り出すと、ビューの選択とは異なる要素を取り出してしまうことになるのだ。

MacBloggerGlass の場合で言うと、記事の一覧をタイトルまたは日付けでソートした状態で一覧から記事の選択を行うと、表示される記事の内容がずれてしまう。

こうしたビューとモデルにおける要素の並び順の不一致を避けるには、両者を結びつけている NSArrayController を使えば良い。

具体的には、NSArrayController から selectedObjects で要素オブジェクト(の配列)を取り出せば、ビューでソートしているかどうかに関係なく、適切なオブジェクト(つまりビュー上の選択と同じオブジェクト)が取り出せる。

MacBloggerGlass の場合、以下のようなコードを使う(AppController.m より抜粋)。

    Entry *entry = [[feedController selectedObjects] objectAtIndex:0];

feedController が記事一覧(と記事内容)用のモデル配列のための NSArrayController だ。AppController の IBOutlet として確保し、Interface Builder で NSArrayController のインスタンスと結びつけている。記事一覧では複数選択を許していないから、selectedObjects の返す配列の最初の要素を取り出している。

ちなみに、この NSArrayController を使う解決方法はググって見つけた(→Cocoaの日々: NSArrayController を使った NSTableView で選択行の情報を取得する)

NStableView と NSArrayController の関係

では、なぜ NSArrayController からは、ソートの状態に関係なく、適切なモデルオブジェクトを取り出すことができるのだろうか?

オブジェクトの配列をモデルとして、その内容を NSTableView に表示させる場合(配列の要素になっているオブジェクトのプロパティを表のカラムに表示させる、という意味)、Cocoa Bindings では両者を NSArrayController で結びつける。

これは詳細に言えば、NSTableColumn の値として、NSArrayController の arrangedObjects が返す配列の要素のプロパティを結びつける、ということだ。初期状態では、arrangedObjects はモデルの配列の並び順そのままの配列を返す。だから、モデル配列の順序がそのままビューの表示順序になる。一方、NSTableView の見出し行でソートしたときは、NSArrayController の arrangedObjects が返す配列の並び順が変わる。その結果、ビューの表示順序が変わる。

実はビュー(NSTableView)の行がソートされるのはむしろ結果で、実際にはコントローラ(NSArrayController)が返す要素の順序がソートされていたのだ。これは、NSArrayController がソートされた状態のモデル配列を保持している、と言っても良い。だから、ビュー上でソートされた状態のインデックスを使っても、適切なオブジェクトを取り出すことができるのだ。

では、なぜ NSArrayController の arrangedObjects の並び順が変わるのか? それは、NSTableView の見出し行がクリックされたときに NSArrayController に対して setSortDescriptors が呼ばれ、NSSortDescriptor がセットされることによる。これはソートの方法を指示するオブジェクトで、NSArrayController は arrangedObjects を作り出す際に参照するものだ。初期状態ではこれがセットされていないため(ソートされず)、arrangedObjects はモデルの要素順と同じ順序の配列になっている。

  • 見出しをクリックしてからの一連の動きを時系列で並べると以下のようになる。
    1. NSTableView の見出しがクリックされる。
    2. NSTableView が NSArrayController に対して、NSSortDescriptor をセットする。
    3. NSTableView (実際には NSTableColumn) が NSArrayController に対して arrangedObjects を要求する(メソッドを呼び出す)。
    4. NSArrayController は先にセットされた NSSortDescriptor にしたがってソートした配列を返す。

    まとめると、ソートはビューで起きているのではなく、コントローラの内部で起きている、ということになる。だから、コントローラはソートされた状態のインデックスから適切なオブジェクトを取り出すことができる、と。

    iOS アプリではどうする?

    Cocoa Bindings が使えない iOS アプリでは NSArrayController に相当する部分を自前で用意しなければならないはず。なんだか面倒なことになりそうな予感がする。ソートしなければ(できないようにすれば)良いんだけど。そもそも、iOS のアプリで表をソートするアプリって見かけないような……。

    関連リンク

    関連記事

    2010-12-22

    ドロワー (NSDrawer) の使い方 - ラベル検索の実装 #2 (MacBloggerGlass)

    ラベルの一覧を表示する UI として、ドロワーを使うことにした。ドロワーとは、アプリウィンドウの端に(大抵は左右のどちらか)付加的に表示できるウィンドウのことだ。通常のアプリウィンドウと同じく、様々なビューを置くことができる。

    最近の Mac OS X 用アプリではあまり見かけなくなった(少々古臭い) UI だが、表示中はメインウィンドウにくっついており見失うこともないし、不要なときは隠しておけばメインウィンドウ上のレイアウトを圧迫することもない。

    MacBloggerGlass のメインウィンドウは記事一覧表示とその内容表示で、すでに 2 分割(2 ペイン)しているが、ここにラベル一覧表示領域を追加するとさらに分割することになってしまう。その方が使いやすいならそうするが、まずは表示領域の分割ではなく、必要な時だけ領域を追加する方式で試してみることにした。

    Interface Builder での操作

    Cocoa アプリにドロワーを追加するには、ライブラリツールから Windows グループにある Windows and Drawer (右のスクリーンショットを参照)をドラッグしドキュメントウィンドウにドロップする。

    ライブラリツール上は 1 つのオブジェクトのように置かれているが、ドキュメントウィンドウにドロップすると以下の 3 つのオブジェクトが現れる(下のスクリーンショット参照。)。

    • Window
    • Drawer Content View
    • Drawer

    この 3 つのオブジェクトは、ドロップされた状態ですでに互いに関連づけられている。具体的には Drawer に備わったアウトレットのうち、parentWindow が Window に、contentView が Drawer Content View にそれぞれ結びつけられている。

    大抵の場合、アプリウィンドウはプロジェクトを作ったときから (MainMenu.xib 内に) 存在しているため、上記の Window オブジェクトは不要だ。当然、削除することになるが、そのときは Drawer オブジェクトの parentWindow を適切に設定しなければならない。

    Drawer オブジェクト(の parentWindow)にアプリのウィンドウを結びつけるには、Ctrl キーを押しながら Drawer オブジェクトからアプリウィンドウまでドラッグする。アプリウィンドウ上に接続するアウトレットの候補が現れるので、parentWindow を選ぶ。

    この Interface Builder のドキュメントにも書いてある正式な手順(Ctrl + ドラッグ)は正直言ってわかりづらい。いつもどっちからドラッグするのかで迷ってしまう。それよりも、アウトレットを備えたオブジェクト上でポップアップメニューを出す方がずっとわかりやすい。マウスなら(特に設定を変えていない限り)右クリック、TrackPad なら 2 本の指でタップした状態でクリックでポップアップメニューが出てくる。メニューにあるアウトレットの右端の端子(○)を接続したいオブジェクトまでドラッグする。Ctrl キーを押す必要はない。

    ドロワーの表示

    ドロワーを表示するにはアプリコントローラからアウトレットを通して openOnEdge: メソッドを呼ぶ。このとき、親ウィンドウのどの端にドロワーを付けるかを引数として指定する。隠す際は close メソッドだ。

    以下は、AppController.m からの抜粋だ。labelsDrawer は AppController クラスのアウトレットで、Interface Builder 上で Drawer オブジェクトに接続してある。

    以下のコードでは、現在のドロワーの状態を取得し、それに応じて表示中なら close、非表示中なら openOnEdge: を呼んでいる。openOnEdge: にわたしている NSMinXEdge はウィンドウの左端を意味する定数だ。ドロワーを右端に表示したいなら NSMaxXEdge を指定する。

    - (IBAction)toggleLabelsDrawer:(id)sender {
        NSDrawerState state = [labelsDrawer state];
        if (state == NSDrawerOpeningState || state == NSDrawerOpenState) {
            [labelsDrawer close];
            [showDrawerMenu setTitle:@"Show Labels"];
        } else {
            [labelsDrawer openOnEdge:NSMinXEdge];
            [showDrawerMenu setTitle:@"Hide Labels"];
        }
    }
    

    上記で、showDrawerMenu に対して setTitle: しているのは、ドロワーの状態によってドロワーを表示(または非表示)にするためのメニュー項目の文字列を変えるためだ。非表示状態では「Show Labels」に、表示中なら「Hide Labels」に置き換えている。showDrawerMenu も AppController のアウトレットで NSMenuItem * として宣言し、Interface Builder 上でメニュー項目と接続してある。

    ラベル一覧の表示

    前回(→「ラベル検索の実装 #1」)のラベル列挙と合わせると、ラベル一覧表示が可能になる。以下がそのスクリーンショットだ。

    MacBloggerGlass プロトタイプ #0
    ラベル一覧表示
    ラベル一覧をドロワーとして実現した状態。一覧表示にふくまれているすべての記事に付いたラベルを集めてドロワー内のテーブルに表示している。

    関連リンク

    関連記事

    2010-12-21

    ラベル検索の実装 #1 (MacBloggerGlass)

    ラベル検索を実現するために必要な仕組みは以下の 2 つ。

    • ラベルの列挙
    • 指定されたラベルの付いた記事の抽出

    今回は、まず簡単な方、ラベルの列挙を実装してみる。

    GDataCategory

    GData ライブラリで Blogger の記事は GDataEntryBlogPost クラスが表現している。各記事に付けられたラベルはこのクラスの categories というプロパティに収められている(実際に categories が定義されているのは GDataEntryBlogPost の基底クラスである GDataEntryBase)。このプロパティは NSArray として取り出すことができ、その各要素は GDataCategory クラスのインスタンスになっている。

    もともと、Blogger の記事データを表す Atom フィードでは、カテゴリは以下のような XML 要素として表現されている。この term 属性が Blogger の記事に付けたラベルになっている。

    <category
        scheme="http://www.blogger.com/atom/ns#"
        term="1. Macをプログラムする">
    </category>
    

    GDataCategory クラスには上記の term 属性を参照するための term プロパティが定義されている。ややこしいことに label というプロパティも定義されているが、こちらは Blogger のラベルとは関係ない。

    実装

    Entry クラス

    Entry クラスでは、内部に抱えた GDataEntryBlogPost の categories プロパティから各 GDataCategory のインスタンスの term プロパティを取り出し、NSArray の一時オブジェクトとして返すメソッド(プロパティへのアクセサ)を定義した。

    - (NSArray *)labels {
        NSArray *categories = [gDataEntry categories];
        if (! categories) return nil;
        NSMutableArray *lbls = [NSMutableArray arrayWithCapacity:[categories count]];
        GDataCategory *category;
        for (category in categories) {
            [lbls addObject:[category term]];
        }
        return lbls;
    }
    
    Feed クラス

    Feed クラスでは、初期化時に各 Entry からラベルを(上述の labels メソッドで)取り出し、NSMutableSet にまとめておき、labels プロパティとして保持するコードを追加した。

    - (void)gatherLabels {
        labels = [[NSMutableSet alloc] init];
        Entry *entry;
        NSString *label;
        for (entry in entries) {
            for (label in entry.labels) {
                [labels addObject:label];
            }
        }
    }
    

    配列ではなく集合を使ったのは、集合は addObject 時に要素の重複を排除してくれるから。

    FeedManager クラス

    また、内部に抱えた Feed インスタンスの labels プロパティをそのまま返す、同名のプロパティを FeedManager クラスにも用意することにした。アプリコントローラからは FeedManager 経由でアクセスすることにしたいから。

    次は?

    ラベルの「見せ方」を考えているところだが、まずは単純にドロワー(NSDrawer)に NSTableView でラベルを列挙する方式にするつもり。

    ドロワーは最近ではあまり見かけなくなったけど、標準で用意されている部品だし、使わないときは隠しておけるし。

    関連記事

    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)に組み込むときは、もちろん変更もできるようにしなければならない。

    関連リンク

    関連記事

    2010-12-09

    アプリに Apple イベントの処理を作り込む - アプリを開くリンク #2

    前回(→「アプリを開くリンク #1」)に引き続き、Launch Services が送ってくる Apple イベントを処理するための作り込みについて調べた。簡単なサンプルアプリを作って動作を確認した。

    以下では、WebView でアプリの独自スキームを持った URL へのリンクをクリックしたときに、アプリに送られてくる get URL という Apple イベントを処理するための作り込みの手順を説明する。

    今回の記事のネタは主に「Cocoa Scripting Guide: How Applications Handle Apple Events」から拾っている。

    Cocoa アプリにおける Apple イベントの処理

    Automator.app を使ってアプリを使った作業の自動化(→「AppleScript を使って URL を Safari のタブで開く」)をしようとしてみると、アプリの中には Apple Script でさまざまな制御が可能なものと、そうでないものがあることに気付く。Safari などは前者で、ウィンドウのサイズを変えたり、指定した URL を開かせたりといったことが Apple Script から制御できる。一方、Cocoa Emacs のように限られた制御(アプリの起動等)しかできないアプリもある。前者のように様々な制御を受けるアプリのことを「スクリプト制御可能(scriptable)」だと言う。

    アプリがスクリプト制御可能なように作られているかどうかに関係なく、Cocoa は Mac OS X が GUI アプリに送る必須イベントに対するデフォルトの処理(ハンドラ)を提供している。一番、わかりやすい例はアプリを起動する open application イベントだろう。このデフォルトの実装があるおかげで「Apple Script やイベントのことなんて知らない」と言うアプリでも、Launch Services から起動することができる。また、デフォルトハンドラはデリゲートの仕組みを使っており、NSApplication のデリゲート(NSApplicationDelegate プロトコルを実装したクラス)でデリゲートメソッドを定義してやればハンドラの動作をカスタマイズすることができる(イベントとデリゲートメソッドの組み合わせについては「Cocoa Scripting Guide: How Applications Handle Apple Events」を参照のこと)。

    ただ、Cocoa が提供するデフォルトのイベントハンドラは限られていて、具体的には「アプリの起動」「アプリの再起動(アクティベート)」「ドキュメントを開く」「印刷」「コンテンツを開く」、そして「アプリの終了」だ。残念ながら URL を開くイベント(get URL)に対するハンドラはデフォルトの実装には含まれていない。これについては、アプリが独自ハンドラとして追加しなければならない。

    Apple イベントハンドラの追加

    イベントハンドラの追加はそれほど難しいことではない。特定の形のメソッドを定義し、それをハンドラとしてイベントマネージャ(NSAppleEventManager)に登録するだけで良い。

    ハンドラは以下の形にする。ただ、handleAppleEvent の部分はイベントに応じて名前を変えても良いようだ。たとえば、後述するサンプルアプリで定義したハンドラではここは handleGetURLEvent となっている(これは「Cocoa Scripting Guide: How Applications Handle Apple Events」の Installing a Get URL Handler で例として使われている名前でもある)。一方で、Mac OS X Reference Library のサンプルコードでは同じイベントのハンドラに対して handleOpenLocationAppleEvent という名前が付いている。引数の数と型が一致していれば良いのかも。

    - (void)handleAppleEvent:(NSAppleEventDescriptor *)event
              withReplyEvent:(NSAppleEventDescriptor *)replyEvent;
    

    Cocoa Scripting Guide: How Applications Handle Apple Events」には具体的なハンドラの定義は載っていなかったため、Mac OS X Reference Library を検索したところ、サンプルコードに get URL イベントのハンドラの例を見つけた(→ CoreRecipesApp/AppDelegate.m)。次に示すサンプルアプリのハンドラの定義は、ここから流用したものだ。

    サンプルアプリ

    独自スキームの定義

    以下は前回の記事(→「アプリを開くリンク #1」)に載せたスクリーンショットだが、再掲しておく。このサンプルアプリが受け付ける独自 URI スキームに sample を設定している。

    Info.plist に URL タイプを追加する様子
    URI スキームとして sample という文字列を指定している。
    Get URL ハンドラの定義

    ハンドラの定義とその登録は、ドキュメントの推奨どおりにアプリケーションデリゲイトで行っている。以下はその部分の抜粋だ。applicationWillFinishLaunching: で、Get URL ハンドラ handleGetURLEvent:withReplyEvent を登録している。

    - (void)applicationWillFinishLaunching:(NSNotification *)notification
    {
        NSAppleEventManager *appleEventManager = 
        [NSAppleEventManager sharedAppleEventManager];
        [appleEventManager setEventHandler:self 
                               andSelector:@selector(handleGetURLEvent:withReplyEvent:)
                             forEventClass:kInternetEventClass
                                andEventID:kAEGetURL];
    }
    
    - (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
     // Insert code here to initialize your application 
    }
    
    // Apple event handler
    - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
               withReplyEvent:(NSAppleEventDescriptor *)replyEvent
    {
        NSAppleEventDescriptor *directObjectDescriptor =
        [event paramDescriptorForKeyword:keyDirectObject];
    
        if (directObjectDescriptor) {
            NSString *urlString = [directObjectDescriptor stringValue];
            if (urlString) {
                NSURL *objectURL = [[NSURL alloc] initWithString:urlString];
                if (objectURL) {
                    NSString *host = [objectURL host];
                    NSString *path = [objectURL path];
                    NSString *description =
                    [NSString stringWithFormat:@"%@ in (%@)", path, host];
                    [appController updateContent:description];
                }
            }
        }
    }
    

    このハンドラは受け取ったイベントの内容から URL を取り出し、さらにそこからホスト名とパスを抽出して、WebView を抱える AppController に渡している。

    AppController では、イベントハンドラから受け取った文字列を WebView に渡す HTML に埋め込み、再表示させている。以下にその実装部(AppController.m)を示す。

    #import <WebKit/WebKit.h>
    
    #import "LaunchAppAppDelegate.h"
    #import "AppController.h"
    
    NSString * const HTMLTemplate = 
    @"<html>\n"
    @"<body>\n"
    @"<h1>%@</h1>\n"
    @"<ul>\n"
    @"<li>Requested: %@</li>\n"
    @"</ul>\n"
    @"<p><a href='sample://1234567890/0987654321'>Click me!</a>"
    @"(sample://1234567890/0987654321)</p>\n"
    @"</body>\n"
    @"</html>\n";
    
    @interface AppController (PrivateMethods)
    - (void)renderHTML:(NSString *)html;
    @end
    
    
    @implementation AppController
    - (void)awakeFromNib
    {
        appDelegate.appController = self;
        [self renderHTML:[NSString
                          stringWithFormat:HTMLTemplate, @"Sample App", @""]];
    }
    
    - (void)updateContent:(NSString *)request
    {
        [self renderHTML:[NSString
                          stringWithFormat:HTMLTemplate, @"Sample App", request]];
    }
    
    @end
    
    @implementation AppController (PrivateMethods)
    - (void)renderHTML:(NSString *)html
    {
        [[display mainFrame] loadHTMLString:html
                                    baseURL:[NSURL URLWithString:@"http://localhost"]];
    }
    @end
    

    HTMLTemplate の定義では見易くするために文字列オブジェクト定数を並べて置いている。これらはコンパイル時に連結され 1 つの文字列オブジェクト定数となる。

    動作確認

    クリックする前と後のスクリーンショットを示す。これにより、アプリの抱える WebView からそのアプリの独自 URL へのリンクをクリックすることで、実行中のアプリ自身にイベントを飛ばせることが確認できた。MacBloggerGlass で内部リンクの置き換えを実現できる目処が立ったことになる。

    WebView で独自 URL をクリックした際の動作確認
    クリック前
    「Requested」の横には何も表示されていない。Click me! をクリックすると……
    クリック後
    「Requested」の横に URL にふくまれていた情報が抜き出されてきた。

    関連リンク

    関連記事

    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-11-25

    Cocoa Bindings の使い方

    追記@2010-11-30

    Cocoa Bindings の使い方については以下の後続記事も参照のこと。

    ブログの記事一覧を表示するようなアプリでは、記事一覧のデータは、配列(のような何か)に蓄えることになるし、画面の表示は表形式(に似た何か)を使うことになるだろう。

    Cocoa アプリでこれをシンプルに実現すると、配列 (NSMutableArray) に蓄えられたデータをテーブルビュー (NSTableView) に表示するものになる。MVC アーキテクチャで言うなら、配列がモデル(正確には配列に蓄えられたオブジェクトがモデル)で、テーブルビューが(その名の通り)ビューになる。

    NSTableView にデータを表示させる場合、二通りの方法がある。1 つは、NSTableView のデータソースとなるクラスを定義する方法。具体的には、AppController というようなクラスを定義し(MVC の C)、そこで以下の 3 つのメソッドを定義するというもの。

    • - numberOfRowsInTableView:
    • - tableView:objectValueForTableColumn:row:
    • - tableView:setObjectValue:forTableColumn:row:

    3 つ目のメソッドは表示のみのアプリなら必要ない。

    この方法の欠点は、NSMutableArray の内容を NSTableView における表示と同期させるコードは、どれも良く似ているものになることだ。

    良く似たものになるなら、その仕組みを共通化してフレームワークが提供してくれればありがたい。もちろん、そのためにはある種の「抽象化」が必要になり、利用に際しては一定の「作法」に従わなければならない。この「共通化された仕組み」が Cocoa Bindings であり、「抽象化」が KVC (Key-Value Coding)、「作法」が KVO (Key-Value Observing) ということになる。

    サンプルアプリ

    今回、サンプルとして作ったアプリのスクショがこれ(→)だ。機能的には(意匠的にも)ヒレガス本の Chapter 8 で作るもの(→ 「MVC と Cocoaバインディング」参照)と同等だが、こちらは、コードとして書いたのはモデルとなる 2 つのクラスのみで、ビューは当然として、コントローラも Cocoa フレームワークの提供するものだけで作られている。

    モデル

    まずはモデルを構成する 2 つのクラスを示す。これら 2 つは、Atom 形式のフィードから生成することを想定したものだ。Feed オブジェクトが Entry オブジェクトの配列を抱えるようになっている。

    Entry の定義
    //  Entry.h
    #import <Cocoa/Cocoa.h>
    
    @interface Entry : NSObject {
        NSString *title;
        NSDate *updated;
    }
    
    @property (readwrite, retain) NSString *title;
    @property (readwrite, retain) NSDate *updated;
    
    @end
    
    //  Entry.m
    #import "Entry.h"
    
    @implementation Entry
    - (id)init
    {
        [super init];
        title = @"Hi!";
        updated = [[NSDate date] retain];
        return self;
    }
    
    - (void)dealloc
    {
        [updated release];
        [title release];
        [super dealloc];
    }
    
    @synthesize title, updated;
    
    - (void)setTitle:(NSString *)newTitle
    {
        if (newTitle == title) return;
        [title release];
        title = [newTitle retain];
        self.updated = [[NSDate date] retain];
    }
    
    @end
    
    Feed の定義
    //  Feed.h
    #import <Cocoa/Cocoa.h>
    
    @interface Feed : NSObject {
        NSString *title;
        NSDate *updated;
        NSMutableArray *entries;
        int count;
    }
    
    @property (readwrite, retain) NSString *title;
    @property (readwrite, retain) NSDate *updated;
    @property (readwrite, retain) NSMutableArray *entries;
    @property (readwrite) int count;
    
    
    @end
    
    //  Feed.m
    #import "Feed.h"
    #import "Entry.h"
    
    @implementation Feed
    - (id)init
    {
        [super init];
        title = @"My Feed";
        updated = [[NSDate date] retain];
        entries = [[NSMutableArray alloc] init];
        [entries addObject:[[Entry alloc] init]];
        count = [entries count];
        return self;
    }
    
    - (void)dealloc
    {
        [entries release];
        [updated release];
        [title release];
        [super dealloc];
    }
    
    @synthesize title, updated, entries;
    
    - (void)setEntries:(NSMutableArray *)newArray
    {
        if (newArray == entries) return;
        [entries release];
        entries = [newArray retain];
        self.count = [entries count];
    }
    
    @synthesize count;
    @end
    
    コントローラ

    コントローラには NSObjectControllerNSArrayController を 1 つずつ使っている。これらのコントローラは Interface Builder から NIB (今は XIB)ファイルの中にインスタンス化し、設定を変更しているだけだ。コードは一切、書いていない。

    これらコントローラの設定で肝となるのは、それぞれのコントローラが保持することになるモデルクラスとの対応づけだ。NSObjectController は 上述のFeed クラスのオブジェクトを保持する。NSArrayControllerEntry クラスのオブジェクトを生成し(Add ボタンのアクション)、消滅させる(Delete ボタンのアクション)。ただし、Entry オブジェクトたちを保持するのは Feed が抱えた配列だ。このため、NSArrayControllerFeed オブジェクトに対して要素となる Entry オブジェクトの追加、編集、削除を指示する必要がある。このとき用いられるのが Cocoa Bindings だ。

    NSArrayController は配列を抱えた NSObjectController にバインドされることになる。

    ビュー

    スクショを見ればわかるように、いくつかのテキストフィールド(いずれも編集不可にしてある)とテーブルビュー(タイトル欄のみ編集可)、および 2 つのボタンから構成されている。いずれも Cocoa フレームワーク標準のビューたちだ。

    テキストフィールドおよびテーブルビュー(のセル)は、Cocoa Bindings によって、2 つのコントローラのプロパティを経由してモデルと結びつけられている。このため、モデル(のプロパティ)が変更されれば、KVO の通知により自動的に変更がビューに反映されることになり、ビューによる変更はコントローラを経由してモデルに反映されるようになっている。

    まとめ

    MVC のうちモデルとなるクラスを書くだけで後は、Interface Builder の操作でアプリが作れてしまう。標準の部品(ビューとコントローラ)を使うアプリであれば簡単にできる。ただし、Interface Builder を使って Cocoa Bindings の設定するのには、ちょっと慣れが必要だ。似たような設定項目が多く、設定する場所を間違えたら、まったく動かない。今日もそれでかなり苦労した。

    参考文献

    詳解 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-11-24

    日付文字列の変換 - RFC3339 から NSDate へ

    実装

    まずは、今回作った Objective-C による実装から示す。関数の機能は単純で、RFC3399 形式で表現された日時を元に NSDate のオブジェクトを生成する。関数内で NSDate オブジェクトを alloc しているが、返す前に autorelease しているため、荻原(2.0)本 (p.96) で言う「一時的なオブジェクト」となっている。また、関数を呼び出す側で自動解放プールを用意する必要がある。通常の Cocoa アプリの場合は、NSApplication がイベントループの管理とともに自動解放プールも用意してくれる(→ 「荻原(2.0)本」p.98)。

    この実装では NSString の機能のうち、以下のメソッドを多用している。

    - (NSRange)rangeOfCharacterFromSet:(NSCharacterSet*)aSet
    aSet で渡された文字集合中の文字が最初に見つかった場所を返す。
    - (NSString*)substringToIndex:(NSUInteger)aIndex
    先頭から位置 aIndex までの部分文字列を返す。
    - (NSString*)substringWithRange:(NSRange)aRange
    aRange.location を開始位置とし、aRange.length を長さとする部分文字列を返す。
    - (NSString*)substringFromIndex:(NSUInteger)
    aIndex を開始位置としする末尾までの部分文字列を返す。

    上記の substring...の戻り値はいずれも一時的なオブジェクトになっている。

    参照仕様

    GData API の Protocl Reference によれば、各種日付には RFC3339 フォーマットが使われる、とある。これは GData API が使うデータフォーマット(Atom 形式; RFC 4287 で定義されている)の仕様によるものだ。

    (「RFC 4287」より)
    A Date construct is an element whose content MUST conform to the "date-time" production in [RFC3339]. In addition, an uppercase "T" character MUST be used to separate date and time, and an uppercase "Z" character MUST be present in the absence of a numeric time zone offset.

    [...snip...]

    Example Date constructs:
    <updated>2003-12-13T18:30:02Z</updated>
    <updated>2003-12-13T18:30:02.25Z</updated>
    <updated>2003-12-13T18:30:02+01:00</updated>
    <updated>2003-12-13T18:30:02.25+01:00</updated>

    上記の中で例として挙げられている 4 つのパターンを見れば書式のおよそは理解できる。

    より詳細な 書式の定義は RFC 3339 にある。

    date-fullyear   = 4DIGIT
    date-month      = 2DIGIT  ; 01-12
    date-mday       = 2DIGIT  ; 01-28, 01-29, 01-30, 01-31 based on
                              ; month/year
    time-hour       = 2DIGIT  ; 00-23
    time-minute     = 2DIGIT  ; 00-59
    time-second     = 2DIGIT  ; 00-58, 00-59, 00-60 based on leap second
                              ; rules
    time-secfrac    = "." 1*DIGIT
    time-numoffset  = ("+" / "-") time-hour ":" time-minute
    time-offset     = "Z" / time-numoffset
    
    partial-time    = time-hour ":" time-minute ":" time-second
                      [time-secfrac]
    full-date       = date-fullyear "-" date-month "-" date-mday
    full-time       = partial-time time-offset
    
    date-time       = full-date "T" full-time
    

    この定義から、日時の表現は「日付」と「時刻」に二分され、その区切りが文字「T」であること、また「時刻」は「時刻本体」と「時差」に分かれることが読みとれる。

    「時差」については、時刻による差分で表現したもの(「+09:00」や「-5:00」など)か、あるいは文字「Z」を用いる(時差として文字「Z」を指定した場合、その時刻は UTC を意味する)。

    さらに「時刻本体」の表現については、「時」「分」「秒」をそれぞれ 2 桁の十進数で表現する(1 桁の場合は先頭に「0」を付加する)。また「秒」については 1 秒以下を小数表現で「秒」に付けても良い、となっている。

    GData API での時刻の取扱い

    実際に GData API が返すフィードを見ると、updated 要素等は以下のようになっていて、確かに上記の書式に従っていることがわかる。

    <published>2010-10-03T21:18:00.000+09:00</published>
    <updated>2010-10-03T21:18:00.840+09:00</updated>
    

    一方、GData Objective-C Library が返す時刻データは GDataDateTime クラスのインスタンスになっていて、そこから文字列データを取り出すと RFC3339 フォーマットの文字列となる。ただし、それを作る部分は以下のようになっているため、「秒数」の小数点以下が無視されている(GData Objective-C Client ver. 1.10.0)。これは、Cocoa の NSDateComponents が小数点以下の秒数をサポートしていないことによるものだと思われる(以下のコード中の dateComponents は NSDateComponents のインスタンスを保持している)。

    - (NSString *)stringValue {
      return [self RFC3339String];
    }
    
    - (NSString *)RFC3339String {
      NSDateComponents *dateComponents = [self dateComponents];
      NSInteger offset = [self offsetSeconds];
    
      NSString *timeString = @""; // timeString like "T15:10:46-08:00"
    
      if ([self hasTime]) {
    
        NSString *timeOffsetString; // timeOffsetString like "-08:00"
    
        if ([self isUniversalTime]) {
         timeOffsetString = @"Z";
        } else if (offset == NSUndefinedDateComponent) {
          // unknown offset is rendered as -00:00 per
          // http://www.ietf.org/rfc/rfc3339.txt section 4.3
          timeOffsetString = @"-00:00";
        } else {
          NSString *sign = @"+";
          if (offset < 0) {
            sign = @"-";
            offset = -offset;
          }
          timeOffsetString = [NSString stringWithFormat:@"%@%02ld:%02ld",
            sign, (long)(offset/(60*60)) % 24, (long)(offset / 60) % 60];
        }
        timeString = [NSString stringWithFormat:@"T%02ld:%02ld:%0l2d%@",
          (long)[dateComponents hour], (long)[dateComponents minute],
          (long)[dateComponents second], timeOffsetString];
      }
    
      // full dateString like "2006-11-17T15:10:46-08:00"
      NSString *dateString = [NSString stringWithFormat:@"%04ld-%02ld-%02ld%@",
        (long)[dateComponents year], (long)[dateComponents month],
        (long)[dateComponents day], timeString];
    
      return dateString;
    }
    

    おまけ

    最初に挙げたプログラムを Xcode で Command Line Tool としてビルドし、実行するとコンソールには以下のように出力される。水平線で区切られた 4 行が、testGetDateObject の一回の呼び出しによるもので、最初が引数として与えられた RFC3339 形式の日時文字列、2 行目が getDateObject による変換結果(の NSDate のインスタンス)、3 行目がそのインスタンスをデフォルトのタイムゾーン(ウチではもちろん JST)で評価した結果、最後の 4 行目は同インスタンスを UTC で評価した結果となっている。

    2010-11-24 22:06:14.819 ExpApp[24929:a0b] ----------------------------------------
    2010-11-24 22:06:14.821 ExpApp[24929:a0b] RFC3339 Format: 2010-11-22T12:34:56Z
    2010-11-24 22:06:14.824 ExpApp[24929:a0b] NSDate object: 2010-11-22 21:34:56 +0900
    2010-11-24 22:06:14.824 ExpApp[24929:a0b] Default TZ: 2010-11-22 21:34:56 +0900
    2010-11-24 22:06:14.825 ExpApp[24929:a0b] UTC: 2010-11-22 12:34:56 +0000
    2010-11-24 22:06:14.825 ExpApp[24929:a0b] ----------------------------------------
    2010-11-24 22:06:14.825 ExpApp[24929:a0b] RFC3339 Format: 2010-11-22t12:34:56z
    2010-11-24 22:06:14.826 ExpApp[24929:a0b] NSDate object: 2010-11-22 21:34:56 +0900
    2010-11-24 22:06:14.826 ExpApp[24929:a0b] Default TZ: 2010-11-22 21:34:56 +0900
    2010-11-24 22:06:14.826 ExpApp[24929:a0b] UTC: 2010-11-22 12:34:56 +0000
    2010-11-24 22:06:14.827 ExpApp[24929:a0b] ----------------------------------------
    2010-11-24 22:06:14.827 ExpApp[24929:a0b] RFC3339 Format: 2010-11-22T12:34:56.123Z
    2010-11-24 22:06:14.827 ExpApp[24929:a0b] NSDate object: 2010-11-22 21:34:56 +0900
    2010-11-24 22:06:14.828 ExpApp[24929:a0b] Default TZ: 2010-11-22 21:34:56 +0900
    2010-11-24 22:06:14.828 ExpApp[24929:a0b] UTC: 2010-11-22 12:34:56 +0000
    2010-11-24 22:06:14.828 ExpApp[24929:a0b] ----------------------------------------
    2010-11-24 22:06:14.829 ExpApp[24929:a0b] RFC3339 Format: 2010-11-22t12:34:56.123z
    2010-11-24 22:06:14.829 ExpApp[24929:a0b] NSDate object: 2010-11-22 21:34:56 +0900
    2010-11-24 22:06:14.829 ExpApp[24929:a0b] Default TZ: 2010-11-22 21:34:56 +0900
    2010-11-24 22:06:14.830 ExpApp[24929:a0b] UTC: 2010-11-22 12:34:56 +0000
    2010-11-24 22:06:14.830 ExpApp[24929:a0b] ----------------------------------------
    2010-11-24 22:06:14.830 ExpApp[24929:a0b] RFC3339 Format: 2010-11-22T12:34:56+09:00
    2010-11-24 22:06:14.831 ExpApp[24929:a0b] NSDate object: 2010-11-22 12:34:56 +0900
    2010-11-24 22:06:14.831 ExpApp[24929:a0b] Default TZ: 2010-11-22 12:34:56 +0900
    2010-11-24 22:06:14.832 ExpApp[24929:a0b] UTC: 2010-11-22 03:34:56 +0000
    2010-11-24 22:06:14.832 ExpApp[24929:a0b] ----------------------------------------
    2010-11-24 22:06:14.832 ExpApp[24929:a0b] RFC3339 Format: 2010-11-22t12:34:56+09:00
    2010-11-24 22:06:14.833 ExpApp[24929:a0b] NSDate object: 2010-11-22 12:34:56 +0900
    2010-11-24 22:06:14.833 ExpApp[24929:a0b] Default TZ: 2010-11-22 12:34:56 +0900
    2010-11-24 22:06:14.834 ExpApp[24929:a0b] UTC: 2010-11-22 03:34:56 +0000
    2010-11-24 22:06:14.834 ExpApp[24929:a0b] ----------------------------------------
    2010-11-24 22:06:14.834 ExpApp[24929:a0b] RFC3339 Format: 2010-11-22T12:34:56.123+09:00
    2010-11-24 22:06:14.835 ExpApp[24929:a0b] NSDate object: 2010-11-22 12:34:56 +0900
    2010-11-24 22:06:14.836 ExpApp[24929:a0b] Default TZ: 2010-11-22 12:34:56 +0900
    2010-11-24 22:06:14.836 ExpApp[24929:a0b] UTC: 2010-11-22 03:34:56 +0000
    2010-11-24 22:06:14.836 ExpApp[24929:a0b] ----------------------------------------
    2010-11-24 22:06:14.837 ExpApp[24929:a0b] RFC3339 Format: 2010-11-22t12.34.56.123+09:00
    2010-11-24 22:06:14.837 ExpApp[24929:a0b] NSDate object: 2010-11-22 12:34:56 +0900
    2010-11-24 22:06:14.838 ExpApp[24929:a0b] Default TZ: 2010-11-22 12:34:56 +0900
    2010-11-24 22:06:14.838 ExpApp[24929:a0b] UTC: 2010-11-22 03:34:56 +0000
    2010-11-24 22:06:14.838 ExpApp[24929:a0b] ----------------------------------------
    2010-11-24 22:06:14.839 ExpApp[24929:a0b] RFC3339 Format: 2010-11-22T12:34:56-05:00
    2010-11-24 22:06:14.839 ExpApp[24929:a0b] NSDate object: 2010-11-23 02:34:56 +0900
    2010-11-24 22:06:14.839 ExpApp[24929:a0b] Default TZ: 2010-11-23 02:34:56 +0900
    2010-11-24 22:06:14.840 ExpApp[24929:a0b] UTC: 2010-11-22 17:34:56 +0000
    2010-11-24 22:06:14.840 ExpApp[24929:a0b] ----------------------------------------
    2010-11-24 22:06:14.840 ExpApp[24929:a0b] RFC3339 Format: 2010-11-22T12:34:56.123-05:00
    2010-11-24 22:06:14.841 ExpApp[24929:a0b] NSDate object: 2010-11-23 02:34:56 +0900
    2010-11-24 22:06:14.841 ExpApp[24929:a0b] Default TZ: 2010-11-23 02:34:56 +0900
    2010-11-24 22:06:14.842 ExpApp[24929:a0b] UTC: 2010-11-22 17:34:56 +0000
    

    参考文献

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

    関連リンク

    関連記事

    2010-11-21

    記事一覧を取得する (MacBloggerGlass)

    今回は、GData Objective-C Client Library (以下、GData ライブラリ) を使って、Blogger で作ったブログの記事一覧を取得するところまでを作ってみる。

    サンプルプログラムがドキュメント

    残念ながら公式ドキュメントは充実しているとは言い難い。参考になるのはサンプルプログラム、そしてライブラリ自体のソースコードだ。

    Blogger API のサンプルは充実していて、API を使ってやれることは一通り網羅されている。Google アカウントでログインさせた後、ブログの一覧を表示する。一覧からブログを選ぶと、次は選んだブログの記事を取得し一覧で表示する。記事一覧から記事を選べば記事の内容(とコメントがあればコメントも)表示する。記事の内容は更新することも可能になっている。UI こそ単純なテーブルとテキストフィールドを組み合わせたものだが、機能的には Blogger 専用のブログエディタと言っても良いぐらい。

    MacBloggerGlass として作りたいと思っているモノの骨格はこのサンプルにある。言い方を換えれば、データの取得やその保持に関してはライブラリに任せてしまえば良いということになる。アプリの作り手としては、ユーザ体験とそれを実現する UI のデザインに専念すれば良い、と。

    ま、そのライブラリの使い方を解明するのが、ちょっと大変なんだけどね。Python のライブラリより複雑っていうか、癖があるっていうか。慣れていないだけかな。

    MacBloggerGlass のプロトタイプ

    右のスクショが今回作ったプロトタイプ(のかけら)のウィンドウだ。使い方は単純、Blog ID を入力して Read ボタンを押すだけ。Blog ID のブログに蓄えられた記事の一覧を取得してウィンドウのテーブルに表示する。使い方同様、機能も単純だ。ちなみに、スクショはこのブログの記事を取得させたときのものだ(Blog ID はダミー)。

    以下が今回書いた主要部分になる(MacBloggerGlass/AppController.m より抜粋)。getFeed が Read ボタンが押されたときに実行されるアクションになる。

    - (IBAction)getFeed:(id)sender
    {
        NSString *blogID = [blogIdField stringValue];
        if ([blogID length] == 0) return;
    
        [feed removeAllObjects];
    
        NSString *blogPostURL = @"http://www.blogger.com/feeds/%@/posts/default";
        NSURL* feedURL = [NSURL URLWithString:[NSString stringWithFormat:blogPostURL, blogID]];
        NSLog(@"%@", feedURL);
        
        GDataServiceGoogleBlogger *service = [self bloggerService];
        GDataServiceTicket *ticket;
        
        ticket = [service fetchFeedWithURL:feedURL
                                 feedClass:[GDataFeedBlogPost class]
                                  delegate:self
                         didFinishSelector:@selector(blogPostsTicket:finishedWithFeed:error:)];
    
    }
    
    - (GDataServiceGoogleBlogger *)bloggerService {
        
        static GDataServiceGoogleBlogger* service = nil;
        
        if (!service) {
            service = [[GDataServiceGoogleBlogger alloc] init];
            
            [service setShouldCacheDatedData:YES];
            [service setServiceShouldFollowNextLinks:YES];
        }
        return service;
    }
    
    // blog feed fetch callback
    - (void)blogPostsTicket:(GDataServiceTicket *)ticket
           finishedWithFeed:(GDataFeedBase *)afeed
                     error:(NSError *)error
    {
        GDataFeedBase *blogFeed = [afeed retain];
        int count = [[blogFeed entries] count];
    
        for (int i = 0; i < count; i++) {
            GDataEntryBase *entry = [blogFeed entryAtIndex:i];
            Entry *e = [[Entry alloc] init];
            e.title = [[entry title] stringValue];
            e.date = [[entry publishedDate] stringValue];
            [self willChangeValueForKey:@"feed"];
            [feed addObject:e];
            [self didChangeValueForKey:@"feed"];
        }
    }
    

    GData ライブラリの使い方の概略は、(1) 利用するサービス(Blogger 等)に対応した GDataServiceGoogle の派生クラスをインスタンス化する、(2) API が返すデータ(Atom フィード)を受け取るクラスを指定して (1) のインスタンスに対して fetchFeedWithURL:feedClass:delegate:didFinishSelector を呼び出す、となる。このとき、didiFinishSelector として、データ取得が完了したときに呼ばれるコールバックメソッドを指定する。

    上記のコードでは、65 〜 76 行目の bloggerService がサービスクラスをインスタンス化している部分で、58 行目がデータ取得のための呼び出しになっている。また、コールバックは 79 〜 95 行目の blogPostsTicket:finishedWithFeed:error を指定している。

    もとのサンプルでは、Google アカウントとパスワードを入力させてユーザ認証を行っているが、今回のプロトタイプではユーザ認証は省いている。そのために fetchFeedWithURL:... にわたす URL を 51 行目で指定している。アドホックなやり方だと思うが、スマートな(というかライブラリが想定している適切な)方法を調べ切ることができなかったのでこうなった。

    表示するブログの選択についてはユーザ体験の観点からどうすべきかを考える必要がある。ブログ ID 自体を指定させる方法が使い易いとは思えないから。ま、おいおい。

    関連リンク

    関連記事

    2010-11-20

    GData Objective-C Client Library を組み込む (MacBloggerGlass)

    Blogger Glass を Cocoa アプリとして作り直すにあたり、UI を考える前に、まずは GData Objective-C Client Library (以下、GData ライブラリ)を使って Blogger で作ったブログのデータ(Atom フィード)を取得できるようにしておきたい。Blogger Glass を作り始めたときも、最初は(GData ライブラリは使っていなかったが)そこから始めたのだった(→「Blogger で作ったブログの記事一覧を作成する」)。

    GData ライブラリを Cocoa アプリ(または Cocoa Touch アプリ)に組み込むには、フレームワークとして(動的に)リンクする方法と、静的ライブラリとしてリンクする方法の二通りがある(→「Adding the Google Data APIs to a Project」)。Cocoa アプリの場合は主に前者を使い、Cocoa Touch アプリなら後者を使う。

    では、GData ライブラリをフレームワークとして動的にアプリに組み込むには、Xcode プロジェクトをどう構成すれば良いのだろうか?

    1 つの方法は、アプリとは別個にフレームワークをビルドして ~/Library/Frameworks 等に置くことだ。別の方法として、アプリのプロジェクトから GData ライブラリのプロジェクトを参照する方法がある。こちらが、GData ライブラリの配布パッケージにふくまれているサンプルアプリの使っている方法だ。

    Cocoa 版 Blogger Glass (MacBloggerGlass と呼ぶことにした)でも、プロジェクトを参照する方法を使うことにした。で、今回の記事では、GData ライブラリのプロジェクトを(サブプロジェクトとして)ふくむ Cocoa アプリプロジェクトの構成手順を説明する。

    ただし、以下に述べる手順は試行錯誤の末にたどりついたものなので、この先、アプリの開発を進めていくと何か不都合が出てこないとも限らない。現状、確認できているのは (a) アプリをビルドすれば同時に GData.framework がアプリのビルド構成(Debug または Release)に合わせてビルドされること、(b) アプリのコードから GData.framework の機能を使うことができること、(3) アプリのデバッグ中にステップ実行でフレームワークのソースに入っていけること、の 3 点になる。

    手順

    概略をまず述べる。GData ライブラリのプロジェクト一式をアプリのプロジェクトにコピーする。その後、Xcode でアプリのプロジェクトに GData プロジェクトを追加し、依存関係の追加やら、ビルドスクリプトの追加やらの調整を行う。お手本にしたのは、GData ライブラリ配布パッケージのサンプル。ビルドスクリプトもそこから拝借してきた。

    MacBloggerGlass/
    +-- MacBloggerGlass.xcodeproj
    |   ...
    +-- GData/
        +-- GData.xcodeproj
        |   ...
    

    以下に、ステップごとの手順を示す。

    1. GData Library の配布パッケージを展開する。
    2. Srouce ディレクトリをアプリプロジェクトのディレクトリに GData という名前で移動する。
    3. Xcode でアプリプロジェクトを開く。
    4. メニューから「プロジェクト」>「プロジェクトに追加…」を実行する。
    5. ファイル選択パネルからアプリプロジェクトフォルダ内にある GData フォルダを選び、GData.xcodeproj を選択し、「追加」ボタンを押す。
    6. 「追加したフォルダにフォルダ参照を作成する」を選び、「追加」ボタンを押す。
    7. 「ターゲット」からアプリを選択し、メニューから「ファイル」>「情報を見る」を実行する(⌘ + I でも良いし、ツールバーに「情報」ボタンがあるならそれでも良し)。
    8. 「"ターゲット "(アプリ名)""の情報」パネルが開くので、「一般」タブを開き「直接依存関係」に「GData.xcodeproj」の「GDataFramework」を追加する。
    9. buildCopyScipt を用意する。GData パッケージに付属のものを修正して使う。
    10. メニューから「プロジェクト」>「新規ビルドフェーズ」>「新規スクリプトを実行」を実行する。
    11. 「""(アプリ名)"のスクリプトを実行するフェーズ"の情報」パネルが開くので、「一般」タブで「スクリプト」に「./buildCopyScript」と書き込む。
    12. buildStripHeaders を用意する。GData パッケージに付属のものを修正して使う。
    13. buildStripHeaders スクリプトについても buildCopyScript と同様の手順で、ビルドフェーズとして追加する。
    14. 追加した 2 つのスクリプト実行フェーズの順序を調整する。buildCopyScript は「バンドルリソースをコピー」の前、buildStripHeaders は「バイナリをライブラリにリンク」の後に移動する。
    15. グループとファイルの「GData.xcodeproj」を開き「GData.framework」を、ターゲットの「バイナリをライブラリにリンク」までドラッグしてそこのドロップ。Xcode のウィンドウレイアウトとして「コンデンス」を選んでいる環境なら、この操作は「詳細」ウィンドウを使うとやりやすい(メニューから「表示」>「詳細」を実行すると表示される)。
    16. GData.xcodeproj をダブルクリックするなどして、GData プロジェクトを開き、ビルドオプション等を調整する。とくに、ビルドするアーキテクチャを調整しておかないとリンク時にエラーが出る。双方のアーキテクチャを一致させておけば間違いはない(64-bit intel とかに)。
    17. ビルドを実行。エラーが出ていないことを確認する。
    18. アプリプロジェクトフォルダ内の build/Debug あるいは build/Release に 「アプリ名.app」と「GData.framework」が出来ていれば OK。

    正直メンドウだったので、雛形になるプロジェクトを(Cocoa アプリ用、Cocoa Touch アプリ用それぞれに)作っておこうか、と考えている。いっそのこと Xcode のプロジェクトテンプレートにできないかなとも思うが、そのためにはフレームワークを別途ビルドしてシステムにインストールする必要がある(ように思う)。雛形プロジェクトの方が簡単かな。やり方もわかっていることだし。

    関連リンク

    関連記事

    追記@2010-11-30

    Google App Engine 用アプリに Python 版 Google Data Client Library を組み込むことについては以下の記事を参照のこと。

    2010-11-18

    欲しいのはオフライン機能 - Cocoa アプリとして作り直す #0 (Blogger Glass)

    Blogger Glass も、内部リンクの置き換えができるようになったことで、目標であった「Blogger で作ったブログを Blogger とは独立した表示システムで見る」に到達したと言える。もちろん、あれこれ気になる点は残っているし、実現したいアイデアもある。けれど、ここらでちょっと別方向に進んでみようかと考えている。それは、Cocoa アプリとして作り直すこと、つまり Objective-C で書く。

    Blogger Glass を Cocoa アプリにすることで実現したいことは、何よりオフラインで記事を読めるようにすることだ。(iMac のような)デスクトップ型の Mac で動かすアプリならオフライン機能にほとんど意味はない。しかし、MacBook や iPhone のように外に持ち出すことの多い機器で動かすなら、オフライン状態に対応することはユーザ体験を向上させる。

    まずは Mac 版を作り、その後 iPhone アプリに作り直す。ユーザ体験やデザイン(意匠)はともかく、設計と実装は共通化したいものだけど、作る前から欲張るのはやめておこう。まずは動くモノを作ることだ。

    という思いのもと、今日から Xcode との格闘を始めたのだけど、以前、ヒレガス本で勉強したことをすっかり忘れてしまっていることがわかった。もう、どこに何を書けば良いのかがさっぱりわからなくなっている。

    GAE 版の進化の跡をたどるようにして作るかな。つまり、こんな感じ。

    1. 一覧表示
    2. Google Data API Objective-C Client Library の組み込み
    3. 記事表示
    4. ラベル検索
    5. 内部リンクの置き換え

    オフライン機能(つまり取得したフィードの保存)はどこで実現するか。「内部リンクの置き換え」の前あたりが適当だろう。「内部リンクの置き換え」ではひもづけ情報の保存が必要なのだから。

    今日の感じだと、「一覧表示」を曲がりなりにでも Cocoa アプリとして実現する最初のステップが、実は一番時間がかかるかもな。とりあえずリクエストハンドラ(だけ)を書けば、一応のアプリとして動かすことのできる GAE とは違うわ。

    ま、今日は予告だけ。

    関連リンク

    関連記事

    2010-11-06

    素数を列挙する #2

    前回の続き。

    素数表の検索にバイナリサーチを使う

    前回、素数の判定法としてはもっとも素直な「√n 以下の整数で割る」よりは割り算が少なくなると考え、素数表を使った「√n 以下の素数で割る」方法を実装してみた。ところが、列挙範囲の素数表が完成している場合であっても(つまり割り算は一度も行わない)、「√n 以下の整数で割る」方が速いという結果になった。

    その原因は Array#include? を使った素数表の検索にあるのではないかと考え、今回は検索に「バイナリサーチ(二分検索)」を使ってみることにした。

    (listprimes_binsearch.rb より)
    def ($primes).binsearch(n)
      len = self.size
      first = 0
      last = len - 1
      while len > 0
        mid = first + len / 2
        v = self[mid]
        if n == v
          return true
        elsif n < v
          last = mid - 1
        else
          first = mid + 1
        end
        len = last - first + 1
      end
      false
    end
    

    結果として、Array#include? を使ったものより高速になったことはもちろん、(素数表が完成していれば)列挙する範囲によっては「√n 以下の整数で割る」ものよりも速くなった。とはいえ、やはり素数表を生成しながらの場合は、比較にならないほど遅い。

    エラトステネスの篩

    素数表の生成でもう少し速そうな方法を試してみることにした。有名な「エラトステネスの篩」だ。コードを以下に示す。

    「√n 以下の素数で割る」版、「篩」版、そして「シンプルに √n 以下の整数で割る」版を実行してみた結果は以下のようになった。

    [imac] mnbi% rm primes.txt
    [imac] mnbi% time ./listprimes_binsearch.rb 100000 100
    START (99991):
    100003 (66)
    100019 (66)
    100043 (66)
    100049 (66)
    100057 (66)
    100069 (66)
    Elapsed: 0.000275
    ./listprimes_binsearch.rb 100000 100  17.60s user 0.02s system 100% cpu 17.601 total
    [imac] mnbi% time ./listprimes_sieve.rb 100000 100
    100003
    100019
    100043
    100049
    100057
    100069
    Elapsed: 0.019148
    ./listprimes_sieve.rb 100000 100  0.02s user 0.00s system 83% cpu 0.033 total
    [imac] mnbi% time ./listprimes_simple.rb 100000 100
    100003 (158)
    100019 (158)
    100043 (158)
    100049 (158)
    100057 (158)
    100069 (158)
    Elapsed: 0.00028
    ./listprimes_simple.rb 100000 100  0.01s user 0.01s system 89% cpu 0.020 total
    

    「篩」版は確かに速いが、「シンプル」版はさらに速い。

    もっとも、これは表示させる領域を大きな数を基点として「狭めて」いるから起きることだ。1 から 100000 までの範囲の素数を列挙するなら「シンプル版」の方が遅くなる。

    一番、速いのは

    範囲(それも比較的大きな数から始まるものを)を指定して列挙させる場合、速いのは「√n 以下の整数で割る」方法をシンプルに実装したものだ。2 から始まる素数表を生成する方式では、範囲の上限に至るまでのすべての素数を生成する分不利になる。

    たとえば、画面に一定範囲にふくまれる素数を表示する(だけの)アプリを作るとしたら、素数表を作る方式は適さないことになる。画面に表示できる範囲は限られているのだから、素数表を(作って)保持しておくより、見えている分だけをシンプルな方法(√n 以下の整数で割る)で判定する方がずっと素早く表示できるはずだ。

    関連リンク

    関連記事

    2010-11-05

    素数を列挙する

    プログラミングの練習。指定された範囲にある素数を列挙するプログラムを作ってみる。

    素数表を使った判定

    利用するアルゴリズムは自明のもの。つまり、正整数 n に対して √ n を超えない素数で割り切れるかどうかをチェックしている。初回の実行では 2 (最小の素数)から始めて素数表を作っている。実行中に作った素数表は最後にファイルに書き出す。2 回目以降の実行では、以前に作った素数表を読み込んでいる。

    列挙した素数の後にカッコ付きで表示する数字は計算回数だ。

    検証

    念には念を入れて、listprimes.rb で作られた primes.txt を検証する。以下がそのプログラムだ。素数を判定するアルゴリズムは大して変わっていない(素数表を使わず √ n 以下の整数で割り切れるか否かをチェック)から、どちらかと言えばプログラムのバグを確認するためのものというべきだ。

    実行結果

    [imac] mnbi% ls primes.txt
    ls: primes.txt: No such file or directory
    [imac] mnbi% ./listprimes.rb 1000 50
    START (2):
    1009 (12)
    1013 (12)
    1019 (12)
    1021 (12)
    1031 (12)
    1033 (12)
    1039 (12)
    1049 (12)
    Elapsed: 0.00011
    [imac] mnbi% tail primes.txt
    991
    997
    1009
    1013
    1019
    1021
    1031
    1033
    1039
    1049
    [imac] mnbi% ./listprimes.rb 1000 50
    START (1049):
    1009 (0)
    1013 (0)
    1019 (0)
    1021 (0)
    1031 (0)
    1033 (0)
    1039 (0)
    1049 (0)
    Elapsed: 0.000444
    

    初回の listprimes.rb の実行で、primes.txt が作られ、2 回目の実行では、(素数判定のための)計算がされていないことが(計算回数の表示から)読み取れる。意外なことに計算のない 2 回目の方が時間がかかっている。

    「√ n 以下の整数」を使うより「√ n 以下の素数」の方が計算回数がぐっと少なくなると考えて素数表を使うようにしたんだが、実際のところでは素数表(配列)の操作にかかる時間の方が律速段階になっているようだ。素数表を使う方法で 2 回目以降の実行(つまり割り算は一切ない)でも、Time で計測してもわかるほどの差が出る。

    素数表の検索を工夫すれば逆転するだろうか?

    関連リンク

    2010-11-02

    一意性の確保 - ハッシュ関数

    以下は実験中の Blogger Glass のコード、そのセッション管理を担当するモジュールの一部でセッション ID を生成す関数になっている。

    (src/session.py より)
    def generate_session_id(seed):
        m = hashlib.sha1()
        m.update(datetime.datetime.now().isoformat())
        m.update(str(seed))
        return m.hexdigest()
    

    以前、ウェブアプリにおけるセッションを病院に治療に来る患者の関係にたとえた(→「Cookie の使い方 - GAE におけるセッションの保持」)。病気(やケガ)の治療では病院側が患者についてさまざまな情報を記録しておき(カルテ)、その記録と患者個人をひもづけるために番号を使うと書いた。この番号に相当するのがセッション ID なのだ、とも書いた。

    カルテと患者のひもづけで大事なことはそれが一通りに定まることだ。これが混乱すると(あるカルテが別の患者のものだと扱われる等)大変なことになる。これを確実にするための 1 つの方法は、空白のカルテ用紙にあらかじめ重複しない通し番号を振っておくというものだ。番号の一意性 (uniqueness) が確保できていることが、患者という個人の識別に利用できる理由だ。

    ウェブアプリにおけるセッション ID についても、同じことがそのままあてはまる。すなわち、ウェブアプリとクライアント(ブラウザ)のひもづけには、一意性の保証された何かを利用しなければならない。↑で示した関数は、この一意性の保証された何かを、ハッシュ関数(一方向ハッシュ関数とも言う)によって生成している(標準ライブラリの hashlib)。

    ところで、このハッシュ関数が生成する「何か」は本当に一意 (unique) なのだろうか? あるいは完全に一意なものではないとしてどれぐらい一意なのだろう?

    一方向ハッシュ関数

    先のコードで使っている hashlib.sha1 という関数はハッシュ関数の中でも一方向ハッシュ関数(Wikipedia:ja では暗号学的ハッシュ関数)と呼ばれるものだ。

    (「入門SSH」p.44 より)
    一方向ハッシュ関数は、任意の長さの入力から固定長の出力を返す関数です。一方向ハッシュ関数の出力は一方向性(出力から入力を得るのが困難)と衝突困難性(同じ出力を得る 2 つの入力を得るのが困難、衝突耐性)を持ちます。この特徴から、ファイルなどが改ざんされていないことのチェック(もしファイルが改ざんされていれば、出力が異なるはず)や電子署名のために入力を短く固定長にするためなどに利用されます。[...snip...]

    代表的な一方向ハッシュ関数には、MD5、SHA-1、SHA-256、SHA-512 があります。

    もともと、一方向ハッシュ関数はデータの改ざんの有無を検査するような(セキュリティに関係する)目的に使われるが、上記の 2 つの特性のうち「衝突困難性」があるため「一意性の保証された何か」を生成するためにも利用できるわけだ。

    さて、hashlib.sha1 で使われている SHA-1 という一方向ハッシュ関数では常に 160 ビットの値を生成する。言い換えると、生成できるのは高々 2^160 個の「何か(符号なしの整数値)」でしかない。もし、毎回異なる値を生成できるとしても、2^160 回以上 ID を生成させれば同じ ID が出てくることになる。そう、これで生成できる「何か」は完全に一意なものではないのだ。

    ほどほどの一意性

    確かに一方向ハッシュ関数では「完全な一意性」を得ることはできない。でも、良く考えてみれば 2^160 (2 の 160 乗) というのは、結構大きな数だ(→ 「2^64 (2 の 64 乗) って、どれぐらい?」)。うん、いや、結構っていうか、日常生活のスケールでは決して出会うことのないぐらいの大きさだ。ちょっと、Python で計算させてみた。

    [imac] mnbi% python
    Python 2.6.1 (r261:67515, Feb 11 2010, 00:51:29) 
    [GCC 4.2.1 (Apple Inc. build 5646)] on darwin
    Type "help", "copyright", "credits" or "license" for more information.
    >>> n = 2 ** 160
    >>> len(str(n))
    49
    >>> print n
    1461501637330902918203684832716283019655932542976
    

    10 進数で 49 ケタ。つまり、10^48 より大きく、10^49 より小さい。読み方は、10^48 が「極(ごく)」なので……

    (2^160 の 10進数表現の読み方)
    1    (極)
    4615 (載)
    0163 (正)
    7330 (澗)
    9029 (溝)
    1820 (穣)
    3684 (じょ)
    8327 (垓)
    1628 (京)
    3019 (兆)
    6559 (億)
    3254 (万)
    2976
    

    ……となる。つまり「一極四千六百十五載(飛んで)百六十三正七千三百三十澗九千(飛んで)二十九溝千八百二十穣三千六百八十四じょ八千三百二十七垓千六百二十八京三千(飛んで)十九兆六千五百五十九億三千二百五十四万二千九百七十六」だ。

    意図的に(たいていは悪意を持って)同じハッシュ値を作ろうとしない限り、同じ値が生成されることはないと言って良い。「完全な」ではないにしろ、「ほどほどの一意性」(実際には「実用的な」と言うべきだろうな)は保証されているようだ。

    ちなみに「新版暗号技術入門 秘密の国のアリス」によれば、SHA-1 の「強衝突耐性」は 2005 年に破られたとのこと(同 p.176)。ここで「強衝突耐性」とは

    (「新版暗号技術入門 秘密の国のアリス」p.170 より)
    強衝突耐性とは、ハッシュ値が一致するような、異なる 2 つのメッセージを見つけ出すことが非常に困難である、という性質です。

    のことだ。

    もっとも、ウェブアプリとクライアント(ブラウザ)のひもづけるセッション ID としてハッシュ値を使う場合、セッション ID を知られただけでアウトだけどね。病院のたとえで言えば、診察券に書かれた患者番号を誰かに知られてしまい、診察券を偽造されてしまうかもってところか。ま、病院と患者の場合は、そんなことをしても意味がないがね(診察券に病院で治療を受ける以外の使い道があれば別)。

    ウェブアプリのセッションを管理する「何か」としてハッシュ値を使う場合、大事なことは異なるクライアントに同じ「何か」をわたさないようにすることだ。「何か」を(通信の)途中で盗まれないようにすることは別問題。

    そもそも、Blogger Glass のような「公開情報を読み取り専用で扱う」アプリでセッションを盗まれたとしても大した問題にはならないよ。

    参考文献

    入門SSH (My UNIX series (04))
    春山 征吾
    アスキー ( 2004-11 )
    ISBN: 9784756145536
    おすすめ度:アマゾンおすすめ度
    新版暗号技術入門 秘密の国のアリス
    結城 浩
    ソフトバンククリエイティブ ( 2008-11-22 )
    ISBN: 9784797350999
    おすすめ度:アマゾンおすすめ度

    「6.4 ハッシュ法」の一部としてハッシュ関数を扱っている(p.487 〜 492)。ただし、扱っているのは一般的なハッシュ関数であり一方向ハッシュ関数ではない。

    関連リンク

    関連記事

    2010-11-01

    リングバッファを作る - リクエスト URL をためておく仕組み (BloggerGlass)

    「Back」ボタンを実装するためには、戻るべき画面を記録しておかなければならない。現状の画面遷移だけなら 1 つ前の画面を覚えておけば十分なはずだが、将来のことも考えて、画面遷移の履歴を保持するための仕組みを作ってみることにした。

    履歴保存用クラス

    実体はリクエスト URL (文字列)を保存しておくためのリスト(Ruby で言うなら配列)に過ぎない。ただし、「前の画面に戻る」ための履歴だから、最後に登録した URL を最初に取り出すことになる。つまり、LIFO (Last In First Out)と呼ばれるデータ構造になる。別の呼び方にスタックというのもある。

    スタックなら、操作のためのインタフェースは以下のようになる。

    スタックの操作
    名称 機能
    is_empty 空かどうかを返す
    push データを 1 つ追加する
    pop 最後に追加したデータを取り出す(取り出しされたデータはスタックから削除する)
    peek 最後に追加したデータを見る(取り出さない)

    最低限必要なのは push と pop で、他はあると便利なものだ。

    また、この履歴保存用クラスは、スタックであると同時にリングバッファでもある。これは平たく言えば、バッファが一杯になったとき、古いデータが新しいデータで上書きされていくデータ構造だ。

    リングバッファにしたかったのには理由がある。直前の画面に戻るための履歴なのだから、バッファが一杯になったからと言って履歴が保存できなくなるのは困る。一方で、古い履歴よりも新しい履歴の方が重要だ。そういう意味で、新しいデータを常に保存することができる(その代わり古いデータは消えていく)リングバッファは最適だと言える。

    export_historyimport_history は、履歴をセッションデータとしてデータストアに収めるときに使用する(ことになるはず)。

    単体テスト

    RequestHistory クラスは、以下のような単体テストを書きながら、そしてテストしながら実装した。50 行足らずの短いプログラムだと言うのに意外に難しかった。何度「これで良い(はず)!」と思ってからテストに失敗したことか。こういう一般的なプログラムを書くときは、本当にテストが役に立つ。

    (tests/util_test.py より)
    import unittest
    
    import pathconf
    # target module 
    import util
    
    class RequestHistoryTest(unittest.TestCase):
        def setUp(self):
            self.history = util.RequestHistory()
    
        def test_is_empty(self):
            """empty?(EMPTY) == True"""
            self.assert_(self.history.is_empty())
    
        def test_peek(self):
            """peek(push(EMPTY, A)) == A,
            then pop() also returns A
            """
            self.history.push('/1')
            self.assertEqual(self.history.peek(), '/1')
            self.assertEqual(self.history.pop(), '/1')
    
        def test_pop_against_empty(self):
            """pop(EMPTY) == None"""
            self.assertEqual(self.history.pop(), None)
    
        def test_push_1_pop_1(self):
            """pop(push(EMPTY, A)) == A"""
            self.history.push('/')
            self.assertEqual(self.history.pop(), '/')
    
        def test_push_2_pop_2(self):
            """pop(push(push(EMPTY, A), B)) == B
            pop(pop(push(push(EMPTY, A), B))) == A
            """
            self.history.push('/')
            self.history.push('/foo')
            self.assertEqual(self.history.pop(), '/foo')
            self.assertEqual(self.history.pop(), '/')
    
        def test_push_10_pop_10(self):
            """H = push(push(...(push(EMPTY, 0), 1), ...), 9)
            then, pop(pop(...(pop(H))...)) = 0
            """
            for i in range(10):
                self.history.push("/%d" % i)
            for i in reverse_range(10):
                self.assertEqual(self.history.pop(), ("/%d" % i))
    
        def test_push_11_pop_10(self):
            """H = push(push(...(push(EMPTY, 0), 1), ...), 10)
            then, pop(pop(...(pop(H))...)) = 1
            """
            for i in range(11):
                self.history.push("/%d" % i)
            r = reverse_range(11)
            r.pop()
            for i in r:
                self.assertEqual(self.history.pop(), ("/%d" % i))
    
        def test_export_history(self):
            for i in range(10):
                self.history.push("/%d" % i)
            h = self.history.export_history()
            for i in range(10):
                self.assertEqual(h[i], ("/%d" % i))
    
        def test_import_history(self):
            h = []
            for i in range(10):
                h.append("/%d" % i)
            self.history.import_history(h)
            r = range(10)
    
        def test_pickle(self):
            """Make sure that it can be pickled in and out."""
            for i in range(10):
                self.history.push("/%d" % i)
            pickled = pickle.dumps(self.history)
            history = pickle.loads(pickled)
    
            for i in reverse_range(10):
                self.assertEqual(history.pop(), ("/%d" % i))
    
    def reverse_range(n):
        r = range(n)
        r.reverse()
        return r
    
    def suite():
        return unittest.TestSuite((
                unittest.makeSuite(RequestHistoryTest, 'test'),
                ))
    
    if __name__ == '__main__':
        unittest.TextTestRunner().run(suite())
    

    関連リンク

    関連記事

    2010-10-31

    Cookie の使い方 - GAE におけるセッションの保持

    前回(「iPhone アプリらしく #2」)にも書いたように、Blogger Glass は「画面遷移だけの(とても古臭い)ウェブアプリ」だ。これを iPhone アプリらしく見せるためには、ぜひとも「Back」ボタンが必要だ。というのも、iPhone アプリでは、ある画面からボタンやら何やらを押すことで子画面を開き、そこで作業が完了すると親画面に戻る、という操作が良く実装されている。このユーザ体験を実現することは、iPhone アプリとしてごく標準的なことなのだ。

    しかし、ウェブアプリでこれ(一つ前に開いていた画面に戻る)をやろうとすると、セッションの保持(と管理)という壁にぶつかる。ステートレスな HTTP 上に作られるウェブアプリの宿命だ。

    Blogger Glass では、ここまでセッション管理にまつわることを避けてこれたが、それもそろそろ限界。このあたりで、ちゃんとセッション周りを実装することにしよう。

    実現方法

    ウェブアプリにおけるセッションの概念とは、たとえるなら病院(医者)とそこに治療に来る患者の間にあるものだ。初診時に治療セッションが始まり、完治によって終了する。

    患者は病気なりケガなりの治療で数回、病院を訪れることになる(セッションの継続)。その度に、病院(医者)の側が患者のことをすっかり忘れてしまっては治療は成立しない。病院(医者)は個々の患者について、病気(やケガ)の状態と治療の経過を記録しておかなければならない。それが患者ごとに用意されるカルテと呼ばれる記録だ。

    一方、カルテに書かれた記録を有効に利用するためには、患者一人一人を識別できるようにする必要がある。大抵は(カルテに書かれた)患者の名前で間に合うが、中には同姓同名の患者もいるから常に確実な方法とは言えない。そんなわけで患者に一意の番号を割り当て、それでカルテと患者をひもづける。でも、患者にとったら何桁にもなる番号を覚えるのは大変なので、病院はこの番号を記録したカード、すなわち診察券を用意して、初診時に患者にわたす。

    病院がウェブアプリで患者がブラウザ、カルテはウェブアプリが記録するセッション情報で、カルテと患者をひもづける番号がセッション ID という対応関係になる。

    残るは診察券に対応するモノ(ブラウザ側にセッション ID をわたすための仕組み)だが、これには 3 つの方法がある。すなわち、(1) URL に埋め込む方式、(2) HTML のフォームに隠し要素として埋め込む方式、(3) cookie を使ってわたす方式、の 3 つだ。

    練習も兼ねて、今回は (3) の方式でセッションを実現してみる。

    サンプルプログラム

    以下のサンプルプログラムでは、セッション情報(整数値 1 つ)は GAE の提供するストレージサービスの 1 つである memcache を利用している。このサービスはもう 1 つのデータストアとは違い、「揮発性」のストレージだ。容量も(データストアに比べればかなり)小さい。その代わり、ずっと高速に動作するらしい。頻繁にアクセスする少量のデータは、memcache に置く方が良い。

    このプログラムを GAE アプリのリクエストハンドラにしてアクセスすると、「Back」と「Forward」という 2 つのリンクを持つページが開く。Forward をたどると memcache 中のデータ(カウンタ)がインクリメントされ、Back をたどればデクリメントされる。ただ、それだけのプログラムだが、内部的にはセッション管理がなされており、memcache に保持される値はクライアントごとに用意される。実際に複数のブラウザで開けばそのことがわかる。

    memcache に保存するカウンタとは別に、データストアにも 1 つ値を保存している。これはセッション管理そのものとは関係ない。この値は、セッションを作るために必須の ID (先の病院と患者のたとえで言うなら、カルテと患者をひもづける番号だ) を作るための「種」になっている。

    セッション管理の仕組み自体は単純で、リクエストを受けたら(ハンドラの get メソッドが呼ばれたら)、ブラウザが送ってきた cookie からセッション ID を取り出す。次にセッション ID をキーとして memcache からカウンタの値を取り出す。

    ブラウザが cookie を送ってこなかった、あるいは(このプログラム用の)セッション ID がふくまれていないときは、新しくセッション ID を生成しレスポンスヘッダに入れる。

    「Back」ボタンを実装するには

    Blogger Glass (の iPhone 用画面)に「Back」ボタンを実装するには、画面を開くときにリクエスト URL をセッション情報として保存すれば良い。「Back」ボタンには専用のリクエスト URL を用意しておく(たとえば /back)。そのリクエストハンドラでは、セッション情報から保存されたリクエスト URL を取り出し、そこにリダイレクトする。

    まだ、コードを書いていないから確信はないけれど、だいたいこんな感じで動きそうだ。

    関連リンク

    関連記事

    2010-10-27

    絞り込み検索とパンくずリスト

    Blogger Glass を「iPhone アプリらしく」仕立てるためのアレコレを考えていて、ラベルによる絞り込み検索というモノを思い付いた(「iOS デバイスに対応する (基礎編) - Blogger Glass」の「もっと iPhone らしく」を参照)。

    ユーザ体験としてはこんな感じになる。

    1. 「一覧画面」から「ラベル検索画面」を開く。
    2. 元の「一覧画面」に出ていた記事それぞれに付いていたラベルがまとめて表示されている。
    3. ラベルの中から 1 つ選ぶと、検索実行。
    4. 「検索結果画面」になる。 (*)
    5. 絞り込みをする場合、再度「ラベル検索画面」を開く。
    6. さきほどの「検索結果画面」の記事それぞれに付いていたラベルがまとめて表示されている。
    7. ラベルの中から 1 つ選ぶと、検索実行。このときは、先に選んだラベルとの複合検索(AND)になる。
    8. (*) に戻る。

    最初に「一覧表示」から開いた「ラベル検索画面」では、すべての記事に付いているラベルを表示する方が良いかも。

    パンくずリスト

    日本語では「パンくずリスト」、英語では「breadcrumbs」あるいは「breadcrum trail」と呼ばれるモノがある。現在表示中のウェブページの位置を、たどってきたパスの階層構造等で表現するナビゲーションアイテムだ。Mac OS X の Finder で、表示中のフォルダの階層構造を示すために使われている「パスバー」もこの一種だ(Finder に表示されていなければ、メニューから「表示」>「パスバーを表示」を選ぶ)。

    Finder のパンくずリスト

    上述の「ラベル検索」では、検索に使われたラベルをユーザにフィードバックしなければならない。そのためにも「パンくずリスト」が適しているはず。これならユーザが選んだ順序をふくめて表現できる。検索結果はラベルを選んだ順序によらないが、ユーザにとっては自分の操作を正確にフィードバックしてくれる方が良い(少なくともわたしはそうだ)し、なにより「パンくずリスト」なら、逆にたどって以前の画面(検索結果)に戻っていける(という機能をわかりやすく提示できる)。

    文字入力させたら負けだと思う

    iPhone のように使用状況を選ばないデバイス(歩きながらでも、満員電車で吊り革につかまっているときでも使えるし、使いたい)では、文字入力のような複雑な操作はなるべく避けたい。項目をタップし、画面をドラッグそれにスワイプする。片手で操作できるなら歩きながらでも使える。

    検索では、最初にラベルを複数選んでおいてから検索実行、という方式も考えられる。だが iPhone で実現するなら、操作が自ずと小さなステップに分割され、さらに個々のステップでフィードバックも得られる「順次絞り込み」タイプの方が適している。また、現在の状況と操作の履歴を同時に表現できる「パンくずリスト」はナビゲーションの仕組みとして最適だろう。

    ま、実際に使ってみない間は、ただの一般論でしかないがな。

    関連リンク

    関連記事

    2010-10-25

    iOS デバイスに対応する (基礎編) - Blogger Glass

    Blogger の提供するスタイルにあれこれ変更を加え始めたのは何より iPhone (と iPad) で見やすくしたかったからだ。それが高じてブログの記事を表示する GAE アプリを作るまでに至った。ある程度の実用性を備えた今こそ、iOS デバイスの対応を始めるときだ。

    iOS デバイスへの対応と言っても、これまでのように単にスタイルを最適化する程度で終わりたくない。ブログコンテンツを表示するためのアプリと呼べるものに仕上げたいのだ。最終的には Blogger Glass を iOS デバイス用のウェブアプリと呼ぶにふさわしいモノにするつもり。何をどこまで作り込めばそう呼べるかはまだわからない。そもそも BG 自体まだ機能的に不足しているし、実装ずみの機能も洗練されているとは言えないしな。課題は(見えないものもふくめて)多いが、まずは一歩を踏み出そう。そうすれば次に進むべき方向も見えてくるから。

    今回は基礎編。まずは以前の成果(スタイル定義)を流用して、現在、Blogger (の LOG+REPO) を iOS デバイスで開いたときと同等の外観を実現する。それには主に以下の 2 つの作業が必要だ。

    • Viewport の設定
    • デバイスごとの CSS ファイルの用意

    それでは順番に見ていこう。

    viewport の設定

    (「iPhoneアプリケーション開発ガイド―HTML+CSS+JavaScript による開発手法」p.17)
    特に指定がない場合、iPhone 版の Safari ではページの横幅が 980 ピクセルであるとみなされます(図2-3)。多くの場合はこの設定でも問題はありませんが、iPhone の小さな画面に特化したコンテンツを作成するためには横幅を明示する必要があります。

    さて、この横幅の指定は meta タグによって「viewport」を設定することで行う。viewport の設定(の meta タグ)は(iOS デバイスの Safari)以外のブラウザでは無視されるということだ(→「iPhoneアプリケーション開発ガイド」p.18)が、そこはウェブアプリなのだからリクエストデバイスに応じて追加するようにしたい。

    それが以下の部分だ。app オブジェクトの is_ios_device 属性が True のときだけ viewport 設定のmeta タグを追加している。

    (src/base.html より)
    <title>{{ app.title }}</title>
    {% if app.is_ios_device %}
      <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
    {% endif %}
    <link type='text/css' rel='stylesheet' href="{{ app.stylesheet }}">
    

    app オブジェクトは、各リクエストハンドラからわたってくる info.AppInfo クラスのインスタンスだ。このオブジェクトに情報を詰めているのが、util.fill_appinfo 関数で、リクエストデバイスの識別を行う util.get_device 関数を利用してデバイスごとの情報を生成している。それにはデバイス専用の CSS ファイルもふくまれている。

    (src/util.py より)
    def fill_appinfo(appinfo, request_url, user_agent):
        [...snip...]
        device = get_device(user_agent)
        if device == 'iphone':
            appinfo.is_ios_device = True
            appinfo.stylesheet = '/stylesheets/iphone.css'
        elif device == 'ipad':
            appinfo.is_ios_device = True
            appinfo.stylesheet = '/stylesheets/ipad.css'
        else:
            appinfo.is_ios_device = False
            appinfo.stylesheet = settings.get('stylesheet')
    

    デバイスの識別はユーザエージェント文字列中のデバイス名によるもので、3 つのタイプを返す(iphone、ipad、そして mac)。iOS デバイス以外は mac 扱いにしている。

    (src/util.py より)
    def get_device(user_agent):
        if user_agent.find('iPad') > -1:
            device = 'ipad'
        elif user_agent.find('iPhone') > -1 or user_agent.find('iPod') > -1:
            device = 'iphone'
        else:
            device = 'mac'
        return device
    

    デバイスごとの CSS ファイルの用意

    デフォルトスタイルの時と同様に、Style Repository で LOG+REPO 用に提供している各デバイス用のスタイルをほぼそのまま流用した。

    iPhone 用では実験的に少し色を変更してあるが、これはすぐに変更することになる(後述)。

    Blogger Glass が生成する HTML は HTML5 に準拠することを目指している。headersection、そして footer のような区画分けのための要素を使っているのも、そのためだ。今回、iPad 用のスタイルを用意していて、現在の iPad 上の Safari (OS: 3.2.2 (7B500)) ではこれら区画分けの要素に単独で指定したスタイルが効かないことがわかった。具体的には以下のような定義が無効になる。

    (区画分け要素のスタイル)
    header {
        background-color: #eeffcc;
    }
    

    同じ定義が iPhone および iPod touch (どちらも OS 4.1 (8B117)) では有効になる。iPad の OS がアップデートされれば解決されるだろう。

    ま、わたし自身が十分に HTML5 のスペックを読み込んでいないため、BG の HTML5 準拠も手探りだからな。BG が生成する HTML の構造もまだまだ変わる。スタイルもそれに合わせて変えなくてはならない。今のスタイルも暫定版でしかない。

    もっと iPhone らしく

    さて、以上が iOS デバイス対応の基礎編だ。ここから先は、デバイス対応というよりも、むしろ BG の iOS デバイス専用ウェブアプリ化になる。その手始めとして、今、実験中のスタイル定義によるスクショを示す。iPod touch による表示だ。

    これは「iPhoneアプリケーション開発ガイド」の p.19 〜 23 の書かれている手法を適用したものだ。少しリストと見出しのスタイルをいじるだけで、ずいぶん iPhone アプリらしくなる。

    ゴールを、iPhone でも使えるモノから iPhone で専ら使うモノに変えると、機能のデザインも(とくにユーザ体験に関して)いろいろと変わってくる。たとえば、ラベル検索による絞り込みも、Mac (上の Safari)で使うことが前提なら、詳細検索画面を作って複数のラベルを指定させて、なんていう拡張を考える。けれど iPhone に特化するなら、まずラベルを 1 つ選択して検索し、その結果表示画面でさらにもう 1 つラベルを選択して(2 つのラベルによる複合検索で)絞り込む、というような使い方を考える。

    iPhone では一度に表示できる情報が限られているし、モバイル機器であるためユーザとの対話は短いほど望ましい(ユーザは椅子に座ってじっくりと iPhone の画面を眺めているとは限らない)。そういった機器の特性はユーザ体験のデザインに大きく影響する。ウェブアプリとして実現されていたとしても、iPhone 用を標榜するなら、iPhone アプリらしくあらねばならない。単にスタイルを変えるだけで済む問題ではないってことだ。

    参考文献

    関連リンク

    関連記事