数据驱动是一种思想,数据驱动型编程是一种编程范式。基于数据驱动的编程,基于事件的编程,以及近几年业界关注的响应式编程,本质其实都是观察者模型。数据驱动定义了data和acton之间的关系,传统的思维方式是从action开始,一个action到新的action,不同的action里面可能会触发data的修改。数据驱动则是反其道而行之,以data的变化为起点,data的变化触发新的action,action改变data之后再触发另一个action。如果data触发action的逻辑够健壮,编程的时候就只需要更多的去关注data的变化。思考问题的起点不同,效率和产出也不同。
业务场景:
假设:
2017年1月,公司根据某个Idear开始研发一个新的App 1.0版本,App中首页有一个列表,顶部轮播广告图,剩余都是同样式的纯文字描述Cell条目;
2017年3月,2.0版本添加需求,要求首页Cell条目可以展示图片,与微博等一样,上方文字,下方图片,图片可以有也可以没有;
2017年6月,3.0版本添加需求,因与某电商平台合作,要求在Cell条目中添加如淘宝、京东一样的商品条目,可以跳转到合作伙伴的商城中去购物
2017年12月,10.0版本因公司拓展了金融业务并且相关金融App已上线,为了给其导流,决定首页新增理财产品类型的Cell条目;
通常情况下一些程序猿的做法是:
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if ([self.listItem[indexPath.row] isKindOfClass:@"Cell1"]) {
//创建/复用 cell 1
//cell 1 赋值
//return cell1;
}else if ([self.listItem[indexPath.row] isKindOfClass:@"Cell2"]){
//创建/复用 cell 2
//cell 2 赋值
//return cell2;
}else if ([self.listItem[indexPath.row] isKindOfClass:@"Cell3"]){
//创建/复用 cell 3
//cell 3 赋值
//return cell3;
....
....
....
}else{
//创建/复用 cell 10
//cell 10 赋值
//return cell10;
}
}
基本上比较久远的项目经常能看到这种写法,而且不只是tableView的代理中,甚至某些特定场景中也都是这种无止境的if-else判断,过去的2017年针对原有项目进行优化的时候经常碰见这种代码,试过很多方式,比如替换成switch-case方式去调整可观赏性;或者高度封装代码,创建一个通用Cell,然后判断不同数据源再向Cell上add不同的内容UI,但其实发现无论怎样,其实都没有摆脱if-else的判断,而且还都是在涉及到UI加载或者创建时候出现的if-else判断,这对于tableView的流畅度影响极其严重,卡顿掉帧经常见。。。
有一天突然想到了数据驱动模式,并结合MVVM的理念,决定让ViewModel去驱动UI的创建。
主要思路:
~model只负责源数据的归档存储
~定义一套Protocol,内含Cell的创建、赋值、高度返回等公共方法,如果必要可以将一些交互事件方法也定义进去
~ViewModel 实现具体的Protocol方法,创建并返回Cell、Cell高度返回等~***每一种Cell类型单独创建一个UITableViewCell类,并创建对应的一种ViewModel,且每一种ViewModel只负责为一种Cell提供服务,具体的讲就是ViewModel1实现的协议方法中,只会创建并返回Cell1;只会返回Cell1的高度。而Cell实现协议中的赋值方法,接收传入的数据,给自身UI赋值。
罗列具体代码:
先创建相关文件,大致结构如下:
创建了2种cell,2种ViewModel,2种Model(此处不多做介绍了,无非是一些字段的定义,且基本无.m实现),一套协议
接下来完成Protocol的定义
#import <Foundation/Foundation.h>
@protocol Cell_Config_Protocol <NSObject>
/**
提供tableView参数完成某个Cell的创建/重用,并返回cell对象
@param tableView 必须提供tableView
@return cell 对象
*/
- (id)getSpecificCellWithTableView:(id)tableView;
/**
返回指定cell的高度
@return 浮点类型数值
*/
- (float)getCellHeight;
/**
传入数据用于给cell的UI赋值
@param data viewModel/Model,最好传入ViewModel,MVVM模式下可以利用ViewModel针对原始的数据模型(Model)做一层封装,将数据处理成更容易被UI使用的数据,比如label的内容需要依靠Model中的三个字段内容拼接起来,那就在ViewModel中定义一个labContent字段,直接将Model中的三个字段内容汇总,赋值给labContent字段,最后label只需要设置.text的值为ViewModel.labContent就可以了,这种方式可以将空指针、空值、复杂逻辑业务阻挡在cell类外,并且利于代码移植,充分发挥ViewModel的作用
*/
- (void)updateSpecificCellWithData:(id)data;
@end
3个方法,两个将由ViewModel去实现(创建和高度返回),1个由Cell实现(赋值方法).
接下来让ViewModel去实现他需要实现的协议:
#import "Cell_ViewModel1.h"
#import "Cell_1_TableViewCell.h"
/**
编写习惯,平时不喜欢在.h中编写相关类的.h文件的导入,毕竟.h是要被编译器编译的,引入的太多了会影响编译速度,能抛给运行时就尽量抛出来
*/
#import "Cell_Config_Protocol.h"
@interface Cell_ViewModel1 ()<Cell_Config_Protocol>
@end
@implementation Cell_ViewModel1
#pragma mark - CELL_PROTOCOL
- (id)getSpecificCellWithTableView:(id)tableView{
static NSString* cellId = @"Cell_1_TableViewCell";
Cell_1_TableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:cellId];
if (!cell) {
cell = [[Cell_1_TableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellId];
}
return cell;
}
- (float)getCellHeight{
return 100.f;
}
@end
同理,Cell_ViewModel2.m中也写入相同代码,把服务对象换成Cell_2_TableViewCell即可。这样,相关ViewModel的任务就完成了。
接下来是让Cell去实现部分协议:
#import "Cell_1_TableViewCell.h"
#import "Cell_Config_Protocol.h"
#import "Cell_ViewModel1.h"
@interface Cell_1_TableViewCell ()<Cell_Config_Protocol>
@end
@implementation Cell_1_TableViewCell
#pragma mark - CELL_PROTOCOL
- (void)updateSpecificCellWithData:(id)data{
/*
内部控件赋值刷新
Cell_ViewModel1 *viewModel = data;
self.titleLabel = viewModel.title;
self.detailContentLabel = viewModel.detailContent;
.......
.......
.......
*/
}
- (void)awakeFromNib {[super awakeFromNib];}
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {[super setSelected:selected animated:animated];}
@end
然后,是ViewController中tableViewDelegate方法的实现
#pragma mark - tableView delegate && dataSource
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
return [self.dataSource[indexPath.row] getCellHeight];
}
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
//返回指定Cell
id cell = [self.dataSource[indexPath.row] getSpecificCellWithTableView:tableView];
//cell 赋值
[cell updateSpecificCellWithData:self.dataSource[indexPath.row]];
return cell;
}
最后,是数据的请求以及处理成tableView的数据源:
/**
假设这是ViewController内的某一个请求结果回调的方法,请求结束后会将请求下来的原始数据传入进来进行解析,之后装入数组,作为tableView的数据源
@param respondData 请求的原始数据
*/
- (void)dataParsingWithData:(NSDictionary *)respondData{
[data[@"content_list"] enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if([obj[@"content_style"] isEqualToString:@"cell_1"]){
//1、创建Cell_Model1并保存原始数据
// 2、创建Cell_ViewModel1,并处理相关数据成容易被UI使用的数据
[self.dataSource addObject:ViewModel1];
}
if([obj[@"content_style"] isEqualToString:@"cell_2"]){
//1、创建Cell_Model2并保存原始数据
//2、创建Cell_ViewModel2,并处理相关数据成容易被UI使用的数据
[self.dataSource addObject:ViewModel2];
}
}]
[self.myTableView reloadData];
}
以上就是简易代码,主要还是介绍了如何使用ViewModel来给指定Cell提供服务,最直接的目的就是去掉TableViewDelegate方法中冗余并且风险极大的if-else代码。
大家也都看出来了,其实if-else一直都在,只不过从原有的TableViewDelegate方法中放到了数据解析阶段。。。。。。没什么区别啊!!!
其实是有区别的,而且区别太大了,这种做法是延长了数据请求--》解析的时间,让列表渲染前,菊花多转了几圈,这总好过在cellForRowAtIndexPath方法里临时决定创建哪一个Cell好吧,用户的体验习惯就是 菊花转的时候 肯定再走网络请求,这时候慢,用户可能下意识的认为自己网络环境不好;而若是滚动过程中卡,傻子都会说是App太烂,程序猿笨。。。。。这就是区别。
总结一下优缺点
缺点
代码量偏多,存在相同功能重复编写的情况(目前自我感觉有这么一个缺点。。。哈哈)
优点
1、让TableViewDelegate相关业务更简洁
2、易于维护,如果哪一天产品经理想要再加一种Cell,直接创建一个Cell、一个ViewModel,实现相关协议,解析数据的时候创建对应ViewModel并加入tableView数据源内就可以了
3、可移植性强,如果某一天另外一个App也想用这种Cell,可以直接把Cell类和ViewModel类移植过去,然后只需在数据解析阶段处理一下源数据与ViewModel内容的对接就可以(因为我的ViewModel会针对Cell的UI定义一些字段,用来直接给Cell的UI赋值,而不是使用原始的Model)