利用数据驱动模式编写复杂样式的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赋值。

罗列具体代码:

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

image

创建了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)

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

推荐阅读更多精彩内容