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