將UITableView封裝到極致

介紹

“極致”這種情懷問題揖盘,手上做不到?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é)

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



最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末驶悟,一起剝皮案震驚了整個(gè)濱河市胡野,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌痕鳍,老刑警劉巖硫豆,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異额获,居然都是意外死亡够庙,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門抄邀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來耘眨,“玉大人,你說我怎么就攤上這事境肾√弈眩” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵奥喻,是天一觀的道長偶宫。 經(jīng)常有香客問我,道長环鲤,這世上最難降的妖魔是什么纯趋? 我笑而不...
    開封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上吵冒,老公的妹妹穿的比我還像新娘纯命。我一直安慰自己,他們只是感情好痹栖,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開白布亿汞。 她就那樣靜靜地躺著,像睡著了一般揪阿。 火紅的嫁衣襯著肌膚如雪疗我。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天南捂,我揣著相機(jī)與錄音吴裤,去河邊找鬼。 笑死黑毅,一個(gè)胖子當(dāng)著我的面吹牛嚼摩,可吹牛的內(nèi)容都是我干的钦讳。 我是一名探鬼主播矿瘦,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼愿卒!你這毒婦竟也來了缚去?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤琼开,失蹤者是張志新(化名)和其女友劉穎易结,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體柜候,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡搞动,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了渣刷。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鹦肿。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖辅柴,靈堂內(nèi)的尸體忽然破棺而出箩溃,到底是詐尸還是另有隱情,我是刑警寧澤碌嘀,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布涣旨,位于F島的核電站,受9級(jí)特大地震影響股冗,放射性物質(zhì)發(fā)生泄漏霹陡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望烹棉。 院中可真熱鬧惠呼,春花似錦、人聲如沸峦耘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽辅髓。三九已至泣崩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間洛口,已是汗流浹背矫付。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留第焰,地道東北人买优。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像挺举,于是被迫代替她去往敵國和親杀赢。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 概述在iOS開發(fā)中UITableView可以說是使用最廣泛的控件湘纵,我們平時(shí)使用的軟件中到處都可以看到它的影子脂崔,類似...
    liudhkk閱讀 9,049評(píng)論 3 38
  • UITableViewCell 父類是UIView UITableView的每一行都是一個(gè)UITableViewC...
    翻這個(gè)墻閱讀 6,602評(píng)論 0 1
  • 傳統(tǒng)模式下的開發(fā)MVCMVVM基于面向協(xié)議MVP的介紹MVP實(shí)戰(zhàn)開發(fā)說在前面:相信就算你是個(gè)iOS新手也應(yīng)該聽說過...
    行走的菜譜閱讀 3,160評(píng)論 1 5
  • *7月8日上午 N:Block :跟一個(gè)函數(shù)塊差不多,會(huì)對(duì)里面所有的內(nèi)容的引用計(jì)數(shù)+1梧喷,想要解決就用__block...
    炙冰閱讀 2,488評(píng)論 1 14
  • 古有韓信可忍胯下之辱 當(dāng)自己強(qiáng)大還有誰不尊重你
    小Prince_閱讀 257評(píng)論 0 0