複数の非同期処理を前の処理結果を受けとりつつ数珠つなぎに直列に書きたかった時に参考にしたコード
以前
@xcatsan さんのこちらの記事をさっきみてて思い出したので備忘録として。
以前、非同期で実行される処理の塊を直列に順序を決めて実行したいというシチュエーションがありました。そのときにぱっと浮かんだのは昨日の Cocoa 勉強会関西で@yashigani が発表した内容 にも登場した NSOperation
と NSOperationQueue
でした。「非同期で実行される処理の塊を直列に順序を決めて実行」だけならそれでも良かったんですが、その処理の塊の結果を次の処理の塊に受け渡す必要がありました。さらにその処理結果によって次以降に実行する処理の塊も振り分けるとか…。基本的に非同期処理のコールバックは Blocks を使うことは僕の中できまっていたので Blocks の入れ子とか地獄なのは分かっていたのでどうにかしたかった。
やりたかったこと要求されてたこと
- UI から1つのタスク(大)が依頼される
- そのタスク(大)は一個のメソッドとして実装され、 completionBlock でその結果を UI に返される
- タスク(大)は実は複数のタスク(中)を数珠つなぎにしたもの
- たまにタスク(大)はタスク(中)の実行状況に合わせてタスク(中)を追加したりする
- 実はタスク(中)もタスク(小)の集合だったりする
参考にしたライブラリ
前振りが無駄に長かったですがそのときに参考にしたライブラリを列挙します。
STDeferred
今回の要求を聞いて最初に思い浮かんだのがこのライブラリ。処理結果を次の結果に受け渡しています。 1つのメソッドに Blocks を数珠つなぎになる感じだったのでそのまま使うことはなかったですが、受け渡しかたの参考にさせていただきました。
Sequencer
冒頭にあげた @xcatsan さんのこちらの記事でも紹介されています。参照先に書かれているとおりシンプルだったので参考になりました。STDeferred よりも書き方がシンプルに見えて好みでした。
AAMCommandKit
Serial 実行する Command とか参考にさせてもらいました。
結局こんな実装に
コードは諸事情で出せないのですが、うろ覚えの実装はこんな感じだったです。
NSOperation
とNSOperationQueue
っぽいものを自作- オレオレ Operation クラスには前の処理結果を取り出すプロパティと自分の処理結果を入れるプロパティを設けた
- オレオレ Queue は突っ込まれた Operation を実行し、キューイング時に、実行済みの Operation の処理結果を取り出して次の Operation の previousResult 的なプロパティに突っ込んでから次の Operation を実行
- オレオレ Queue には Delegate が実装してあり Operation をキューイングする時に前の Operation と次の Operation を垣間見れてフックしたりもできる
- Operation 内部では execute されたときに previousResult を見つつその内容によって実行内容を調整
- 例えば前の処理がエラーだったら、そのエラーを自身の結果として詰め込むとか。つまり、1, 2, 3, 4 と処理が並べてあっても、もし 2 でこけてたらそのまま 2 がコケたよという結果をそのまま伝搬することで最終的に結果をコールバックされた呼び出し元(タスク(大)の呼び出し元)では誰がコケたのかわかる。
- Queue には全 Operation の実行が完了したことを伝える Delegate を実装
懺悔
参考にしたライブラリを複雑そうだ思ったりもした割に、自分の実装の方がはるかに複雑です。ごめんなさい。
継承とかも入ってたので複雑度マックスです。ごめんなさい。
全体像の把握がツライです。ごめんなさい。
でも、そのプロジェクト内では、新規タスクの追加や仕様変更にはそこそこ強かったのでこれはこれでよかったかなと思ってます。
次回似たような要求があったときにはもっと整理した形で実装したいと思っています。
最近よくやってる Blocks の使い方
Blocks 怖いような、便利なような。そんな物ですがみなさんはどんな感じに使っていますか?
僕は最近こんな感じのメソッドを実装して使ってます。
UINavigationController *nvc = [DetailViewController navigationBasedViewControllerWithConfigurationBlock:^(DetailViewController *viewController) { NSDate *object = _objects[indexPath.row]; viewController.detailItem = object; }]; [self.navigationController presentViewController:nvc animated:YES completion:NULL];
特に凄く便利かというとそうでもないと思うんですけど、典型的なモーダルな ViewController を表示させるコードってこんな感じだと思います。
DetailViewController *vc = [[DetailViewController alloc] init]; NSDate *object = _objects[indexPath.row]; vc.detailItem = object; UINavigationController *nvc = [[UINavigationController alloc] initWithRootViewController:vc]; [self.navigationController presentViewController:nvc animated:YES completion:NULL];
要するにこんなことをしてるんですよね。
- 表示させたい ViewController クラスを生成
- 表示させたい ViewController クラスに何か値をセット
- NavigationController を生成
- 表示
いろいろとタイプすることが多いのをちょっとだけ省略したくてこうしてます。
実装はこんな感じです
+ (UINavigationController *)navigationBasedViewControllerWithConfigurationBlock:(void(^)(id viewController))configurationBlock { UIViewController *vc = [[[self class] alloc] init]; if (configurationBlock) { configurationBlock(vc); } return [[UINavigationController alloc] initWithRootViewController:vc]; }
引数で受けた Block は同期的な感じで使うのでコピーしてないので循環参照を気にする必要もないですね。 この例では UIViewController の生成を init にしちゃってますがそのあたりは実際の実装との相談でバリエーションを持たせてもいいかもしれないです。 Block に渡す引数の型は id 型にしておけば、呼び出し側で任意のクラス型に書き換えられるので、ブロック内でキャストする必要もないです。 カテゴリメソッドとして実装するとある程度汎用的に使えます。
思いつきで進化版
もっと発展させて、さっき思いつきで実装したカテゴリメソッドはこんな感じで使います。
__weak typeof(self) weakSelf = self; UINavigationController *nvc = [DetailViewController cs_navigationBasedViewControllerWithConfigurationBlock:^(DetailViewController *viewController) { NSDate *object = _objects[indexPath.row]; viewController.detailItem = object; } wantsDismiss:^(DetailViewController *viewController) { [weakSelf.navigationController dismissViewControllerAnimated:YES completion:^{ // do something NSLog(@"%s dismissViewControllerAnimated: %@", __PRETTY_FUNCTION__, weakSelf); }]; }];
昔、師匠に教えてもらったんですけど「モーダルを閉じるのはモーダルを出した側がやる方がいい」と。Apple のサンプルでも Delegate 経由でモーダルを出した側からモーダルを閉じるようにしているのがあります。あれって実装するのが面倒くさくてついついモーダル側で自身を閉じてしまいがちでした。このカテゴリメソッドではモーダル側の例えば cancel ボタンなりを押されたときにインスタンス生成時に渡された Block を呼ぶことで呼び出し側に閉じるか否かを委譲させています。さすがに今回は引数として受けた wantsDismiss Blcok は Copy して保持しているので循環参照対策として weak な self を使っています。実装はこんな感じです。
#import "UIViewController+CSAdditions.h" #import <objc/runtime.h> static char kUIViewControllerCSAdditionsKey; @implementation UIViewController (CSAdditions) + (UINavigationController *)cs_navigationBasedViewControllerWithConfigurationBlock:(void(^)(id viewController))configurationBlock { UIViewController *vc = [[[self class] alloc] init]; if (configurationBlock) { configurationBlock(vc); } return [[UINavigationController alloc] initWithRootViewController:vc]; } + (UINavigationController *)cs_navigationBasedViewControllerWithConfigurationBlock:(void(^)(id viewController))configurationBlock wantsDismiss:(void(^)(id viewController))callbackBlock { UIViewController *vc = [[[self class] alloc] init]; if (configurationBlock) { configurationBlock(vc); } if (callbackBlock) { [vc cs_setWantsDismisCallbackBlock:callbackBlock]; } return [[UINavigationController alloc] initWithRootViewController:vc]; } - (void)cs_setWantsDismisCallbackBlock:(void(^)(id viewController))callbackBlock { objc_setAssociatedObject(self, &kUIViewControllerCSAdditionsKey, callbackBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); } - (void(^)(id viewController))cs_wantsDismisCallbackBlock; { return (void(^)(id viewController))objc_getAssociatedObject(self, &kUIViewControllerCSAdditionsKey); }
// DetailViewController 側の使用例 // DetailViewController の leftBarButtonItem に設置した Cancel ボタンを押したときに呼ばれるメソッド - (void)wantsDismis:(id)sender { /// 登録されている Block を呼び出して引数として自身をわたす [self cs_wantsDismisCallbackBlock](self); }
associatedObject を使って wantsDismisCallbackBlock を保持するようにしています。保持した wantsDismisCallbackBlock の解放をどうしようかと思いましたが、明示的に解放しなくても自分自身が解放されるときにいっしょに解放してくれるようだったので特に何もしていません。
Blocks ってやっぱり便利
とはいえ、Blocks は万能ではないので traditional な delegate パターンや target-action パターンなんかもうまく活用していきたいですね。
簡単なバージョン番号の比較方法
最近、頑張って比較してるのにバグっている見たコードをみました。簡単な比較方法を知らない人もいるのかもと思って書いてみます。
たまに求められるバージョン番号比較
何らかの理由でバージョン番号の比較をしたいときってありますよね?○○バージョン以上なら△△するとか。求められる内容によって
- Class オブジェクトが生成できるかどうか?
- respondsToSelector: で期待しているセレクターが呼べるか?
とか方法はいろいろありますが、今回は文字列での比較の話です。
文字列ベースで比較
文字列ベースでのバージョン番号の比較ってどうしてますか?
integerValue とか floatValue とかで数値化して比較してますか?最近見たコードでもそのような比較をしてました。でも、バージョン番号って 「5.1」とかなら単純に数字にしてしまって比較できますが、「5.1.1」とかだと少数でもないですよね?バラして数値化して比較することもできますがちょっと面倒ですし、バグりそうです。実際に僕が見たコードはバグってました。
NSNumericSearch
こんなときは NSString の
- (NSComparisonResult)compare:(NSString *)aString options:(NSStringCompareOptions)mask
を使うと簡単です。
compare:options: の option 引数で NSNumericSearch を指定すると数字を含んだ文字列を数字として評価して結果を返してくれます。これは Mac の Finder でのファイル名ソートにも使われています。 "file_1.txt", "file_5.txt", "file_10.txt" とかってファイルをソートするとちゃんと 1 の 後には 5, 10 と続きますよね?OSとかによっては 1 の後が 10, 5 ってソートされて残念な気分になったことがあります。
具体例 1
NSComparisonResult result = [@"4.1.1" compare:@"3" options:NSNumericSearch]; // result is "NSOrderedDescending"
上記の場合は Descending なので
"4.1.1" > "3"
ということです。
具体例 2
NSComparisonResult result = [@"5.1.1" compare:@"6.0" options:NSNumericSearch]; // result is "NSOrderedAscending"
つまり
"5.1.1" < "6.0"
具体例 3
NSComparisonResult result = [@"6.1" compare:@"6.1" options:NSNumericSearch]; // result is "NSOrderedSame"
つまり
"6.1" = "6.1"
ですね。
具体例 4
こんな変な比較も人間的な感覚にマッチした比較結果を返してくれます。
NSComparisonResult result = [@"4.0.0.1.0.0" compare:@"4.0.0.0" options:NSNumericSearch]; // result is "NSOrderedDescending"
具体例 5
僕はこんな感じのコードを UIDevice のカテゴリメソッドで生やしています。
+ (BOOL)cs_isOSVersionGreaterThanOr:(NSString *)version { NSString *currentVersion = [[self currentDevice] systemVersion]; NSComparisonResult result = [version compare:currentVersion options:NSNumericSearch]; return (result == NSOrderedAscending || result == NSOrderedSame) ? YES : NO; }
注意点
- 引数に渡す文字列が nil の場合には戻り値が不定(Reference より)
- レシーバ側が nil の場合は 0 が返るので結果が NSOrderdSame になる
- [@"6" compare:@"6.0" option:NSNumericSearch] の結果が NSOrderedAscending を返してしまう(2013/02/25 追記)
注意点が増えました 3番目の問題は @norio_nomura さんに指摘していただきました。これは地味にうざい挙動です。やらかしてしまいそうです。
そつなく
compare:options: は Foundation.framework で結構古くから実装されていますし Mac OS でも実績があるはずの枯れたものですのでバグがある可能性はあまり考えられません。注意点に上げたポイントさえ気をつければヘタに文字列を分割して数値化して比較するコードを手で書くよりも簡単で無難ですのでどんどん使いましょう。
特定のアプリが触ったファイルを監視
@norio_nomura さんに教えてもらったことを忘れないための備忘録。
sudo fs_usage -f pathname <プロセスID>
とかするとこんな感じで出力される。
20:11:32 stat64 ple.Safari/Webpage Previews/FFEE0AA49397DF9D900BD8B88BA37224.jpeg 0.000003 Safari
20:11:32 stat64 pple.Safari/Webpage Previews/FFEE0AA49397DF9D900BD8B88BA37224.png 0.000003 Safari
20:11:32 statfs64 /Users/Stewie/Library/Caches/com.apple.Safari/Webpage Previews 0.000004 Safari
20:11:32 statfs64 /Users/Stewie/Library/Caches/com.apple.Safari/Webpage Previews 0.000002 Safari
20:11:32 fsctl /Users/Stewie/Library/Caches/com.apple.Safari/Webpage Previews 0.000003 Safari
20:11:32 lstat64 /Users/Stewie/Library/Safari/HistoryIndex.sk 0.000025 Safari
20:11:32 stat64 /Users/Stewie/Library/Safari/HistoryIndex.sk 0.000004 Safari
20:11:32 open /Users/Stewie/Library/Safari/HistoryIndex.sk 0.000012 Safari
20:11:32 open orks/CoreFoundation.framework/Versions/A/Resources/tokruleLE.data 0.000022 Safari
20:11:32 stat64 /usr/lib/libmecab.1.0.0.dylib 0.000007 Safari
20:11:32 open /usr/lib/libmecab.1.0.0.dylib 0.000011 Safari
20:11:32 close 0.000005 Safari
20:11:32 stat64 /usr/share/tokenizer/ja 0.000007 Safari
20:11:32 open /Users/Stewie/.mecabrc 0.000016 Safari
NSDateFormatter で YYYY を使っちゃだめ
ダメってことはないです。ただ、私たちが通常使っている概念と違ってくるので普通は使わないよねって話です。
NSDateFormatterのYYYY利用時の注意点 - 風日記 からの引用
Y(大文字)はその週の年、つまり1月1日が週の後半(厳密には木曜日以降)だったら、その週は前年の週と見なされる。
Data Formatting Guide: Date Formatters によると Unicode のルールではそうなるらしいです。 NSDateFormatter は OS バージョンによってベースにしているルールのバージョンが違うので注意。
RubyからGrowlに通知するためのライブラリ、Meowをちょっと使ってみたので自分メモ
Route 477 - RubyからGrowlに通知するためのライブラリ、Meow (とGrowlNotifier)
こちらですてきなライブラリを知りました。*1
基本的なことはリンク先を参照して貰った方がいいとは思いますが、一応書いておきます。
インストール
Meowはライブラリ内でRubyCocoaを使ってGrowlを操作してるみたいなのでRubyCocoaが必要です。
ライブラリがRubyCocoaを使うだけなのでMeowを使ったスクリプト自体はRubyCocoaである必要はないです。
僕が使っているMacBookはRubyはデフォルトのまま使ってますし、LeopardなのでRubyCocoaもデフォルトでインストール済みです。
なので単にMeowを
gem install meow
するだけで終了。*2
使ってみた
以下のドキュメントを参照してみてください。
スタイルの設定は?
スクリプト内からの指定はできないっぽいです。
meep = Meow.new('Meow Test') meep.notify('Click Me', 'Go to Google!') do system "open -a Safari http://www.google.com/" end
上記の様なスクリプトを一回実行したあと、Growlの環境設定を開きます。
すると、
newするときに与えた文字列と同じものが登録されているのでそこから指定すればいいようです。
アイコンの設定
クラスメソッドの import_imageの引数に使いたい画像のパスを渡せば良いみたいです。
iconImage = Meow.import_image("/Applications/NatsuLion.app/Contents/Resources/NatsuLion_error.icns") meep.notify('NatsuLion', 'I am crying' ,{:icon => iconImage,:sticky => true,:priority => 2}) do system "open -a Safari http://www.google.com/" end
上の例では夏ライオンのアイコンファイルを指定しました。
pngやjpgとかの画像ファイルも大丈夫っぽいです。
しかし、画像のパスが
"~/Library/hogehoge/images/google.png"
みたいなチルダの入ってるパスだとダメっぽいです。
"/Users/ユーザ名/Library/hogehoge/images/google.png"
みたいに直しておくと大丈夫っぽいです。
ちなみに、画像ファイルのパスをサクッと知るためにAmCopyPathCMXを便利に使わせて貰ってます。
アイコンの指定はnotifyを実行する時に指定してもいいですし、newするときに
iconImage = Meow.import_image("google.png") meepNotice = Meow.new('Meow Testinggggg','Notice',iconImage)
みたいに第三引数として渡してもいいみたいです。
第二引数は通知の名前です。
通知をクリックした時の処理
ブロック内に書けば良いっぽいです。
meep.notify('Click Me', 'Go to Google!') do system "open -a Safari http://www.google.com/" end
上記例では通知をクリックすることでSafariで http://www.google.com/ を開きます。
当たり前ですが、callbackを使うと通知をクリックされるまでスクリプトの実行プロセスが終わらないです。
優先度の設定
MeowでGrowlの優先度の指定をしようとしてドキュメント上のサンプルの用に
:priority => :very_high
と書いてもダメです。:very_highは最新の2.0では定義されてないっぽいです。*5
だから、定義されている
:priority => :very_low
としてもダメでした。
結局
:priority => -2
と数字でしていしてやるとうまく優先度を指定できました。
指定できる範囲は0を標準として-2〜2までの5段階です。
複数のオプションを追加する。
一つだけ追加する場合は以下でOKです。
meep.notify('Click Me', 'Go to Google!',:sticky => true) do system "open -a Safari http://www.google.com/" end
さらに優先度の設定もしたいって時はこんな感じです。
meep.notify('Click Me', 'Go to Google!',{:sticky => true, :priority => 2}) do system "open -a Safari http://www.google.com/" end
スクショの撮影に使ったのは
GrowlのスタイルはGrowlHUDを使ってます。
優先度が文字で表示されてて個人的に気に入ってます。
今調べたら配布先が無くなってました。残念です。
スクリプトはこんな感じです。
#!/usr/bin/ruby # # Created by on 2008-08-23. # Copyright (c) 2008. All rights reserved. require "rubygems" require "meow" meep = Meow.new('Meow Test') meep.notify('Click Me', 'Test 1') meep.notify('Click Me', 'Go to Google!',:sticky => true) do system "open -a Safari http://www.google.com/" end iconImage = Meow.import_image("/Applications/NatsuLion.app/Contents/Resources/NatsuLion_error.icns") meep.notify('NatsuLion', 'I am crying' ,{:icon => iconImage,:sticky => true,:priority => 2}) do system "open -a Safari http://www.google.com/" end iconImage = Meow.import_image("/hatena_logo.png") meep.notify('Click Me', 'Do it!' ,{:icon => iconImage,:sticky => true,:priority => 2}) do system "open -a Safari http://www.google.com/" end iconImage = Meow.import_image("/google.png") meep.notify('Click Me', 'Go to Google!' ,{:icon => iconImage,:sticky => true,:priority => 1}) do system "open -a Safari http://www.google.com/" end iconImage = Meow.import_image("/tumblr_logo.png") meep.notify('Tumblr', 'Low priority' ,{:icon => iconImage,:sticky => false,:priority => -2})