iOS開発をしていて常々、HTMLのイベントバブリングみたいな感じでボタン押下のタイミングをViweController側で受け取れたらなーと思ってました。
そしてどうやらそれは、Responder Chainを利用することで可能そうだと気づいたので試しに実装してみました。
(ちなみに今回のサンプルをGithubにアップしました)
▼ Responder chain参考(ドキュメントから引用)
ドキュメントを見ると以下のように書かれています。
For the app on the left, the event follows this path:
- The initial view attempts to handle the event or message. If it can’t handle the event, it passes the event to its superview, because the initial view is not the top most view in its view controller’s view hierarchy.
- The superview attempts to handle the event. If the superview can’t handle the event, it passes the event to its superview, because it is still not the top most view in the view hierarchy.
- The topmost view in the view controller’s view hierarchy attempts to handle the event. If the topmost view can’t handle the event, it passes the event to its view controller.
- The view controller attempts to handle the event, and if it can’t, passes the event to the window.
- If the window object can’t handle the event, it passes the event to the singleton app object.
- If the app object can’t handle the event, it discards the event.
The app on the right follows a slightly different path, but all event delivery paths follow these heuristics:
A view passes an event up its view controller’s view hierarchy until it reaches the topmost view.
The topmost view passes the event to its view controller.
The view controller passes the event to its topmost view’s superview.
Steps 1-3 repeat until the event reaches the root view controller.The root view controller passes the event to the window object.
The window passes the event to the app object.
まさにイベントのバブリングですね。
要は、該当のイベント(or メッセージ)が処理できるビューにたどり着くまで親を遡っていく、というわけです。
サンプルコード
// イベントを透過する処理をマクロ化しておく
#define PassResponderChain(sel, sender, proto) \
id next = self;\
do {\
next = [((UIResponder*)next).nextResponder targetForAction:@selector(sel)\
withSender:sender];\
if (next) {\
if ([next conformsToProtocol:@protocol(proto)]) {\
[next sel sender];\
break;\
}\
}\
} while (next)
※ ↑コメントで指摘していただいた点を考慮してます。(意図せず、同名メソッドがマッチしてしまった場合にイベントの伝達が滞るのを防ぐ)
// メソッド名がかぶって誤動作しないようにプロトコルにする
@protocol AnyEvent <NSObject>
@optional
- (void)tappedButton1:(id)responder;
- (void)tappedButton2:(id)responder;
@end
//////////////////////////////////////////////////
/**
* ボタンを押下した際のイベントハンドラ
*/
- (void)tappedButton1:(id)sender
{
PassResponderChain(tappedButton1:, self, AnyEvent);
}
こんな感じで、イベントを発行する予定のビュークラスにプロトコルとその処理を実装します。
これを、受け取りたい側(例えばViewController)で同じメソッド名で実装しておけば、自動的にResponderとして認識され、実行されます。
@interface ViewController <AnyEvent>
// ... 中略
@end
@implementation ViewController
/**
* ボタンイベントをトラッキング
*/
- (void)tappedButton1:(id)sender
{
// do anything.
// さらに上に伝播させる
PassResponderChain(tappedButton1:, sender, AnyEvent);
}
@end
ちなみに、ビューをいくつかに分割して実装すると思いますが、VC側だけじゃなく、AnyView
を内包している別のビュークラスでイベントをトラッキングしたい場合も、同様にプロトコルを実装してあげればトラッキングが可能です。
さらにそれを上に通知するマクロを実行してやれば、滞りなくVC側にもイベントを伝えることが可能です。
今まで現場では、Blocksを複数定義して、それを渡す形で実装していました。
が、間に挟まるビューが増えてくるとその取り回しも非常に多くなり、かつ、デリゲートするためだけのメソッドなども実装する必要があったりしたので非常に面倒でした。
上記の方法を利用すれば、必要な処理だけVC側で実装してやれば事足りるので非常に簡潔に書くことができます。
[追記]
コメントで指摘をいただきましたが、以下の方法でもアクションを伝達することができます。
名前衝突しないように配慮した上でメソッドを実装した場合は、こちらのほうがさらに簡潔に書くことが出来ます。
[UIApplication.sharedApplication sendAction:@selector(action:)
to:nil
from:self
forEvent:nil];