TableView之协议托管

2018-03-02  本文已影响13人  Joyous

auu.space

TableView之协议托管

一直以来,UITableView在我们的程序开发中都扮演着一个重要的角色,随着开发者的日益增多和需求的不断变化,就出现了各种各样的写法,今天我们来说一种很早以前就出现的方式,可以叫做协议托管(我是没见过别处专业的叫法,暂且如是称呼吧)。
其实这种模式早期是在Facebook开源的Three20就有了,不过这个库最后不再更新;随后这个开发团队的一个开发者做了又一套类似的库Nimbus,文档很全,用着也很方便,但是更新了一段时间以后又停止了维护。当然了,这两个库也是包含了很多的内容,今天所说的也只是其中的一部分。
现在由于swift的大热,又出现一个很强势的库Eureka,有兴趣的也可以去看一看。

一、协议托管

看了上篇文章或者没看的,都知道,对于一个UITableView来说,我们主要的时间都是花在其两个协议上,而且由于苹果对协议方法的分散,让我们在多个地方做了过多重复的工作,那么,第一步我们就来接管这两个协议:

  1. 新建一个类AUUTableModel来接管UITableViewDataSource,用来处理TableView的数据源。
  2. 新建一个类AUUTableAction来接管UITableViewDelegate,用来处理TableView的方法回调、事件控制等等。

此时我们的TableView可以这样写了:

- (UITableView *)tableView {
    if (!_tableView) {
        _tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStyleGrouped];
        _tableView.dataSource = self.tableModel;
        _tableView.delegate = self.tableAction;
    }
    return _tableView;
}

tableModeltableAction里接收的都是一些协议方法,因为几乎所有的方法里都带有UITableView的参数,所以,他们的关系如下图:

image

二、定制UITableViewCell

我们这里主要讲的就是接管整个tableView的定制和处理,减少外部不断判断所出现的代码浪费,那么我们就需要一个类来管理每个cell对应的数据,这里我们来用一个协议来做限定,让使用者可以自由定制:

/**
 Cell对应的Object需要实现的方法
 */
@protocol AUUCellObject <NSObject>

@required

/**
 当前object对应的cell的class
 */
- (Class)cellClass;

/**
 是否是可复用的
 */
- (BOOL)reuseable;

@end

现在我们接管每个cell的数据源,但是我们还并不知道每个cell到底长啥样,那么我们再来用一个协议限定cell的一些属性:

/**
 UITableViewCell 需要实现的协议方法
 */
@protocol AUUCell

@required

/**
 当前cell的高度
 */
+ (CGFloat)heightWithObject:(id <AUUCellObject>)object;

/**
 每次显示一个cell的时候都调用的方法,用于刷新cell上的数据
 */
- (BOOL)shouldUpdateWithObject:(id <AUUCellObject>)object;

@end

三、数据展示

现在TableView的协议我们也接管过来了,cell和数据我们也先定好了,就可以来显示数据了。
对于一个tableView是有多个分组的概念的(虽然我们有时也能看到只有一个分组的情况,但是这不也是分组么),在分组下管理的是这个分组的各个cell,但是这里我们管理的是每个分组下的数据信息,下面我们就来定义一个分组的模型(暂时就设置这么简单的一个属性):

@interface AUUTableSection : NSObject

/**
 此分组下的cell object,就是每个cell所对应的数据信息
 */
@property (nonatomic, strong) NSMutableArray <id <AUUCellObject>> *rows;

@end

然后在定义一个属性来存所有的分组:

/**
 table中的分组
 */
@property (nonatomic, strong) NSMutableArray *sections;

数据现在有了,那么就可以开始以前的一项重复的工作来显示各条数据了,但是现在我们只需要在tableModel里写一次即可:

// 设置分组的个数
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return self.sections.count;
}

// 设置每个分组下的行数
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return [[[self.sections objectAtIndex:section] rows] count];
}

// 设置每行的cell视图
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    // 获取当前tableView的indexPath这个位置对应的数据信息
    id <AUUCellObject> object = [self objectForTable:tableView atIndexPath:indexPath];
    
    // 拼接复用的标识,如果是可复用的cell,直接用cell的类名来当做标识
    NSString *identifier = NSStringFromClass(object.cellClass);
    if (!object.reuseable) {
        // 如果不可复用,就拼接上数据的hash值,来控制唯一性
        identifier = [identifier stringByAppendingFormat:@"%@", @([(NSObject *)object hash])];
    }
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
    if (!cell) {
        // 根据数据信息对应的cell类型创建实例
        cell = [[object.cellClass alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
    }
    
    // 更新cell数据
    if ([cell conformsToProtocol:@protocol(AUUCell)] && [cell respondsToSelector:@selector(shouldUpdateWithObject:)]) {
        [(id <AUUCell>)cell shouldUpdateWithObject:object];
    }
    
    return cell;
}

四、高度控制

cell的视图我们创建好了,但是高度怎么控制呢?高度设置是在tableViewdelegate里就是现在的tableAction,但是数据都在dataSource里就是现在的tableModel,看看上面画的关系图,我们可以定义一个协议来要求tableModel给我指定的数据:

/**
 AUUTableModel需要实现的方法
 */
@protocol AUUCellFactory

@required

/**
 获取指定位置的object

 @param tableView 当前的tableView
 @param indexPath 当前的索引
 @return object
 */
- (id <AUUCellObject>)objectForTable:(UITableView *)tableView atIndexPath:(NSIndexPath *)indexPath;

@end

拿到了数据信息,那么高度的话就好说了:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    if ([tableView.dataSource conformsToProtocol:@protocol(AUUCellFactory)] &&
        [tableView.dataSource respondsToSelector:@selector(objectForTable:atIndexPath:)]) {
        // 拿到当前位置的数据信息
        id <AUUCellObject> object = [(id <AUUCellFactory>)tableView.dataSource objectForTable:tableView atIndexPath:indexPath];
        if ([object.cellClass respondsToSelector:@selector(heightWithObject:)]) {
            // 获取高度
            return [object.cellClass heightWithObject:object];
        }
    }
    return 44.0;
}

五、测试一下

实现部分的基类

因为我们上面做的对于cell和数据的控制都是通过协议来做的,目的当然很明确,就是为了能够减少耦合,所以,为了在项目里写的方便,可以在实现的部分写一个基类来简化一些重复的工作,如:

@interface AUUBaseCell : UITableViewCell <AUUCell>
- (void)initialize;
@end

@implementation AUUBaseCell
+ (CGFloat)heightWithObject:(id<AUUCellObject>)object {
    return 44.0;
}
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
        [self initialize];
    }
    return self;
}
- (void)initialize { }
- (BOOL)shouldUpdateWithObject:(id<AUUCellObject>)object {
    return YES;
}
@end

@interface AUUBaseObject : NSObject <AUUCellObject>
+ (instancetype)objectWithClass:(Class)cls;
+ (instancetype)object;
@end

@interface AUUBaseObject()
@property (nonatomic) Class cls;
@end
@implementation AUUBaseObject
+ (instancetype)object {
    return nil;
}
+ (instancetype)objectWithClass:(Class)cls {
    AUUBaseObject *object = [[self alloc] init];
    object.cls = cls;
    return object;
}
- (Class)cellClass {
    return self.cls;
}
- (BOOL)reuseable {
    return YES;
}
@end

举例的cell

下面在实现各个cell的时候,就可以如下写了:

@interface AUUTextCell : AUUBaseCell
@end

@interface AUUTextObject : AUUBaseObject
+ (instancetype)objectWithText:(NSString *)text;
@end
@interface AUUTextObject ()
@property (copy, nonatomic) NSString *text;
@end

@implementation AUUTextObject

+ (instancetype)objectWithText:(NSString *)text {
    AUUTextObject *object = [super objectWithClass:[AUUTextCell class]];
    object.text = text;
    return object;
}

@end

@interface AUUTextCell()
@property (nonatomic, strong) UILabel *label;
@end

@implementation AUUTextCell

+ (CGFloat)heightWithObject:(id<AUUCellObject>)object {
    return  44;
}

- (BOOL)shouldUpdateWithObject:(AUUTextObject *)object {
    self.label.text = object.text;
    return YES;
}

- (void)initialize {
    self.label = [[UILabel alloc] initWithFrame:CGRectMake(20, 0, [UIScreen mainScreen].bounds.size.width - 40, 44)];
    self.label.font = [UIFont systemFontOfSize:14];
    self.label.textColor = [UIColor redColor];
    [self addSubview:self.label];
}

@end

这里呢就先写这一个,更多的可以看一下测试代码。

组装数据到页面

在一开始的时候我们就创建了tableModel下面我们就来添加数据:

[self.tableModel appendObject:[AUUTextObject objectWithText:@"title"]];
[self.tableModel appendObject:[AUUInputObject object]];
[self.tableModel appendObject:[AUUImageObject object]];
[self.tableModel appendObject:[AUUImageScrollerObject object]];

好了,我们添加了4个celltableView里,效果如下:

image

至此,一个tableView已经完美展示。

六、事件响应

上面的操作都只是出于看静态页面的状态,我们需要的是可以交互啊,就像最原始的写法一样:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath

我们在里面收到tableView的调用以后就去做一些事情,但是现在我们的目的是接管这些协议方法,不给外面使用的机会。
为了解决这个问题,就该强化我们的tableAction了。

响应对应缓存

我们来创建一个缓存类_AUUActionCache,用来缓存每一行设定的响应方法,里面包含如下几个属性:

@property (weak, nonatomic) id target;
@property (nonatomic) SEL sel;
@property (copy, nonatomic) AUUCellSelectedAction action;

提供外部注册事件的方法

为了适应不同的需求,我们提供一下两种回调设置方式:

// 通过block回调
- (id<AUUCellObject>)attachObject:(id<AUUCellObject>)object action:(AUUCellSelectedAction)action {
    if (object) {
        [self.objectActions setObject:[_AUUActionCache cacheWithAction:action] forKey:[self keyForObject:object]];
    }
    return object;
}

// 通过方法调用来回调
- (id <AUUCellObject>)attachObject:(id<AUUCellObject>)object target:(id)target action:(SEL)action {
    if (object) {
        [self.objectActions setObject:[_AUUActionCache cacheWithTarget:target selector:action] forKey:[self keyForObject:object]];
    }
    return object;
}
// 通过block回调
- (void)attachClass:(Class)cls action:(AUUCellSelectedAction)action {
    [self.classActions setObject:[_AUUActionCache cacheWithAction:action] forKey:(id <NSCopying>)cls];
}

// 通过方法调用来回调
- (void)attachClass:(Class)cls target:(id)target action:(SEL)action {
    [self.classActions setObject:[_AUUActionCache cacheWithTarget:target selector:action] forKey:(id <NSCopying>)cls];
}

事件响应

在上面我们添加了AUUCellFactory这么一个协议,那么数据肯定不在话下了,既然能拿到数据,那就能找到其对应的回调操作,如下:

- (void)performActionForObject:(id <AUUCellObject>)object {
    if (!object) {
        return;
    }
    
    _AUUActionCache *action = [self.objectActions objectForKey:[self keyForObject:object]];
    if (!action) {
        Class keyClass = [(NSObject *)object class];
        
        for (Class class in self.classActions.allKeys) {
            if ([keyClass isKindOfClass:class] || [keyClass isSubclassOfClass:class]) {
                // 需要做处理,比如  A<-B<-C
                // 我添加了A、B的监听,当我拿C class来取响应者的时候,到底是取谁?
                // 所以这里只是简单的写了一下
                action = [self.classActions objectForKey:(id<NSCopying>)class];
            }
        }
    }
    
    if (!action) {
        return;
    }
    
    if (action.action) {
        action.action(object);
    }
    if (action.target && action.sel && [action.target respondsToSelector:action.sel]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [action.target performSelector:action.sel withObject:object];
#pragma clang diagnostic pop
    }
}

七、再测试一下

响应事件添加了,我们再来看一下实现,跟第一次测试也没多大的却别,跑起来试一下,在控制台就会出现想要的log结果了。

[self.tableAction attachClass:[AUUTextObject class] action:^(AUUTextObject *object) {
    NSLog(@"tap : %@", object.text);
}];
    
[self.tableAction attachClass:[AUUImageObject class] target:self action:@selector(imageAction:)];
    
[self.tableModel appendObject:[AUUTextObject objectWithText:@"title"]];
[self.tableModel appendObject:[AUUInputObject object]];
[self.tableModel appendObject:[self.tableAction attachObject:[AUUImageObject object] action:^(id<AUUCellObject> object) {
    NSLog(@"tap 你点击了这个图片cell");
}]];
[self.tableModel appendObject:[AUUImageScrollerObject objectWithDelegate:self]];

八、更多的操作

这里我们的测试例子,只是很简单的写了一下这个操作过程,不过对于一般不过于复杂的项目来说,貌似这就够用了啊。

非要自己实现一些协议

当然,tableView的协议方法有很多,就算自己再封装一个库也不会做到刚好符合每一个人的需求,而且对于苹果公司来说,随着技术的不断更新和进步,也肯定会有更多的API开放出来,于是,作为应用层的一次封装,提供给使用者更多的选择还是很有必要的。
在这里,我推荐去看一看NimbusNITableAction下对于Forward Invocations的一些实现。

Cell的自更新

响应式设置,就像是集合了ReactiveCocoa的操作一样,每当有数据变动,都能及时的将变动后的结果展示出来,这里的这种写法当然也行啊,就看你怎么封装了。
不过,我推荐去看个现成的内容Eureka,里面对于数据和cell的封装更紧密。

九、总结

这种方式把tableView协议里的操作进行了打散,重点的工作就是前期的tableModeltableAction的定制,在到应用的时候,是把各种类型的判断使用数据缓存来做了区分,这样在添加一个cell的时候就能省去更多的时间和判断逻辑。
不信,可以增加一些增删的测试操作试试,如果用原始的写法来做的话,是不是有种要哭的感觉?

测试代码

上一篇下一篇

猜你喜欢

热点阅读