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

前言

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

傳統iOS的對象間交互模式就那麼幾種:直接property傳值、delegate、KVO、block、protocol、多態、Target-Action。此次來講說基於ResponderChain來實現對象間交互。git

這種方式經過在UIResponder上掛一個category,使得事件和參數能夠沿着responder chain逐步傳遞。github

這至關於借用responder chain實現了一個本身的事件傳遞鏈。這在事件須要層層傳遞的時候特別好用,然而這種對象交互方式的有效場景僅限於在responder chain上的UIResponder對象上。算法

2、MVVM分離邏輯,解耦

網上關於MVVM的文章不少並且每一個人的理解可能都有小小的差異,這裏不作贅述,這裏說說我在項目中所用到的MVVM,若是錯誤,請看官多多指教。個人tableView定義在Controller中,其代理方法也在Contriller實現,ViewModel爲其提供必要的數據:設計模式

頭文件中:bash

#import <UIKit/UIKit.h>

@interface QFViewModel : NSObject

- (NSInteger)numberOfRowsInSection:(NSInteger)section;

- (id<QFModelProtocol>)tableView:(UITableView *)tableView itemForRowAtIndexPath:(NSIndexPath *)indexPath;

@end
複製代碼

實現文件中兩個關鍵方法:ide

- (NSInteger)numberOfRowsInSection:(NSInteger)section {
    return self.dataArray.count;
}

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

這裏用到了協議Protocol作解耦,兩個協議,一個視圖層的協議QFViewProtocol,一個模型的協議QFModelProtocolui

  • 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:spa

- (void)initAppreaence {
    [self.tableView registerClass:[QFCellOne class] forCellReuseIdentifier:kCellOneIdentifier];
    [self.tableView registerClass:[QFCellTwo class] forCellReuseIdentifier:kCellTwoIdentifier];
    [self.view addSubview:self.tableView];
}
複製代碼

3、基於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];
複製代碼

4、策略模式避免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;
}
複製代碼

5、事件處理

通過上面的步驟,咱們能夠在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];
}
複製代碼

6、後記

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

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

Demo演示
有任何意見和建議歡迎交流指導,若是能夠,請順手給個star。

感謝評論區各位的賜教,糾正了我把tableView定義在VM中的顛覆性錯誤!

最後,萬分感謝Casa大佬的分享!
一種基於ResponderChain的對象交互方式

相關文章
相關標籤/搜索