首先感謝下 Tian Wei Yu 的 [一種基於ResponderChain的物件互動方式](https://casatwy.com/responder_chain_communication.html) 這篇文章,讓我知道物件間的互動還有這種姿勢。說實話,第一遍沒看懂,自己跟著敲了一遍才理解,所以有了這篇文章,算是個記錄。
前言
Responder Chain ,也就是響應鏈,關於這方面的知識因為不是本文重點,還不太理解的可以去看看這篇文章:史上最詳細的iOS之事件的傳遞和響應機制-原理篇。
在 iOS 中,物件間的互動模式大概有這幾種:直接 property 傳值、delegate、KVO、block、protocol、多型、Target-Action 等等,本文介紹的是一種基於 UIResponder 物件互動方式,簡而言之,就是 通過在 UIResponder上掛一個 category,使得事件和引數可以沿著 responder chain 逐步傳遞。對於那種 subviews 特別多,事件又需要層層傳遞的層級檢視特別好用,但是,缺點也很明顯,必須依賴於 UIResponder 物件。
具體事例
我們先來看看下面這種很常見的介面:
簡單講解下:最外層是個 UITableView,我們就叫做 SuperTable,每個 cell 裡面又巢狀了個 UITableView,叫做 SubTable,然後這個 SubTable 的 cell 裡面有一些按鈕,我們理一下這個介面的層級:
UIViewController -> SuperTable -> SuperCell -> SubTable -> SubCell -> UIButton
如果我們需要在最外層的 UIViewController 裡捕獲到這些按鈕的點選事件,比如點選按鈕需要重新整理 SuperTable,這時候該怎麼實現呢?
方法有很多,最常見的就是 delegate ,但是因為層級太深,導致我們需要一層層的去實現,各種 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];
}
複製程式碼
事件響應:
- (void)btnClick1:(UIButton *)btn userInfo:(NSDictionary *)userInfo {
NSLog(@"%@ %@",btn,userInfo);
}
複製程式碼
如果想在傳遞過程中新增引數,比如想在 SuperCell 這一層加點引數,只需要在對應的地方實現方法就行:
- (void)routerEventWithSelectorName:(NSString *)selectorName object:(id)object userInfo:(NSDictionary *)userInfo {
NSMutableDictionary *mDict = [userInfo mutableCopy];
mDict[@"test"] = @"測試";
[super routerEventWithSelectorName:selectorName object:object userInfo:[mDict copy]];
}
複製程式碼
設計思路
- (void)routerEventWithSelectorName:(NSString *)selectorName
object:(id)object
userInfo:(NSDictionary *)userInfo
複製程式碼
細心的可以發現,我這裡直接把 SEL
設計成以 NSString
的形式傳遞了,再在外面通過 NSSelectorFromString(selectorName)
轉成對應的 SEL
。原文中傳的是個用來標識具體是哪個事件的字串,還需要維護專門的 NSDictionary
來找到對應的事件,我覺得太麻煩,但是好處是 @selector(....)
宣告和實現在一個地方,可讀性高,也不容易出現拼寫錯誤,導致觸發不了對應方法的問題,具體怎麼設計,大家見仁見智吧~
關於引數的傳遞,比如我觸發 UITableViewDelegate
中的 didSelectRowAtIndexPath:
方法,<2
個引數的情況,performSelector:
方法也可以滿足,但一旦 >2
個引數的話,就不行了,這時候我們就可以用 NSInvocation
來實現,我寫了個分類,支援傳遞多個引數,搭配使用很方便:
@interface NSObject (PerformSelector)
- (id)performSelector:(SEL)aSelector withObjects:(NSArray <id> *)objects;
@end
複製程式碼
@implementation NSObject (PerformSelector)
- (id)performSelector:(SEL)aSelector
withObjects:(NSArray <id> *)objects {
//建立簽名物件
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;
}
//建立 NSInvocation 物件
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
//儲存方法所屬的物件
invocation.target = self;
invocation.selector = aSelector;
//設定引數
//存在預設的 _cmd、target 兩個引數,需剔除
NSInteger arguments = signature.numberOfArguments - 2;
//誰少就遍歷誰,防止陣列越界
NSUInteger objectsCount = objects.count;
NSInteger count = MIN(arguments, objectsCount);
for (int i = 0; i < count; i++) {
id obj = objects[i];
//處理引數是 NULL 型別的情況
if ([obj isKindOfClass:[NSNull class]]) {obj = nil;}
[invocation setArgument:&obj atIndex:i+2];
}
//呼叫
[invocation invoke];
//獲取返回值
id res = nil;
//判斷當前方法是否有返回值
if (signature.methodReturnLength != 0) {
[invocation getReturnValue:&res];
}
return res;
}
@end
複製程式碼
最後附上 Demo