介紹
“極致”這種情懷問題,手上做不到?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é)
只要是想封裝,總是可以封裝的。