整潔的TableView代碼

UITableView是日常開發(fā)中使用的最為頻繁的控件之一,干凈整潔的TableView會方便開發(fā)者日后代碼的維護(hù)夹攒,也會增強(qiáng)代碼的可讀性蜘醋,輕化視圖。

活捉一只十元.jpeg

【譯文】翻譯自objc.io作者Florian Kugler的博客咏尝,原文可查看Clean Table View Code 压语。

-------------------------------------------------------可愛的分割線-----------------------------------------------------------------------------------

TableView 是 iOS 應(yīng)用程序中非常通用的組件。許多代碼和 TableView 都有直接或間接的關(guān)系编检,隨便舉幾個例子胎食,比如提供數(shù)據(jù)、更新 TableView蒙谓,控制它的行為以及響應(yīng)選擇事件斥季。在這篇文章中,我們將會展示保持 TableView 相關(guān)代碼的整潔和良好組織的技術(shù)累驮。

UITableViewController vs. UIViewController

Apple 提供了 UITableViewController 作為 TableView 專屬的 view controller 類酣倾。UITableViewController 實現(xiàn)了一些非常有用的特性,來幫你避免一遍又一遍地寫那些死板的代碼谤专!但是話又說回來躁锡,UITableViewController 只限于管理一個全屏展示的 TableView。大多數(shù)情況下置侍,這就是你想要的映之,但如果不是拦焚,還有其他方法來解決這個問題,就像下面我們展示的那樣杠输。

TableViewControllers 的特性

Table view controllers 會在第一次顯示 table view 的時候幫你加載其數(shù)據(jù)赎败。另外,它還會幫你切換 table view 的編輯模式蠢甲、響應(yīng)鍵盤通知僵刮、以及一些小任務(wù),比如閃現(xiàn)側(cè)邊的滑動提示條和清除選中時的背景色鹦牛。為了讓這些特性生效搞糕,當(dāng)你在子類中覆寫類似 viewWillAppear: 或者 viewDidAppear: 等事件方法時,需要調(diào)用 super 版本曼追。

Table view controllers 相對于標(biāo)準(zhǔn) view controllers 的一個特別的好處是它支持 Apple 實現(xiàn)的“下拉刷新”窍仰。目前,文檔中唯一的使用 UIRefreshControl 的方式就是通過 table view controller 礼殊,雖然通過努力在其他地方也能讓它工作(見此處)驹吮,但很可能在下一次 iOS 更新的時候就不行了。

這些要素加一起膏燕,為我們提供了大部分 Apple 所定義的標(biāo)準(zhǔn) table view 交互行為钥屈,如果你的應(yīng)用恰好符合這些標(biāo)準(zhǔn)悟民,那么直接使用 table view controllers 來避免寫那些死板的代碼是個很好的方法坝辫。

TableViewControllers 的限制

TableViewControllers 的 view 屬性永遠(yuǎn)都是一個 TableView。如果你稍后決定在 table view 旁邊顯示一些東西(比如一個地圖)射亏,如果不依賴于那些奇怪的 hacks近忙,估計就沒什么辦法了。

如果你是用代碼或 .xib 文件來定義的界面智润,那么遷移到一個標(biāo)準(zhǔn) viewcontroller 將會非常簡單及舍。但是如果你使用了 storyboards,那么這個過程要多包含幾個步驟窟绷。除非重新創(chuàng)建锯玛,否則你并不能在 storyboards 中將 tableViewController 改成一個標(biāo)準(zhǔn)的 viewController。這意味著你必須將所有內(nèi)容拷貝到新的 viewController兼蜈,然后再重新連接一遍攘残。

最后,你需要把遷移后丟失的 tableViewController 的特性給補回來为狸。大多數(shù)都是 viewWillAppear:viewDidAppear: 中簡單的一條語句歼郭。切換編輯模式需要實現(xiàn)一個 action 方法,用來切換 TableViewediting 屬性辐棒。大多數(shù)工作來自重新創(chuàng)建對鍵盤的支持病曾。

在選擇這條路之前牍蜂,其實還有一個更輕松的選擇,它可以通過分離我們需要關(guān)心的功能(關(guān)注點分離)泰涂,讓你獲得額外的好處:

使用 Child View Controllers

和完全拋棄 table view controller 不同鲫竞,你還可以將它作為 child view controller 添加到其他 view controller 中(關(guān)于此話題的文章)。這樣逼蒙,parent view controller 在管理其他的你需要的新加的界面元素的同時贡茅,table view controller 還可以繼續(xù)管理它的 table view。

- (void)addPhotoDetailsTableView
{
    DetailsViewController *details = [[DetailsViewController alloc] init];
    details.photo = self.photo;
    details.delegate = self;
    [self addChildViewController:details];
    CGRect frame = self.view.bounds;
    frame.origin.y = 110;
    details.view.frame = frame;
    [self.view addSubview:details.view];
    [details didMoveToParentViewController:self];
}

如果你使用這個解決方案其做,你就必須在 child view controllerparent view controller 之間建立消息傳遞的渠道顶考。比如,如果用戶選擇了一個 table view 中的 cell妖泄,parent view controller 需要知道這個事件來推入其他 view controller驹沿。根據(jù)使用習(xí)慣,通常最清晰的方式是為這個 table view controller 定義一個 delegate protocol蹈胡,然后到 parent view controller 中去實現(xiàn)渊季。

@protocol DetailsViewControllerDelegate
- (void)didSelectPhotoAttributeWithKey:(NSString *)key;
@end

@interface PhotoViewController () 
@end

@implementation PhotoViewController
// ...
- (void)didSelectPhotoAttributeWithKey:(NSString *)key
{
    DetailViewController *controller = [[DetailViewController alloc] init];
    controller.key = key;
    [self.navigationController pushViewController:controller animated:YES];
}
@end

就像你看到的那樣,這種結(jié)構(gòu)為 view controller 之間的消息傳遞帶來了額外的開銷罚渐,但是作為回報却汉,代碼封裝和分離非常清晰,有更好的復(fù)用性。根據(jù)實際情況的不同沟使,這既可能讓事情變得更簡單脓规,也可能會更復(fù)雜,需要讀者自行斟酌和決定翩伪。

分離關(guān)注點(Separating Concerns)

當(dāng)處理 table views 的時候,有許多各種各樣的任務(wù)谈息,這些任務(wù)穿梭于 models缘屹,controllers 和 views 之間。為了避免讓 view controllers 做所有的事侠仇,我們將盡可能地把這些任務(wù)劃分到合適的地方轻姿,這樣有利于閱讀、維護(hù)和測試逻炊。

這里描述的技術(shù)是文章更輕量的 View Controllers 中的概念的延伸互亮,請參考這篇文章來理解如何重構(gòu) data sourcemodel 的邏輯。結(jié)合 table views嗅骄,我們來具體看看如何在 view controllersviews 之間分離關(guān)注點胳挎。

搭建 Model 對象和 Cells 之間的橋梁

有時我們需要將想顯示的 model 層中的數(shù)據(jù)傳到 view 層中去顯示。由于我們同時也希望讓 modelview 之間明確分離溺森,所以通常把這個任務(wù)轉(zhuǎn)移到 table viewdata source 中去處理:

- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    PhotoCell *cell = [tableView dequeueReusableCellWithIdentifier:@"PhotoCell"];
    Photo *photo = [self itemAtIndexPath:indexPath];
    cell.photoTitleLabel.text = photo.name;
    NSString* date = [self.dateFormatter stringFromDate:photo.creationDate];
    cell.photoDateLabel.text = date;
}

但是這樣的代碼會讓 data source 變得混亂慕爬,因為它向 data source 暴露了 cell 的設(shè)計窑眯。最好分解出來,放到 cell 類的一個 category 中医窿。

@implementation PhotoCell (ConfigureForPhoto)

- (void)configureForPhoto:(Photo *)photo
{
    self.photoTitleLabel.text = photo.name;
    NSString* date = [self.dateFormatter stringFromDate:photo.creationDate];
    self.photoDateLabel.text = date;
}

@end

有了上述代碼后磅甩,我們的 data source 方法就變得簡單了。

- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    PhotoCell *cell = [tableView dequeueReusableCellWithIdentifier:PhotoCellIdentifier];
    [cell configureForPhoto:[self itemAtIndexPath:indexPath]];
    return cell;
}

在我們的示例代碼中姥卢,table viewdata source 已經(jīng)分解到單獨的類中了卷要,它用一個設(shè)置 cell 的 block 來初始化。這時独榴,這個 block 就變得這樣簡單了:

TableViewCellConfigureBlock block = ^(PhotoCell *cell, Photo *photo) {
    [cell configureForPhoto:photo];
};

讓 Cells 可復(fù)用

有時多種 model 對象需要用同一類型的 cell 來表示僧叉,這種情況下,我們可以進(jìn)一步讓 cell 可以復(fù)用棺榔。首先瓶堕,我們給 cell 定義一個 protocol,需要用這個 cell 顯示的對象必須遵循這個 protocol症歇。然后簡單修改 category 中的設(shè)置方法郎笆,讓它可以接受遵循這個 protocol 的任何對象。這些簡單的步驟讓 cell 和任何特殊的 model 對象之間得以解耦忘晤,讓它可適應(yīng)不同的數(shù)據(jù)類型宛蚓。

在 Cell 內(nèi)部控制 Cell 的狀態(tài)

如果你想自定義 table views 默認(rèn)的高亮或選擇行為,你可以實現(xiàn)兩個 delegate 方法设塔,把點擊的 cell 修改成我們想要的樣子凄吏。例如:

- (void)tableView:(UITableView *)tableView
        didHighlightRowAtIndexPath:(NSIndexPath *)indexPath
{
    PhotoCell *cell = [tableView cellForRowAtIndexPath:indexPath];
    cell.photoTitleLabel.shadowColor = [UIColor darkGrayColor];
    cell.photoTitleLabel.shadowOffset = CGSizeMake(3, 3);
}

- (void)tableView:(UITableView *)tableView
        didUnhighlightRowAtIndexPath:(NSIndexPath *)indexPath
{
    PhotoCell *cell = [tableView cellForRowAtIndexPath:indexPath];
    cell.photoTitleLabel.shadowColor = nil;
}

然而,這兩個 delegate 方法的實現(xiàn)又基于了 view controller 知曉 cell實現(xiàn)的具體細(xì)節(jié)壹置。如果我們想替換或重新設(shè)計 cell竞思,我們必須改寫 delegate 代碼表谊。View 的實現(xiàn)細(xì)節(jié)和 delegate 的實現(xiàn)交織在一起了钞护。我們應(yīng)該把這些細(xì)節(jié)移到 cell 自身中去。

@implementation PhotoCell
// ...
- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated
{
    [super setHighlighted:highlighted animated:animated];
    if (highlighted) {
        self.photoTitleLabel.shadowColor = [UIColor darkGrayColor];
        self.photoTitleLabel.shadowOffset = CGSizeMake(3, 3);
    } else {
        self.photoTitleLabel.shadowColor = nil;
    }
}
@end

總的來說爆办,我們在努力把 view 層和 controller 層的實現(xiàn)細(xì)節(jié)分離開难咕。delegate 肯定得清楚一個 view 該顯示什么狀態(tài),但是它不應(yīng)該了解如何修改 view 結(jié)構(gòu)或者給某些 subviews 設(shè)置某些屬性以獲得正確的狀態(tài)距辆。所有這些邏輯都應(yīng)該封裝到 view 內(nèi)部余佃,然后給外部提供一個簡單的 API。

控制多個 Cell 類型

如果一個 table view 里面有多種類型的 cell跨算,data source 方法很快就難以控制了爆土。在我們示例程序中,photo details table 有兩種不同類型的 cell:一種用于顯示幾個星诸蚕,另一種用來顯示一個鍵值對步势。為了劃分處理不同 cell 類型的代碼氧猬,data source 方法簡單地通過判斷 cell 的類型,把任務(wù)派發(fā)給其他指定的方法坏瘩。

- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSString *key = self.keys[(NSUInteger) indexPath.row];
    id value = [self.photo valueForKey:key];
    UITableViewCell *cell;
    if ([key isEqual:PhotoRatingKey]) {
        cell = [self cellForRating:value indexPath:indexPath];
    } else {
        cell = [self detailCellForKey:key value:value];
    }
    return cell;
}

- (RatingCell *)cellForRating:(NSNumber *)rating
                    indexPath:(NSIndexPath *)indexPath
{
    // ...
}

- (UITableViewCell *)detailCellForKey:(NSString *)key
                                value:(id)value
{
    // ...
}

編輯 Table View

Table view 提供了易于使用的編輯特性盅抚,允許你對 cell 進(jìn)行刪除或重新排序。這些事件都可以讓 table viewdata source 通過 delegate 方法得到通知倔矾。因此妄均,通常我們能在這些 delegate 方法中看到對數(shù)據(jù)的進(jìn)行修改的操作。

修改數(shù)據(jù)很明顯是屬于 model 層的任務(wù)哪自。Model 應(yīng)該為諸如刪除或重新排序等操作暴露一個 API丰包,然后我們可以在 data source 方法中調(diào)用它。這樣壤巷,controller 就可以扮演 viewmodel 之間的協(xié)調(diào)者烫沙,而不需要知道 model 層的實現(xiàn)細(xì)節(jié)。并且還有額外的好處隙笆,model 的邏輯也變得更容易測試锌蓄,因為它不再和 view controllers 的任務(wù)混雜在一起了。

總結(jié)

Table view controllers(以及其他的 controller 對象3湃帷)應(yīng)該在 modelview 對象之間扮演協(xié)調(diào)者和調(diào)解者的角色瘸爽。它不應(yīng)該關(guān)心明顯屬于 view 層或 model 層的任務(wù)。你應(yīng)該始終記住這點铅忿,這樣 delegatedata source 方法會變得更小巧剪决,最多包含一些簡單的樣板代碼。

這不僅減少了 table view controllers 那樣的大小和復(fù)雜性檀训,而且還把業(yè)務(wù)邏輯和 view 的邏輯放到了更合適的地方柑潦。Controller 層的里里外外的實現(xiàn)細(xì)節(jié)都被封裝成了簡單的 API,最終峻凫,它變得更加容易理解渗鬼,也更利于團(tuán)隊協(xié)作。

擴(kuò)展閱讀

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末荧琼,一起剝皮案震驚了整個濱河市譬胎,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌命锄,老刑警劉巖堰乔,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異脐恩,居然都是意外死亡镐侯,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進(jìn)店門驶冒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來苟翻,“玉大人搭伤,你說我怎么就攤上這事⊥嗨玻” “怎么了怜俐?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長邓尤。 經(jīng)常有香客問我拍鲤,道長,這世上最難降的妖魔是什么汞扎? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任季稳,我火速辦了婚禮,結(jié)果婚禮上澈魄,老公的妹妹穿的比我還像新娘景鼠。我一直安慰自己,他們只是感情好痹扇,可當(dāng)我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布铛漓。 她就那樣靜靜地躺著,像睡著了一般鲫构。 火紅的嫁衣襯著肌膚如雪浓恶。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天结笨,我揣著相機(jī)與錄音包晰,去河邊找鬼。 笑死炕吸,一個胖子當(dāng)著我的面吹牛伐憾,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播赫模,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼树肃,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了嘴瓤?” 一聲冷哼從身側(cè)響起扫外,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎廓脆,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體磁玉,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡停忿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蚊伞。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片席赂。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡吮铭,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出颅停,到底是詐尸還是另有隱情谓晌,我是刑警寧澤,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布癞揉,位于F島的核電站纸肉,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏喊熟。R本人自食惡果不足惜柏肪,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望芥牌。 院中可真熱鬧烦味,春花似錦、人聲如沸壁拉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽弃理。三九已至凤瘦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間案铺,已是汗流浹背蔬芥。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留控汉,地道東北人笔诵。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像姑子,于是被迫代替她去往敵國和親乎婿。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,490評論 2 348

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