2012年9月11日火曜日

iOSにおけるFirstResponderについて

ファーストレスポンダとは、Cocoaにおいてフォーカスが当たっているGUI要素のことです。そしてタッチ以外のイベントを、最初に受け取ることになるオブジェクトです。

そんなことは入門書にも腐るほど書いてあるので分かっていると思います。そのはずなのに「InterfaceBuilderのFirstResponderプレースホルダの使い方は分からない」「使ってみたけど期待した動作をしなかった」という人向けの記事です。

---

タッチイベントが発生すると、UIApplicationからUIWindowへ、さらにUIViewControllerからUIViewへと下層に向かってヒットテストが発生します。そして最下層(ユーザーから見ると最前面)のビューがヒットテストビューとなり、受け取ったイベントを処理する責務を負うのです。

もしヒットテストビューがイベントを処理できなかった場合は、替わって上層のUIViewやUIViewControllerが対処に当たります。そして遡り続けても誰も処理できなかった場合、そのイベントは無効とみなされ破棄されます。

---

では「文字入力」や「シェイクモーション」などのタッチ以外のイベントはどのオブジェクトが受け取り、またどうその責務を伝播させればいいのでしょう?

その答えがファーストレスポンダレスポンダチェーンなのです。

特定のオブジェクトにbecomeFirstResponderメッセージを送ると、ファーストレスポンダオブジェクトになります。そしてタッチ以外の発生したイベントは全て、まずこのファーストレスポンダに送信されるのです。

そしてそのオブジェクトがイベントを処理できなかった場合、レスポンダチェーンを辿って他のオブジェクトに横流しされます。レスポンダチェーンは、タッチイベントがビュー階層を上層に遡っていったのと同じ繋がり方をしています。

---

InterfaceBuilderのFirstResponderプレースホルダと関連付けた場合、そのメッセージは現在のファーストレスポンダに送信されることになります。

ただしここには2つ注意すべきことがあります。

---

まず第一に、ファーストレスポンダは一つとは限らないということです。

現在のファーストレスポンダを管理しているオブジェクトは何でしょうか?

その答えはウインドウオブジェクトです。Cocoa Touchではプライベートメソッドになっていますが、UIWindowにfirstResponderメッセージを送ると、現在保持しているファーストレスポンダオブジェクトが何かを教えてくれます。(プライベートメソッドはリジェクト対象になるので、ストアに出すアプリでは使わない方がいいです)

これはMac OSXが一つのアプリが複数のウインドウを持っていたためです。ウインドウという概念の乏しいiOSですが、OSXの名残はいくつか見受けられます。

例えばMac OSXでは複数のウインドウのうち、現在ユーザーと対話しているウインドウのことを、「キーウインドウ」と呼びます。iOSではそれを意識することはありませんが、アプリケーション起動時に必ず実行しなければならないUIWindowのメソッドは、makeKeyAndVisible(対象をキーウインドウにして表示せよ)です。

単一のアプリが複数のウインドウを持つOSXのCocoaの流れを汲むため、「個々のウインドウが現在フォーカスしているGUI要素を管理する」という仕組みも引き継いでいます。つまりWindowの数だけファーストレスポンダが存在しているのです。

しかしiOSは一つのUIWindowの下に全てのビューが収まっているため、関係ないように思います。実は違うのです。


問題が起きるのはキーボードを出現させたときです。ソフトキーボードのViewは、UITextEffectsWindow配下となっていて、UIWindowとは独立しているのです。

ゆえに上の画像のようにinputAccessoryViewに閉じるボタンを設置し、そこからInterfaceBuilderのFirstResponderプレースホルダに対してresignFirstResponderメッセージを送るような処理を書いても、UITextEffectsWindowのファーストレスポンダに送信しようとして失敗するため、キーボードは閉じないのです。

---

まったく無駄な知識ですが、UITextEffectsWindowに何らかのテキスト編集要素を持たせることで、UITextEffectsWindowのファーストレスポンダと、UIWindowのファーストレスポンダの、双方が存在する状態にすることが可能です。

その場合、UITextEffectsWindowのファーストレスポンダにresignFirstResponderを送信すると、UIWindowのファーストレスポンダにフォーカスが移ります。


なお「UITextEffectsWindowがファーストレスポンダを持っているときは、UIWindowへのタッチ操作は全て無効になる」という興味深い現象が発生します。

---

そしてもう一つは、もしファーストレスポンダが存在しなかったとき、あるいはファーストレスポンダにイベントを送信したものの、レスポンダチェーンを辿っても処理できなかったときです。

その場合、なぜかヒットテストビューに対してメソッドが実行されるのです。

…そんなことはありえない気がするのですが、実際の挙動を見るとそう考えないと辻褄が合いませんので、そうなのでしょう。

ヒットテストビューに該当するメソッドが実装されていなければ、例によって上層に遡っていき、AppDelegateが処理できなければ、そのメソッドは破棄されます。

---

そもそも、InterfaceBuilderのFirstResponderプレースホルダにメソッドを送信するとはどういうことなのでしょうか?

それを知るためには、まず普通にFile's Ownerのメソッドにbindした時の挙動を知る必要があります。

例えばボタンにメソッドをbindして、ボタンをタップすると、File's Ownerの指定されたメソッドが実行されます。この挙動で「ターゲットとセレクタを保持して、performSelectorを用いているんだろう」と当たりは付くと思いますが、もう少し捻りがあります。

InterfaceBuilderでメソッドとbindするか、もしくはコード上でGUI要素にaddTarget:action:forControlEvents:でアクションと紐付けると、GUI要素のスーパークラスであるUIControlは、内部にアクションとセレクタを保持します。

そしてbindしたイベントと合致するイベント(タップなど)が送信されると、sendAction:to:from:forEvent:を実行します。しかしこのメソッド内部で直接処理を行わず、UIApplicationの同名のメソッドに転送します。そしてUIApplicationからperformSelectorメッセージが送信されるのです。

この、一度UIWindowより上層であるUIApplicationを経由するというのがミソです。

---

ではFirstResponderプレースホルダとbindしたときはどうなるのでしょうか?UIControl内部でセレクタが保持されるのは同じです。しかしこのときは、ターゲットは指定されずnilになっているのです。

なので、FirstResponderプレースホルダへのbindと同じことを、コード上でも実現できます。addTarget:action:forControlEvents:のターゲットをnilにすればいいのです。

あるいは直接、UIApplicationのsendAction:to:from:forEvent:を、to(ターゲット)をnilにすることで、現在のファーストレスポンダに対してメソッドを実行することができます。これをnilターゲットアクションと呼びます。

なぜかStackOverflowを見ても知名度が低いのですが、nilターゲットアクションはファーストレスポンダに対して確実にresignFirstResponderを送信するベストプラクティスでもあります。

---

これでなぜUIControlがターゲットアクションでメソッドを起動しているだけなのに、ウインドウをまたがるとファーストレスポンダへメッセージを送信できないのか?という謎も解けます。

単純なターゲットアクションではなく、sendActionメソッドを通じてUIControlから、ウインドウよりさらに上層のアプリケーションオブジェクトに渡されるので、そこから属しているウインドウのファーストレスポンダを探す処理が入っているのでしょう。

---

まとめ


  • 一般に「ファーストレスポンダ」と呼ばれているものは、各々のウインドウが最初に応答するオブジェクトを保持しているものである。
    • ウインドウオブジェクトは通常の画面を表示するUIWindowと、ソフトキーボードを表示するためのUITextEffectsWindowとがあり、それぞれがファーストレスポンダを管理している。
  • InterfaceBuilderの「FirstResponderプレースホルダ」にbindされたメソッドは、nilターゲットアクションとして実行される。
    • 結果としてもし属しているウインドウにファーストレスポンダが存在すればそこを起点にメソッドが実行される。
    • ファーストレスポンダが存在しない、或いはファーストレスポンダとそのレスポンダチェーンが処理を実行できなかったときは、ヒットテストビューを起点にメソッドが実行される。これはnilターゲットアクションとは関係ない謎現象で、なんでそうなってるのか全然理解できない。
参考文献

0 件のコメント:

コメントを投稿