ResponderChain+Strategy+MVVM實現一個優雅的TableView

Ly夢k發表於2019-03-02

前言

在iOS開發中,常見的MVC中,複雜介面的Controller中的程式碼極其臃腫,動則上千行的程式碼量對後期維護簡直是一種災難,因此MVC也被調侃為Messive ViewController,特別是有多種型別Cell的TableView存在時,在-tableView:cellForRowAtIndexPath:代理方法中充斥著大量的if-else分支,這次我們嘗試用一種新的方式來“優雅”地實現這個方法。

傳統iOS的物件間互動模式就那麼幾種:直接property傳值、delegate、KVO、block、protocol、多型、Target-Action。這次來說說基於ResponderChain來實現物件間互動。

這種方式通過在UIResponder上掛一個category,使得事件和引數可以沿著responder chain逐步傳遞。

這相當於借用responder chain實現了一個自己的事件傳遞鏈。這在事件需要層層傳遞的時候特別好用,然而這種物件互動方式的有效場景僅限於在responder chain上的UIResponder物件上。

二、MVVM分離邏輯,解耦

網上關於MVVM的文章很多而且每個人的理解可能都有小小的差別,這裡不做贅述,這裡說說我在專案中所用到的MVVM,如果錯誤,請看官多多指教。我的tableView定義在ViewModel中,其代理方法也在ViewModel實現:

標頭檔案中:

#import <UIKit/UIKit.h>

@interface QFViewModel : NSObject

/// 暴露一個tableView的屬性 提供Controller使用
@property (nonatomic, strong) UITableView *tableView;

@end
複製程式碼

實現檔案中兩個關鍵方法:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    id<QFModelProtocol> model = self.dataArray[indexPath.row];
    id<QFViewProtocol> cell = [tableView dequeueReusableCellWithIdentifier:model.identifier];
    [cell configCellDateByModel:model];
    return (UITableViewCell *)cell;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    id<QFModelProtocol> model = self.dataArray[indexPath.row];
    return model.height;
}
複製程式碼

這裡用到了協議Protocol做解耦,兩個協議,一個檢視層的協議QFViewProtocol,一個模型的協議QFModelProtocol

  • QFViewProtocol 這裡提供一個方法,通過模型資料設定介面的展示
/**
 協議用於儲存每個cell的資料來源設定方法,也可以不用,直接在每個型別的cell標頭檔案中定義,考慮到開放封閉原則,建議使用
 */
@protocol QFViewProtocol <NSObject>

/**
 通過model 配置cell展示

 @param model model
 */
- (void)configCellDateByModel:(id<QFModelProtocol>)model;
複製程式碼
  • QFModelProtocol 這裡提供兩個屬性,一個重用標誌符,一個行高
#import <UIKit/UIKit.h>

/**
 協議用於儲存每個model對應cell的重用標誌符和行高,也可以不使用這個協議 直接在對一個的model裡指明
 */
@protocol QFModelProtocol <NSObject>

- (NSString *)identifier;

- (CGFloat)height;

@end
複製程式碼

在控制器層中直接addSubView:

- (void)initAppreaence {
    [self.view addSubview:self.viewModel.tableView];
}
複製程式碼

三、基於ResponderChain傳遞點選事件

在iOS的事件傳遞響應中有一棵響應樹,使用此可以消除掉各個類中的標頭檔案引用。使用這個特性只需要一個方法即可,為UIResponder新增分類,實現一個方法:

/**
 通過事件響應鏈條傳遞事件

 @param eventName 事件名
 @param userInfo 附加引數
 */
- (void)routerEventWithName:(NSString *)eventName userInfo:(NSDictionary *)userInfo {
    [[self nextResponder] routerEventWithName:eventName userInfo:userInfo];
}
複製程式碼

傳送事件的時候使用:

[self routerEventWithName:kEventOneName userInfo:@{@"keyOne": @"valueOne"}];
複製程式碼

這裡使用了一個字典來做引數的傳遞,這裡可以使用裝飾者模式,在事件層層向上傳遞的時候,每一層都可以往UserInfo這個字典中新增資料。那麼到了最終事件處理的時候,就能收集到各層綜合得到的資料,從而完成最終的事件處理。

如果要把這個事件繼續傳遞下去直到APPDelegate中的話,呼叫:

// 把響應鏈繼續傳遞下去
[super routerEventWithName:eventName userInfo:userInfo];
複製程式碼

四、策略模式避免if-else

在《大話設計模式》一書中,使用了商場打折的案例分析了策略模式對於不同演算法的封裝,有興趣可以去看看,這裡我們使用策略模式封裝的是NSInvocation,他用於做方法呼叫,在訊息轉發的最後階段會通過NSInvocation來轉發。我們以一個方法呼叫的例項來看NSInvocation

#pragma mark - invocation呼叫方法
- (void)invocation {
    SEL myMethod = @selector(testInvocationWithString:number:);
    NSMethodSignature *sig = [[self class] instanceMethodSignatureForSelector:myMethod];
    NSInvocation * invocatin = [NSInvocation invocationWithMethodSignature:sig];
    
    [invocatin setTarget:self];
    [invocatin setSelector:myMethod];
    
    NSString *a = @"string";
    NSInteger b = 10;
    [invocatin setArgument:&a atIndex:2];
    [invocatin setArgument:&b atIndex:3];
    
    NSInteger res = 0;
    [invocatin invoke];
    [invocatin getReturnValue:&res];
    
    NSLog(@"%ld",(long)res);
}

- (NSInteger)testInvocationWithString:(NSString *)str number:(NSInteger)number {
    
    return str.length + number;
}
複製程式碼
  • 第一步通過方法,生成方法簽名;
  • 第二步設定引數,注意這裡[invocatin setArgument:&a atIndex:2];的index從2開始而不是0,因為還有兩個隱藏引數self和_cmd佔用了兩個
  • 第三步呼叫[invocatin invoke];

好了我們迴歸主題,這裡用一個dictionary,儲存方法呼叫的必要引數,字典的key是事件名,value是對應的invocation物件,當事件發生時,直接呼叫

- (NSDictionary <NSString *, NSInvocation *>*)strategyDictionary {
    if (!_eventStrategy) {
        _eventStrategy = @{
                           kEventOneName:[self createInvocationWithSelector:@selector(cellOneEventWithParamter:)],
                           kEventTwoName:[self createInvocationWithSelector:@selector(cellTwoEventWithParamter:)]
                           };
    }
    return _eventStrategy;
}

複製程式碼

這裡呼叫UIResponder中的的方法- (NSInvocation *)createInvocationWithSelector:(SEL)selector生成invocation:

/**
 通過方法SEL生成NSInvocation

 @param selector 方法
 @return Invocation物件
 */
- (NSInvocation *)createInvocationWithSelector:(SEL)selector {
    //通過方法名建立方法簽名
    NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:selector];
    //建立invocation
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    [invocation setTarget:self];
    [invocation setSelector:selector];
    return invocation;
}
複製程式碼

五、事件處理

經過上面的步驟,我們可以在Controller中通過- (void)routerEventWithName:(NSString *)eventName userInfo:(NSDictionary *)userInfo拿到事件做響應的處理,如果有必要,把這個事件繼續傳遞下去:

#pragma mark - Event Response
- (void)routerEventWithName:(NSString *)eventName userInfo:(NSDictionary *)userInfo {
    // 處理事件
    [self handleEventWithName:eventName parameter:userInfo];
    
    // 把響應鏈繼續傳遞下去
    [super routerEventWithName:eventName userInfo:userInfo];
}

- (void)handleEventWithName:(NSString *)eventName parameter:(NSDictionary *)parameter {
    // 獲取invocation物件
    NSInvocation *invocation = self.strategyDictionary[eventName];
    // 設定invocation引數
    [invocation setArgument:&parameter atIndex:2];
    // 呼叫方法
    [invocation invoke];
}

- (void)cellOneEventWithParamter:(NSDictionary *)paramter {
    NSLog(@"第一種cell事件---------引數:%@",paramter);
    QFDetailViewController *viewController = [QFDetailViewController new];
    viewController.typeName = @"Cell型別一";
    viewController.paramterDic = paramter;
    [self presentViewController:viewController animated:YES completion:nil];
}

- (void)cellTwoEventWithParamter:(NSDictionary *)paramter {
    NSLog(@"第二種cell事件---------引數:%@",paramter);
    QFDetailViewController *viewController = [QFDetailViewController new];
    viewController.typeName = @"Cell型別二";
    viewController.paramterDic = paramter;
    [self presentViewController:viewController animated:YES completion:nil];
}
複製程式碼

六、後記

本篇到此結束了,總結起來,用到的東西還是不少,很多東西都值得深入:

  • Protocol的使用
  • 事件處理:事件產生、傳遞及響應的機制
  • 設計模式:策略模式、裝飾者模式以及MVVM的使用
  • NSInvocation的使用及訊息轉發機制

Demo演示
有任何意見和建議歡迎交流指導,如果可以,請順手給個star。

最後,萬分感謝Casa大佬的分享!
一種基於ResponderChain的物件互動方式

相關文章