基於 ResponderChain 的物件互動方式

leeeisok發表於2018-03-06

首先感謝下 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 物件。

具體事例

我們先來看看下面這種很常見的介面:

基於 ResponderChain 的物件互動方式

簡單講解下:最外層是個 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

相關文章