iOS面向协议编程应用

1、UIKit中的协议编程

面向协议编程即面向接口编程,在iOS中大家比较熟悉的协议,比如UITableViewDelegateUITableViewDataSource,apple为什么要设置这样两个协议呢?

  • 在使用过程中,我们知道,通过delegate方法,我们可以处理tableView的一些响应事件,比如点击了cell,通过dataSource,我们设置好tableView的数据源,几行几列,采用的什么样的cell,UITableView框架在获取了我们提供好的数据源之后,就可以展示出一个滑动列表出来,并提供了各种API接口来给我们做各种各样的事情,试想一下,如果我们开发过程中,苹果没有提供这样一个UITableView框架,我们是不是得造一个这样简单易用的轮子呢
  • 事实上,在现在的各种编码社区中,都存在很多优秀的轮子,造轮子的目的是为了让别人能尽可能少的做一些复杂繁琐的操作,而快速达到某个目的,我们在平时开发的过程中是不是很感谢这些第三方大佬提供的各种优秀库,优秀API
  • 回过头来再看看UITableView的架构,实际上,这个封装好的UIVIew框架,把所有复杂的整体性的事情都做了,开发者只需要对某一个cell的数据源和事件做相应的处理,特点如下:
    • UITableView只负责整体框架搭建
    • 框架与数据源无干扰
    • 框架与事件无干扰

2、协议编程项目运用

在项目开发中,特别是团队协作,接手别人的项目,总感觉代码乱入麻,比如在某一个模块增加一个功能或者维护一个功能,代码逻辑跳来跳去,需要改动众多代码,动老旧业务和新需求都存在很大风险,那么我们不妨仿造下UITableView的这种面向协议编程的方式,来优化下我们在业务开发中的架构

  • 举一个iOS开发常遇到的例子,UIViewController里包含一个UITableView,假如涉及5中不同类型的cell,即有五种不能类型的业务要需要处理,每种cell或者说每种业务可能有多个子功能,比如说点击cell的按钮a,点击按钮b,等,业务多起来后,整个controller容器就显的不能看了,具体表现为一下几种现象:
    • cellForRow:方法,根据不同的业务类型,创建获取cell,赋值cell,众多的if else /switch,写的在一个视图获取函数里
    • didSelected:,根据不同的业务类型,众多的if else /switch处理不同的业务

我们发现,只要与业务挂上钩,由于业务是在不断地更新迭代,就很容易出现以上问题,但是项目不可能没有业务的存在,那么如何合理的拆分业务,设计合理的架构呢

  • 比如我们常说的组件化、模块化,这是一种大的,某个比较完整的功能模块的拆分
  • 比如说mvvm,mvp等设计模式,也能达到业务拆分的效果

如何把cellForRowdidSelected也从业务中脱离出来呢?

其实仔细想想,所有的数据源在确定之后(本地的或者服务端返回的),就已经确定某一条数据源,对应什么cell,需要处理那些事件,即数据源和事件都已经确定好了,我们可以在参考mvvm里的vm模型,我们也可以设计这样的一个model,既提供cell需要的展示数据,也可以处理cell的事件.

有了这一步的vm想法,但是并不想采用mvvm的结构,vm有点太固化了,比如说相同的类型的cell,如果存在巨大的业务差距,用同一个vm模型去处理就不是那么合理了,那么应该怎样去优化这种结构呢?上面提到的协议就是一个不错的选择。制定好协议方法,让model去遵守协议方法就好了

一个协议,可以被相同cell类型,但是业务类型不一样的model遵守,这样就可以实现业务隔离,但是采用mvvm的形式去实现cell样式一样,业务又不一样的效果,那就要用继承方式,先创建一个basevm,然后根据业务去创建不同的subvm。继承和协议各有优劣势,但是如果在这种业务实现上采用继承,会比协议更加重,继承需要的文件会多一个baseVM类,过多的依赖继承,会使相关继承业务的逻辑处理流程变得杂糅。

3、面向协议编程Demo

最终实现效果如下


3.1几个关键因素
  • UITableView
  • 两种cell
  • 5个业务,即5个viewModel,如果存在可以复用的vm,可以减少vm个数
3.2demo设计架构图
3.3关键代码

下面通过各个模块的代码来感受下这样设计的好处【由于条件不便加偷懒,代码以图片形式展示】
GLProtocolProgramDemoVC

  • 获取数据源


1、5个model对应5个业务
2、style1model1、style1model3,因为业务逻辑相同,公用同一个viewmodel,即GLPPDStyle1Model1
3、style1model2虽然样式和style1model1、style1model3相同,但是由于业务处理形式不一样,需要自己处理,所以单独建一个model,具体可以查看GLPPDStyle1Model1GLPPDStyle1Model2中的,didSelected方法处理区别
4、style2model1、style1model2由于需要与vc交互(上面的跳转是模拟的一个交互),这个交互由于与具体的业务无关,即与vm无关,所以需要抛出,由vc处理
5、所有的model遵循基本的协议GLPPDModelProtocol,下面会看到该协议的好处

  • UITableView相关代理方法


1、由于所有的model都遵循GLPPDModelProtocol,我们无需区分是哪种业务类型的model,既可以获取identifier,从而获取对应的cell
2、cell我们也无需关心是什么样式的cell,由于cell都遵循对应的cellProtocol,即实现了configCellWithModel:方法,只是参数model遵循的协议不一样,这里也可以只定义一种cellProtocol,参数遵循GLPPDModelProtocol即可,分开设计,一是为了增加可读性,二是可以针对不同类型的cell,扩展更多API,这里demo仅有configCellWithModel:
3、didSelectedRowAtIndexPath:方法,将事件转发给viewModel,业务的事情就应该由具体的业务处理

Protocol

参考设计架构图protocol设计

Cells

  • GLPPDStyle1Cell.h
#import "GLDemoProtol.h"
@interface GLPDDStyle1Cell : UITableViewCell<GLPPDCell1Protocol>
@end
  • GLPPDStyle1Cell.m
@interface GLPPDStyle1Cell()
@property (nonatomic, strong) UIImageView *iconView;
@property (nonatomic, strong) UILabel *accessLabel;
@property (nonatomic, strong) id<GLPPDStyle1Protocol> model;
@end

- (instancetype)initwithStyle:...
- (void)setupView{
    //cell视图搭建
}
- (void)configCellWithModel:(id<GLPPDStyle1Protocol>)model {
    self.model = model;
    if ([model respondsToSelector:@selector(imageName)]) {
        self.iconView.image = [UIImage imageNamed:[model imageName]];
    } else {
        self.iconView.image = nil;
    }
    
    //同理通过代理方法accessTitle获取accessLabel.text
}

*GLPPDStyle2Cell.m

//基本方法和GLPPDStyle1Cell相同
- (void)clickHandle:(UIButton *)sender {
    if([self.model respondsToSelector:@selector(didClicked)]){
        [self.model didClicked];
    }
}

Models

  • GLPPDStyle1Model1<GLPPDStyle1Protocol>
- (NSString *)identifier {
    return kStyleCellID;
}
- (NSString *)imageName {
    return self.params[@"icon"] ?: @"tubiaozhizuomoban-- 1";
}

- (NSString *)accessTitle {
    return self.params[@"title"]?:@"手机";
}

- (void)didSelected {
    DemoBaseVC *target = self.params[@"target"];
    if(target){
        DemoBaseVC *vc = [[DemoBaseVC alloc] init];
        vc.title = [self accessTitle];
        [target.navigationController pushController:vc animated:YES];
    }
}
  • GLPPDStyle1Model2<GLPPDStyle1Protocol>
    同上述model一样的cell样式,但是可能由于业务不同,需要单独建一个vm,看代码

GLPPDStyle1Model1不同的是,configParams:在处理上有所不同,GLPPDStyle1Model1这个子业务可以从外面传递icontitle信息

  • GLPPDStyle2Model1<GLPPDStyle2Protocol>

1、通过didClicked接受cell的按钮点击事件
2、由于存在外界交互,通过callBack进行回调,与vc交互

3.4代码分析
  • view 、viewModel、vc分离,分工明确
  • vc只负责从本地或者服务器获取数据源,生产viewModel,即createDatas
  • UITableView的相关代理方法处理简单,无需关心数据源类型及业务处理
  • cell只与视图展示、事件响应有关,不做业务具体操作
  • model视图展示数据处理,业务处理
  • 各业务model分工明确,可读性,可维护性强
  • 新增cell样式,或者新增业务类型,对老业务无影响,只需要新增cellProtocolModelProtocol
  • cell样式相同,可复用已有的cell,业务类型相同,可以复用已有的Model,业务不同或者业务比较复杂,也可以新建Model

4.总结

本文章demo采用的是一个UITableView主题结构,实际开发中面向协议编程,主题结构可以是任意的形式的模块,只要存在view组合【UITableView就是由cell组合而成】、业务组合【各种Models】,都可以采用这种形式进行开发,并且可以递归拆解,大拆中,中拆小。
设计模式有很多,这个demo只是面向协议编程的一个小例,项目比较简单,在实际开发中,经常会遇到一个视图包含N多个功能模块,这些功能或相似或不相似,会根据实际情况、个人理解产生一些形变,但是无论用怎么的设计模式,我们需要尽可能遵守软件设计模式的六大准则,减少后续开发成本,迭代风险