美文网首页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中进行勾选,很是方便。

    总结

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



    相关文章

      网友评论

      • 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