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定义在Controller中,其代理方法也在Contriller实现,ViewModel为其提供必要的数据:

头文件中:

#import <UIKit/UIKit.h>

@interface QFViewModel : NSObject

- (NSInteger)numberOfRowsInSection:(NSInteger)section;

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

@end

实现文件中两个关键方法:

- (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,一个模型的协议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.tableView registerClass:[QFCellOne class] forCellReuseIdentifier:kCellOneIdentifier];
    [self.tableView registerClass:[QFCellTwo class] forCellReuseIdentifier:kCellTwoIdentifier];
    [self.view addSubview:self.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的对象交互方式

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容