ResponderChain對象交互方式本質(zhì)
響應(yīng)者鏈簡介
Responder Chain也就是響應(yīng)鏈镶苞,響應(yīng)者鏈?zhǔn)怯啥鄠€(gè)響應(yīng)者對象連接起來的鏈條。在iOS中響應(yīng)者鏈的關(guān)系可以用下圖表示:
響應(yīng)者鏈的事件傳遞過程:
1>如果當(dāng)前view是控制器的view柒傻,那么控制器就是上一個(gè)響應(yīng)者,事件就傳遞給控制器;如果當(dāng)前view不是控制器的view裤唠,那么父視圖就是當(dāng)前view的上一個(gè)響應(yīng)者砂客,事件就傳遞給它的父視圖
2>在視圖層次結(jié)構(gòu)的最頂級視圖泥张,如果也不能處理收到的事件或消息,則其將事件或消息傳遞給window對象進(jìn)行處理
3>如果window對象也不處理鞠值,則其將事件或消息傳遞給UIApplication對象
4>如果UIApplication也不能處理該事件或消息媚创,則將其丟棄
交互本質(zhì)
通過對UIResponder添加分類,實(shí)現(xiàn)事件沿響應(yīng)鏈條傳遞彤恶。(事件傳遞的方向與ResponderChain是保持一致的钞钙,如果未達(dá)到需要響應(yīng)的對象,可拋棄事件繼續(xù)向上進(jìn)行傳遞)
DEMO
如圖所示声离,如果我們對Cell中的灰色按鈕點(diǎn)擊以實(shí)現(xiàn)控制器中彈出alert芒炼,一般的做法是點(diǎn)擊Cell按鈕后,通過代理或者block回調(diào)至控制器术徊,但兩者的缺點(diǎn)是:
代理:代碼較多
block:代碼較為分散
通過ResponderChain對象交互本刽,因?yàn)镃ell屬于控制器的子view,當(dāng)點(diǎn)擊Cell上的按鈕時(shí)弧关,事件會(huì)傳遞至控制器盅安,我們在控制器中攔截對應(yīng)的事件名稱,調(diào)用控制器的方法實(shí)現(xiàn)alert彈出世囊,而且如果有多個(gè)subView别瞭,事件也可以進(jìn)行統(tǒng)一的管理。
「Talk is cheap. Show me the code」
為UIResponder category
@interface UIResponder (Router)
/**
為響應(yīng)鏈添加配對的方法株憾,以實(shí)現(xiàn)事件沿響應(yīng)鏈傳遞
@param eventName 事件名稱
@param userInfo 傳遞的參數(shù)
*/
- (void)routerEventWithName:(NSString *)eventName userInfo:(NSDictionary *)userInfo;
@end
@implementation UIResponder (Router)
- (void)routerEventWithName:(NSString *)eventName userInfo:(NSDictionary *)userInfo{
[[self nextResponder] routerEventWithName:eventName userInfo:userInfo];
}
@end
Cell代碼
#import "ResponderTestTableViewCell.h"
#import "UIResponder+Router.h"
#import "ResponderChainName.h"
@implementation ResponderTestTableViewCell{
UILabel *_testLabel;
}
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier{
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
[self initUI];
}
return self;
}
- (void)initUI{
// cell中可點(diǎn)擊按鈕
UIButton *testBtn = [UIButton buttonWithType:UIButtonTypeCustom];
[testBtn setFrame:CGRectMake(100, 30, 110, 40)];
[testBtn setBackgroundColor:[UIColor lightGrayColor]];
[testBtn addTarget:self action:@selector(testBtnClick) forControlEvents:UIControlEventTouchUpInside];
[self.contentView addSubview:testBtn];
_testLabel = [[UILabel alloc] initWithFrame:CGRectMake(15, 0, 100, 100)];
[_testLabel setFont:[UIFont systemFontOfSize:18]];
[self.contentView addSubview:_testLabel];
}
- (void)configureTextWithIndexRow:(NSInteger)row{
[_testLabel setText:[NSString stringWithFormat:@"row:%zd", row]];
}
- (void)testBtnClick{
// 接收到點(diǎn)擊事件時(shí)蝙寨,將事件往上層傳遞
[self.nextResponder routerEventWithName:kTestCellBtnClickedEvent userInfo:@{kUserInfoObject : _testLabel.text}];
}
@end
由于彈出alert的事件是發(fā)生在控制器中的晒衩,cell里不做處理,只是將事件名稱和參數(shù)向外傳遞
控制器相關(guān)代碼
- (void)routerEventWithName:(NSString *)eventName userInfo:(NSDictionary *)userInfo{
[[EventProxy shareInstance] handleEvent:eventName userInfo:userInfo];
}
當(dāng)傳遞至控制器時(shí)墙歪,進(jìn)行事件的攔截听系,此處用了EventProxy類來進(jìn)行事件的統(tǒng)一處理
EventProxy是將根據(jù)外部傳入的事件名以及方法參數(shù),找到對應(yīng)target和對應(yīng)的方法虹菲,通過NSInvocation的方式進(jìn)行調(diào)用靠胜。
EventProxy相關(guān)實(shí)現(xiàn)
@implementation EventProxy
+ (EventProxy *)shareInstance{
static EventProxy *shareInstance = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
shareInstance = [[EventProxy alloc] init];
});
return shareInstance;
}
- (void)handleEvent:(NSString *)eventName userInfo:(NSDictionary *)userInfo{
NSInvocation *invocation = [self.eventStrategy objectForKey:eventName];
if (invocation) {
if (invocation.methodSignature.numberOfArguments > 2) {
[invocation setArgument:&userInfo atIndex:2];
}
[invocation invoke];
}
}
- (NSInvocation *)createInvocationWithTarget:(id)target selector:(SEL)action{
if (!target) {
return nil;
}
NSMethodSignature *methodSignature = [(NSObject *)target methodSignatureForSelector:action];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
invocation.selector = action;
invocation.target = target;
return invocation;
}
- (NSMutableDictionary<NSString *,NSInvocation *> *)eventStrategy{
if (_eventStrategy == nil) {
_eventStrategy = @{}.mutableCopy;
}
return _eventStrategy;
}
// 重寫set方法是為了在設(shè)置其他target的時(shí)候,添加對應(yīng)的事件
- (void)setMainVc:(ViewController *)mainVc{
_mainVc = mainVc;
if (![self.eventStrategy objectForKey:kTestCellBtnClickedEvent]) {
[self.eventStrategy setObject:[self createInvocationWithTarget:self.mainVc selector:NSSelectorFromString(@"testCellBtnClicked:")] forKey:kTestCellBtnClickedEvent];
}
}
- (void)setTestAVc:(TestAViewController *)testAVc{
_testAVc = testAVc;
if (![self.eventStrategy objectForKey:kTestABtnClickedEvent]) {
[self.eventStrategy setObject:[self createInvocationWithTarget:self.testAVc selector:NSSelectorFromString(@"hasPushedTestAViewController")] forKey:kTestABtnClickedEvent];
}
}
@end
控制器中彈出TestA控制器并調(diào)用kTestABtnClickedEvent對應(yīng)事件實(shí)際開發(fā)中是用不到的毕源,此處只為了演示不在同一響應(yīng)鏈上的事件是怎樣傳遞的浪漠。
- (void)testABtnClicked{
TestAViewController *testAVc = [[TestAViewController alloc] init];
[self presentViewController:testAVc animated:YES completion:nil];
[EventProxy shareInstance].testAVc = testAVc;
[testAVc routerEventWithName:kTestABtnClickedEvent userInfo:nil];
}
小結(jié)
在我看來,響應(yīng)鏈傳遞事件這種做法最適合的場景是位于同一view上的多個(gè)子view的事件傳遞霎褐,對于不同target的事件傳遞址愿,并不是那樣契合,EventProxy類我的實(shí)現(xiàn)方式也不是很Elegant冻璃,在此只為拋磚引玉响谓,希望有好想法的小伙伴可以私聊或者評論。