将UITableView封装到极致
介绍
“极致”这种情怀问题,手上做不到没关系,嘴上是肯定要做到的。只要不是能力太打脸,坚持一下下倒是也模棱两可。
本文参考了更轻量的 View Controllers ,对table用到的两个个协议,进行了不同思路的封装。这段时间辞职避暑,时间大大的有,整理下这一年的经验,分享给大家。
代码在这github
行业需求
我也不知道是不是网易新闻客户端的问题,近年来,大量只用过网易新闻客户端的小伙伴就出来做产品了(当然,他们也摇过微信)。再加上无app不web的思想,造就了大量的套皮app。
在感谢其提供大量工作机会的同时,也不免吐槽下,对于这种app,大量的工作无非就是请求几下json,展示到table里。然后加个MJ或者EGO,做下缓存。你需要知道的仅仅是哪个json字段对应哪个label,仅此而已。
这本是脚手架该干的事情啊。
不管你是否对代码质量有要求,简化这种机械化劳动都是一件符合人性的事。
<UITableViewDataSource>
分析
就先从<UITableViewDataSource>入手。
遵从这个协议,主要是给table提供数据源。大致可以分为这么几种。
-、基本数据,也就是那两个@required方法,提供table每个Section的行数,以及每个行数所应该返回的cell。
二、提供table中Sections的数量。
三、Section的Header和Footer中的文字。
四、table中cell移动和删除操作的数据源支持。
五、提供右边索引的数据源
让我把这些功能全部封装,我是拒绝的,我可以重写一遍table,但是使用者一定会骂我,说这个不好用,根本没有这样的table。根据我的经验(曾一下午写了10多个table)。最常用的功能就是一和二。
简单table的实现
声明一个类WELDataSource,实现<UITableViewDataSource>,并将其作为table的dataSource,然后在cellForRowAtIndexPath中调用block,进行cell的配置。
WELDataSource.m代码如下
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return !m_Models ? 0: m_Models.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:self.cellIdentifier
forIndexPath:indexPath];
id model = [self modelsAtIndexPath:indexPath];
self.cellConfigureBlock(cell, model);
return cell;
}
@end
在ViewController中的使用方法大概如下,
- (void)viewDidLoad {
[super viewDidLoad];
_dataDelegate = [[WELDataSource alloc] initWithIdentifier:@"Cell" configureBlock:^(UITableViewCell *cell, id model) {
cell.textLabel.text = model;
}];
_table.dataSource = _dataDelegate;
[_dataDelegate addModels:@[@"a",@"b",@"c"]];
[_table reloadData];
}
另外,和更轻量的 View Controllers 中有一点不一样。
管理数据是通过一个类型为可变数组的实例变量来实现的。
#import "WELDataSource.h"
@interface WELDataSource () {
NSMutableArray *m_Models;
}
并提供增加方法
- (void)addModels:(NSArray *)models {
if(!models) return;
if(!m_Models) {
m_Models = [[NSMutableArray alloc] init];
}
[m_Models addObjectsFromArray:models];
}
这么做的原因是因为,很多时候table里的数据都是从网络请求过来的,并且会有分页。有了这个方法,只需要将请求回来的数组传入addModels:,然后reloadData就可以了,无需进行任何判断。同时,init方法,去掉了传数组这个参数。每次传个nil,也是挺无聊的。
UICollectionView也一样
UICollectionView是个很强大的控件,但很多时候,仅仅是用它来做一些简单的展示。
两者的dataSource在只有一个section的时候,逻辑是一样的,所以来兼容下Collection。
实现UICollectionViewDataSource协议
@interface WELDataSource : NSObject <UITableViewDataSource,UICollectionViewDataSource>
实现这两个方法
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return !m_Models ? 0: m_Models.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:self.cellIdentifier forIndexPath:indexPath];
id model = [self modelsAtIndexPath:indexPath];
self.cellConfigureBlock(cell, model);
return cell;
}
代码很简单,这样在只有一个section的时候,就可以直接使用WELDataSource而无需考虑是table,还是Collection。
还能更简单
像我这种懒人,代码是能不写就不写的。像给table设置dataSource这种事,能拖线,则脱线。而且对于使用storyboard的我,每每把cell的identifier复制到代码里,也是挺累的。所以,如果使用storyboard,那么代码可以写成这个样子。
- (void)viewDidLoad {
[super viewDidLoad];
[_dataDelegate addModels:@[@"a",@"b",@"c"]];
[_table reloadData];
}
来分析下。
首先是WELDataSource的初始化,这里传了两个个参数,第一个是cell的Identifier。然后是一个回调,用来给cell上的view赋值。初始化之后,将其设置为table的datasource。
先搞掉这句代码。
_table.dataSource = _dataDelegate;
这里使用StoryBoard中的object。
拖一个到vc里,然后将其class设置为WELDataSource。之后,就可以通过“拉线”的方式,将table的dataSource 设置为object。
由于使用了object,调用者不需要手动去init,但是参数还是得传。对于Cell的重用Id,这个可以使用IBInspectable修饰,在storyboard上直接进行复制。接着就是那个block。block里面的代码,一般就是用一个model给cell上的元素赋值。对于简单的业务,这个过程并不需要VC参与。我们可以让cell遵守一个协议,由WELDataSource直接通知cell。
其实我本身并不赞同这种封装,这种方式跳过了VC,让我感觉比较不灵活,但使用了一段时间,我感觉VC其实并没有怎么参与这个过程。跳过了也就跳过了。。
于是cell实现个类似这样的协议
@protocol CellConfigure <NSObject>
-(void)configureCellWithModel:(id)Model;
@end
VC只需要add数据,然后reloadData就可以了。
当然,也有折中方案。
实现如下block
typedef void (^CellConfigureBefore)(id cell, id model, NSIndexPath * indexPath);
在cellForRowAtIndexPath中这样写。
if(self.cellConfigureBefore) {
self.cellConfigureBefore(cell, model,indexPath);
}
if ([cell respondsToSelector:@selector(configureCellWithModel:)]) {
[cell performSelector:@selector(configureCellWithModel:) withObject:model];
}
于是,可以自由的选择,是否要VC参与配置cell。
不如,一行代码也不要写
思路大致是这样,WELDataSource保留一个对table的弱引用,数据请求层直接提供对WELDataSource的支持,在add之后,直接reloadData。
调用代码可能会简化成这样。。
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self loadNextPageWithDataSource:_dataDelegate];
}
不去实现复杂的数据源
想了想,我还是删除了多cell和多section的情况。封装这个的初衷是为了简单,快速。面对复杂的情况,意味着需要更多的block,block里需要更多的代码。这时候,写进一个初始化方法中,会显得比较臃肿,反倒不如原生的delegate看着舒服。
<UITableViewDelegate>怎么办?
主要问题是代码复用
看下面这一段代码,这段代码用来解决ios8中cell下面的线,左面不能顶到头的问题。
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{
if ([tableView respondsToSelector:@selector(setSeparatorInset:)]) {
[tableView setSeparatorInset:UIEdgeInsetsZero];
}
if ([tableView respondsToSelector:@selector(setLayoutMargins:)]) {
[tableView setLayoutMargins:UIEdgeInsetsZero];
}
if ([cell respondsToSelector:@selector(setLayoutMargins:)]) {
[cell setLayoutMargins:UIEdgeInsetsZero];
}
}
类似这种代码,怎么灵活的复用呢?
是否可以按照DataSoure的思路,简单的将table的delegate设置为另一个类呢?答案显然是否定 的。<UITableViewDelegate>中的方法较多,且一些回调方法需要频繁的和VC交互,封装出的Delegate很可能比较庞大,或者仅仅是把Delegate用block重写了一次,很是画蛇添足。
然后我想到的是Category,不过这个想法很快就被我否决 了。对于系统的方法使用Category还是存在风险的。在分类中实现的方法,不管是否import,都可以respondsToSelector到。也 就是说,在分类中实现了dalegate的一个方法,就等于继承自该类的子类都实现了这个方法。
我曾经接手过一个没有文档的app,里面差不多70多个VC。为了快速知道哪个页面对应的是哪个Class,我随便写了这么一个Category。倒是挺好用的。
@implementation UIViewController (VCChat)
-(void)viewDidAppear:(BOOL)animated {
NSLog(@"===%@===",NSStringFromClass([self class]));
}
@end
如果项目中的VC有统一的父类,就可以把代码写在父类中,然后用一个bool属性来选择是否开启该功能。
但是,如果你没使用父类,或者你根本不打算使用父类。那么正片来了。
写一个过滤器
写一个类WELTableDelegate,作为Table的Delegate。
由WELTableDelegate来决定,是自己处理委托事件,还是交由UIViewController去处理。这样,就可以把一些固定功能的代码放入其中,而且保证UIViewController可以随意定制table。
直接上代码了
@interface WELTableDelegate : NSObject <UITableViewDelegate>
@property (nonatomic, weak) IBOutlet id <UITableViewDelegate>viewController;
@end
@implementation WELTableDelegate
- (id)forwardingTargetForSelector:(SEL)aSelector {
if([super respondsToSelector:aSelector]) {
return self;
} else if ([self.viewController respondsToSelector:aSelector]) {
return self.viewController;
}
return self;
}
- (BOOL)respondsToSelector:(SEL)aSelector
{
return [super respondsToSelector:aSelector] || [self.viewController respondsToSelector:aSelector];
}
代码主要是运用了oc的消息转发机制,做了一层过滤。
可以把本文最上面的方法写入WELTableDelegate中,也可以写入如下代码,用来实现一个简单的反选动画效果。
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
if([self respondsToSelector:@selector(tableView:didSelectRowAtIndexPath:)]) {
[self.viewController tableView:tableView didSelectRowAtIndexPath:indexPath];
}
}
另外,可以使用一些BOOL类型的属性来选择是否开启这个功能,在Storyboard中进行勾选,很是方便。
总结
只要是想封装,总是可以封装的。