封装一个简单易用的UITableView框架

平常我们使用 UITableView 创建列表主要关心的是:cell/header/footer 的创建和高度的设置,以及点击事件的处理。我们通常将 controller 作为 tableView 的 delegate 和 dataSource,然后在 controller 中实现所需要的代理方法。假如 cell 中有多个 button,点击不同的 button 需要发送不同的网络请求,这时我们通常给 cell 设置多个block 作为 button 的点击回调,这样我们就可以在 controller 中通过这些 block 发送相应的网络请求。

遇到的问题

block问题

block 使代码变的简单,但是有时候也存在一些问题:比如在block里该使用 weakSelf 的时候却使用了 self;比如使用者有时候搞不清什么时候需要设置block,什么时候不需要设置,而 protocol 可以通过 @required@optional 比较好的解决这个问题。还有一个我个人认为比较影响代码美观的问题就是,block会额外增加一个代码缩紧,当然很多人可能并不太在意这个问题。我并不反对使用 block,我也经常使用,它可以让你的代码逻辑变得简单紧凑,便于阅读,但是有时候, protocol 可能更合适一些。

计算高度问题

还有一个关键的问题就是如何计算cell的高度。从 iOS7 开始可以通过使用自动布局来解决这个问题,只要我们设置好了 cell 内的约束,然后进行如下设置:

self.tableView.rowHeight = 44.0f;
self.tableView.estimatedRowHeight = UITableViewAutomaticDimension;

这时系统就会自动帮我们计算出 cell 的高度,而且我们也不需要再实现这个讨厌的方法:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath

对于 header 和 footer,通过自动布局采用类似的方式,系统也可以自动计算它们的高度,不再赘述。

对于一些老旧代码,尽管当时iOS可能已经出现了自动布局,但考虑到iOS版本的兼容性问题,以及自动布局带来的性能损耗(据说性能问题一直到iOS12才得到了很好的解决),所以至今仍然在使用 frame 进行布局。

cell有多种类型的情况

如果上面提到的计算cell高度因为自动布局的引入而不算作一个问题的话,那么处理多种类型的cell对很多人来说确实是个棘手的问题。因为一不小心我们就会因为 if - else 的过多使用,让本就复杂的业务代码显得丑陋而臃肿。有没有一种更优雅的方式来处理多种类型的cell呢?

cell的代码复用问题

通常我们所说的cell复用指的是它在内存上的复用。一个cell在滑出 tableView 之后,它不会被释放掉,当一个同类型的 cell 在滑入 tableView 时,就会使用那个刚滑出的 cell,并对这个cell进行重新设置,而不是再重新创建一个 cell。这样可以提高滑动性能,因为对CPU来说,cell 的频繁创建和释放是一个耗时的操作,而iOS的屏幕会在1/60秒刷新一次,如果在这个时间内,tableView 还没有完成 cell 的创建、初始化和布局等操作,GPU就无法计算这一帧的内容,从而导致丢帧。所以当 tableView 快速滑动的时候,如果没有进行cell复用,就会很容易产生卡顿问题。

但是我们现在要说的不是cell在内存上的复用,而是在代码层面的复用。一般情况下,我们会用一个 model 对 cell 进行设置。为了方便,我们甚至给 cell 添加一个相应的 model 属性,在 cell 内部进行设置,这样就导致了 cell 和 model 之间的耦合。为了解耦,我们需要 cell 对外提供一些需要设置的属性,然后在 cell 外的某个地方对 cell 进行设置。这样,我们的 cell 就会更加通用。

代码块的稳定性

我经常会对某一块代码(这块代码可能是一个类,也可能是一个方法,一个接口,一个函数)进行分级,分级的标准是"稳定性"。所谓"稳定性"指的是代码未来变动的可能性大小。我们的业务代码会经常随着业务需求的改变而变动,所以这部分代码就是不稳定代码。我们所依赖的第三方库属于稳定代码,如果没有什么重大bug或者特殊需要,你甚至很少升级你的第三方库。我们一般不应该直接在业务代码里直接使用第三方SDK,而是应该封装一个中间层,这个中间层有两个作用:一是解耦,二是便于业务调用。这个中间层代码的稳定性介于业务代码和第三方SDK之间,是"比较稳定"代码,它基本能满足你业务层面的所有需要,但是随着业务复杂度的增加,这部分代码可能将不再适用,这时候就需要进行修改。

当然这个稳定性在每块代码里还可以细分,不再多说。我为啥要给代码分"稳定性"?因为在设计代码块之间的依赖关系时,"稳定性"是我要重点思考的一个问题,也就是说,我要设计的这块代码,它的"稳定性"应该是怎样的,它所依赖的代码的稳定性是怎样的,依赖它的代码的稳定性是怎样的。而我所遵循的一个基本原则是:稳定性低的代码要依赖稳定性高的代码,而不是相反。稳定性是一个相对的概念,也就是说,代码的稳定性是相对于项目中其他地方的代码而言的,而不能孤立的说它的稳定性。一般来说,越稳定的代码,通用性越好。我们要根据具体情况写出相应"稳定度"的代码,而不是过分的追求稳定性。明确代码的定位,把握好代码的稳定度,能给我减少很多麻烦。

数据驱动

很多时候,我们希望自己的代码是这样工作的:首先进行配置信息的设置,然后提交配置信息,最后页面自动刷新。更进一步来说,我们希望只要监听到配置信息的变化,配置信息的提交和页面的刷新都是自动完成的,或者说都是在框架内完成的。这也是很多前端框架的基本思想,比如 vue 和微信小程序。我今天要说的这个框架也简单的借鉴了这种思想。

首先,我们创建一个通用类 TableViewProxy,这个类遵循 UITableViewDelegateUITableViewDataSource 这两个协议,并实现了这两个协议中的一些方法。所以,它的主要作用就是同时作为 tableView 的 delegate 和 dataSource。这个类主要对外提供了一个 block 类型的属性:

@property (nonatomic, copy) NSArray<BaseCellModel *> *(^getDataSource)(TableViewProxy *proxy);

这个属性的意思很明显,就是从外界直接获取数据源。注意这里有一个 BaseCellModel 类,这个类主要有两个属性:

@property (nonatomic, copy) Class cellClass;
@property (nonatomic, strong) id<BaseCellDelegate> cellDelegate;

这个类一般需要子类化后使用,比如:

@interface Person: BaseCellModel

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;

@end

现在这个 Person 类其实就是一个配置信息,它的 cellClass 表示要创建一个什么类型的 cell;它的 cellDelegate 表示 cell 内部的一些操作要通过这个 delegate 回调出来;它的 nameage 表示对 cell 的设置。根据cellClassTableViewProxy 才知道创建什么样的 cell。现在说一下这个 cell 的类型,它继承自 BaseCell

@interface BaseCell : UITableViewCell

@property (nonatomic, strong) BaseCellModel *cellModel;
@property (nonatomic, weak) id<BaseCellDelegate> cellDelegate;

@end

BaseCell 的定义可以看出,TableViewProxy 在创建了 cell 之后,会将 BaseCellModel.cellDelegate 赋值给 cell.cellDelegate,同时将 BaseCellModel 赋值给 cell.cellModel。所以,在子类化 BaseCell 的时候,只要我们重写了 cell.cellModel 的 setter 方法就对 cell 进行了设置。有时候为了避免 cell 和 model 耦和,我们不希望通过这种方式设置 cell,这时候 cell.cellDelegate 就派上了用场。我们看下 BaseCellDelegate 的定义:

@protocol BaseCellDelegate <NSObject>

@optional
- (void)setModel:(BaseCellModel *)model forCell:(BaseCell *)cell;

@end

当要求某种类型的 cell 可以使用不同类型的 model 时,需要设置 model.cellDelegate,而且该 delegate 需要遵循 BaseCellDelegate 协议,并实现 - (void)setModel:(BaseCellModel *)model forCell:(BaseCell *)cell 方法来对 cell 进行设置。这样就可以创建不依赖于特定 model 的 cell,使 cell 更具有通用性。

当然 BaseCellDelegate 最主要的作用还是将 cell 内的一些操作回调出来,这时候我们就需要定义一个 BaseCellDelegate 的子协议:

@protocol PersonCellDelegate <BaseCellDelegate>

- (void)didTouchNameInPersonCell:(PersonCell *)cell;

@end

假如当 PersonCell 内点击了 name 标签时会发生如下调用:

if ([self.cellDelegate respondsToSelector:@selector(didTouchNameInPersonCell:)]) {
    [self.cellDelegate didTouchNameInPersonCell:self];
}

所以设置的 model.cellDelegate 需要遵循 PersonCellDelegate 协议,并实现 - (void)didTouchNameInPersonCell:(PersonCell *)cell 方法来处理这个 cell 内的点击事件。

综上,通过这个框架创建列表的步骤如下:

  1. 创建 TableViewProxy 的实例 proxy,并使 tableView.delegate = tableView.dataSource = proxy。
  2. 根据需要子类化 BaseCellBaseCellModel,或者定义 BaseCellDelegate 的子协议。
  3. 创建 BaseCellModel 的数组,同时对每个 BaseCellModelcellClasscellDelegate 进行设置。
  4. BaseCellModel.cellDelegate 所对应的类里实现 BaseCellDelegate 子协议的方法。
  5. 设置 TableViewProxy.getDataSource 返回刚才创建的 BaseCellModel 数组。
  6. 调用 [tableView reloadData] 刷新列表。
  7. 每当 BaseCellModel 数组或数组中的元素发生了变化,调用 [tableView reloadData] 刷新列表。

results matching ""

    No results matching ""