首先感謝下 Tian Wei Yu 的 一種基于ResponderChain的對象交互方式 這篇文章,讓我知道對象間的交互還有這種姿勢。說實話,第一遍沒看懂,自己跟著敲了一遍才理解,所以有了這篇文章,算是個記錄。
前言
Responder Chain ,也就是響應(yīng)鏈,關(guān)于這方面的知識因為不是本文重點,還不太理解的可以去看看這篇文章:史上最詳細的iOS之事件的傳遞和響應(yīng)機制-原理篇。
在 iOS 中,對象間的交互模式大概有這幾種:直接 property 傳值、delegate、KVO、block、protocol、多態(tài)、Target-Action 等等,本文介紹的是一種基于 UIResponder 對象交互方式,簡而言之,就是 通過在 UIResponder上掛一個 category,使得事件和參數(shù)可以沿著 responder chain 逐步傳遞。對于那種 subviews 特別多,事件又需要層層傳遞的層級視圖特別好用,但是,缺點也很明顯,必須依賴于 UIResponder 對象。
具體事例
我們先來看看下面這種很常見的界面:

簡單講解下:最外層是個 UITableView,我們就叫做 SuperTable,每個 cell 里面又嵌套了個 UITableView,叫做 SubTable,然后這個 SubTable 的 cell 里面有一些按鈕,我們理一下這個界面的層級:
UIViewController -> SuperTable -> SuperCell -> SubTable -> SubCell -> UIButton
如果我們需要在最外層的 UIViewController 里捕獲到這些按鈕的點擊事件,比如點擊按鈕需要刷新 SuperTable,這時候該怎么實現(xiàn)呢?
方法有很多,最常見的就是 delegate ,但是因為層級太深,導(dǎo)致我們需要一層層的去實現(xiàn),各種 protocol、delegate 聲明,很繁瑣,這種時候,基于 Responder Chain 就很方便了。
具體使用
只需要一個 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 的點擊處理:
- (IBAction)btnClick1:(UIButton *)sender {
[self routerEventWithSelectorName:@"btnClick1:userInfo:" object:sender userInfo:@{@"key":@"藍色按鈕"}];
}
外層 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);
}
如果想在傳遞過程中新增參數(shù),比如想在 SuperCell 這一層加點參數(shù),只需要在對應(yīng)的地方實現(xiàn)方法就行:
- (void)routerEventWithSelectorName:(NSString *)selectorName object:(id)object userInfo:(NSDictionary *)userInfo {
NSMutableDictionary *mDict = [userInfo mutableCopy];
mDict[@"test"] = @"測試";
[super routerEventWithSelectorName:selectorName object:object userInfo:[mDict copy]];
}
設(shè)計思路
- (void)routerEventWithSelectorName:(NSString *)selectorName
object:(id)object
userInfo:(NSDictionary *)userInfo
細心的可以發(fā)現(xiàn),我這里直接把 SEL 設(shè)計成以 NSString 的形式傳遞了,再在外面通過 NSSelectorFromString(selectorName) 轉(zhuǎn)成對應(yīng)的 SEL。原文中傳的是個用來標(biāo)識具體是哪個事件的字串,還需要維護專門的 NSDictionary 來找到對應(yīng)的事件,我覺得太麻煩,但是好處是 @selector(....) 聲明和實現(xiàn)在一個地方,可讀性高,也不容易出現(xiàn)拼寫錯誤,導(dǎo)致觸發(fā)不了對應(yīng)方法的問題,具體怎么設(shè)計,大家見仁見智吧~
關(guān)于參數(shù)的傳遞,比如我觸發(fā) UITableViewDelegate 中的 didSelectRowAtIndexPath: 方法,<2 個參數(shù)的情況,performSelector: 方法也可以滿足,但一旦 >2 個參數(shù)的話,就不行了,這時候我們就可以用 NSInvocation 來實現(xiàn),我寫了個分類,支持傳遞多個參數(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)建簽名對象
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 對象
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
//保存方法所屬的對象
invocation.target = self;
invocation.selector = aSelector;
//設(shè)置參數(shù)
//存在默認的 _cmd、target 兩個參數(shù),需剔除
NSInteger arguments = signature.numberOfArguments - 2;
//誰少就遍歷誰,防止數(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