[iOS]ボタンがタッチされてから登録したアクションが発火されるまでに起こる事
背景
Swift5では下記のようなコードのコンパイルが通ります。
addTarggetに渡しているself
はNSObjectのインスタンスメソッドであるself()
であり、targetの登録は成功しません。
この記述の後にUIControlのallTargetメソッドを呼び出してもnilが入ったSetが返却されることを確認できます。
にも関わらずaddTarggetに渡しているselectorが示すメソッドが、UIButtonのタップ時に呼び出される。
そのため、ボタンがタップされてからボタンに登録したメソッドが呼び出されるまでに何が起こっているか気になりました。
class ViewController: UIViewController {
private let button: UIButton = {
let b = UIButton()
control.addTarget(self, action: #selector(action), for: .touchUpInside)
return b
}()
}
参考
## ボタンがタップされてから起こること
iOS
⚠ 勝手な憶測や古い2次情報から得た情報が多分に含まれています
iOSはSprinbBoardと呼ばれるGUIアプリケーションが実行されています。XCTestからSprinbBoardを起動すると、SprinbBoardを介してアプリの消去や、課金やSign with Apple等のシステムが表示するダイアログの操作等、アプリ外のシステム操作が行なえます。そのため、UIテストにてSprinbBoardを操作されている方も多く居ると思います。
このSprinbBoardはOSからイベントを受け取り、現在表示されているアプリケーションへイベントを伝搬させる役割も持っています。従って、タッチイベントもSprinbBoardから配信されます。
参考
-
https://stackoverflow.com/questions/22116698/does-uiapplication-sendevent-execute-in-a-nsrunloop
- リバースエンジニアリングした軌跡が載ってます
UIKit
⚠ 勝手な憶測や古い2次情報から得た情報が多分に含まれています
まず何者かが(おそらくUIKit)がRunLoopに対してSprinbBoardからのイベントを入力ソースとしてRunLoopに登録します。RunLoopに登録することによって、起動されたアプリケーションのスレッドにタッチイベントを配信することが可能になります。UIKitはRunLoopからFireされたイベントハンドラーを基にタッチイベントをアプリケーションへ伝搬しているのではないかと予測します。
ここで、RunLoopとは外部からの入力に応答してイベントハンドラを実行するスレッドに付き1つ存在するオブジェクトです。Timerでの処理のスケジュールやマウスドラッグの応答もRunLoopにて行われています。
次に渡されたタッチイベントを基に、UI階層からどのUIコンポーネントがイベントをハンドリングするかを探索します。このとき、探索の起点となるのがfirst responderになります。
UIKitはfirst responderの特定のためにhitTestを行います。hitTestを行った結果、最下層にあったViewがfirst responderになります。
(が、細かい仕様はTouch evnetやPress event等のEvent typeによって異なります)
最後に、first responderからView階層の親の方向に向かってイベントハンドルを行うActionを持ったオブジェクトを探索します(いわゆる、Responder Chain)。addTargetメソッドを使って登録したtargetはUIControlに関連づいており、探索が可能となっています。first responderがtargetを持っていれば探索は終了しますが、nilならResponder Chainの要領で探索を続けます。
Actionが見つかれば、そのActionを実行しアプリケーションにタッチイベントの発火を通知する事ができます。
参考
-
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW1
- RunLoopについての詳細な説明
-
https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/using_responders_and_the_responder_chain_to_handle_events
- Responder Chainについて
アプリケーション
addTargetメソッド
を使って、タッチイベントが発生したときにイベントをハンドルするTargetと、実行する関数Actionを登録します。
すると、以上までにした説明通り、イベントが伝達されタッチイベントをアプリ側でハンドルできます。
おわりに
この記述の後にUIControlのallTargetメソッドを呼び出してもnilが入ったSetが返却されることを確認できます。
にも関わらずaddTarggetに渡しているselectorが示すメソッドが、UIButtonのタップ時に呼び出される。
これがなぜかと言うと
first responderがtargetを持っていれば探索は終了しますが、nilならResponder Chainの要領で探索を続けます。
だからでした。
このようなtargetをnil targetと呼ぶらしく、この仕組を活用して特定のオブジェクトにイベントハンドリングを集約させる取り組みをがある事を知りました。
参考:
- https://qiita.com/shiz/items/5a75fbd903de4a474b1d#uicontrolsの場合
- http://cocoadays.blogspot.com/2011/06/ios-responder-chain-uiviewcontroller.html
理解曖昧なところや誤ってるとこあるかもなので、もし見つけたらコメントでご指摘いただけると嬉しいです。
Discussion