iOS常看iOS知识收集

将UITableView封装到极致

2015-06-08  本文已影响16131人  WELCommand

介绍

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

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

总结

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



上一篇下一篇

猜你喜欢

热点阅读