As Sloth As Possible

可能な限りナマケモノでありたい

タグ:Cocoa

どうも、「iOS Advent Calendar 2011」5日目担当のfaultierです。つい最近使ったのでNSURLProtocolネタで。

NSURLProtocolって何?

Foundationフレームワークで最初から扱えるプロトコルはhttp、https、ftp、fileの4つ。これ以外のプロトコルでの通信をNSURLConnectionやNSURLDownloadなどで扱う場合や、特定のリクエストに限って特別な処理をしたい場合などに、NSURLProtocolを継承して登録することで使えるようになる。ちなみに、他のアプリからopenURLしたときにアプリを起動させるカスタムURLスキームとはまた別なので注意。こちらはアプリ内でURL Loading Systemを使うときにだけ影響するもの。

使い方

最低限必要なのは、+canInitWithRequest:、+canonicalRequestForRequest:、-startLoading、-stopLoadingの4つ。

まずはこんな感じで、どんなリクエストのときにそのURLProtocolをが処理するのかを決める。

+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
    return [[[request URL] scheme] isEqualToString:@"udon"];
}

ここでYESを返すとこのクラスがインスタンス化されて通信処理に進む。NOの場合は他に登録されているURLProtocolのこのメソッドが登録時の逆順に呼ばれて行く。この場合はudonというスキーム、例えばudon://marukame/bukkake/coolみたいなURLへのリクエストの時に処理をすることになる。別にこれは独自のスキームである必要ではなく、httpやfileなどをフックすることもできるし、特定のホストや特殊なヘッダが付いている時だけ処理するようなこともできる。

次は+canonicalRequestForRequest:。リクエストをcanonicalな形に変える必要がある場合はここで弄るのだけど、特に何もすることが無ければrequestをそのまま返してやればいい。

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
    return request;
}

その後実際の通信が始まり、-startLoadingが呼ばれる。NSURLProtocolのオブジェクトは、id<NSURLProtocolClient>のclientと、NSURLRequestのrequestというプロパティを持っているので、requestからどんなリソースが必要なのかを判断して、clientに対してデータを返してやる、という形で通信の中継をする。NSURLProtocolClientのメソッドは大体NSURLConnectionのdelegateと対応しているので、NSURLConnectionでの通信を実装したことがあれば分かるはず。NSURLConnection側でキャンセルされたときはstopLoadingが呼ばれる。簡単な例としては以下のようになる。

- (void)startLoading
{
    // 本来非同期で通信するのだけど、
    // この例では単に文字列データを返すだけなので、
    // その場で返してしまう
    NSData *data = [@"うどんが食べたい" dataUsingEncoding:NSUTF8StringEncoding];
    NSDictionary *headers = [NSDictionary dictionaryWithObjectsAndKeys:
                              @"text/plain", @"Content-Type",
                              [NSString stringWithFormat:@"%d", [data length]], @"Content-Length",
                              nil];
    NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:[self.request URL]
                                                              statusCode:200
                                                             HTTPVersion:@"1.1"
                                                            headerFields:headers];
    // NSURLResponseのオブジェクトを返し、
    [self.client URLProtocol:self
          didReceiveResponse:response
          cacheStoragePolicy:NSURLCacheStorageAllowedInMemoryOnly];
    // データを渡し、
    [self.client URLProtocol:self didLoadData:data];
    // 通信を終了。
    // 通信失敗の場合は -URLProtocol:didFailWithError:を呼ぶ。
    [self.client URLProtocolDidFinishLoading:self];
}

- (void)stopLoading
{
    // この例ではキャンセルしようが無い…
    // 内部で別なNSURLConnectionを使っていたり、
    // NSOperationQueueやGCDなどで非同期処理をしている場合、
    // ここでキャンセルの処理を実行する。
}

これだけだとudonスキームはまだURL Loading Systemに登録されていないので、適宜NSURLProtocolの+registerClass:、+unregisterClass:呼んであげることで使えるようになる。その独自スキームを使うUIViewControllerとか、あるいはもうクラスのloadメソッドで登録してしまうとか。

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [NSURLProtocol registerClass:[self class]];
    });
}

使いどころ

それどういうときに使うの?http以外のプロトコルとかそんな実装しないよ?と思うかもしれないけど、意外と使い道がある。UIWebView等での通信は裏側でNSURLConnectionを使っているので、WebView内での通信をフックしてアプリ側で弄ったりできるのだ。例えばCoreDataにあるデータをJSON形式にして返すようにしておくとか、JSからのトリガーでアプリ内のバックグラウンド処理を走らせるとか、画像をファイルキャッシュしておいてオフラインでも表示するとか、複数の異なる形式のWebAPIをプロキシして同じ形で扱えるようにしたり、ということも簡単にできる。

また、逆に「特定のリクエストに限って特別な処理をする」ではなく「特定のリクエストはスルーするけど他はブロックする」のような使い方もできる。最近(というには結構前からだけど)話題になったのは、iPhoneのWebViewにこういう脆弱性がある、という話。

何故デバイスのアドレス帳や着信履歴みたいなのの生のデータにJSからアクセスできる必要があるのか良く分からないし、おかしな仕様だと思うのでバージョンアップで塞がれるだろうとは思うけど、少なくともiOS5.0まではこの問題は残っているので、「アプリ内でUIWebViewにloadHTMLStringさせて、外部から取得したHTMLをfile://等のスキームで表示させている」ような場合には対処する必要がある。簡単な対策としてはきちんとbaseURLを設定しておけばいいのだけど、そうすると今度はCSS、JS、画像などのバンドル内部に持っているリソースが使えなくなる。「外部から取得したHTMLをアプリ内のUIWebViewで表示したいけど、アプリ内の安全なリソースにはアクセスさせたい」場合に、NSURLProtocolで通信をフックする仕組みが使える。

前者の記事だと、NSURLProtocolのサブクラスでバンドル内のファイルを開いて、そのデータを返してしまう、というやり方をしている。また、後者の記事やPhoneGapの実装では、NSURLProtocolの「登録時の逆順に対応できるかどうかチェックする」「YESを返せばそこで止まり、NOを返すと次のNSURLProtocolのチェックに進む」という性質を利用して、「ホワイトリストに載ってないリクエストの場合は、canInitWithRequest:でYESを返した上でエラーなり空レスポンスなりを返し、ホワイトリストに載っている場合はNOを返して通常のhttpやfileプロトコルとして処理させる」という方法でサンドボックス外のリソースにアクセスさせないようにしている。

まとめ

  • NSURLProtocolを使えばさくっと独自スキームを定義できるよ
  • WebViewみたいに細かく制御しづらいものの内部の通信もフックできるよ
  • セキュリティ面でも意味があるよ

という感じなので、是非試してみてください。

前の記事で予告した通り、今度はNSRegularExpressionの話。

正規表現でマッチした部分文字列を取得する

まずNSRegularExpressionオブジェクトを作って、それのメソッドにNSStringのオブジェクトを渡す、という形で使う。まぁ説明するよりコード見た方が早い。

NSString *string = @"「そんな正規表現で大丈夫か?」「大丈夫だ、問題ない」";
NSError *error   = nil;
NSRegularExpression *regexp =
  [NSRegularExpression regularExpressionWithPattern:@"「そんな(.+)で大丈夫か?」「(.+)」"
                                            options:0
                                              error:&error];
if (error != nil) {
  NSLog(@"%@", error);
} else {
  NSTextCheckingResult *match =
    [regexp firstMatchInString:string options:0 range:NSMakeRange(0, string.length)];
  NSLog(@"%d", match.numberOfRanges); // 3のはず
  NSLog(@"%@", [string substringWithRange:[match rangeAtIndex:0]]); // マッチした文字列全部
  NSLog(@"%@", [string substringWithRange:[match rangeAtIndex:1]]); // "正規表現"
  NSLog(@"%@", [string substringWithRange:[match rangeAtIndex:2]]); // "大丈夫だ、問題ない"
}

地味にややこしい。Rubyで書いたらこんなんで済むのに。

# coding: utf-8
if "「そんな正規表現で大丈夫か?」「大丈夫だ、問題ない」" =~ /「そんな(.+)で大丈夫か?」「(.+)」/
  puts $&
  puts $1
  puts $2
end

まぁRubyやPerlと比べるのは(少なくとも文字列操作や正規表現に関して言えば)フェアじゃないですけど!とにかくこれで正規表現で部分文字列を探せるようになりました、と。

ちなみに、-firstMatchInString:options:range:というメソッド名で分かると思うけど、これは最初にマッチした箇所しか取ってこない。マッチした箇所全て欲しければ、-matchesInString:options:range:を使えば、NSTextCheckingResultが入ったNSArrayが返ってくる。別に返り値はずっと取っておく必要はなくて、単にマッチする毎になんか処理をしたいんだよ、ってときは、-enumerateMatchesInString:options:range:usingBlock:が使える。さっきの-firstMatchInString:options:range:を書き換えるとこんな感じになる。

NSRegularExpressionOptions options = 0;
NSRange range = NSMakeRange(0, string.length);
id block = ^(NSTextCheckingResult *match, NSMatchingFlags flag, BOOL *stop){
  NSLog(@"%d", match.numberOfRanges);
  NSLog(@"%@", [string substringWithRange:[match rangeAtIndex:0]]);
  NSLog(@"%@", [string substringWithRange:[match rangeAtIndex:1]]);
  NSLog(@"%@", [string substringWithRange:[match rangeAtIndex:2]]);
};
[regexp enumerateMatchesInString:string options:options range:range usingBlock:block];

Blocksの使い方は以前書いた記事とか読んでもらえると分かるかもしれない。あの記事を書いた時点ではiOS4.0を想定してアプリ作れなかったので実質まともに使えるのがSnow Leopardだけだったのだけど、今ならiPhone/iPadともに4系前提で作れるし、そもそもNSRegularExpression自体がiOS4.0以降にしか無いのでNSRegularExpressionを使える環境ならBlockも使えるので問題ない。

置換する

正規表現が使えるなら一番やりたいのは置換だろう、ということでもちろん置換もできる。-stringByReplacingMatchesInString:options:range:withTemplate:というのがそれ。

  NSString *string = @"「そんな正規表現で大丈夫か?」「大丈夫だ、問題ない」";
  NSString *template =
    @"$0\n→($2砕け散る)\n→「神は言っている、ここで死ぬ運命ではないと」\n→「$1」「一番いいのを頼む」";
  NSRegularExpression *regexp =
    [NSRegularExpression regularExpressionWithPattern:@"「(そんな(.+)で大丈夫か?)」「.+」"
                                              options:0
                                                error:nil];
  NSString *replaced =
    [regexp stringByReplacingMatchesInString:string
                                     options:0
                                       range:NSMakeRange(0,string.length)
                                withTemplate:template];
  NSLog(@"%@",replaced);

最初は話を聞かなかったあいつもちゃんと一番いいのを頼んできたので、今度は大丈夫だろう。しれっと$0とか$1とか使ってるけど、もちろんちゃんと置換文字列の中でキャプチャした部分文字列を参照したりできてるはず。

ただ、-stringByReplacingMatchesInString:options:range:withTemplate:は文字列そのものを置換してるわけじゃなくて、引数のNSStringのオブジェクトをcopyして置換したものを返してくる。なので、元のstringは何も変わってないので変わったつもりで使おうとしたらアレ?ってなるし、毎回文字列のコピーをするので場合によっては無駄になる。その場合は-replaceMatchesInString:options:range:withTemplate:の方を使う。基本的には-stringByReplacingMatchesInString:options:range:withTemplate:と同じなんだけど、以下の点が違う。

  • 引数にNSStringでは無くNSMutableStringを取る
  • 引数のオブジェクトのコピーではなく引数のオブジェクト自体を置換する
  • 返り値は置換後の文字列ではなく整数値で、置換箇所の数を返す

というわけで、ある正規表現で置換した文字列をさらに別な正規表現で置換して、みたいなことをやる場合はこっちのメソッドを使うべき。

ちなみに、上記二つのメソッドはマッチした箇所を全部置換する。例えば下のようなコードだと「大丈夫か」と「大丈夫だ、」が両方置換される。

  NSString *string = @"「そんな正規表現で大丈夫か?」「大丈夫だ、問題ない」";
  NSString *template =
    @"チョ☆チョニッシーナ☆まっソコぶれっシュ☆エスボグリバンバーベーコンさんだね!";
  NSRegularExpression *regexp =
    [NSRegularExpression regularExpressionWithPattern:@"大丈夫(か|だ)、?"
                                              options:0
                                                error:nil];
  NSString *replaced =
    [regexp stringByReplacingMatchesInString:string
                                     options:0
                                       range:NSMakeRange(0,string.length)
                                withTemplate:template];
  NSLog(@"%@",replaced);

もしマッチした箇所の内特定の部分だけを置換したい場合は、-firstMatchInString:options:range:とか-matchesInString:options:range:でNSTextCheckingResultのオブジェクトを取得しておいてから、-replacementStringForResult:inString:offset:template:を使う、みたいな感じになるかしら。ちょっと面倒な気もするけど。

RegexKitLite or NSRegularExpression

両方書いてみた感想で言うと、個人的にはRegexKitLiteのNSStringにメソッド生やしてくアプローチのAPIのが使い易いと思った。CoreFoundation使ってごりごり書いてるのでパフォーマンスも悪くないし、割と早い段階からBlocksに対応してたりとアクティブに開発されてるし、その気になればソース読めるし(まぁ、チラ見しては見たもののあんまり読む気にはならないのだけども)…とか考えると、既にRegexKitLiteを使ってるなら別に無理にNSRegularExpressionに乗り換える必要は無い気がしてくる。iOS4.0以前のバージョンもターゲットにするなら他に選択肢はないし、あと何故かNSRegularExpressionクラスはiOSにしか無くてMacOSXでは使えないという面白いことになってるので、iOSでもMacでも動くようなコードを書く場合もやっぱりNSRegularExpressionは使えない。

とは言えNSRegularExpressionの方はFoundationの一部なので、数カ所正規表現での置換を使いたいが為に外部のコード落としてきてプロジェクトに組み込んでlibicucoreに忘れずにリンクして…ってしないで済むなぁとか、万が一iOSの内部の実装が変わったりなんかの規約が変わったりしてもおそらく書き換えないで済むだろうなぁという多少の安心感とかはある。ので、これから作るアプリで、4.0以降のみをターゲットにしてる場合は、NSRegularExpressionを使って書こうかなぁなんて思ったりしてたり。

ちょっと前に書こうと思ってて忘れてたネタ。iOSアプリ内で正規表現を使ってごにょごにょしようと思ったらRegexKitLiteを導入するのが一番てっとりばやいのだけど、iOS 3.2以降はFoundation Framework内でも地味に正規表現が使えるようになってきてるのでメモがてら記事にしておく。

NSRegularExpressionSearch

Cocoaで文字列中に別な文字列が含まれているかどうかを知りたいときは、NSStringの-rangeOfString:というメソッドを使う。RubyのString#indexみたいな感じで、見付かった文字列がどこにあるかの位置を返してくれる。こんな感じ。

NSString *string = @"I love Udon.";
NSRange match = [string rangeOfString:@"Udon"];
if (match.location != NSNotFound) {
  NSLog(@"Found: %@",[string substringWithRange:match]);
} else {
  NSLog(@"Not Found");
}

これにもう少し細かく色々なオプションを指定できる-rangeOfString:options:というメソッドがあるのだけど、iOS3.2以上のバージョンだとこのオプションにNSRegularExpressionSearchというのが指定できるようになっている。実際に使うときはこう。

NSString *string = @"1日3食のうち4食はうどんを食べたいと思っている。";
NSRange match = [string rangeOfString:@"[0-9]+食" options:NSRegularExpressionSearch];
if (match.location != NSNotFound) {
  NSLog(@"Found: %@",[string substringWithRange:match]);
} else {
  NSLog(@"Not Found");
}

rangeOfString:に正規表現(の文字列)を渡せるようになってちょっと便利。書式はICU-comaptibleだそうだけど、RegexKitLiteもlibicucoreを使ってるので、RegexKitLiteを使ってた人は得に気にすることなく使えると思う。

これだけでも大分マシにはなったんだけど、さっきのサンプルコード見て分かる通り最初にマッチした部分しか取ってこれないし、もしかして-stringByReplacingOccurrencesOfString:withString:options:range:とかにも正規表現使えるのかなとwktkしたのだけど、「You can use this option only with the rangeOfString:... methods.」だそうで。マッチした箇所を全部取ってくるとか置換するとかは別な方法でやるようだ。

NSRegularExpression

さっきのはNSStringの文字列検索のオプションだったけど、正規表現そのものを扱うNSRegularExpressionというクラスがある。NSRegularExpressionSearchオプションは3.2以降であれば使えるけど、NSRegularExpressionクラスは4.0以降。つまり今までiPadでは使えなかったので、RegexKitLiteを置き換えるには至らなかった。

が。そろそろiPad版を含むiOS4.2がリリースされるので、ようやくiPadでも4系の機能が使えるようになるのです。弟の仇をトルノデス。ということで次回はNSRegularExpressionを使った文字列検索を記事にします。予告。

Objective-Cで、参照先のオブジェクトから参照元に通知を送る方法。」を読んてで思ったんですが、若干冗長な気がする。

プロトコルを使う

プロトコルを使うんだったら、最初からそのプロトコルに適合するオブジェクトしかdelegateになれないようにしちゃった方がいいと思う。こんな感じで。

@interface Foo : NSObject {
  id<SampleDelegate> delegate;
}
@property (assign) id<SampleDelegate> delegate;
@end

こうしちゃえば、そもそもSampleDelegateに適合しないオブジェクトはdelegateになれないので、conformsToProtocolのチェックは必要なくなる。UIKitでも(例えばUITableViewのdelegateとか)大体そうなってますね。あと、ObjCはレシーバがnilの場合はメッセージ送信を単に無視するので、respondToSelectorとか送ってもYESが返らないのでこの場合はnitチェックもしなくていいと思う。だから、notifyObjectChangedの定義はこれで十分。

- (void)notifyObjectChanged {
  if ([self.delegate respondsToSelector:@selector(objectChanged:)]) {
    [self.delegate objectChanged:self];
  }
}

非形式プロトコル(カテゴリ)を使う

プロトコルがoptionalなメソッドしか規定してなくて、あるメソッドを定義したデリゲートがある場合はそれを呼ぶけど、そうでない場合は無視するかデフォルトの動作をする、っていうような場合は、そもそもプロトコルで規定しないでカテゴリで実現しちゃう手もある。

@interface NSObject (SampleDelegate)
- (void)objectChanged:(id)object;
@end

@interface Foo : NSObject {
  id delegate;
}
@property (assign) id delegate;
@end

ちなみにnotifyObjectChangedの実装は同じでいい。このタイプの例はNSURLConnectionとか。

こっちの利点はプロトコルの宣言がいらないので、既存のクラスや外部のライブラリのクラスのインスタンスでもdelegateにできること。それが何であるかはどうだっていいんだ、ただ送ったメッセージに答えてくれる何かでありさえすれば、みたいなときにはこの方法でもいい。

欠点は、プロトコルと違ってコンパイル時にdelegateが期待してるオブジェクトかどうかを解決できないこと。元記事の例みたいにoptionalなメソッドしか規定してない場合だと、プロトコルに適合してるからと言って期待してるオブジェクトかどうかははっきりしないので大差ないんだけど、仮にそうであってもプロトコル宣言を強制することによってデリゲートになり得るクラスを限定するってこともあるので、この辺はケースバイケース。

Key-Value Observingを使う

あと、「あるオブジェクトのあるプロパティが変更されたことを知りたい」っていう用途に限って言えば、Key-Value Observingを使う手もある。

@implementation Bar

- (void)test {
  Foo *f = [[Foo alloc] init];
  [f addObserver:self
      forKeyPath:@"hoge"
         options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld)
         context:NULL];
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    // do something
}

@end

こういう風にしておくと、fのhogeプロパティの値が変更されたときに、BarのインスタンスのobserveValueForKeyPath:ofObject:change:context:メソッドが呼ばれる。その際、keyPathには@"hoge"が、objectにはfが、changeには{ old: /* 変更前の値 */, new: /* 変更後の値 */ }というDictionaryが、contextにはNULLが渡ってくる。

これの利点は、「Fooクラス側には全く手を加えなくていい」「あらゆるオブジェクトに対して、一貫したインターフェースで同じように設定できる」ということ。Cocoaのクラスだろうが外部のライブラリのクラスだろうが自分で作ったクラスだろうが同じように「あるプロパティの値が変更されたら教えてねー」っていう設定ができるし、参照先のオブジェクトが参照元のオブジェクトのことを気にする必要が無くなって関係性が緩くできる。Barの仕様を変更したくなってもFooをいじる必要はない。あと、delegateの場合と違って通知を受けとるオブジェクトが複数設定できるので、アプリケーションのあちこちが同時多発的に状態変化するみたいなことができる。

欠点は、「f.hogeに代入するか、[f setValue:obj forKey:@"hoge"]が呼ばれたときだけしか通知されない」ということ。なのでFooクラスの内部でインスタンス変数を直接弄って内部状態が変わったりしたときには通知されない。あとは、あらゆるオブジェクトに対して使えるので便利なんだけど、どのオブジェクトからの通知も必ずobserveValueForKeyPath:ofObject:change:context:を呼ぶので、沢山通知を設定するとobserveValueForKeyPath:ofObject:change:context:の中身が大分カオスなことになるので、ご利用は計画的に。

他には

NSNotificationを使う方法もあるけど、多用すると処理がどこでどうなるかわかりづらくなるので割と慎重に使った方がいいかも。まぁこれはKey-Value Observingでもそうなんだけど、NSNotificationの方がより汎用性が高いのでよりこんがらがり易くてよくハマる。

scrollView.canCancelContentTouches = NO;
[scrollView setCanCancelContentTouches:NO];

って書き方があってこれみんなどういう使い分けしてるんだろうなぁ。。って思ってます。

セッター - poohtarouの日記

セッターというか、ドット記法の話かな。

ドット記法と普通のメソッドの使い分け

まぁ、まずはこんなクラスがあったとします。

@interface Book : NSObject {
  NSObject *title;
}
@property (retain) NSString *title;
@end

このクラスのオブジェクトを作ってtitleを設定/参照するコードはこんな感じです。

Book *book = [[Book alloc] init];
// ドット記法
book.title = @"Dynamic Objective-C";
NSLog(@"%@", book.title);
// メソッド
[book setTitle:@"詳解 Objective-C 2.0"];
NSLog(@"%@", [book title]);

これは上と下どっちのコードも同義。ドット記法でアクセスすると、setTitle:メソッドやtitleメソッドが呼ばれる。getter/setterを自動生成させないで自分で実装した場合もちゃんとそのメソッドを呼んでくれる。じゃあこんな場合はどうか。

id book = [[Book alloc] init];
book.title = @"Dynamic Objective-C";
NSLog(@"%@", book.title);

これはコンパイルエラーになる。id型のオブジェクトのメンバにドット記法でアクセスしようとすると怒られる。いやid型じゃなくてちゃんとクラスを明示して変数宣言すればいいじゃんて?NSArrayのobjectAtIndex:やNSDictionaryのobjectForKey:は返り値の型がid型ですよね。キャストすればいい?だって中に入ってるのがBookクラスのオブジェクトかどうかわかんないじゃん。isKindOfClass:で調べてからキャストする?いやいや、そのArrayの中にはGameクラスのオブジェクトも一緒に入ってて、GameクラスもsetTitleできるからそこは区別なく扱えた方が便利なんだよ!…みたいなこともあるわけで。そんなときはこう書くかなぁ。

id obj = [array objectAtIndex:0]; // Bookのオブジェクトが入ってると期待できるとする
if ([obj respondToSelector:@selector(setTitle:)]) {
  [obj performSelector:@selector(setTitle:) withObject:@"化物語 上"];
}

はいできた。これでBookクラスのオブジェクトをid型の変数で受けてもちゃんとsetTitle:できました。ついでにsetTitle:メソッドを実装してるオブジェクトであれば、Bookクラスと継承関係になくてもsetTitle以外は全然無関係のインターフェースを実装してようとも同じように扱えます。アヒルのように鳴くものはアヒルではなく隣のおばあちゃんでした。じゃなかった、アヒルのように鳴くものはアヒルです。ちなみに、上のコードは実は

id obj = [array objectAtIndex:0];
if ([obj respondToSelector:@selector(setTitle:)]) {
  [obj setTitle:@"本当は怖いグリム童話"];
}

って書いてもコンパイル通るし実行時にエラーも出ない。まぁ警告出るけどね。んで、こういう場合はプロトコルを定義してあれば(そしてobjがそのプロトコルに適合してれば)警告が出ない。

// こんなプロトコルがあるとする
@protocol Title <NSObject>
- (NSString *)title;
- (void)setTitle;
@end
...
id <Title> obj;
...
[obj setTitle:@"本当は怖いグリム童話"];

簡単にまとめると、明示的にクラス名を指定して変数を宣言してるときはドット記法で、id型で受けるべき時はメソッドでっていうのが一応の使い分けかなー。元記事の例の場合はscrollViewはUIScrollView型で変数宣言してて、そのコンテキストではUIScrollViewであることがはっきりしてるのでドット記法でいいと思う。具体的なクラスを想定しているわけではなくて、あるアクセサを持っている何かのオブジェクトっていう扱いをするときは、respondToSelector:とperformSelector:を使うか、プロトコルを定義しといてメソッド呼び出しするか、って感じになると思う。

Key Value Conding

ところで、ObjCにはドット記法とメソッド呼び出しの他にもう一つ、オブジェクトのメンバにアクセスする方法がある。しかもこれはプロパティとドット記法が導入されたObjective-C 2.0になる前からある。例えばさっきのBookクラスのオブジェクトに対してだったらこんなことができる。

[book setValue:@"イチャイチャパラダイス" forKey:@"title"];
NSLog(@"%@", [book valueForKey:@"title"]);

恐ろしいですね。まるでオブジェクトがDictionaryか何かのよう。これはNSObjectの子孫にあたる全てのクラスで使える。そしてこれの恐しいのは、アクセサメソッドやプロパティが宣言されてなくても使えてしまうということ。例えばvalueForKey:@"title"だったら、

  1. titleメソッドが定義されてたらそれを呼ぶ
  2. titleメソッドが無くて、getTitleメソッドが定義されてたらそれを呼ぶ
  3. どっちも無くて、インスタンス変数titleが宣言されてたらそれを返す
  4. どれも無くて、インスタンス変数_titleが宣言されてたらそれを返す

っていう順番で解決されるので、Bookクラスの場合titleをプロパティとして宣言してる、つまりtitleメソッドが定義されてるのでそれが呼ばれる。もしプロパティとして宣言してなくても、title変数があるので3番目でひっかかってそれが返る。ちなみにインスタンス変数が@privateで宣言されてても関係ない。便利なんだけど怖い。1、2はともかく3、4は大分アレげなので、内部でしか使わない意図せず変更して欲しくないインスタンス変数は@privateで宣言した上でname_とかして上のルールにひっかからないようにしておく癖を付けたほうがいいと思う。外部からアクセスしてもいいやつでも名前は変えた上で明示的にプロパティで宣言した方が安全かなー。

元記事の話とは直接関係無いけど余談でした。なんでそんな仕組みがあるのってあたりは気になる人は「Key Value Coding」とかで調べてみるといいと思う。これはこれで便利だし、AppKitとかUIKitとかでは結構使われてるので。

追記

そうだブコメで指摘をもらってたんで追記しようと思ってたのすっかり忘れてた。iPhoneアプリでKey-Value Codingを一番使うであろう場面はCoreDataのNSManagedObjectですね。CoreDataでエンティティを定義してデータをつっこんだり取得したりするときは、NSManagedObjectかそれを継承したクラスがモデルオブジェクトになる。サブクラスを作るときはプロパティを定義しとけばいいんだけど、NSManagedObjectにはもちろん中のデータへのアクセサは定義されてない。じゃあどうするかと言うと、valueForKey:とsetValue:forKey:で値の取得や変更をするってことになる。

あと、これの次の記事で書いたけど、Key-Value Observingって仕組みがとても便利なので、プロパティについて調べたらついでにKey-Value CodingとKey-Value Observingについて調べておくといいと思います。まる。

詳解 Objective-C 2.0詳解 Objective-C 2.0
著者:荻原 剛志
販売元:ソフトバンククリエイティブ
発売日:2008-05-28
おすすめ度:4.0
クチコミを見る
Dynamic Objective-CDynamic Objective-C
著者:木下 誠
販売元:ビー・エヌ・エヌ新社
発売日:2009-03-27
おすすめ度:4.5
クチコミを見る

Rubyと比べながらBlocksをいじってみたりBlocksでNSArrayにmapメソッドを生やしてみたりしてきたので、そろそろGrand Central Dispatch(GCD)も試してみる。あんま関係ないけど、グランド・セントラル・ディスパッチってなんか必殺技っぽいよね。じゃあ一緒に高らかに叫んでみようか。せーの、グランド!セントラル!ディスパッチ!!

GCDってなにさ

ドキュメント嫁。

…だけだと流石に不親切なので、一応簡単に説明すると、APIを通してぽんぽん処理をqueueにつっこんでってやると、ランタイムの方でそれを上手いこと並列実行しといてやるよ!安心しろチェリーボーイ共、スレッドのことは俺が面倒見てやるぜ!って仕組み。そんな口調なのかどうかはわかんないけど、まぁ大体そんな感じ(適当)。

例によってRubyと比較

まぁ、こんなコードがあったとします。

# ruby
f = lambda {
  puts "0.25秒後から本気出す"
  sleep 0.25
}
t = Time.now
20.times do
  f.call
end
p Time.now - t

んで、それを例によってObjCでBlocksを使って書くとこんなコードになります。ちなみにどっちも別にBlocksやlambdaでやる必要は無いんだけど、この後のコードと比較の為にわざとそうしてるのでスルーしておくれやす。

# ObjC
void (^f)(void) = ^{
  NSLog(@"あと0.25秒だけ寝させてー");
  [NSThread sleepForTimeInterval:0.25];
};
NSDate d = [NSDate date];
unsigned int i = 20;
while (i--) {
  f();
}
NSLog(@"%f", [[NSDate date] timeIntervalSinceDate:d]);

まぁ見ての通り、標準出力に一言言ってから0.25秒sleepするだけの簡単なお仕事を20回ほどやってもらってます。あたり前のことだけど、0.25*20で5秒+αくらいの時間がかかるし、0.25秒っつってんのに5秒待たせるとか相当いい加減なやつだ。

んで、こんな風に、それぞれの処理が独立してるけど一個一個は結構時間かかる、みたいなのは、並列に実行させちゃったらいいんじゃね、みたいなことを偉い人は言いました。

# ruby
f = lambda {
  puts "0.25秒後から本気出す"
  sleep 0.25
}
t = Time.now
20.times do
  Thread.new { f.call }
end
(ThreadGroup::Default.list - [Thread.current]).each{|th|th.join}
p Time.now - t

まだ若干怠けてるけど、まぁ0.3秒行かない程度で終わる。5秒に比べたら一瞬みたいなもんだよね。

じゃあ次はそれをObjCで…と言いたいところなんだけど、ObjCで上のRubyのコードをNSThreadってクラスを使って書こうとすると、割と面倒い。特定のコンテキストを別スレッドで実行しようと思うと、detachするのにtargetとselector、つまりスレッドで実行されるオブジェクトとそいつから呼び出されるメソッドがなくちゃいけない。んでもって、作ったスレッドを自分で管理しなきゃいけない。一応適当なサンプルは書いたけど、あんまりこれをObjCで自前で書くことは無いと思う(理由は後述する)。

さて、GCDです

最初の方に言ったけど、大分ざっくり言うとGCDってのは「処理のブロックをキューにつっこんでってやると裏で上手いこと並列に処理してくれる」ものです。要は並列処理のめんどい感じを多少楽にしてくれるのがGCDの兄貴だってことです。兄貴なのか姉貴なのかは知りませんが。どっちかというと僕はお姉さんが好きですがどうでもいいことです。

# ObjC
dispatch_block_t block = ^{
  NSLog(@"あと0.25秒だけ寝させてー");
  [NSThread sleepForTimeInterval:0.25];
//  NSLog(@"%@", [NSThread currentThread]);
};
//NSLog(@"%@", [NSThread currentThread]);

NSDate d = [NSDate date];

// ここからGCD登場
dispacth_group_t group = disptach_group_create();
disptach_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
unsigned int i = 20;
while (i--) {
  disptch_group_async(group, queue, block);
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
// ここまでGCDのお仕事

NSLog(@"%f", [[NSDate date] timeIntervalSinceDate:d]);

はい。Rubyの方をThreadを使って書き直したときと同じく、0.3秒行かないくらいの時間でさくっと処理してくれました。コメントアウトしてるのを戻せば、ちゃんと別々のスレッドで動いてるのも確認できると思います。ちなみにObjCの文法で書いてるとこを削れば普通にCでも使えます。使いたいときは#include <dispatch/dispatch.h>してください。

変わったところを解説すると、まずブロックの型がdispatch_block_tに変わってる。これは後で出てくるdispatch_group_asyncの引数の型なんだけど、void (^)(void)、つまり何も取らず何も返さないブロックって定義になってるので実はさっきと何も変わってない。

次にdisptach_group_createをしてgroupを作ってるけど、これはまぁ名前通り非同期に実行する処理をグルーピングするためのもの。Rubyの方でもThreadGroupが出てきたけど、あれと一緒で最後のdispatch_group_waitで一連の処理が全部終了するまで待ってやる為に使う。今回は全部の処理が終わるまでの時間が見たかったのと、アプリケーションとかじゃない普通のCUIのコマンドとして作ったときに何も考えずに非同期処理させるとdispatchしたのが終わる前にmainが終わっちゃうのでwaitする必要があったけど、GUIアプリケーションとかデーモンとかだとその心配はないのでグルーピングせずに単にdispatch_asyncしちゃってもいい。

んでここからが本質、dispatch_get_global_queueとdispatch_async(またはdispatch_group_async)。といっても別に大したことではなくて、

  1. queueを用意します
  2. dispatch_asyncにqueueとblockを渡します
  3. あとは裏でよしなにやってくれます

以上。中では「システムの負荷を見てスレッドを作るか待つか決める」「スレッドが一個も空いてなければ作るけど、さぼってるやつがいたら再利用する」「あっちこっちから放り込まれたブロックをどのスレッドに割り当てたら効率良いか考える」とか色々やってんだけど、使う側としてはそんなこと気にする必要無いし、それどころかスレッドが作られてることすら隠蔽されてる。やったのは単に関数にブロックを渡しただけ。ゆとりの僕でもできる簡単なお仕事です。ちなみにdispatchしてwaitするあたりの処理を続けて何回も実行すると、ちゃんとスレッド再利用してるのが確認できる。

他にも、globalって名前が付いた関数があるからにはglobalじゃないqueueを作る関数もあるとか、asyncって名前が付いた関数があるからにはsyncして実行する関数もあるとか、メインスレッドで動作するqueueがあるとか、さっきのコードではわざわざforループ回したけどループにはループ専用のdispatch_applyがあるとか、まぁ色々あるんだけど、Xcodeのあのアホみたいに使い辛いドキュメントビューワの検索窓にdispatch_って入れてやるといっぱい出てくるので見てみてくれればいいかと思います。

でもそれCじゃん

ええ。ここまでは誰がどうみてもCの関数、っていうかさっきも書いた通り実際<dispatch/dispatch.h>をincludeすればCでも使えるAPIです。いやさ、確かにObjCの文法はキモいけどさ、せっかくObjCで書いてるのにCの関数使うってどうなのよって?ご心配なく。ObjCならObjC流に実装する方法はもちろんある。

「並列実行」「どんどんキューにつっこむ」「スレッドの面倒はキューが見てくれる」あたりで、LeopardまでのOSXとかiPhoneSDKとかでアプリを書いたことある人は「それNSOperationとNSOperationQueueでできるじゃん」と思ったはず。これもキューにオペレーションオブジェクトをどんどんつっこんで行けば非同期でよしなにやってくれるクラスで、それ自体はGCDやBlocksとは関係なく前から使える。ので、もともとあんまり自前でNSThreadを作ったり管理したりはやったことなかった。じゃあObjCだと何も変わらないじゃんと思ったけど、Blocksが導入されたことでNSArrayやNSDictionary同様便利な機能が追加されているという。

# ObjC
void (^block)(void) = ^{
  NSLog(@"あと0.25秒だけ寝させてー");
  [NSThread sleepForTimeInterval:0.25];
//  NSLog(@"%@", [NSThread currentThread]);
};

//NSLog(@"%@", [NSThread currentThread]);
NSDate d = [NSDate date];

// こっからNSOpera(ry
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
unsigned int n = 20;
while (n--) {
  [queue addOperationWithBlock:block];
}
[qeueu waitUntilAllOperationsAreFinished];
// ここまでNSOpera(ry

NSLog(@"%f", [[NSDate date] timeIntervalSinceDate:d]);

おお、これは便利だな。Blocsを使わない場合は事前にNSOperationクラスを継承して独自のOperationクラスを作っとくとか、NSInvocationOperationを使うとかしてたところを、-[NSOperationQueue addOperationWithBlock:]を使えばブロックを渡してやるだけで非同期実行できちゃう。ブロックをひとまとめにしてオペレーションにしてくれるNSBlockOperationってクラスや、オペレーションが終了した後に実行される処理をブロックで設定できる-[NSOperation setCompletionBlock:]ってメソッドが追加されてて、「処理を一括りにして並列実行する」コードが大分書きやすくなっている。

MacRuby

RubyとObjCの話をしてるのにMacRubyさんを完全にスルーするという素敵なプレイをやってのけてきたわけですが、実はMacRubyさんはとっくにGCDに対応してやがります(MacRuby » An Introduction to GCD with MacRuby)。凄いな、ほんと、どこまで行くんだろう。

ついでに

Blocks入門NSArrayにmap生やしたのとこの記事のコードをのサンプルはgistに上げてみたので、まぁ一応一通り動く例になってるはず。あーあと簡易ベンチマーククラスみたいのも作ってみたので入れといた。適当なので実用するのはおすすめしません。Snow Leopardでrubyとrakeが入ってる環境なら、cloneしてきてrakeすればコンパイルできるはず。

参考記事

RubyエンジニアのためのObjective-C Blocks入門に引き続き、Blocksネタ。そっちの記事ではBlocksはクロージャ的ななにかだって言ってるのに単なる関数ポインタみたいにしか使ってなかったので、せっかくなのでクロージャ的に使ってみる。

eachできるならmapも欲しい

-[NSArray enumerateObjectsUsingBlock:]を使えば、Array#each相当のことができるとこまでは前回の記事でできた。そうすると、Array#map相当のこともNSArrayにさせられるはず。例えばこんなの。

# ruby
array_a = %w(ひたぎ 真宵 駿河 撫子 翼)
array_b = array_a.map {|x| "#{x}が可愛過ぎて生きるのが辛い" }

Rubyistには説明の必要もないと思うけど、Array#mapが何をしてるかというと、

  1. 1引数のブロックを受けて
  2. 自分自身の要素を一つずつブロックに渡して
  3. その返り値を要素にした新しいArrayを作って返す(※自分自身は変更しない)

みたいなところ。さてそれをObjCで実装する。まず、ベタに書くとこんな感じ。

// ObjC
@interface NSArray (Map)
- (NSArray *)mapUsingBlock:(void (^)(id))block;
@end
@implementation NSArray (Map)
- (NSArray *)mapUsingBlock:(void (^)(id))block {
  NSMutableArray *newArray = [NSMutableArray array];
  for (id item in self) {
    id obj = block(item);
    [newArray addObject:obj];
  }
  return array;
}
@end

これはこれでなんかスッキリしてていいんじゃねって気がしてきた。でもせっかくなので、enumerateObjectsUsingBlock:を使って書き直してみる。

// ObjC
- (NSArray *)mapUsingBlock:(void (^)(id))block {
  NSMutableArray *newArray = [NSMutableArray array];
  [self enumerateObjectsUsingBlock:^(id item,NSUInteger idx,BOOL *stop){
    id obj = block(item);
    [newArray addObject:obj];
  }];
  return array;
}

ちょこっと説明する。ブロックは、RubyやPerlのそれと同じく、生成されたコンテキストにある変数を参照できる。上のコードで言うとnewArrayはブロックの中じゃなくて外で宣言されてるけど、ブロックはその時点でのコンテキストを持ってるので、ブロックを生成したスコープで見えるものはブロック内でも同じように見える。

で、使う側ではこんな風に書けるようになる。

// ObjC
NSArray *array = [NSArray arrayWithObjects:@"ひたぎ",@"真宵",@"駿河",@"撫子",@"翼",nil];
NSArray *newArray = [array mapUsingBlock:^(id item){
  return [NSString stringWithFormat:@"%@ー!俺だーッ!結婚してくれー!",item];
}];

おお。それっぽいそれっぽい。

__block

ちょうどタイミング良く昨日弾さんとこで同じような話をしてたんだけど、上の例はしれっとブロックの外の変数に破壊的操作をしてるけど、例えばこういうのはできない。

// C or ObjC
typedef int (^bint)(void);

bint make_incr() {
  int n  = 0;
  bint f = ^int(void){ return n++; };
  return f;
}

ObjCのBlocksの実装だと、何も指定してないとブロックの外の変数は「見える」だけで「変更できない(ブロックの中から再代入できない)」。でも__block修飾子を付けて変数を宣言すると、ブロックの中から変数を変更できるようになる。それから、そのままfを返しちゃうと、関数のスコープ抜けたときにfが消えちゃうので呼び出し側で使えないところに注意する必要がある(試しにやってみたら、実際には最初の一回だけ実行できたものの、二回目を実行しようとしたところでbus errorになった。ブロックが解放されちゃってるはず)。nをブロック内で変更できて、スコープ外でも使えるブロックを返す関数を書くなら、

// C or ObjC
typedef int (^bint)(void);

bint make_incr() {
  __block int n = 0;
  bint f = ^int(void){ return n++; };
  return Block_copy(f);
}

こんな感じになる。ちなみに、このコードはObjCで書かれてる部分が無いのでMacOSX 10.6だったらCのコードとしてもObjCのコードとしてもコンパイルできるけど、ObjCの場合は<Foundation/Foundation.h>を、Cの場合は<Block.h>をimoport(include)する必要がある。あと、Block_copyして関数のスコープ外で使えるようにしたときは、当然呼び出し側で責任持ってBlock_releaseしてブロックを開放してやる必要がある。

// Cの場合
BLOCK_TYPE block = ^{ ... };
Block_copy(block);
...
Block_release(block);

// ObjCの場合
// Cと同じやりかたでも問題ないし、
// copyメソッドがBlock_copyと、releaseメソッドがBlock_releaseと対応してるので
// そっちも使える
BLOCK_TYPE block = [^{ ... } copy];
...
[block release];

// あとautoreleaseも使える
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
BLOCK_TYPE block = [[^{ ... } copy] autorelease];
...
[pool release]; // ここでblockも開放される

ちなみになんでNSMutableArrayの方は破壊的な操作をできるのかというと、多分変数の操作じゃなくてオブジェクトへのメッセージ送信をしてるだけだから。[newArray count]とかやるのと意味的には同じなのでできるってことじゃないのかな。で、newArray = ... とか変数自体をいじろうと怒られるはず。

次回予告

Grand Central Dispatchで遊んでみた編。多分。

書こう書こうと思ってたけど忘れてたのを、PerlエンジニアのためのObjective-C Blocks入門を見て思い出した。すいませんタイトルは便乗です。

試しに書いてみる

Blocksってのが何者なのかはさっきの記事なりAppleのドキュメントなりを見てもらえばいいと思うんですが、まぁウケが良さそうなので付けたタイトルにのっとってRubyと比較してみる。

f = lambda {|x|
  puts "#{x}のこと以外は何も考えられない"
}
f.call("うどん")
void (^f)(id) = ^(id x) {
  NSLog(@"世界の全てを敵に回しても、僕は%@の味方だ", x);
};
f(@"うどん");

なんだ、そっくりじゃない。似てる似てる。

上がRuby版、下がObjC版。下は普通引数にはNSString*とかを使うと思うけど、まぁRuby版と挙動を合わせるためにidにしてみた。それぞれコピペして動かしてみるといいよ。好きな子の名前とか入れてみるといいよ。

なに?ブロックを変数に入れてるとこの宣言がキモい?じゃあこうだ。こうすればいい。

id f = ^(id x) {
  NSLog(@"世界の全てを敵に回しても、僕は%@の味方だ", x);
};

id型の変数に代入できてしまった。実はこのブロックはオブジェクトなので、普通にid型として扱えるし、メッセージのレシーバになったりできる。もちろんid型の変数に入ってるときは関数みたいに使ったりできないので、実際使うときは

((void (^)(id))f)(@"うどん");

とかキャストしてやらなきゃいけないわけで、id型で宣言する意味はあんまりない。ただ、id型として扱えるってことは、NSArrayとかNSDictionaryにつっこんどいたりできるってことでもある。コレクションクラスとか自前で作らなくても他のオブジェクトと一緒にArrayにつっこんどいてうまいこと処理するとかできる。ところでObjC上ではObjCのオブジェクト扱いだけど、Cで使ってるときはこれ何として扱われてるんだろう。気になる。

せっかくなのでブロック構文

これだけだと何それおいしいので終わりなので、実際に使ってるところをみてみる。Rubyでblockって言ったらブロック構文を思い浮かべると思う。例えばこんなの。

languages = ['Ruby','Objective-C','Perl','PHP','JavaScript','Haskell']
languages.each do |l|
  puts "#{l}なら多分書ける"
end

Blocksと言うからにはこういう使い方をするメソッドがいくつかFoundationのクラスにもある。上記のコードをObjCでブロックを使って書くとこんなかんじになる。

NSArray *languages = [NSArray arrayWithObjects:@"Ruby",@"Objective-C",@"Perl",@"PHP",@"JavaScript",@"Haskell",nil];
[languages enumerateObjectsUsingBlock:^(id item, NSUInteger idx, BOOL *stop) {
  NSLog(@"%@なら多分書ける", item);
}];

う、うん、似てる…よね?ちょっとわかり辛いかもだけど enumerateObjectsUsingBlock:ってのが引数にブロックを取るNSArrayのインスタンスメソッドで、Array#eachと同じように自分自身の要素を一個ずつ順にブロックに渡して実行してくれる。ちなみに、「最初の引数がNSArrayの要素のオブジェクト、次がインデックスの整数値、最後がループを止めるためのBOOLのポインタ」という3引数のブロックを渡さないといけない。どっちかっつーとEnumerable#each_with_indexのが近いかな。

上の例ではitemしか使ってないけど、全部使うとしたらこんな感じ。3個目の引数にYESを入れてやるとそれ以降は実行されなくなる。

[languages enumerateObjectsUsingBlock:
  ^(id item, NSUInteger index, BOOL *stop) {
    NSString *status;
    if ([item isEqualToString:@"PHP"]) {
      status = @"アタシはしんだ。";
      *stop  = YES;
    } else {
      status = @"楽しかった。";
    }
    NSLog(@"%d日目。%@を書いた。%@", index+1, item, status);
  }
];

や、別にPHPについて何か言いたいわけじゃないですよ?やだなぁ、ちょっとした冗談じゃないですか。

今度書く

この記事の例では書いてないけど、所謂クロージャなのでブロックが生成されたコンテキストの変数とかブロックの中から参照できる。NSArrayにmapメソッド生やしてみたり簡易ベンチマーク関数作ってみたりしたので今度晒す。

あと、Enumeratorもいいけど、Blocksが真の力を発揮するのは、Cならdispatch_async、ObjCならNSOperationとかを使って非同期に処理をぽんぽん投げてくとき。というか、BlocksがそもそもGrand Central Dispatchという仕組みと一緒に導入されたものなので、それについてはもうちょい調べて今度書く。ちなみに例えばNSArrayでもenumerateObjectsUsingBlock:の他にenumerateObjectsWithOptions:usingBlock:というメソッドがあって、これにNSEnumerationConcurrentってオプションを渡してやると並列に実行してくれたりする。

余談

構文がキモいのはもうObjCだしどうしようもないけど、気に入らなければtypedefしておいた上でブロック生成のマクロでも書いとくと少しはマシに見えなくもないような気がしなくもないような感じがしたりしなかったり。うん。キモい。

余談2

gccだと独自拡張らしいので多分使えるのMacOSX 10.6のXcodeに付属してるやつだけだと思うけど、Clangには組み込まれてるらしい。試してないけど、Clangでなら他のプラットフォームでもBlocks使ったCのコードコンパイルできるんじゃないかな。

余談3

iPhoneSDKで使えるのかい?ってのは聞かないでくだしあ。泣きたい。

お掃除

iPhoneアプリのコードのお掃除をしている。やれiPadだ新型iPhoneだOS4.0だ、うはぁ頼みの汎用ライブラリはUndocumentedなAPI使ってて動かない、なんですってー他の言語からのトランスレータは規約で禁止だってー、とまぁなんだかんだでiPhoneデベロッパはアプリの改修に追われててAppleに恨みつらみが募ってたりもするんじゃないかと思うけど、まぁ俺も大体そんな感じです。正確に言うとそうじゃなくてもしょっちゅう直してますけど。

リファクタリング

そんなわけで内部のコードの整理とかバグ取りとかついでに切り出したAtomPubクライアント汎用フレームワーク化しようかなーとかやってて、そうなるとちゃんとしたテスト書いてないと辛いのでOCUnitでテストケースをもりもり増やしてるんだけど、非同期にAPIと通信してるところがいまいちテストし辛い。というわけでNSURLConnectionを使ってるところを上手いことテストできないか試行錯誤してみた。

スタブに差し替えておく

まず実際にリクエストを投げないように、NSURLConnectionの実装を差し換える。実装の差し替えって言ったらposeAsClass:あたりかなーとか思ってたら、もうとっくにdeprecatedになってたのね。っていうかそういやそんな話聞いたの大分前だな。

そうするとRuntimeAPIのmethod_exchangeImplementationsあたりでメソッドを差し替えるのがいいのかなー。こんな感じか。

#import <objc/runtime.h>
#import <Foundation/Foundation.h>

void swizzle(Class target,SEL orig_sel,SEL alt_sel) {
    Method orig_method = class_getInstanceMethod(target,orig_sel);
    Method alt_method = class_getInstanceMethod(target,alt_sel);
    method_exchangeImplemantations(orig_method,alt_method);
}

これで例えば、

@interface NSURLConnection (ForTest)
- (void)_stab_start;
@end
@implementation NSURLConnection (ForTest)
- (void)_stab_start {
    // 元の実装だとこれを呼ぶと通信が始まるので、
    // 通信しないで直に
    // [self.delegate connectionDidFinishLoading:self]
    // とか
    // [self.delegate connection:self didFailWithError:error]
    // とかを送る処理を書く。
    // ただ非同期に処理が走(ってるように見せ)る必要があるので、
    // NSTimerとかを使って時間差で呼ぶようにしておく。
}
@end

みたいにしておいて、テストケースの方で

- (void)setUp {
    swizzle([NSURLConnection class], @selector(start), @selector(_stab_start));
}
- (void)tearDown {
    swizzle([NSURLConnection class], @selector(start), @selector(_stab_start));
}
- (void)testConnection {
    id hoge = [[Hoge allco] init]; // 内部でNSURLConnectionを使ってるクラス
    // 通信してますよー的な処理
    [[NARunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5]];
}

とかやればいいはず。load時じゃなくてsetUpで差し替えてtearDonwで戻してるのは、他のテストにまで影響が出ないように。これでいいんだっけ。差し替えられたら以後そのセレクタになるはずだから戻すときは同じのをもう一度呼べばいいんだよね、多分。

○○はループ説(ひぐらし的な意味で)

ちなみに、テストケース中にそのまま非同期通信を含む処理を書いても上手く動かない。NSTimerとかNSThreadとかを使う処理はNSRunLoopが回ってるときじゃないと期待する動きをしない(例えばNSTimerは発火しないのでタイマーから呼ばれるはずの処理はそもそも通らない)んだけど、テストケースの実行はアプリケーションじゃないのでもちろんNSRunLoopは回ってない。NSURLConnectionの実装を差し替えたおかげで処理にどのくらいの時間がかかるか予想できるので、適当な時間を指定してrunUntilDate:とかで走らせてやればちゃんと動く。

あとは普通にテストケース増やしてけばビルドする度に網羅的にテストが走るので大分安心感が増す。しかしUIの方はどうしたもんかね。こればっかりは見ながらデバッグするしかないのかなー。世のデベロッパのみなさんはどうしてるんだろう。

MacRubyとRubyCocoaの微妙な違い (1)MacRubyとRubyCocoaの微妙な違い (2)の続き。

クラスをいじってみる

またまた抜粋

  # MacRuby版
  it 'Objective-Cのクラスを書き換えられること' do
    lambda { eval 'class Sloth; def name; "nisefaultier"; end; end' }.should.not.raise
    obj = Sloth.alloc.init
    obj.name.should.be.eql "nisefaultier"
    # @obj.name.should.be.eql "nisefaultier"
  end
  # RubyCocoa版
  it 'Objective-Cのクラスを書き換えられること' do
    lambda { eval 'class Sloth; def name; "nisefaultier"; end; end' }.should.not.raise
    obj = Sloth.alloc.init
    obj.name.should.be.eql "nisefaultier"
    @obj.name.should.be.eql "nisefaultier"
  end

違いは一点、MacRuby版でクラスの定義を書き換えたとき、新しく作られるオブジェクトには影響するけど、既にあるオブジェクトには影響しない。が、RubyCocoa版はクラスの定義を書き換えるとそのクラスの全てのオブジェクトに影響する。面白い。Rubyは全てのオブジェクトがメタクラスのオブジェクトを見てるけど、ObjCのオブジェクトはそれぞれが関数ポインタか何かでメソッドを保持してるってことかな?RubyCocoaにおける「Cocoaのオブジェクト」って結局「(ObjCのオブジェクトを内部に持った)Rubyのオブジェクト」だけど、MacRubyのオブジェクトは全てが「ObjCのオブジェクト」なので、若干CRubyと挙動が違ってくるみたい。

ところで、上のコードはインスタンスメソッドを書き換えてるんだけど、ふと思い立ってこんなコードを書いてみた。

$ cat mrb_class.rb 
framework 'build/Release/Namake.framework'

p Sloth.new

class Sloth
  def self.new
    "string"
  end
end

p Sloth.new
$ macruby mrb_class.rb 
#<Sloth:0x80063c740>
"string"

MacRubyだと、Sloth::newの実装が差し換えられた。RubyCocoaだとどうだろう。

$ cat rbc_class.rb
require 'osx/cocoa'
include OSX

bundle = NSBundle.bundleWithPath('build/Release/Namake.framework')
bundle.load

p Sloth.new

class Sloth
  def self.new
    "string"
  end
end

p Sloth.new
$ ruby rbc_class.rb 
#<OSX::Sloth:0x2385ac class='Sloth' id=0x3734a0>
"string"

あれっ。別に普通だ。同じだった。というのも、昔RubyCocoa+RSpecでごにょごよやってたときに、Cocoaのクラスにstubを仕込んだら「それはObjC側の実装だから勝手に変えるな」みたいに怒られた記憶があるんだけど、んー、この感じだと特に問題無さそう?っていうかあれか、RSpecのstub!はもっと中で複雑なことしてるんだよなきっと。こんな単純な話じゃなくて。後で読んでみよう。もしかしたらそこが「RubyCocoaには無理だけどMacRubyならできる」とこかもしれないし、逆にMacRubyだと下手にいじると大変なことになる罠だったりするかもしれないし。

試してないこと

意図的に上の例では抜いてるけど、MacRubyの大きな特徴と言えば、「キーワード付き引き数」。そもそもそんな機能自体Rubyには無いので、これを上手く使えたら大分面白いことになりそう。あと「CocoaのクラスのサブクラスをRuby側で定義する」とか。MacRubyは何度も言うけど「全部がNSObjectのサブクラス」なので、ぶっちゃけ普通のRubyスクリプトを書くだけでもそういう風になってるんだけど、RubyCocoaはそうではないので、なんか違いがあるんじゃないかと思ってみたり。

そう言えば

MacRubyではオブジェクトを「release」することができない。undefined methodで例外が出る。まぁそもそもGC有りなのでオブジェクトにreleaseを送っても無意味なんだけど。だから存在する必要がないし「undefined」で当然か。ちなみに、RubyCocoaだとCocoaのオブジェクトにはちゃんとreleaseメッセージを投げられる。もっともRubyのGCでオブジェクトが開放されるときに自動的に呼ばれてるはずなので、RubyCocoaのコードでreleaseする必要もやっぱりない、はず。むしろ予想外のバグになりそうなのでやらない方がいいのかも[要出展]。

MacRubyとRubyCocoaの微妙な違い (1)の続き。

フレームワークの読み込み

まず最初に違うのは、framework。これはRubyCocoaで書くと多分こんな感じ。

require 'osx/cocoa'

def framework(name)
    OSX.require_framework name
rescue
    bundle = OSX::NSBundle.bundleWithPath(name)
    bundle.load
end

MacRubyのframeworkは名前じゃなくてパスを渡してやるとそのFrameworkを読み込んでくれるけど、OSX::require_frameworkは名前しか受け付けない、つまりパス上にあるFrameworkしか探さない。ので、任意のパスにあるFrameworkを動的に読み込みたいときは、NSBundleのクラスメソッドを使ってバンドルのオブジェクトを作っておいて、それのloadメソッドを呼ぶ必要がある。ちなみにこれはRubyCocoaではなくCocoaの方の機能なのでもちろんMacRubyでも使える。

Stringの扱い

RubyCocoa版の方で、Slothクラスのname、setNameを呼び出してるとこだけ抜粋。

  it 'Objective-Cのメソッドを呼べること' do
    @obj.name.should.be.nil
    # lambda { @obj.name = "faultier" }.should.not.raise
    lambda { @obj.name = "faultier".to_ns }.should.not.raise
    @obj.name.should.be.== "faultier"
    # @obj.name.should.be.eql "faultier"
    # @obj.name.should.be.kind_of String
    @obj.name.should.be.kind_of NSString
    lambda { @obj.setName "ふぁうるてぃあ".to_ns }.should.not.raise                     
    @obj.name.to_s.should.be.== "ふぁうるてぃあ"
  end

コメントアウトしてるところは、実行するとテストが通らないところ。RubyCocoaではStringとNSStringは全く別のクラスなので、引き数にNSStringが来るはずのCocoaのメソッドを呼ぶときはRubyのString#to_nsを呼んで一度NSStringのオブジェクトに変換してから渡してやる必要がある。また、当然kind_of?(String)はfalseになる。面白いのは、

"string".to_ns == "string" #=> true

になるってこと。==のときは変換かけてから比較するらしい。もう一つ面白いのは、asciiのときにはStringとNSCFStringのeql?がtrueになるのに、日本語のときは何故かfalseになる。$KCODEを指定してやっても駄目。ふぅん。あれ。ていうか、"string".to_ns.eql?("string") ってそもそもtrueじゃないよな。なんでshould.be.eql("string")だと通るんだろう。通っちゃう方がおかしい気がする。

全然そんなことなかった。@obj.name.to_s.should.be.eql("faultier")にしてた。そりゃ通るわ!というわけで訂正。==はNSStringとStringの変換を行なった後比較するけど、eql?およびequal?はもう少し厳密で、NSStringとStringを別のものとして扱う。マルチバイトでも特に違いはない。

ちなみに、MacRubyでは以下が全て成立する。

  it 'Objective-Cのメソッドを呼べること' do
    @obj.name.should.be.nil
    lambda { @obj.name = "faultier" }.should.not.raise
    @obj.name.should.be.== "faultier"
    @obj.name.should.be.eql "faultier"
    @obj.name.should.be.kind_of String
    @obj.name.should.be.kind_of NSString
    lambda { @obj.setName "ふぁうるてぃあ" }.should.not.raise
    @obj.name.should.be.== "ふぁうるてぃあ"
    @obj.name.should.be.eql "ふぁうるてぃあ"
  end

理由は簡単。

$ ruby -r'osx/cocoa' -e 'p String.ancestors'
[String, Enumerable, Comparable, Object, Kernel]
$ ruby -r'osx/cocoa' -e 'p OSX::NSString.ancestors'
[OSX::NSString, OSX::NSObject, OSX::OCObjWrapper, OSX::NSKeyValueCodingAttachment, OSX::NSKVCAccessorUtil, OSX::ObjcID, Object, Kernel]
$ macruby -e 'p String.ancestors'
[NSMutableString, NSString, Comparable, NSObject, Kernel]
$ macruby -e 'p NSString.ancestors'
[NSString, Comparable, NSObject, Kernel]

見ての通り、MacRubyではRubyのStringはNSStringのサブクラスになってる。てことは、Slothクラスのname/setNameにはRubyのStringオブジェクとがそのまま渡ってそのまま返ってきてるはず。この辺は流石にMacRubyの方がずっと自然に感じられる。

さらに続く。なんか妙に筆が進んでしまった。

MacRubyでBaconを調理する準備ができたので、早速Bacon焼くぜー超焼くぜー、と書き始めたんだけど、その前にRubyCocoaとMacRubyの違いを比べてみたくなった。のでちょっと脱線。

# Namake.framework内にあるSloth.h
#import 

@interface Sloth : NSObject {
    NSString *name;
}

- (NSString *)name;
- (void)setName:(NSString *)aName;

@end
$ cat mrb_bacon.rb
# coding: utf-8
require 'bacon'

framework 'build/Release/Namake.framework'

describe Sloth do
  before do
    @obj = Sloth.alloc.init
  end

  it 'Objective-Cのオブジェクトであること' do
    @obj.should.be.kind_of Sloth
    @obj.should.be.kind_of NSObject
  end

  it 'Objective-Cのメソッドを呼べること' do
    @obj.name.should.be.nil
    lambda { @obj.name = "faultier" }.should.not.raise
    @obj.name.should.be.== "faultier"
    @obj.name.should.be.eql "faultier"
    @obj.name.should.be.kind_of String
    @obj.name.should.be.kind_of NSString
    lambda { @obj.setName "ふぁうるてぃあ" }.should.not.raise
    @obj.name.should.be.== "ふぁうるてぃあ"
    @obj.name.should.be.eql "ふぁうるてぃあ"
  end

  it 'Objective-Cのクラスを書き換えられること' do
    lambda { eval 'class Sloth; def name; "nisefaultier"; end; end' }.should.not.raise
    obj = Sloth.alloc.init
    obj.name.should.be.eql "nisefaultier"
    # @obj.name.should.be.eql "nisefaultier"
  end
end
$ macbacon mrb_bacon.rb
Sloth
- Objective-Cのオブジェクトであること
- Objective-Cのメソッドを呼べること
- Objective-Cのクラスを書き換えられること

3 specifications (11 requirements), 0 failures, 0 errors
$ cat rbc_bacon.rb
require 'bacon'
require 'osx/cocoa'
include OSX

bundle = NSBundle.bundleWithPath('build/Release/Namake.framework')
bundle.load

describe Sloth do
  before do
    @obj = Sloth.alloc.init
  end

  it 'Objective-Cのオブジェクトであること' do
    @obj.should.be.kind_of Sloth
    @obj.should.be.kind_of NSObject
  end

  it 'Objective-Cのメソッドを呼べること' do
    @obj.name.should.be.nil
    # lambda { @obj.name = "faultier" }.should.not.raise
    lambda { @obj.name = "faultier".to_ns }.should.not.raise
    @obj.name.to_s.should.be.eql "faultier"
    # @obj.name.should.be.kind_of String
    @obj.name.should.be.kind_of NSString
    lambda { @obj.setName "ふぁうるてぃあ".to_ns }.should.not.raise                     
    # @obj.name.should.be.eql "ふぁうるてぃあ"                                          
    # @obj.name.should.be.eql "ふぁうるてぃあ".to_ns                                    
    # $KCODE = 'u'
    # @obj.name.should.be.eql "ふぁうるてぃあ"
  end

  it 'Objective-Cのクラスを書き換えられること' do
    lambda { eval 'class Sloth; def name; "nisefaultier"; end; end' }.should.not.raise
    obj = Sloth.alloc.init
    obj.name.should.be.eql "nisefaultier"
    @obj.name.should.be.eql "nisefaultier"
  end
end
$ bacon rbc_bacon.rb
OSX::Sloth
- Objective-Cのオブジェクトであること
- Objective-Cのメソッドを呼べること
- Objective-Cのクラスを書き換えられること

3 specifications (12 requirements), 0 failures, 0 errors

上がMacRuby版、下がRubyCocoa版。ぱっと見は思ったより違いはない。まぁgetter/setterしかないクラスなのでアレだけども。RubyCocoaの方はCocoaのクラスはOSXって名前空間の下にあるけど、MacRubyは全てのRubyのクラスがCocoaのクラスのサブクラスって位置付けなので、Cocoaのクラスに特別な名前空間は無い。もともとCocoaのクラスはNSなんとかとかUIなんとかとかそういうprefixが付いてるので、Rubyの組み込みのクラスとバッティングすることはないのでそれでも困らないしね。まぁ、それはRubyCocoaの方でもinclude OSXしてやれば見ためそんなに変わらないので、そこは然程違和感はないね。

長いので分割して書くことにしよう。次に続く。

以前、「Objective-CのテストやるのにOCUnit使うよりも、RSpec使えたら素敵なんじゃね」と思って、色々試行錯誤してみたことがある。

とりあえずそれっぽいことができるところまでは行ったものの、そのあと特に使う機会もなくそのまま放置していたのだけど、今度は本格的にObjective-Cのフレームワークを作らなきゃならなくなりそうなのでまたRubyCocoaなどいじる。そこでふと思い付いた。せっかくMacRubyがあるんだから、今度はMacRubyで同じことをやってみたらどうか、と。RubyとObjCのブリッジであるRubyCocoaよりも、VMのレベルでObjCと融合してるMacRubyのが色々融通効くんじゃね、もしかしたら結構深くObjCのオブジェクトいじりたくなるかもしんないし。

あれ、読み込めない

というわけでまずはObjCのFrameworkを作ってRubyのスクリプトから読み込ませてみる。NSBundle#loadを呼びだすのでもいいけどMacRubyだったらframeworkって構文があるな、これフルパス指定してあげれば任意のFramework読み込めるみたい。

こういうクラスを含むNamake.frameworkというのを作ったとして、

#import 

@interface Sloth : NSObject {
    NSString *name;
}

- (NSString *)name;
- (void)setName:(NSString *)aName;

@end

それを使うRubyスクリプトはこんなふうに書く。

#!/usr/local/bin/macruby

framework 'build/Release/Namake.framework'
# こっちでもOK
#bundle = NSBundle.bundleWithPath('build/Release/Namake.framework')
#bundle.load
# /System/Library/Frameworks以下にあるフレームワークとかだったら、
# フレームワーク名だけで行ける
#framework 'Cocoa'

sloth = Sloth.new # Sloth.alloc.init
sloth.name = 'faultier'
p sloth
puts sloth.name

framework 'hoge'のお陰ですっきりして見える。紛うことなきRubyのコードだ。早速実行。

$ macruby mrb_test.rb
2009-04-18 22:19:09.756 macruby[773:10b] Error loading /Users/faultier/Projects/Namake/build/Debug/Namake.framework/Namake:  dlopen(/Users/faultier/Projects/Namake/build/Debug/Namake.framework/Namake, 265): no suitable image found.  Did find:
	/Users/faultier/Projects/Namake/build/Debug/Namake.framework/Namake: mach-o, but wrong architecture
./mrb_test.rb:3:in `framework': framework at path `build/Debug/Namake.framework' cannot be loaded: Error Domain=NSCocoaErrorDomain Code=3585 UserInfo=0x800621ee0 "The bundle “Namake” could not be loaded because it does not contain a version for the current architecture." (dlopen_preflight(/Users/faultier/Projects/Namake/build/Debug/Namake.framework/Namake): no suitable image found.  Did find: (RuntimeError)
	/Users/faultier/Projects/Namake/build/Debug/Namake.framework/Namake: mach-o, but wrong architecture)
	from ./mrb_test.rb:3:in `<main>'

おおぅ…。何、何言ってるの。architectureがおかしいってなんだよ。だってこれRubyCocoaや素のObjective-Cからはちゃんと読めるよ…?

で、調べてみたところ、MacRubyはデフォルトでは64-bitのバイナリを読むらしい。なるほど。なので、↑のを実行するときに、archで-i386を指定してやると一応実行できる。

$ arch -i386 macruby mrb_test.rb
#<Sloth:0x12d06d0>
faultier

もうちょいちゃんとした解決策としては、ターゲットにx86_64が含まれるようにビルドすればいい。あと、MacRubyはObjCのGCを使ってるので、GCサポートを有効にしてビルドしてないと勿論読み込めないので注意。

macgemがアレげ

さてフレームワークはちゃんと読み込めた。そしたら今度はRSpecだ。えーと、macruby系のコマンドはmacってprefixが付くのでgemはmacgemか。

$ sudo macgem install rspec
Password:
ERROR:  Error installing rspec:
	rspec requires  (, runtime)

えー…。ここでも詰まるか。一応調べては見たけど、なんかバグレポートにも上がってたし、これはまだ未解決なのかな。そうでなくてもRSpecは案外依存関係が多くて、macrubyみたいな環境で使おうと思うとあっちのパスを直してこっちのライブラリを直して、みたいなことになりそうなのでちょっと心が折れかける。

で、しかたないなーtest/unit使うのかーとかぼやいてたら「そんなあなたにBacon」と言われたので、Baconを入れてみることにした。RSpecっぽい構文だけど最低限の要素に絞ってシンプルにしたものらしい。Ramazeで使われているようだ。mockとかが入ってないのは大分物足りないけども、同じ機能でも見た目の差は考え方の差になる程度には大きい。背に腹はかえられない。

$ sudo macgem install bacon
Password:
Successfully installed bacon-1.1.0
1 gem installed
Installing ri documentation for bacon-1.1.0...
Updating class cache with 17 classes...
Installing RDoc documentation for bacon-1.1.0...

おお、サクっと入った。素敵。ところでbaconコマンドが見あたらないんだけどどこに行ったんだ、と思ってたら、

/Library/Frameworks/MacRuby.framework/Versions/0.4/usr/lib/ruby/Gems/1.9.1/gems/bacon-1.1.0/bin/bacon

こんなところにいらっしゃいました。確かにデフォルトのRubyから入れたコマンドを上書きされても困るし、これはこれでいい。他のコマンドにならってmacbaconって名前でPATHの通ってるとこにシンボリックリンクを張ってやることにする

$ macbacon --version
/opt/local/lib/ruby/vendor_ruby/1.8/rubygems/custom_require.rb:31:in `gem_original_require': no such file to load -- bacon (LoadError)
	from /opt/local/lib/ruby/vendor_ruby/1.8/rubygems/custom_require.rb:31:in `require'
	from /usr/local/bin/macbacon:88
	from /opt/local/lib/ruby/1.8/optparse.rb:1262:in `call'
	from /opt/local/lib/ruby/1.8/optparse.rb:1262:in `parse_in_order'
	from /opt/local/lib/ruby/1.8/optparse.rb:1249:in `catch'
	from /opt/local/lib/ruby/1.8/optparse.rb:1249:in `parse_in_order'
	from /opt/local/lib/ruby/1.8/optparse.rb:1243:in `order!'
	from /opt/local/lib/ruby/1.8/optparse.rb:1334:in `permute!'
	from /opt/local/lib/ruby/1.8/optparse.rb:1355:in `parse!'
	from /usr/local/bin/macbacon:93
	from /opt/local/lib/ruby/1.8/optparse.rb:787:in `initialize'
	from /usr/local/bin/macbacon:11:in `new'
	from /usr/local/bin/macbacon:11

ありゃ。ああそっか、多分shebangとかLOAD_PATHとか書き換えなきゃなんだな。

と、思ったんだけど、実はそんなことしなくても良かったことが判明。この次に書いてあることは実行せず、追記のところまで読んで下さい。

#!/usr/bin/env ruby
# -*- ruby -*-

require 'optparse'
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '../lib/')
module Bacon; end 

ビンゴ。ってことでこれを、

#!/Library/Frameworks/MacRuby.framework/Versions/0.4/usr/bin/macruby                    
# vim: filetype=ruby fileencoding=utf-8                                                 

require 'optparse'
$LOAD_PATH.unshift '/Library/Frameworks/MacRuby.framework/Versions/0.4/usr/lib/ruby/Gems/1.9.1/gems/bacon-1.1.0/lib/'
module Bacon; end

こんな風に書き換える。マジックコメントをvimにしたのは単に俺がvim使いだから。MacRubyは1.9.1相当なので、encodingはちゃんと指定しておいた方がいいな。パスがベタ書きなのは気持ち悪いと言えば悪いけど、まぁ移動することも無いので別にいいか。

$ macbacon --version
bacon 1.1

できたできた。ふー。このくらいのとこまではmacgemでやってくれるようになったら嬉しいんだけど、なんつーか面倒臭そうではあるね。とにかくこれでMacRuby+Bacon環境が出来たので少し休憩。

追記

上の作業をする前にデフォルトのRubyの方でもBaconを入れてたんだけど、今度はそっちの挙動が何か変だ、と思ったら…

$ cat /usr/bin/bacon
#!/Library/Frameworks/MacRuby.framework/Versions/0.4/usr/bin/macruby
#
# This file was generated by RubyGems.
#
# The application 'bacon' is installed as part of a gem, and
# this file is here to facilitate running it.
#

require 'rubygems'

version = ">= 0"

if ARGV.first =~ /^_(.*)_$/ and Gem::Version.correct? $1 then
  version = $1
  ARGV.shift
end

gem 'bacon', version
load 'bacon'

おい。待てコラ。さっき「デフォルトのRubyから入れたコマンドを上書きされても困るし」って書いたけど、しっかり上書きされてました。あうー。まぁでもこれを使えば上に書いたような気持ち悪い書き換えはしないで済むみたいだ。ので、

  1. さっき書き換えたところは全部元に戻し
  2. /usr/bin/baconを/usr/local/bin/macbaconにrenameし
  3. 再度/usr/bin/gemの方でBaconをインストール

した。と言うわけで、デフォルトのRuby(/usr/bin/ruby)を使ってる人はmacgemの実行は気を付けた方がいいみたい。まぁ、macgem自体は今殆どのgemがインストール時にコケるし、デフォルトのRubyはバージョンが若干古いので基本的には自分でbuildして/usr/local/binあたりかMacPortsから/opt/local/binあたりに入れてると思うので、そんなに困ることはないかもしれないけど、念のため。

おまけ

Baconってなんだろう、ってググったら当然ベーコンに関する情報がヒットするんだけど、Google先生がなんか妙なものを拾ってきた。

bacon

右端の変態、あんた何やってんだ。

最近Objective-Cばかり書いてるナマケモノです、どうもこんばんは。Cocoaに始めて触れたときはこれで仕事になるとか思ってもなかったけど世の中わからないものだなー。

ダメな子なので可愛かったりするのだけども

iPhoneアプリの開発もそろそろ佳境に入ってきてるのだけど、つくづく悩まされてるのがWebViewの不安定さ。実はなかなか使い勝手は良くて、Safariもどきとかならサクっと作れちゃうし、Webサービスと連携するアプリにしても困ったらサイトに行ってログインしてもらえばいいや、とか思ってたのだけど、いかんせん落ちまくるのがいただけない。Safariが駄目なんじゃないんだ。WebViewがそもそも駄目っぽい。

なんてことを痛感したおかげで、正直今までiPhoneやWindows mobile端末向けのWebUIってあんまり興味なかったんだけど(正直PC向けのページ見りゃいいじゃん、と思ってた)、ちょっとアレら向けに徹底的に最適化したUIを作らなきゃだめだなと思ってるところ。あと、API。WebViewで楽できないとわかった以上、ついつい後回しにしがちなAPIを口実付けて整理しなおして、ネイティブアプリはネイティブアプリで完結できるようにしないとだなぁ。とりあえずアプリのリリースができて一段落したら、モバイルの高機能端末向けのUIとAPIについてちゃんと考えたい。ブログとかクリップとか、ね。

あと全然関係ないけど

ARToolKitの本買った。あんま読めてないけど。最近浮気しすぎだなー。まぁ、今のうちにインプットできるだけインプットしとかないと。

この間のiPhoto Exporter作りの続き。

先週の時点でX-WSSEヘッダ付けてAtomAPIのエンドポイントGETしてレスポンスからNSXMLDocumentを作る、ってとこまではやったのだけど、どうもWSSE認証に失敗してるらしくLogin Invalidとしか返ってこなかった。Rubyでだったらサクっと書けたんだけど、CocoaでSHA1とかBase64とか結構面倒臭い。むぅ。

で、その辺で挫けかけたので一週間ほど放置してたんだけど、ググったらopensslのライブラリでやればいいじゃんってことがわかった。こんな感じでNSStringとNSDataにメソッドを生やしてみた。あとはXcodeのビルドオプションで「その他のリンカフラグ」に「-lcrypto」と追加してコンパイルしてやるだけ。

// Crypto.h
#import <openssl/md5.h>
#import <openssl/sha.h>
#import <openssl/evp.h>
#import <openssl/bio.h>
#import <openssl/buffer.h>

#import <Foundation/Foundation.h>

@interface NSString (Crypto)

- (NSData *)dataHashedWithSHA1;
- (NSString *)stringHexHashedWithSHA1;
- (NSString *)stringEncodedWithBase64;

@end

@interface NSData (Crypto)

+ (NSData *)dataWithBase64String:(NSString *)pstrBase64;
- (NSData *)dataHashedWithSHA1;
- (NSString *)stringHexHashedWithSHA1;
- (NSString *)stringEncodedWithBase64;

@end
// Crypto.m
#import "Crypto.h"

@implementation NSString (Crypto)

- (NSData *)dataHashedWithSHA1
{
	return [[self dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:NO] dataHashedWithSHA1];
}

- (NSString *)stringHexHashedWithSHA1
{
	return [[self dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:NO] stringHexHashedWithSHA1];
}

- (NSString *)stringEncodedWithBase64
{
	return [[self dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:NO] stringEncodedWithBase64];
}

@end

@implementation NSData (Crypto)

- (NSString *)stringHexHashedWithSHA1
{
	unsigned char digest[20];
	char finaldigest[40];
	int i;
	
	SHA1([self bytes],[self length],digest);
	for(i=0;i<20;i++) sprintf(finaldigest+i*2,"%02x",digest[i]);
	
	return [NSString stringWithCString:finaldigest length:40];
}

- (NSData *)dataHashedWithSHA1
{
	unsigned char digest[20];
	
	SHA1([self bytes],[self length],digest);
	
	return [NSData dataWithBytes:&digest length:20];
}

+ (NSData *)dataWithBase64String:(NSString *)aString
{
	BIO *b64, *bmem;
	int length = [aString lengthOfBytesUsingEncoding:NSASCIIStringEncoding];

	char *buffer = (char *)malloc(length);
	memset(buffer, 0, length);
	b64 = BIO_new(BIO_f_base64());
	bmem = BIO_new_mem_buf((char *)[aString cStringUsingEncoding:NSASCIIStringEncoding], length);
	bmem = BIO_push(b64, bmem);

	BIO_read(bmem, buffer, length);
	BIO_free_all(bmem);
	
	return [NSData dataWithBytes:buffer length:length];
}

- (NSString *)stringEncodedWithBase64
{
	BIO *bmem, *b64;
	BUF_MEM *bptr;
	
	b64 = BIO_new(BIO_f_base64());
	bmem = BIO_new(BIO_s_mem());
	b64 = BIO_push(b64, bmem);
	BIO_write(b64, [self bytes], [self length]);
	BIO_flush(b64);
	BIO_get_mem_ptr(b64, &bptr);
		
	NSString *buff = [NSString stringWithCString:bptr->data length:bptr->length-1];
	BIO_free_all(b64);
	
	return buff;
}

@end

Objective-Cなので見慣れない[]とか@とか入ってるけど、中身をよくよく見るとどう見てもCです。本当に(ry。Cのコードやライブラリをそのまま転用できるのは素敵だなぁ。Objective-Cで便利なフレームワークとか見つけられなくてもCで探せば大概ある。こりゃいいや。

APIを叩く準備が出来たので、早速iPhoto Exporterに取り掛かってもいいんだけど、どっちかというとフレームワーク作りに興味が向き始めた。livedoorの各種サービスのAPIをCocoaから簡単に使えるようにするフレームワークでも作ろうかなぁ。うふふ。

ここのところ久しくObjective-Cをいじってなかったので、急にCocoaと戯れたい衝動に駆られた。そうだ、iPhotoライブラリのExporterを作ろう、と思い付きでいろいろ調べてみた。iPhotoとかろくに触ったことないけど、Objective-C+Cocoa環境だと、動的にロードしたバンドルでランタイムのクラスを書き換えるとかもできてしまうので、既存のアプリに新しい機能を付けるとかが比較的簡単にできる。その気になれば(Cocoaアプリでさえあれば)どんなものにでもプラグインを作れるので、まぁなんとかなるだろと楽観的発想をしてみる。

で、見付けた参考になりそうな情報はitok’s Labさんとこの一連の記事とそのリンク先。iPhotoには組み込みで写真のExporterを追加できる機能があるらしく、思ったより簡単にできそう。大雑把に言うと、「Exporterのプロトコルに準拠したバンドルを作って、拡張子を.iPhotoExporterにして、/Applications/iPhoto.app/Contents/PlugIns以下につっこむ」などすればいいらしい。

元記事は詳細に作り方が書いてある上サンプルコードもあって素敵なのだけど、上で挙げた記事の通り、若干内容が古い。Leopard+iPhoto'08環境で使えるプラグインを作りたいので、自力でclass-dumpを使ってクラスを調べあげてExportPluginProtocolのヘッダファイルを作るところから挑戦することにする。

が。

これがなかなか厄介で、まずここで挫折しかけた。ヘッダファイルを作りはじめてはみたものの、何かサンプルコードの時点から大分増えたり減ったりしてて何が必要なのかわからないし、クラスやプロトコロルの定義はともかくCの構造体の定義とかまではちゃんとはdumpできない。途方に暮れかけながらExportPluginProtocolとかExportImageProtocolとかでググってたら、GoogleCodeにiPhotoPluginのプロジェクトがあるのを見つけた。ひゃー。欲しかったヘッダファイル全部ある。現在の環境で動くサンプルが作れる。これは素敵。

お陰で「書き出し」メニューの中にカスタムビューを追加するだけのプラグインを作れるところまでは出来たので、あとは写真共有サイトのAPIを叩くロジックを書くだけ。これは単にHTTPでGETとかPOSTとかするだけなのでそんなに難しくない。ほほぅ。やっぱりCocoaは面白いな。

CoreDataにつっこんであるデータを表示するViewを作ろうといろいろ試行錯誤してて、せっかくCoreDataでデータを管理してるのにいちいちそれを操作するコントローラを作るってのもなんだか泥臭いなと思ってたんだけど、調べたらちゃんとそれ専用のViewがあった。それがNSCollectionView。

使い方は簡単

  1. InterfaceBuilder上でNSCollectionViewを配置すると、一緒にNSCollectionViewltemとNSViewが作られる。
  2. NSCollectionViewのBindingを設定する。
    1. 「Content」を(使いたいCoreData entityを管理している)NSArrayControllerのarrangedObjectにBinding。「Model Key Path」は空のまま。
    2. 「Selection Indexes」をNSArrayControllerのselectionIndexesにBinding。「Model Key Path」は空のまま。
  3. 「Collection View Item」は既にOutletが設定されているので、特に変更することはない。
  4. Viewをいじる。
    1. 自動で作られた「View」はNSViewのインスタンスなので、カスタムビューを使う場合はこれのクラスを変更しておく。
    2. 「View」に表示したいものをぽんぽん置いてく。
    3. 配置した各項目のBindingを設定する。
      1. 「Collection View Item」にBindingする。「Model Key Path」は「representedObject.属性名」になる。

こんな感じで設定をすると、「CoreDataからデータをとってくる」→「そのデータをビューにはめこむ」→「スクロールビューに表示」までやってくれる。簡単簡単。ここまででコーディング一切なし。素敵だぜInterface Builder。残念ながらこれを使うとLeopardでしか動作しなくなっちゃうのだけど、これはかなり強力!

CoreDataとCocoaBinding

情報が少ないもんで何か難しいものと思われて敬遠されがちなんだけど、慣れてしまえばこれほど楽なものはない。データの扱いだとかUIの構築なんていう面倒なところを殆ど自作しなくてよくて、しかもそれが標準で使える機構だけでできるってのが素敵だよなー。

追記

参考にした記事を書いた人もTwitterクライアントを作ろうとしてCoreDataを弄ってらっしゃるのね。

正月休み中に形にするぞと息巻いてRubyCocoaのtwitterクライアントを作ってるのだけども、主にtwitterと関係ないところでハマってる*1。モデルの管理とIBとの連携が楽になると思って当初からCoreDataを使ってるんだけど、結構煩雑というかやぼったいというか。オブジェクトの追加は楽にできるんだけどオブジェクトの取得が面倒臭いんだよなぁ。

毎回NSFetchRequestを作ってNSEntityDescriptionを取得して…という処理を書くのがダルかったので、AR風のラッパを作ってみた。ベースのクラスを継承してモデル名と同名のクラスを作ると、そいつの操作が裏でCoreDataの操作になってるようなやつ。findとかdeleteとか、あとアクセサメソッドは定義しないでも勝手に拾ってよきにはからったりするとか、その辺は実装した。うーん、でも、このアプローチに自信が持てない。なんか車輪の再発明な気がしてならないし、多段ラッパになってて処理の効率悪そう。まぁ、勉強目的だから再発明上等だしtwitterクライアント程度でそんなにパフォーマンス気にすることない気もするけども…。RubyCocoaのdefine_wrapperとかARの拡張とかその辺りを補完するようなフレームワークを作れればいいんだけど。

こないだの補足

「CoreDataで管理してるデータは、正常終了時に自動保存されるのだけど、どこでどう処理されてるのかわからない」

テンプレートで自動生成されたAppDelegate.rbの中読んだらちゃんと書いてあった。applicationShouldTerminateメソッドの中で、managedObjectContextにsaveメッセージを送ってるのがそれですね。そこ以外ではデータの保存処理をしてないので、長時間つけっぱなしにしといて異常終了したりするとその間のログが全部ふっとぶのでどこかで定期的にsaveするようにしとかないとだ。まぁ、そもそも保存されなくても大して困らないけど。使えるものは使っておけと。

IBのチュートリアル

書こうと思ってたけど、むしろ俺自身が嵌ってるので書けそうにない。カスタムビューの作り方とか画像の扱いとかわかんないことだらけ。Objective-CやRubyCocoaはわからなくても、昔の資料から類推したりソース読んだりすればなんとでもなるんだけど、InterfaceBuilderの使い方は独力ではキツい。バージョン違うと資料が役に立たないし。今他のアプリのソース見て参考にしてるけど、誰か詳しい人がいたら助けて欲しいっす。

キーバリューコーディング

主にMVCのうちのCとVのバインディングに活躍してるキーバリューコーディングだけど、RubyCocoaと結構相性がいい。ハッシュかなんかをeachで回して全部ObjCのオブジェクトにobjc_send('setValue', value, 'forKey', key)とかしてやるだけでいいので、ラッパを作るのがすごい楽だった*2。素敵素敵。

いいから頑張れ俺

とかなんとか文章でいくら書いてもしょうがないので、作ったもの晒してみんなにツッコミ入れてもらった方が早い。さっさと投下しよう。

*1:二重の意味で

*2:具体的に言うとARでいうところのcreateあたりがまさにそう。あと、method_missingを捕捉してdelegate_object.objc_send('setValue', *args, 'forKey', name.to_s)とかやると後はよきにはからってくれるので、その辺も大して考えずに書けた。

RubyCocoaを極めるプロジェクト中でtwitterクライアントを作るという話が出てたので、正月休み中にみんなでいじって遊べる程度に動くものを作ってしまおうと目下がりがり書いてる最中なのですが、なかなか楽しいね、RubyCocoa。

e84fa15a.png

CoreDataが素敵

折角なのでCoreDataを活用してやろうと、HMDT本やリファレンスと格闘してました。CoreDataってなんぞ?ってレベルからのスタートだったので結構苦労したのだけども、CoreDataってのは要はCocoaアプリ中で簡易DBとO/Rマッパーを使えるようになるフレームワークなんだね。ActiveRecord+pstoreやsqlite、みたいなもんだと言っていいのかな。

で、これが非常に便利。Xcodeのモデリングツールでデータの定義をしておけば、勝手にDBとラッパオブジェクトを作ってくれるのでモデルクラスのとこのコーディングが格段に減る。モデル同士の関連もモデリングツールで作れるし、InterfaceBuilderとの連携でコントローラ部分も自分で作る必要がないので、簡単簡単。RubyCocoaから使う場合はCocoaから使う場合と若干構成が違うので最初躊躇ったけども。自分へのメモの意味でも、Xcode3でRubyCoca-CoreDataアプリを作る際のIBチュートリアル記事をあとで書いとこう。

ちなみに、twitterのデータの読み書きはこんな感じ

まずはデータの新規作成。

# NSManagedObjectのオブジェクトを新しく作ってDBに突っ込む
user = OSX::NSEntityDescription.objc_send("insertNewObjectForEntityForName",
            "User",
            "inManagedObjectContext",
            @managedObjectContext)
user.userId = '11111111'
user.screenName = 'rucotan'                                               
user.name = 'るこたん'

これでるこたんの情報がCoreData経由で使えるようになる。実はこの時点では「るこたんの情報が管理対象になった」だけで、自分でデータの保存処理をするか、CoreDataApplicationテンプレートからプロジェクト作ってるとアプリの正常終了時に自動で保存処理が呼びだされる*1か、そのタイミングで初めてファイルやDBに書き込まれるんだけど、「管理対象になった」時点でアプリ全体で使えるデータになるのでここではあんまり気にしない。

ちなみに、以前はNSManagedObjectのプロパティをいじるのに毎回valueForKeyやsetValue_forKeyを呼び出してたみたいなんだけど、RubyCocoaの改良でラッパオブジェクトを作るようにして、NSManagedObjectをまるでRubyのオブジェクトのように扱えるようになったらしい。軸がぶれてない、素敵*2。この辺がARっぽいとこ。

続いて既にあるデータの取得。

# NSFetchRequestを用意。RDBMSにSELECTクエリを発行するみたいな感じと言えばいいかな。
request = OSX::NSFetchRequest.alloc.init

# モデルを設定。SELECT文のFROMを指定するのに相当すると理解。
entity = OSX::NSEntityDescription.entityForName_inManagedObjectContext('User', @managedObjectContext)
request.setEntity(entity)

# 述語を設定。SELECT文のWHERE句相当、かな。
predicate = OSX::NSPredicate.predicateWithFormat('screenName == %@', 'rucotan')
request.setPredicate(predicate)

error = OSX::OCObject.new
users = @managedObjectContext.executeFetchRequest_error(request, error)

これで「screenNameがrucotanであるUserのリスト」を取得できる。NSFetchRequestとかNSPredicateってのが要はDBハンドラにプリペアードクエリを渡すのと手順は大して変わらないので、日頃DBを使うアプリに慣れてると違和感ないですね。注意点としては、executeFetchRequestの返り値はOSX::NSCFArrayなので、users.empty?とかusers.firstとかやると「そんなメッセージ、わたしが受け付けるとでも思ってるの!?この愚民は言葉もまともに話(ry」と怒られてしまう。Rubyで使うときは最初にto_aしておいた方が楽かも。

これだけだとRubyのARやPerlのClass::DBIに慣れきってる身としてはかったるくてしょうがないんだけど、Xcode上のモデリングツールで受信要求テンプレートというのを作ることができて、それを使うとNSFetchRequestの生成が大分楽になる。

variables = OSX::NSMutableDictionary.dictionary # 述語に渡すパラメータ
variables['screenName'] = 'rucotan'
request = @managedObjectModel.fetchRequestFromTemplateWithName_substitutionVariables('template_name', variables)
error = OSX::OCObject.new
users = @managedObjectContext.executeFetchRequest_error(request, error)

ちなみに、Xcode上で受信要求テンプレートを作る際には述語ビルダというのが使えて、複雑なリクエストでもiTunesやMailのスマートフォルダを作る感覚で述語を生成できてしまうので非常に簡単。だと思われる。いや、まだ使ったことないの。

キーバリューコーディング

に、ついても書こうと思ったんだけど、そろそろ長くなってきたのでまたの機会に。

仮名のRuchettaについて

Rubyの「る」とCocoaの「こ」がつくものがいいなあと考えて真っ先に浮んだのが「ルッコラ」だったんだけど、rucolaって開発環境があるのね、RubyCocoaの。ので、同じくルッコラを意味するイタリア語の「Ruchetta」にしてみた(安易)。ちゃんとr(uby)とc(ocoa)とt(witter)が入ったし、「ついった」と「るけった」で語感的にも悪くないかなと思ったんだけどどうだろう。みんなでいじれるようになったら名前も新しく考えてもらってもいいかな。

先を越された

これを書いてる最中に夏ライオン(OSXのtwitterクライアント)αリリースのお知らせが…。UIかっこいいなぁ。参考にさせてもらおう。

追記

夏ライオン見易くてなかなか良かった。自分でtwitterクライアント作ってる最中なのに当面夏ライオンユーザになりそうな予感。

*1:未確認。アプリ終了時に保存されるのは確かなんだけど、それがどこから何を呼び出してるのか実はわかってない。あとでちゃんと見る。

*2:そういえば絶望先生2期って今クールでしたっけ?

大分放置してましたけど思い出したかのように続き。いや実際忘れてたのだけども。

とりあえず以前書いたものたち。

spceコマンドのラッパを書く

前回は自分のリソース内の*_spec.rbを読み込んで自分のmainバンドルのクラスをテストする、というRubyCocoaアプリを作ったのだけど、specを走らせるためだけに毎回そんなものを作るのは面倒くさすぎる。実際やってることはSpec::Runner::CommandLineにspecファイルを渡してるだけなので、ローダブルバンドルを引き数に取ってspecを走らせる、というspecコマンドのラッパを用意する。

#!/usr/bin/env ruby

require 'rubygems'
require 'spec'
require 'osx/cocoa'

spec_opts   = ARGV.reject {|opt| opt =~ /\.bundle\z/}
bundle_path = ARGV.find {|opt| opt =~ /\.bundle\z/}

if bundle_path && bundle = OSX::NSBundle.bundleWithPath(File.expand_path(bundle_path))
  bundle.load
  bundle
  Dir.foreach(bundle.resourcePath) {|file| spec_opts.push(File.join(bundle.resourcePath, file)) if file =~ /.+_spec.rb\z/}
end

::Spec::Runner::CommandLine.run(spec_opts, STDERR, STDOUT, true, true)

見ての通り、バンドルをロードして*_spec.rbを抜き出し、それらのパスをコマンドライン引数に付け加えてSpecのランナーに渡すだけ。バンドルは最初の一個以外は全部無視する設定になってるけど、俺が想定してる使用方法で扱うバンドルは一個だけなのでとりあえずこれで十分。必要であれば全部受けつけてもいいけど。

拡張子が.bundleのファイル以外の引数は全てspecのランナーに渡るので、適当な名前付けてパスの通ったとこに配置しておくとspecコマンドの代わりに使える。-fとか-cとか-sとかも問題なく使えたりする。

使用方法

で、こいつを実際にどう使うかというと、例えばXcodeを使っているならこんな感じ。

  1. 新規ターゲット追加でCocoa Loadable Bundleのターゲットを追加
  2. そのターゲットに次のスクリプトを実行するビルドフェーズを追加:ruby <上のスクリプトのパス> [<specオプション>] $BUILD_DIR/$CONFIGURATION/$EXECUTABLE_NAME.bundle
  3. そのターゲットにObjective-Cのクラスとspecファイルを入れる(その際specファイルは必ず末尾に「_spec.rb」を付ける必要がある)

これで、このターゲットをビルドすると自動的にspecを実行してくれるようになるという噂。いや、ちゃんとなりました。なりましたよ。スペックが全て通ると何事もなくビルド完了するけど、スペックが通らないとビルドが失敗しビルド結果画面にspecコマンドの結果が出る。例えばビルドターゲットの依存関係でアプリターゲットがspecターゲットに依存するようにしておくと、specが全て通らない限りアプリがビルドできなくなるのでいい感じ。

注意点いくつか

まず当然のことながらRubyCocoaがインストールされてないと動かない。Leopardだと最初から/System/Library/Frameworks/RubyCocoa.frameworkがあるんだけど、それはもともと入ってるruby(/usr/bin/ruby)からしか使えない。普段から/usr/bin/rubyを使ってる人は別に問題ないけど、それ以外*1を使ってる場合、そのRuby用に再度RubyCocoaを入れるか、/usr/bin/rubyの方でRSpecを入れるかしておく必要がある。

あと、RSpecの最新版だとspecの実行結果をsuccessとfailedの他にpendingにすることができるけど、pendingはsuccessと同じ扱いになってビルド成功してしまう。ので、ご注意を。まぁ、実行結果はビルドログに吐きだされてるのでそれを見ることはできるけども。

その他

とりあえず結果表示をどうにかしたい。Growlに通知投げたりしたら面白いかなぁ。

*1:/usr/local/bin/rubyとか/opt/local/bin/rubyとか

↑このページのトップヘ