介紹
“極致”這種情懷問題揖盘,手上做不到?jīng)]關(guān)系项阴,嘴上是肯定要做到的言蛇。只要不是能力太打臉僻他,堅(jiān)持一下下倒是也模棱兩可。
本文參考了更輕量的 View Controllers 腊尚,對(duì)table用到的兩個(gè)個(gè)協(xié)議吨拗,進(jìn)行了不同思路的封裝。這段時(shí)間辭職避暑婿斥,時(shí)間大大的有劝篷,整理下這一年的經(jīng)驗(yàn),分享給大家民宿。
代碼在這github
行業(yè)需求
我也不知道是不是網(wǎng)易新聞客戶端的問題娇妓,近年來,大量只用過網(wǎng)易新聞客戶端的小伙伴就出來做產(chǎn)品了(當(dāng)然活鹰,他們也搖過微信)哈恰。再加上無app不web的思想,造就了大量的套皮app志群。
在感謝其提供大量工作機(jī)會(huì)的同時(shí)着绷,也不免吐槽下,對(duì)于這種app锌云,大量的工作無非就是請(qǐng)求幾下json荠医,展示到table里。然后加個(gè)MJ或者EGO桑涎,做下緩存彬向。你需要知道的僅僅是哪個(gè)json字段對(duì)應(yīng)哪個(gè)label,僅此而已攻冷。
這本是腳手架該干的事情啊幢泼。
不管你是否對(duì)代碼質(zhì)量有要求,簡化這種機(jī)械化勞動(dòng)都是一件符合人性的事讲衫。
<UITableViewDataSource>
分析
就先從<UITableViewDataSource>入手缕棵。
遵從這個(gè)協(xié)議,主要是給table提供數(shù)據(jù)源涉兽。大致可以分為這么幾種招驴。
-、基本數(shù)據(jù)枷畏,也就是那兩個(gè)@required方法别厘,提供table每個(gè)Section的行數(shù),以及每個(gè)行數(shù)所應(yīng)該返回的cell拥诡。
二触趴、提供table中Sections的數(shù)量氮发。
三、Section的Header和Footer中的文字冗懦。
四爽冕、table中cell移動(dòng)和刪除操作的數(shù)據(jù)源支持。
五披蕉、提供右邊索引的數(shù)據(jù)源
讓我把這些功能全部封裝颈畸,我是拒絕的,我可以重寫一遍table没讲,但是使用者一定會(huì)罵我眯娱,說這個(gè)不好用,根本沒有這樣的table爬凑。根據(jù)我的經(jīng)驗(yàn)(曾一下午寫了10多個(gè)table)徙缴。最常用的功能就是一和二。
簡單table的實(shí)現(xiàn)
聲明一個(gè)類WELDataSource嘁信,實(shí)現(xiàn)<UITableViewDataSource>于样,并將其作為table的dataSource,然后在cellForRowAtIndexPath中調(diào)用block吱抚,進(jìn)行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 中有一點(diǎn)不一樣。
管理數(shù)據(jù)是通過一個(gè)類型為可變數(shù)組的實(shí)例變量來實(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];
}
這么做的原因是因?yàn)榧热疲芏鄷r(shí)候table里的數(shù)據(jù)都是從網(wǎng)絡(luò)請(qǐng)求過來的,并且會(huì)有分頁涮坐。有了這個(gè)方法凄贩,只需要將請(qǐng)求回來的數(shù)組傳入addModels:,然后reloadData就可以了袱讹,無需進(jìn)行任何判斷疲扎。同時(shí),init方法捷雕,去掉了傳數(shù)組這個(gè)參數(shù)椒丧。每次傳個(gè)nil,也是挺無聊的救巷。
UICollectionView也一樣
UICollectionView是個(gè)很強(qiáng)大的控件壶熏,但很多時(shí)候,僅僅是用它來做一些簡單的展示浦译。
兩者的dataSource在只有一個(gè)section的時(shí)候棒假,邏輯是一樣的溯职,所以來兼容下Collection。
實(shí)現(xiàn)UICollectionViewDataSource協(xié)議
@interface WELDataSource : NSObject <UITableViewDataSource,UICollectionViewDataSource>
?實(shí)現(xiàn)這兩個(gè)方法
- (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;
}
代碼很簡單帽哑,這樣在只有一個(gè)section的時(shí)候谜酒,就可以直接使用WELDataSource而無需考慮是table,還是Collection祝拯。
還能更簡單
像我這種懶人,代碼是能不寫就不寫的鹰贵。像給table設(shè)置dataSource這種事康嘉,能拖線亭珍,則脫線。而且對(duì)于使用storyboard的我阻荒,每每把cell的identifier復(fù)制到代碼里侨赡,也是挺累的粱侣。所以齐婴,如果使用storyboard柠偶,那么代碼可以寫成這個(gè)樣子。
- (void)viewDidLoad {
??? [super viewDidLoad];
??? [_dataDelegate addModels:@[@"a",@"b",@"c"]];
??? [_table reloadData];
}
來分析下鲫售。
首先是WELDataSource的初始化情竹,這里傳了兩個(gè)個(gè)參數(shù)秦效,第一個(gè)是cell的Identifier。然后是一個(gè)回調(diào)挑秉,用來給cell上的view賦值犀概。初始化之后姻灶,將其設(shè)置為table的datasource。
先搞掉這句代碼产喉。
_table.dataSource = _dataDelegate;
這里使用StoryBoard中的object曾沈。
拖一個(gè)到vc里,然后將其class設(shè)置為WELDataSource塞俱。之后障涯,就可以通過“拉線”的方式像樊,將table的dataSource 設(shè)置為object旅敷。
由于使用了object媳谁,調(diào)用者不需要手動(dòng)去init友酱,但是參數(shù)還是得傳缔杉。對(duì)于Cell的重用Id,這個(gè)可以使用IBInspectable修飾系羞,在storyboard上直接進(jìn)行復(fù)制椒振。接著就是那個(gè)block。block里面的代碼庐杨,一般就是用一個(gè)model給cell上的元素賦值夹供。對(duì)于簡單的業(yè)務(wù)罩引,這個(gè)過程并不需要VC參與。我們可以讓cell遵守一個(gè)協(xié)議揭蜒,由WELDataSource直接通知cell屉更。
其實(shí)我本身并不贊同這種封裝瑰谜,這種方式跳過了VC萨脑,讓我感覺比較不靈活渤早,但使用了一段時(shí)間鹊杖,我感覺VC其實(shí)并沒有怎么參與這個(gè)過程骂蓖。跳過了也就跳過了登下。。
于是cell實(shí)現(xiàn)個(gè)類似這樣的協(xié)議
@protocol CellConfigure <NSObject>
-(void)configureCellWithModel:(id)Model;
@end
VC只需要add數(shù)據(jù),然后reloadData就可以了筐钟。
當(dāng)然李破,也有折中方案壹将。
實(shí)現(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保留一個(gè)對(duì)table的弱引用帜慢,數(shù)據(jù)請(qǐng)求層直接提供對(duì)WELDataSource的支持崖堤,在add之后,直接reloadData撩轰。
調(diào)用代碼可能會(huì)簡化成這樣偎箫。淹办。
-(void)viewWillAppear:(BOOL)animated {
??? [super viewWillAppear:animated];
???
??? [self loadNextPageWithDataSource:_dataDelegate];
???
}
不去實(shí)現(xiàn)復(fù)雜的數(shù)據(jù)源
想了想怜森,我還是刪除了多cell和多section的情況副硅。封裝這個(gè)的初衷是為了簡單恐疲,快速培己。面對(duì)復(fù)雜的情況漱凝,意味著需要更多的block茸炒,block里需要更多的代碼阵苇。這時(shí)候壁公,寫進(jìn)一個(gè)初始化方法中,會(huì)顯得比較臃腫绅项,反倒不如原生的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è)置為另一個(gè)類呢?答案顯然是否定 的遏片。<UITableViewDelegate>中的方法較多,且一些回調(diào)方法需要頻繁的和VC交互,封裝出的Delegate很可能比較龐大,或者僅僅是把Delegate用block重寫了一次敛助,很是畫蛇添足焕数。
然后我想到的是Category设联,不過這個(gè)想法很快就被我否決 了。對(duì)于系統(tǒng)的方法使用Category還是存在風(fēng)險(xiǎn)的耀盗。在分類中實(shí)現(xiàn)的方法,不管是否import,都可以respondsToSelector到。也 就是說,在分類中實(shí)現(xiàn)了dalegate的一個(gè)方法,就等于繼承自該類的子類都實(shí)現(xiàn)了這個(gè)方法。
我曾經(jīng)接手過一個(gè)沒有文檔的app,里面差不多70多個(gè)VC。為了快速知道哪個(gè)頁面對(duì)應(yīng)的是哪個(gè)Class,我隨便寫了這么一個(gè)Category打肝。倒是挺好用的楼吃。
@implementation UIViewController (VCChat)
-(void)viewDidAppear:(BOOL)animated {
??? NSLog(@"===%@===",NSStringFromClass([self class]));
}
@end
如果項(xiàng)目中的VC有統(tǒng)一的父類,就可以把代碼寫在父類中男韧,然后用一個(gè)bool屬性來選擇是否開啟該功能朦前。
但是,如果你沒使用父類,或者你根本不打算使用父類。那么正片來了积锅。
寫一個(gè)過濾器
寫一個(gè)類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];
}
代碼主要是運(yùn)用了oc的消息轉(zhuǎn)發(fā)機(jī)制,做了一層過濾拒迅。
可以把本文最上面的方法寫入WELTableDelegate中开瞭,也可以寫入如下代碼烘绽,用來實(shí)現(xiàn)一個(gè)簡單的反選動(dòng)畫效果。
- (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類型的屬性來選擇是否開啟這個(gè)功能安接,在Storyboard中進(jìn)行勾選,很是方便英融。
總結(jié)
只要是想封裝盏檐,總是可以封裝的。