MVVM模式在多Cell列表中的实践

MVVM模式已经在业界得到广泛应用, 通过将VC/View中的逻辑剥离到viewModel中,专注数据处理和布局计算; 当一个view的元素相同却需要展示成不同样式时,viewModel便扮演类似HTML的CSS角色,多个viewModel对应一个view,实现了view的“代码复用”;

本文将在MVVM的基础上, 通过多个CellModel 对应一个Cell的方式, 减少了Cell的种类和重复布局代码,实现了代码UI和布局分离; 通过子线程提前计算不同方向的布局数据, 加快了转屏速度,滑动流畅性得到提升。

需求和UI设计

  • 列表在竖屏(Portrait)时, 需要进行AB Test:
    A. 需要展示单图/大图/三图/视频等多种Cell样式的文章;

    B. 展示成宫格样式

-w722

  • 横屏(Landscape)时, 展示成一行多个的相册模式;

-w688

实现分析

采用常见“一种文章对应一个Cell”的方法, 有以下缺点:

  • Cell种类太多: 文章类型有4-5种, 加上AB Test和横竖屏样式, Cell样式达到8种;
  • 重复代码: 每种文章展示的元素都大同小异, 但是每个Cell都要重复定义并布局一次;

总体架构图

借鉴MVVM分离模式和Web端CSS控制布局的策略, 可以采用以下的方法:

  1. 只定义1~2种cell;
  2. 将Cell中元素的布局, 抽取到类似CSS的CellModel中.
  3. 横竖屏切换时, 只需要选择不同的CellModel;
    总体的架构图如下:
    network- layers-3
  • ViewModel负责从Loader中请求网络数据, 相应Controller的数据读取与增删改查;
  • UICollectionView 会把当前文章信息传递给CellFactory, 查找当前文章对应的cell和cellModel;
  • CellFactory查找时, 以文章id和当前屏幕方向组合的key, 尝试从内存里的CellModelCache读取CellModel; 没有则创建新的并写入内存;
  • CellModel里计算的是Cell里各个UI元素的布局位置; Cell最终展示的样式, 由CellModel决定; 因此一个Cell可以使用不同的CellModel, 来展示不同的样式;

遇到的问题

实现后, 基本满足了需求; 但是如果多加载几屏数据后, 首次进行转屏, UI响应速度比较慢; 原因是等到旋转时再计算当前方向的CellModel, 耗时较多;
因为CellModel已经与UI脱离, 存储的只是元素的frame数据, 所以, 在Portrait方向时, 尝试在只线程计算Landscape方向的CellModel, 并缓存到CellModelCache中; 旋转时, 直接读取缓存;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//计算Portrait方向CellModel并layout
MyCellModel *cellModel =
[self cellModelForData:listItem context:context rotation:UIInterfaceOrientationPortrait];
if (cellModel) {
[cell layoutWithCellModel:cellModel context:context];
}

//异步计算Landscape CellModel并缓存
dispatch_async(dispatch_queue_create("CellModelAsyncCalcute", DISPATCH_QUEUE_SERIAL), ^{
@retainify(self);
[self cellModelForData:listItem context:context rotation:UIInterfaceOrientationLandscapeLeft];
});

- (MyCellModel *)cellModelForData:(ListItem *)listItem
context:(CellLayoutContext *)context
rotation:(UIInterfaceOrientation)rotation {

Class cellModelClass = [self p_cellModelClassForData:listItem context:context];
if (!cellModelClass) {
return nil;
}

//计算当前方向的CellModel
MyCellModel *cellModel = nil;
NSInteger rotationKey = UIInterfaceOrientationIsPortrait(rotation) ? 1 : 0;
NSString *cacheKey = $(@"%@_%@_%@", listItem.idStr, NSStringFromClass(cellModelClass), @(rotationKey));
if (CHECK_VALID_STRING(cacheKey)) {
MyCellModel *cellModel =
AS(MyCellModel, [self.cellModelCache objectForKey:cacheKey]);
if (!cellModel || cellModel.needRecaculate) {
cellModel = [[cellModelClass alloc] init];
[cellModel calculateWithData:listItem context:context];
[self.cellModelCache setObject:cellModel forKey:cacheKey];
cellModel.needRecaculate = NO;
}
}

return cellModel;
}

实际效果

Jietu20181207-170232-HD

本文如对你有帮助,请鼓励下!