TableView之協(xié)議托管
一直以來,UITableView
在我們的程序開發(fā)中都扮演著一個重要的角色附井,隨著開發(fā)者的日益增多和需求的不斷變化讨越,就出現(xiàn)了各種各樣的寫法,今天我們來說一種很早以前就出現(xiàn)的方式羡忘,可以叫做協(xié)議托管(我是沒見過別處專業(yè)的叫法谎痢,暫且如是稱呼吧)。
其實這種模式早期是在Facebook開源的Three20
就有了卷雕,不過這個庫最后不再更新节猿;隨后這個開發(fā)團隊的一個開發(fā)者做了又一套類似的庫Nimbus
,文檔很全漫雕,用著也很方便滨嘱,但是更新了一段時間以后又停止了維護。當(dāng)然了浸间,這兩個庫也是包含了很多的內(nèi)容太雨,今天所說的也只是其中的一部分。
現(xiàn)在由于swift的大熱魁蒜,又出現(xiàn)一個很強勢的庫Eureka
囊扳,有興趣的也可以去看一看。
一兜看、協(xié)議托管
看了上篇文章或者沒看的锥咸,都知道,對于一個UITableView
來說细移,我們主要的時間都是花在其兩個協(xié)議上搏予,而且由于蘋果對協(xié)議方法的分散,讓我們在多個地方做了過多重復(fù)的工作弧轧,那么雪侥,第一步我們就來接管這兩個協(xié)議:
- 新建一個類
AUUTableModel
來接管UITableViewDataSource
碗殷,用來處理TableView
的數(shù)據(jù)源。 - 新建一個類
AUUTableAction
來接管UITableViewDelegate
速缨,用來處理TableView
的方法回調(diào)锌妻、事件控制等等。
此時我們的TableView
可以這樣寫了:
- (UITableView *)tableView {
if (!_tableView) {
_tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStyleGrouped];
_tableView.dataSource = self.tableModel;
_tableView.delegate = self.tableAction;
}
return _tableView;
}
在tableModel
和tableAction
里接收的都是一些協(xié)議方法鸟廓,因為幾乎所有的方法里都帶有UITableView
的參數(shù)从祝,所以襟己,他們的關(guān)系如下圖:
二引谜、定制UITableViewCell
我們這里主要講的就是接管整個tableView
的定制和處理,減少外部不斷判斷所出現(xiàn)的代碼浪費擎浴,那么我們就需要一個類來管理每個cell對應(yīng)的數(shù)據(jù)员咽,這里我們來用一個協(xié)議來做限定,讓使用者可以自由定制:
/**
Cell對應(yīng)的Object需要實現(xiàn)的方法
*/
@protocol AUUCellObject <NSObject>
@required
/**
當(dāng)前object對應(yīng)的cell的class
*/
- (Class)cellClass;
/**
是否是可復(fù)用的
*/
- (BOOL)reuseable;
@end
現(xiàn)在我們接管每個cell的數(shù)據(jù)源贮预,但是我們還并不知道每個cell到底長啥樣贝室,那么我們再來用一個協(xié)議限定cell的一些屬性:
/**
UITableViewCell 需要實現(xiàn)的協(xié)議方法
*/
@protocol AUUCell
@required
/**
當(dāng)前cell的高度
*/
+ (CGFloat)heightWithObject:(id <AUUCellObject>)object;
/**
每次顯示一個cell的時候都調(diào)用的方法,用于刷新cell上的數(shù)據(jù)
*/
- (BOOL)shouldUpdateWithObject:(id <AUUCellObject>)object;
@end
三仿吞、數(shù)據(jù)展示
現(xiàn)在TableView
的協(xié)議我們也接管過來了滑频,cell和數(shù)據(jù)我們也先定好了,就可以來顯示數(shù)據(jù)了唤冈。
對于一個tableView
是有多個分組的概念的(雖然我們有時也能看到只有一個分組的情況峡迷,但是這不也是分組么),在分組下管理的是這個分組的各個cell你虹,但是這里我們管理的是每個分組下的數(shù)據(jù)信息绘搞,下面我們就來定義一個分組的模型(暫時就設(shè)置這么簡單的一個屬性):
@interface AUUTableSection : NSObject
/**
此分組下的cell object,就是每個cell所對應(yīng)的數(shù)據(jù)信息
*/
@property (nonatomic, strong) NSMutableArray <id <AUUCellObject>> *rows;
@end
然后在定義一個屬性來存所有的分組:
/**
table中的分組
*/
@property (nonatomic, strong) NSMutableArray *sections;
數(shù)據(jù)現(xiàn)在有了傅物,那么就可以開始以前的一項重復(fù)的工作來顯示各條數(shù)據(jù)了夯辖,但是現(xiàn)在我們只需要在tableModel
里寫一次即可:
// 設(shè)置分組的個數(shù)
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.sections.count;
}
// 設(shè)置每個分組下的行數(shù)
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [[[self.sections objectAtIndex:section] rows] count];
}
// 設(shè)置每行的cell視圖
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// 獲取當(dāng)前tableView的indexPath這個位置對應(yīng)的數(shù)據(jù)信息
id <AUUCellObject> object = [self objectForTable:tableView atIndexPath:indexPath];
// 拼接復(fù)用的標(biāo)識,如果是可復(fù)用的cell董饰,直接用cell的類名來當(dāng)做標(biāo)識
NSString *identifier = NSStringFromClass(object.cellClass);
if (!object.reuseable) {
// 如果不可復(fù)用蒿褂,就拼接上數(shù)據(jù)的hash值,來控制唯一性
identifier = [identifier stringByAppendingFormat:@"%@", @([(NSObject *)object hash])];
}
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (!cell) {
// 根據(jù)數(shù)據(jù)信息對應(yīng)的cell類型創(chuàng)建實例
cell = [[object.cellClass alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
}
// 更新cell數(shù)據(jù)
if ([cell conformsToProtocol:@protocol(AUUCell)] && [cell respondsToSelector:@selector(shouldUpdateWithObject:)]) {
[(id <AUUCell>)cell shouldUpdateWithObject:object];
}
return cell;
}
四卒暂、高度控制
cell的視圖我們創(chuàng)建好了啄栓,但是高度怎么控制呢?高度設(shè)置是在tableView
的delegate
里就是現(xiàn)在的tableAction
介却,但是數(shù)據(jù)都在dataSource
里就是現(xiàn)在的tableModel
谴供,看看上面畫的關(guān)系圖,我們可以定義一個協(xié)議來要求tableModel
給我指定的數(shù)據(jù):
/**
AUUTableModel需要實現(xiàn)的方法
*/
@protocol AUUCellFactory
@required
/**
獲取指定位置的object
@param tableView 當(dāng)前的tableView
@param indexPath 當(dāng)前的索引
@return object
*/
- (id <AUUCellObject>)objectForTable:(UITableView *)tableView atIndexPath:(NSIndexPath *)indexPath;
@end
拿到了數(shù)據(jù)信息齿坷,那么高度的話就好說了:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
if ([tableView.dataSource conformsToProtocol:@protocol(AUUCellFactory)] &&
[tableView.dataSource respondsToSelector:@selector(objectForTable:atIndexPath:)]) {
// 拿到當(dāng)前位置的數(shù)據(jù)信息
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;
}
五桂肌、測試一下
實現(xiàn)部分的基類
因為我們上面做的對于cell和數(shù)據(jù)的控制都是通過協(xié)議來做的数焊,目的當(dāng)然很明確,就是為了能夠減少耦合崎场,所以佩耳,為了在項目里寫的方便,可以在實現(xiàn)的部分寫一個基類來簡化一些重復(fù)的工作谭跨,如:
BaseCell
@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
@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
下面在實現(xiàn)各個cell的時候干厚,就可以如下寫了:
.h
@interface AUUTextCell : AUUBaseCell
@end
@interface AUUTextObject : AUUBaseObject
+ (instancetype)objectWithText:(NSString *)text;
@end
.m
@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
這里呢就先寫這一個,更多的可以看一下測試代碼螃宙。
組裝數(shù)據(jù)到頁面
在一開始的時候我們就創(chuàng)建了tableModel
下面我們就來添加數(shù)據(jù):
[self.tableModel appendObject:[AUUTextObject objectWithText:@"title"]];
[self.tableModel appendObject:[AUUInputObject object]];
[self.tableModel appendObject:[AUUImageObject object]];
[self.tableModel appendObject:[AUUImageScrollerObject object]];
好了蛮瞄,我們添加了4個cell
到tableView
里,效果如下:
至此谆扎,一個tableView
已經(jīng)完美展示挂捅。
六、事件響應(yīng)
上面的操作都只是出于看靜態(tài)頁面的狀態(tài)堂湖,我們需要的是可以交互啊闲先,就像最原始的寫法一樣:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
我們在里面收到tableView
的調(diào)用以后就去做一些事情,但是現(xiàn)在我們的目的是接管這些協(xié)議方法无蜂,不給外面使用的機會伺糠。
為了解決這個問題,就該強化我們的tableAction
了斥季。
響應(yīng)對應(yīng)緩存
我們來創(chuàng)建一個緩存類_AUUActionCache
训桶,用來緩存每一行設(shè)定的響應(yīng)方法,里面包含如下幾個屬性:
@property (weak, nonatomic) id target;
@property (nonatomic) SEL sel;
@property (copy, nonatomic) AUUCellSelectedAction action;
提供外部注冊事件的方法
為了適應(yīng)不同的需求泻肯,我們提供一下兩種回調(diào)設(shè)置方式:
- 單一對象的事件監(jiān)聽渊迁,只監(jiān)聽這一個數(shù)據(jù)對應(yīng)的cell的點擊事件
// 通過block回調(diào)
- (id<AUUCellObject>)attachObject:(id<AUUCellObject>)object action:(AUUCellSelectedAction)action {
if (object) {
[self.objectActions setObject:[_AUUActionCache cacheWithAction:action] forKey:[self keyForObject:object]];
}
return object;
}
// 通過方法調(diào)用來回調(diào)
- (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;
}
- 某一類對象的監(jiān)聽,監(jiān)聽這一類及其子類的點擊事件
// 通過block回調(diào)
- (void)attachClass:(Class)cls action:(AUUCellSelectedAction)action {
[self.classActions setObject:[_AUUActionCache cacheWithAction:action] forKey:(id <NSCopying>)cls];
}
// 通過方法調(diào)用來回調(diào)
- (void)attachClass:(Class)cls target:(id)target action:(SEL)action {
[self.classActions setObject:[_AUUActionCache cacheWithTarget:target selector:action] forKey:(id <NSCopying>)cls];
}
事件響應(yīng)
在上面我們添加了AUUCellFactory
這么一個協(xié)議灶挟,那么數(shù)據(jù)肯定不在話下了琉朽,既然能拿到數(shù)據(jù),那就能找到其對應(yīng)的回調(diào)操作稚铣,如下:
- (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的監(jiān)聽惕医,當(dāng)我拿C class來取響應(yīng)者的時候耕漱,到底是取誰?
// 所以這里只是簡單的寫了一下
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
}
}
七抬伺、再測試一下
響應(yīng)事件添加了螟够,我們再來看一下實現(xiàn),跟第一次測試也沒多大的卻別,跑起來試一下妓笙,在控制臺就會出現(xiàn)想要的log結(jié)果了若河。
[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]];
八、更多的操作
這里我們的測試例子寞宫,只是很簡單的寫了一下這個操作過程萧福,不過對于一般不過于復(fù)雜的項目來說,貌似這就夠用了啊辈赋。
非要自己實現(xiàn)一些協(xié)議
當(dāng)然鲫忍,tableView
的協(xié)議方法有很多,就算自己再封裝一個庫也不會做到剛好符合每一個人的需求钥屈,而且對于蘋果公司來說悟民,隨著技術(shù)的不斷更新和進步,也肯定會有更多的API
開放出來焕蹄,于是逾雄,作為應(yīng)用層的一次封裝阀溶,提供給使用者更多的選擇還是很有必要的腻脏。
在這里,我推薦去看一看Nimbus
的NITableAction
下對于Forward Invocations
的一些實現(xiàn)银锻。
Cell
的自更新
響應(yīng)式設(shè)置永品,就像是集合了ReactiveCocoa
的操作一樣,每當(dāng)有數(shù)據(jù)變動击纬,都能及時的將變動后的結(jié)果展示出來鼎姐,這里的這種寫法當(dāng)然也行啊,就看你怎么封裝了更振。
不過炕桨,我推薦去看個現(xiàn)成的內(nèi)容Eureka
,里面對于數(shù)據(jù)和cell
的封裝更緊密肯腕。
九献宫、總結(jié)
這種方式把tableView
協(xié)議里的操作進行了打散,重點的工作就是前期的tableModel
和tableAction
的定制实撒,在到應(yīng)用的時候姊途,是把各種類型的判斷使用數(shù)據(jù)緩存來做了區(qū)分,這樣在添加一個cell的時候就能省去更多的時間和判斷邏輯知态。
不信捷兰,可以增加一些增刪的測試操作試試,如果用原始的寫法來做的話负敏,是不是有種要哭的感覺贡茅?