基于 ResponderChain 的對象交互方式

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 概述 感謝casa大神的分享:一種基于ResponderChain的對象交互方式,這里也只是作為筆記來記錄。閑話少...
    Joshua520閱讀 582評論 0 0
  • 好奇觸摸事件是如何從屏幕轉(zhuǎn)移到APP內(nèi)的?困惑于Cell怎么突然不能點擊了?糾結(jié)于如何實現(xiàn)這個奇葩響應(yīng)需求?亦或是...
    Lotheve閱讀 59,621評論 51 604
  • 在iOS開發(fā)中經(jīng)常會涉及到觸摸事件。本想自己總結(jié)一下,但是遇到了這篇文章,感覺總結(jié)的已經(jīng)很到位,特此轉(zhuǎn)載。作者:L...
    WQ_UESTC閱讀 6,251評論 4 26
  • Understanding Event Handling, Responders, and the Respond...
    frankisbaby閱讀 464評論 0 0
  • 在面試中,我們經(jīng)常會遇到一些原理性的問題,很常識但很難用通俗的語言解釋清楚,這也是大部分業(yè)務(wù)級程序員經(jīng)常失誤的地方...
    歐巴冰冰閱讀 2,019評論 2 21

友情鏈接更多精彩內(nèi)容