こんにちは。iOS / Mac アプリ開発担当の金内です。
今日は GUI アプリ開発で役立つのに、通常の学習フローにはなかなか登場してこないワザをご紹介します。
イベント駆動型プログラム
iOS や Mac で使うような GUI アプリは一般に「イベント駆動型(イベント・ドリブン)」と呼ばれていて、ユーザーやシステムの動きがイベントとしてアプリケーションに通知され、処理が実行されるという作りになっています。
たとえば、スクロールバーをドラッグすると「スクロールしたよ」というイベントが発生して、そのウインドウの内容がスクロールしたり、ウェブブラウザのリンクをクリックすると「クリックしたよ」というイベントが発生してリンク先のページをロードしたりするわけです。
このところ「〇〇駆動」という言葉をよく見かけますが、この「イベント駆動」はソフトウェア関連では元祖といってもいいくらい古くから使われている言葉です。
ありがちなトラブル
アプリを開発するときには、このイベントの発生に応じた処理をプログラミングしていくわけですが、イベントの発生時にそのまま処理しようとすると、どうにもうまくいかないことがあります。
たとえば、ビューの状態や構造を変更する場合にタイミングによって画面のチラつきが起こったり、同じタイミングで変わっているはずのプロパティがまだ変わっていなかったりして、想定した動作にならないことがあるのです。
原因は主に、イベント処理の実装が持つ依存性にあります。あるイベントに応じて発生する処理はいろいろなオブジェクトのいろいろなメソッドに分かれており、一見それらは関係がないようにも見えます。
これらは順番に実行されていくため、たとえばあるイベントで発生する A,B,C という 3 つの処理があった場合、A の実行時には B と C はまだ実行されていません。にもかかわらず、たとえば A が一見つながりのない C の処理結果に依存していたりすると、A の処理がおかしくなってしまうわけです。
あとで実行!
こんなときにまず試してみたいのが「あとで実行」です。上記では A を「あとで実行」することになります。
ある処理を「あとで実行」すると、ひととおりのイベント処理が終わったあとに実行してくれるので、そのイベントで起こるはずのことはすべて起こったものとして安心して処理できます。
どうしても直らなかったチラつきが「あとで実行」したらサクッと直った、なんてことも実際あります。
あとで実行するには
「あとで実行」するにはいくつかの方法があります。イベント処理はメインスレッドで実行されるので、メインスレッドで実行されているコードを前提としています。
メソッドの場合
NSObject には、メソッドを「あとで実行」するための便利なメソッド peformSelector:withObject:afterDelay: があります。具体例としては次のようになります。
[self performSelector:@selector(doShomething) withObject:nil afterDelay:0];
これは同じスレッドで self の doSomething メソッドを「あとで実行」します。afterDelay がキモですね afterDelay の値は 0 でもちゃんと「あとで実行」されるのでご安心ください。明示的にしばらく待ちたい場合は秒数を指定します。
ブロックの場合
メインキューにブロックを dispatch_async してあげれば「あとで実行」されます。”async” がキモです。a を忘れて “sync” にするとフリーズしてしまってトラブル回避どころかバグを増やしてしまうことになるので注意しましょう。
dispatch_async(dispatch_get_main_queue(), ^{ // do something... });
時間指定なしでも dispatch_async なら「あとで実行」されますが、一定時間待ちたい場合は dispatch_after を使ってください。
「あとで実行」の仕組み
afterDelay:0 や dispatch_async が「あとで実行」になるのは、Cocoa におけるイベント駆動の中核である Run Loop という機構に関係があります。ふだん、Run Loop はイベントの発生をひたすら待っています。イベントが発生すると、そのループの周回でイベントを処理して、次の周回に移ります。
afterDelay や dispatch_async を使うと、時間指定とは関係なく Run Loop の次の周回以降での処理に回されるため、ひとつのイベント処理が完全に終わったあとでの実行になるわけです。詳しくは記事の末尾にあげたドキュメントなどを参考にしてください。
根本解決も忘れずに
イベント処理の順番によって起こっている問題は「あとで実行」で解決するケースがよくあります。それで万事オッケーな場合もありますが、上記の例で A の処理が C の処理結果に依存していたような、本来意図していない関係を解決することのほうが大切な場合もあります。A は本来、イベントの発生をトリガーとするのではなく、C の処理をトリガーとして実行すべきものなのかもしれません。
調査段階で原因の切り分けにも使える「あとで実行」、いったん問題がなくなったからといってそれで済ませてしまうと、本来の原因が別の問題を引き起す可能性もありえます。みなさん自身の眼でしっかりと見極めて、意図どおりに動作しつつ、メンテナンスしやすい、リーダブルでビューティフルなコードになるようにしてください。
Sleipnir の開発ノウハウ
フェンリルで開発しているウェブブラウザ Sleipnir の Mac 版や iOS 版の開発を通じてえられた Cocoa や Objective-C に関するノウハウは、今回のようにブログ等を通じてどんどん共有していきたいと考えています。
今後ともフェンリルと Sleipnir への応援をよろしくおねがいします。
Sleipnir Mobile for iPhone / iPad はこちら
参考リンク
さらに詳しく知りたい方は Cocoa のイベントループである Run Loop(ラン・ループ)やイベント処理を中心に調べてみることをおすすめします。
- NSRunLoop Class Reference
- Cocoa Event Handling Guide
- Threading Programming Guide(Run Loop に関するセクションがあります)
- NSObject Class Reference(紹介した peformSelector:withObject:afterDelay: があります)
- Grand Central Dispatch (GCD) Reference(紹介した dispatch_async や dispatch_after があります)