利用数据驱动模式编写复杂样式的UITableView

数据驱动是一种思想,数据驱动型编程是一种编程范式。基于数据驱动的编程,基于事件的编程,以及近几年业界关注的响应式编程,本质其实都是观察者模型。数据驱动定义了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赋值。

罗列具体代码:

先创建相关文件,大致结构如下:

利用数据驱动模式编写复杂样式的UITableView_第1张图片
image

创建了2种cell,2种ViewModel,2种Model(此处不多做介绍了,无非是一些字段的定义,且基本无.m实现),一套协议

接下来完成Protocol的定义

#import 

@protocol Cell_Config_Protocol 
/**
 提供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 ()

@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 ()
@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)

你可能感兴趣的:(利用数据驱动模式编写复杂样式的UITableView)