封装一个简单易用的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
,这个类遵循 UITableViewDelegate
和 UITableViewDataSource
这两个协议,并实现了这两个协议中的一些方法。所以,它的主要作用就是同时作为 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 回调出来;它的 name
和 age
表示对 cell 的设置。根据cellClass
, TableViewProxy
才知道创建什么样的 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 内的点击事件。
综上,通过这个框架创建列表的步骤如下:
- 创建
TableViewProxy
的实例 proxy,并使 tableView.delegate = tableView.dataSource = proxy。 - 根据需要子类化
BaseCell
和BaseCellModel
,或者定义BaseCellDelegate
的子协议。 - 创建
BaseCellModel
的数组,同时对每个BaseCellModel
的cellClass
和cellDelegate
进行设置。 - 在
BaseCellModel.cellDelegate
所对应的类里实现BaseCellDelegate
子协议的方法。 - 设置
TableViewProxy.getDataSource
返回刚才创建的BaseCellModel
数组。 - 调用 [tableView reloadData] 刷新列表。
- 每当
BaseCellModel
数组或数组中的元素发生了变化,调用 [tableView reloadData] 刷新列表。