將UITableView封裝到極致

介紹

“極致”這種情懷問題,手上做不到?jīng)]關(guān)系,嘴上是肯定要做到的。只要不是能力太打臉,堅持一下下倒是也模棱兩可。

本文參考了更輕量的 View Controllers ,對table用到的兩個個協(xié)議,進行了不同思路的封裝。這段時間辭職避暑,時間大大的有,整理下這一年的經(jīng)驗,分享給大家。

代碼在這github

行業(yè)需求

我也不知道是不是網(wǎng)易新聞客戶端的問題,近年來,大量只用過網(wǎng)易新聞客戶端的小伙伴就出來做產(chǎn)品了(當(dāng)然,他們也搖過微信)。再加上無app不web的思想,造就了大量的套皮app。

在感謝其提供大量工作機會的同時,也不免吐槽下,對于這種app,大量的工作無非就是請求幾下json,展示到table里。然后加個MJ或者EGO,做下緩存。你需要知道的僅僅是哪個json字段對應(yīng)哪個label,僅此而已。

這本是腳手架該干的事情啊。

不管你是否對代碼質(zhì)量有要求,簡化這種機械化勞動都是一件符合人性的事。


<UITableViewDataSource>


分析

就先從<UITableViewDataSource>入手。

遵從這個協(xié)議,主要是給table提供數(shù)據(jù)源。大致可以分為這么幾種。

-、基本數(shù)據(jù),也就是那兩個@required方法,提供table每個Section的行數(shù),以及每個行數(shù)所應(yīng)該返回的cell。

二、提供table中Sections的數(shù)量。

三、Section的Header和Footer中的文字。

四、table中cell移動和刪除操作的數(shù)據(jù)源支持。

五、提供右邊索引的數(shù)據(jù)源

讓我把這些功能全部封裝,我是拒絕的,我可以重寫一遍table,但是使用者一定會罵我,說這個不好用,根本沒有這樣的table。根據(jù)我的經(jīng)驗(曾一下午寫了10多個table)。最常用的功能就是一和二。


簡單table的實現(xiàn)

聲明一個類WELDataSource,實現(xiàn)<UITableViewDataSource>,并將其作為table的dataSource,然后在cellForRowAtIndexPath中調(diào)用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 中有一點不一樣。

管理數(shù)據(jù)是通過一個類型為可變數(shù)組的實例變量來實現(xiàn)的。

#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里的數(shù)據(jù)都是從網(wǎng)絡(luò)請求過來的,并且會有分頁。有了這個方法,只需要將請求回來的數(shù)組傳入addModels:,然后reloadData就可以了,無需進行任何判斷。同時,init方法,去掉了傳數(shù)組這個參數(shù)。每次傳個nil,也是挺無聊的。

UICollectionView也一樣

UICollectionView是個很強大的控件,但很多時候,僅僅是用它來做一些簡單的展示。

兩者的dataSource在只有一個section的時候,邏輯是一樣的,所以來兼容下Collection。

實現(xiàn)UICollectionViewDataSource協(xié)議

@interface WELDataSource : NSObject <UITableViewDataSource,UICollectionViewDataSource>

?實現(xiàn)這兩個方法

- (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設(shè)置dataSource這種事,能拖線,則脫線。而且對于使用storyboard的我,每每把cell的identifier復(fù)制到代碼里,也是挺累的。所以,如果使用storyboard,那么代碼可以寫成這個樣子。

- (void)viewDidLoad {
??? [super viewDidLoad];
??? [_dataDelegate addModels:@[@"a",@"b",@"c"]];
??? [_table reloadData];
}

來分析下。

首先是WELDataSource的初始化,這里傳了兩個個參數(shù),第一個是cell的Identifier。然后是一個回調(diào),用來給cell上的view賦值。初始化之后,將其設(shè)置為table的datasource。

先搞掉這句代碼。

_table.dataSource = _dataDelegate;

這里使用StoryBoard中的object。

拖一個到vc里,然后將其class設(shè)置為WELDataSource。之后,就可以通過“拉線”的方式,將table的dataSource 設(shè)置為object。


由于使用了object,調(diào)用者不需要手動去init,但是參數(shù)還是得傳。對于Cell的重用Id,這個可以使用IBInspectable修飾,在storyboard上直接進行復(fù)制。接著就是那個block。block里面的代碼,一般就是用一個model給cell上的元素賦值。對于簡單的業(yè)務(wù),這個過程并不需要VC參與。我們可以讓cell遵守一個協(xié)議,由WELDataSource直接通知cell。

其實我本身并不贊同這種封裝,這種方式跳過了VC,讓我感覺比較不靈活,但使用了一段時間,我感覺VC其實并沒有怎么參與這個過程。跳過了也就跳過了。。

于是cell實現(xiàn)個類似這樣的協(xié)議

@protocol CellConfigure <NSObject>

-(void)configureCellWithModel:(id)Model;

@end

VC只需要add數(shù)據(jù),然后reloadData就可以了。

當(dāng)然,也有折中方案。

實現(xiàn)如下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的弱引用,數(shù)據(jù)請求層直接提供對WELDataSource的支持,在add之后,直接reloadData。

調(diào)用代碼可能會簡化成這樣。。

-(void)viewWillAppear:(BOOL)animated {
??? [super viewWillAppear:animated];
???
??? [self loadNextPageWithDataSource:_dataDelegate];
???
}


不去實現(xiàn)復(fù)雜的數(shù)據(jù)源

想了想,我還是刪除了多cell和多section的情況。封裝這個的初衷是為了簡單,快速。面對復(fù)雜的情況,意味著需要更多的block,block里需要更多的代碼。這時候,寫進一個初始化方法中,會顯得比較臃腫,反倒不如原生的delegate看著舒服。




<UITableViewDelegate>怎么辦?


主要問題是代碼復(fù)用

看下面這一段代碼,這段代碼用來解決ios8中cell下面的線,左面不能頂?shù)筋^的問題。

-(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];
??? }
}

類似這種代碼,怎么靈活的復(fù)用呢?

是否可以按照DataSoure的思路,簡單的將table的delegate設(shè)置為另一個類呢?答案顯然是否定 的。<UITableViewDelegate>中的方法較多,且一些回調(diào)方法需要頻繁的和VC交互,封裝出的Delegate很可能比較龐大,或者僅僅是把Delegate用block重寫了一次,很是畫蛇添足。

然后我想到的是Category,不過這個想法很快就被我否決 了。對于系統(tǒng)的方法使用Category還是存在風(fēng)險的。在分類中實現(xiàn)的方法,不管是否import,都可以respondsToSelector到。也 就是說,在分類中實現(xiàn)了dalegate的一個方法,就等于繼承自該類的子類都實現(xiàn)了這個方法。

我曾經(jīng)接手過一個沒有文檔的app,里面差不多70多個VC。為了快速知道哪個頁面對應(yīng)的是哪個Class,我隨便寫了這么一個Category。倒是挺好用的。

@implementation UIViewController (VCChat)

-(void)viewDidAppear:(BOOL)animated {
??? NSLog(@"===%@===",NSStringFromClass([self class]));
}

@end


如果項目中的VC有統(tǒng)一的父類,就可以把代碼寫在父類中,然后用一個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的消息轉(zhuǎn)發(fā)機制,做了一層過濾。

可以把本文最上面的方法寫入WELTableDelegate中,也可以寫入如下代碼,用來實現(xiàn)一個簡單的反選動畫效果。

- (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中進行勾選,很是方便。

總結(jié)

只要是想封裝,總是可以封裝的。



最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容