美文网首页iOS常看iOS知识收集
将UITableView封装到极致

将UITableView封装到极致

作者: WELCommand | 来源:发表于2015-06-08 00:58 被阅读16131次

介绍

“极致”这种情怀问题,手上做不到没关系,嘴上是肯定要做到的。只要不是能力太打脸,坚持一下下倒是也模棱两可。

本文参考了更轻量的 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中进行勾选,很是方便。

总结

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



相关文章

  • 将UITableView封装到极致

    介绍 “极致”这种情怀问题,手上做不到没关系,嘴上是肯定要做到的。只要不是能力太打脸,坚持一下下倒是也模棱两可。 ...

  • 将TableView封装到极致集成上下拉刷新

    1.完成对tableview的delegate与dataSource完全剥离,并集成上下拉刷新 直接上demo ...

  • 从javascript来看面向对象编程。

    面向对象三大特性: 封装 继承 多态 1.什么是封装: 顾名思义,封:封闭,装:装起来,将具体的实现细节装到一个容...

  • 04.less中的混合,带参数混合

    less中的混合 什么是less中的混合(Mixin)?将需要重复使用的代码封装到一个类中,在需要使用的地方调用封...

  • 编辑UITableView

    UITableView有一个名为editing的属性,如果将editing属性设置为YES,UITableView...

  • D基础

    访问控制 封装:将数据封装到类的内部,将算法封装到方法中。 封装结果:存在但不可见 修饰符: public:任何位...

  • 将ipa 安装到simulator

    1.将xx.ipa 改成xx.zip,解压得到xx.app 2.打开模拟器 3.打开终端,运行xcrun simc...

  • 将TinyCore安装到硬盘

    将Tiny Core Linux安装到硬盘 Tiny Core Linux 上次提过TinyCoreLinux,这...

  • npm install

    -g 将模块安装到全局,具体安装到磁盘哪个位置,要看 npm config prefix 的位置。npm inst...

  • FutureTask demo

    将callable 封装到FutureTask 将futureTask submit 到线程池,并测试cancel...

网友评论

  • Riven2018:创建 datasource 的 init 方法里带 block 有什么作用?
  • AliceJordan:谢谢您 最近也在想让C变个更加清凉正好用上
  • Ko_Neko:不知是否有demo可以分享一下?
  • 中秋梧桐语:返回cell个数的时候,为什么不直接return m_Models.count?
    还有提供的外部方法 addModels感觉不怎么实用.
  • b0478c8debfd:感觉看不懂,是我水平太差么?
  • 啷里个啷_:大神 看了下你的那篇瀑布流的文章 没看懂对于不同类型的cell的处理 能简单介绍下嘛~3Q
  • 1b2ae550dc99:项目中需要的cell样式很多种,我是否可以直接继承WELDataSource,重写- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath就可以?
  • kayakaya:好厲害,如果有swift版就好
  • 咖啡bu加糖:有个地方不懂 _cellConfigureBefore = [before copy];这个block是什么意思啊还有 return m_Models.count > indexPath.row ? m_Models[indexPath.row] : nil;这句是判断什么,求指导下
  • 屈涯: 给大家分享一下代码版的,
    UITableView *table = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, 320, 400) style:UITableViewStyleGrouped];
    [table registerClass:[UITableViewCell class] forCellReuseIdentifier:@"cell"];
    _dataDelegate = [[WELDataSource alloc] initWithIdentifier:@"cell" configureBlock:^(UITableViewCell *cell, id model, NSIndexPath *indexPath) {
    cell.backgroundColor = [UIColor redColor];
    }];
    table.dataSource = _dataDelegate;
    [_dataDelegate addModels:@[@"a",@"b",@"c"]];
    [table reloadData];
    [self.view addSubview:table];
  • 屈涯:写的太好了,我苦苦找寻的
  • WELCommand:统一回复下。需要多cell的朋友可以参考我那篇瀑布流的文章。里面源码里有考虑多cell的实现。
  • 88681fd1c140:你好 如果我在程序中 添加下拉刷新和上拉加载更多 比如刷新方法 请求服务器数据 再次调用

    - (void)addModels:(NSArray *)models 方法 cell 上数据多出来一倍 是怎么回事呢
    WELCommand:@呆毛酱紫 我记得在.h里有个API是专门处理这种情况的。你可以看看源码。
  • 咖啡bu加糖:如果多个分组,注册不同的cell,我应该加一些什么方法呢
  • 78b528600eff:forwardingTargetForSelector这个方法最后返回self会不会造成无限循环?
  • 常义:值得研究
  • 像羽毛那样轻: 关于cell 的重复问题 不考虑一下吗?
  • zhiyi:有个问题,如果用dequeueReusableCellWithIdentifier:forIndexPath:的话,我还要在vc里写registerNib:或者registerClass:,如果按照现在的习惯使用dequeueReusableCellWithIdentifier:的话,那么我的if (nil == cell) { }又打算写在哪里呢,同时又要考虑cell一般都是自定义cell,那么cell的类型又在哪里动态配置呢?
  • AltriaKassem:略开明
  • 花前月下:666 之前也见过封装的很棒的tableview 今天又见到了另一种思路。开阔了视野哇
    中秋梧桐语:@花前月下 给我也发一份学习学习。
    咖啡bu加糖:@花前月下 还有好的tableview封装,有链接或者图片截图发给我看看可以么,学习一下
  • bde04638cca8:你好,我今天刚好再看objc中国的文章的时候,看到你的,对比了一下你的demo,确实要简洁。但是我看了你的文章主要是用storyboard,而我喜欢.h .m .xib。看到你的代码中- (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];
    }

    cell.textLabel.text = model;我很不解,这个cell你是怎么获取到他的textLabel,这不是一个普通的UITableViewCell嘛,还是你贴出来了什么,求解答
    bde04638cca8:@WELCommand 好的,谢谢解答,我感觉这样走这个configureblock两次,楼主看过retablemanager吗,感觉也挺不错的
    WELCommand:@MomoPush _dataDelegate这玩意有个cellid的属性,你可以在代码中设置。configureBlock 这个会根据你的cellid来返回相对应的cell。至于textLabel 这个就是普通UITableViewCell的一个属性
  • 03a78e1a59af:真的写得很给力!一开始自己也傻乎乎写,后面也学会偷懒封装着搞起,只是没有你分析得这么透彻,期待下一篇!
  • 03a78e1a59af:我想要一下示例代码,可以不?
  • WELCommand:@sasukiki 哈哈 原来是 ==nil 改过来后忘了加!
  • fe22e5891cb4: return m_Models ? 0: m_Models.count; 这里错了,,, 位置掉转哦
    fe22e5891cb4:@CodingSha :joy: 他后来改过来了
    CodingSha:@sasukiki 你看错了,是return !m_Models ? 0: m_Models.count,前面是取非的
  • WELCommand:@暗夜血狐 我这个默认是和ib进行配合 重用ID 在ib中进行设置 然后 如果和类名设置一样 可以在代码中直接com点击 进入cell类 不然还得切入到ib 看cell的类
  • 暗夜血狐:IBInspectable cellIdentifier 我不大明白这个 的名字为啥必须是是cell的类名字 WELWordCell ? 能解释一下嘛
  • c03dc17e4ef7:今天无意中发现的这个篇文章,发现table还有这么强大封装,期待下一篇!!!
  • WELCommand:@xclidongbo 我只是抱砖引玉啊,欢迎大家相互学习
  • xclidongbo:很多东西只有理论,没有demo.终于有人出来做tableView的封装了.

本文标题:将UITableView封装到极致

本文链接:https://www.haomeiwen.com/subject/cytdqttx.html