ResponderChain+Strategy+MVVM實現一個優雅的TableView
在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]; [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:¶meter 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的使用及訊息轉發機制
ofollow,noindex">Demo演示
有任何意見和建議歡迎交流指導,如果可以,請順手給個star。
最後,萬分感謝Casa大佬的分享!
一種基於ResponderChain的物件互動方式