問題
この前リリースしたカスタム UIAlertView は表示メソッドを呼び出した後はそのまま処理が続行される。
Cocoaの日々: [iOS] ユーザ名/パスワードの入力ができるカスタム UIAlertView ライブラリを公開
例えば下記のような場合。
- (IBAction)showAlert:(id)sender { [LKAccountPanel showWithTitle:@"Test" completion:^(BOOL result, NSString* username, NSString* password) { NSLog(@"result: %d\nusername: %@\npassword: %@", result, username, password); }]; NSLog(@"done"); }UIAlertView が表示された直後に "done" がログに出力される。つまり showWithTitle:.. メソッドはその場でブロックされない非同期処理になっている。これは UIAlertView の動作がデリゲートを使った非同期モデルだから。
でもアカウント入力用途で使う場合、これだと使い勝手が悪い。UIAlertView が表示され、ユーザがアクションを起こす(OK or Cancel)まではその場でブロックしていて欲しい。言い換えると非同期ではなく同期的に処理したい。非同期を同期にするにはどうしたらよいか?
NSRunLoop
UIAlertView のこの非同期な仕組みは NSRunLoop を使っている。UIAlertView を show で表示した直後のコードはブロックされることなく実行される。先ほどの例での "done"表示がこれにあたる。その後は NSRunLoop が UIAlertView の描画タイミングの制御やタッチイベントの制御(受付と配布)を行う。OK ボタンが押されると NSRunLoop を経由してタッチイベントが UIAlertView のインスタンスへ渡され、その後デリゲートメソッド alertView:clickedButtonAtIndex: が呼び出される。
このような現在の動作を、表示後に処理をブロックするようにして、OK ボタンが押された後に処理を再開させるにはどうしたらよいか?先程の例でいうと、UIAlertView で表示後に処理を一時停止させる。OK ボタンが押されて UIAlertView が消えた後、処理を再開させログに "done" と書きだすようにしたい。
これを実現するには UIAlertView の show メソッドを呼び出した後に、処理が終了するまでブロックすれば良い。
@property (nonatomic, assign) BOOL finished;上記プロパティを追加して while で終了待ちループを構成する。
[self.alertView show]; self.finished = NO; while (!finished) { ... }OK / キャンセル ボタンが押された時にこのプロパティを YES にしてやる。
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { : self.finished = YES; }
ただこれだと while が終了することなく回り続けてしまい、NSRunLoop へ処理が返されない。その結果、UIAlertView は表示のタイミングを得ることなく、画面には表示されない。もちろんイベントの処理もできない(ボタンが表示されないのはもちろん、イベント自体も NSRunLoop に制御が戻らない限り処理されない)。
そこで while 内で一旦 NSRunLoop へ制御を戻してやる。戻すには -[NSRunLoop runUntilDate:] を使う。このメソッドは引数で指定した日時までの間、その場で処理を一時停止し NSRunLoop へ制御を戻す。そして指定の日時がきたら処理を再開する。
NSRunLoop Class Reference
先ほどのコードにこんな感じで追加してやる。
self.finished = NO; while (!self.finished) { [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.5]]; }すると while ループ内でおおよそ 0.5秒間 NSRunLoop へ制御が戻る。この間、while内の処理は停止する。NSRunLoop へ制御が戻れば UIAlertView が描画をして、ボタンが押された時にタッチイベントを受け取ることができるようになる。その結果、UIAlertView をあたかも(処理を一旦停止するという意味で)同期的な処理として扱うことができるようになる。なお指定秒は長すぎると while ループを抜けるまでに時間がかかるので、処理の続行までの遅れが大きくなる。一方短すぎると while ループを回す回数が増えるので無駄が多い。感覚的には 1秒では長すぎ、0.1秒では(なんとなく)無駄。その間を適当にとって 0.5秒とした。
LKAccountPanel の改良
LKAccoutPanel に上記の仕組みを導入した。
lakesoft/LKAccountPanel - GitHub
非同期式の +showWithTitle:completion: に加え、同期式の +showWithTitle:username:password: を追加した。
+ (BOOL)showWithTitle:(NSString*)title username:(NSString**)username password:(NSString**)password;使い方はこんな感じ。
- (IBAction)showAlert2:(id)sender { NSString* username; NSString* password; BOOL result = [LKAccountPanel showWithTitle:@"Test2" username:&username password:&password]; NSLog(@"result: %d\nusername: %@\npassword: %@", result, username, password); }引数 username, password は結果を受け取る為にポインタのポインタを渡す。上記の場合、NSLog(@"result:.."); の処理が実行されるのは UIAlertView が閉じた後になる。
付属のサンプルではボタンを非同期式(Async)と同期式(Sync)の2つを用意して違いが分かるようにしてみた。
サンプルアプリケーションを実行し、非同期式ボタン(Async)をタッチすると +showWithTitle:completion: 直後に書かれている NSLog(@"done1"); が実行される。一方、同期式ボタン(Sync)をタッチすると +showWithTitle:username:password: の直後の NSLog(@"done2"); は UIAlertView が閉じてから実行されるのがわかる。
- - - -
UIAlertView に限らず非同期=>同期変換に runUntilDate: は使えそう。
Toshihide says:
2011年12月2日 11:04
有難うございます。助かりました。仕事中なので本当はコンメトもいけないが感謝の印を表さずにはいられません。
Toshihide says:
2011年12月2日 11:04
有難うございます。助かりました。仕事中なので本当はコンメトもいけないが感謝の印を表さずにはいられません。
xcatsan says:
2011年12月2日 12:30
Toshihideさん、こんにちは。
お役に立てたようで何よりです。
お仕事頑張ってください。
ではでは。
xcatsan says:
2011年12月2日 12:30
Toshihideさん、こんにちは。
お役に立てたようで何よりです。
お仕事頑張ってください。
ではでは。