将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中进行勾选,很是方便。

总结

只要是想封装,总是可以封装的。



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

推荐阅读更多精彩内容