TableView之协议托管

TableView之协议托管

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

一、协议托管

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

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

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

1
2
3
4
5
6
7
8
- (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的参数,所以,他们的关系如下图:

二、定制UITableViewCell

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
Cell对应的Object需要实现的方法
*/
@protocol AUUCellObject <NSObject>
@required
/**
当前object对应的cell的class
*/
- (Class)cellClass;
/**
是否是可复用的
*/
- (BOOL)reuseable;
@end

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
UITableViewCell 需要实现的协议方法
*/
@protocol AUUCell
@required
/**
当前cell的高度
*/
+ (CGFloat)heightWithObject:(id <AUUCellObject>)object;
/**
每次显示一个cell的时候都调用的方法,用于刷新cell上的数据
*/
- (BOOL)shouldUpdateWithObject:(id <AUUCellObject>)object;
@end

三、数据展示

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

1
2
3
4
5
6
7
8
@interface AUUTableSection : NSObject
/**
此分组下的cell object,就是每个cell所对应的数据信息
*/
@property (nonatomic, strong) NSMutableArray <id <AUUCellObject>> *rows;
@end

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 设置分组的个数
- (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给我指定的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
AUUTableModel需要实现的方法
*/
@protocol AUUCellFactory
@required
/**
获取指定位置的object
@param tableView 当前的tableView
@param indexPath 当前的索引
@return object
*/
- (id <AUUCellObject>)objectForTable:(UITableView *)tableView atIndexPath:(NSIndexPath *)indexPath;
@end

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

1
2
3
4
5
6
7
8
9
10
11
12
- (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和数据的控制都是通过协议来做的,目的当然很明确,就是为了能够减少耦合,所以,为了在项目里写的方便,可以在实现的部分写一个基类来简化一些重复的工作,如:

  • BaseCell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@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
  • BaseObject
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@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的时候,就可以如下写了:

  • .h
1
2
3
4
5
6
@interface AUUTextCell : AUUBaseCell
@end
@interface AUUTextObject : AUUBaseObject
+ (instancetype)objectWithText:(NSString *)text;
@end
  • .m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@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下面我们就来添加数据:

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

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

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

六、事件响应

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

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

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

响应对应缓存

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

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

提供外部注册事件的方法

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

  • 单一对象的事件监听,只监听这一个数据对应的cell的点击事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 通过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;
}
  • 某一类对象的监听,监听这一类及其子类的点击事件
1
2
3
4
5
6
7
8
9
// 通过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这么一个协议,那么数据肯定不在话下了,既然能拿到数据,那就能找到其对应的回调操作,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
- (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结果了。

1
2
3
4
5
6
7
8
9
10
11
12
[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开放出来,于是,作为应用层的一次封装,提供给使用者更多的选择还是很有必要的。
在这里,我推荐去看一看Nimbus(Github)的NITableAction下对于Forward Invocations的一些实现。

Cell的自更新

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

九、总结

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

测试代码