首先感謝下 Tian Wei Yu 的 一種基于ResponderChain的對(duì)象交互方式 這篇文章酿雪,讓我知道對(duì)象間的交互還有這種姿勢(shì)。說(shuō)實(shí)話,第一遍沒(méi)看懂洽议,自己跟著敲了一遍才理解,所以有了這篇文章漫拭,算是個(gè)記錄亚兄。
前言
Responder Chain ,也就是響應(yīng)鏈采驻,關(guān)于這方面的知識(shí)因?yàn)椴皇潜疚闹攸c(diǎn)审胚,還不太理解的可以去看看這篇文章:史上最詳細(xì)的iOS之事件的傳遞和響應(yīng)機(jī)制-原理篇匈勋。
在 iOS 中,對(duì)象間的交互模式大概有這幾種:直接 property 傳值膳叨、delegate洽洁、KVO、block菲嘴、protocol饿自、多態(tài)、Target-Action 等等龄坪,本文介紹的是一種基于 UIResponder 對(duì)象交互方式昭雌,簡(jiǎn)而言之,就是 通過(guò)在 UIResponder上掛一個(gè) category健田,使得事件和參數(shù)可以沿著 responder chain 逐步傳遞烛卧。對(duì)于那種 subviews 特別多,事件又需要層層傳遞的層級(jí)視圖特別好用妓局,但是总放,缺點(diǎn)也很明顯,必須依賴于 UIResponder 對(duì)象跟磨。
具體事例
我們先來(lái)看看下面這種很常見(jiàn)的界面:
簡(jiǎn)單講解下:最外層是個(gè) UITableView间聊,我們就叫做 SuperTable,每個(gè) cell 里面又嵌套了個(gè) UITableView抵拘,叫做 SubTable哎榴,然后這個(gè) SubTable 的 cell 里面有一些按鈕,我們理一下這個(gè)界面的層級(jí):
UIViewController -> SuperTable -> SuperCell -> SubTable -> SubCell -> UIButton
如果我們需要在最外層的 UIViewController 里捕獲到這些按鈕的點(diǎn)擊事件僵蛛,比如點(diǎn)擊按鈕需要刷新 SuperTable尚蝌,這時(shí)候該怎么實(shí)現(xiàn)呢?
方法有很多充尉,最常見(jiàn)的就是 delegate 飘言,但是因?yàn)閷蛹?jí)太深,導(dǎo)致我們需要一層層的去實(shí)現(xiàn)驼侠,各種 protocol姿鸿、delegate 聲明,很繁瑣倒源,這種時(shí)候苛预,基于 Responder Chain 就很方便了。
具體使用
只需要一個(gè) UIResponder 的 category 就行:
@interface UIResponder (Router)
- (void)routerEventWithSelectorName:(NSString *)selectorName
object:(id)object
userInfo:(NSDictionary *)userInfo;
@end
@implementation UIResponder (Router)
- (void)routerEventWithSelectorName:(NSString *)selectorName
object:(id)object
userInfo:(NSDictionary *)userInfo {
[[self nextResponder] routerEventWithSelectorName:selectorName
object:object
userInfo:userInfo];
}
@end
最里層 UIButton 的點(diǎn)擊處理:
- (IBAction)btnClick1:(UIButton *)sender {
[self routerEventWithSelectorName:@"btnClick1:userInfo:" object:sender userInfo:@{@"key":@"藍(lán)色按鈕"}];
}
外層 UIViewController 的接收:
- (void)routerEventWithSelectorName:(NSString *)selectorName
object:(id)object
userInfo:(NSDictionary *)userInfo {
SEL action = NSSelectorFromString(selectorName);
NSMutableArray *arr = [NSMutableArray array];
if(object) {[arr addObject:object];};
if(userInfo) {[arr addObject:userInfo];};
[self performSelector:action withObjects:arr];
}
事件響應(yīng):
- (void)btnClick1:(UIButton *)btn userInfo:(NSDictionary *)userInfo {
NSLog(@"%@ %@",btn,userInfo);
}
如果想在傳遞過(guò)程中新增參數(shù)笋熬,比如想在 SuperCell 這一層加點(diǎn)參數(shù)热某,只需要在對(duì)應(yīng)的地方實(shí)現(xiàn)方法就行:
- (void)routerEventWithSelectorName:(NSString *)selectorName object:(id)object userInfo:(NSDictionary *)userInfo {
NSMutableDictionary *mDict = [userInfo mutableCopy];
mDict[@"test"] = @"測(cè)試";
[super routerEventWithSelectorName:selectorName object:object userInfo:[mDict copy]];
}
設(shè)計(jì)思路
- (void)routerEventWithSelectorName:(NSString *)selectorName
object:(id)object
userInfo:(NSDictionary *)userInfo
細(xì)心的可以發(fā)現(xiàn),我這里直接把 SEL
設(shè)計(jì)成以 NSString
的形式傳遞了,再在外面通過(guò) NSSelectorFromString(selectorName)
轉(zhuǎn)成對(duì)應(yīng)的 SEL
昔馋。原文中傳的是個(gè)用來(lái)標(biāo)識(shí)具體是哪個(gè)事件的字串筹吐,還需要維護(hù)專門的 NSDictionary
來(lái)找到對(duì)應(yīng)的事件,我覺(jué)得太麻煩秘遏,但是好處是 @selector(....)
聲明和實(shí)現(xiàn)在一個(gè)地方丘薛,可讀性高,也不容易出現(xiàn)拼寫錯(cuò)誤垄提,導(dǎo)致觸發(fā)不了對(duì)應(yīng)方法的問(wèn)題榔袋,具體怎么設(shè)計(jì),大家見(jiàn)仁見(jiàn)智吧~
關(guān)于參數(shù)的傳遞铡俐,比如我觸發(fā) UITableViewDelegate
中的 didSelectRowAtIndexPath:
方法凰兑,<2
個(gè)參數(shù)的情況,performSelector:
方法也可以滿足审丘,但一旦 >2
個(gè)參數(shù)的話吏够,就不行了,這時(shí)候我們就可以用 NSInvocation
來(lái)實(shí)現(xiàn)滩报,我寫了個(gè)分類锅知,支持傳遞多個(gè)參數(shù),搭配使用很方便:
@interface NSObject (PerformSelector)
- (id)performSelector:(SEL)aSelector withObjects:(NSArray <id> *)objects;
@end
@implementation NSObject (PerformSelector)
- (id)performSelector:(SEL)aSelector
withObjects:(NSArray <id> *)objects {
//創(chuàng)建簽名對(duì)象
NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:aSelector];
//判斷傳入的方法是否存在
if (!signature) { //不存在
//拋出異常
NSString *info = [NSString stringWithFormat:@"-[%@ %@]:unrecognized selector sent to instance",[self class],NSStringFromSelector(aSelector)];
@throw [[NSException alloc] initWithName:@"ifelseboyxx remind:" reason:info userInfo:nil];
return nil;
}
//創(chuàng)建 NSInvocation 對(duì)象
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
//保存方法所屬的對(duì)象
invocation.target = self;
invocation.selector = aSelector;
//設(shè)置參數(shù)
//存在默認(rèn)的 _cmd脓钾、target 兩個(gè)參數(shù)售睹,需剔除
NSInteger arguments = signature.numberOfArguments - 2;
//誰(shuí)少就遍歷誰(shuí),防止數(shù)組越界
NSUInteger objectsCount = objects.count;
NSInteger count = MIN(arguments, objectsCount);
for (int i = 0; i < count; i++) {
id obj = objects[i];
//處理參數(shù)是 NULL 類型的情況
if ([obj isKindOfClass:[NSNull class]]) {obj = nil;}
[invocation setArgument:&obj atIndex:i+2];
}
//調(diào)用
[invocation invoke];
//獲取返回值
id res = nil;
//判斷當(dāng)前方法是否有返回值
if (signature.methodReturnLength != 0) {
[invocation getReturnValue:&res];
}
return res;
}
@end
最后附上 Demo