【譯】讓你的tableView代碼整潔

tableView 是 iOS 應(yīng)用程序中非常通用的組件弄跌,許多代碼和tableView都有直接或者間接的關(guān)系,比如數(shù)據(jù)提供劲件、更新tableView托慨、控制它的行為以及響應(yīng)選擇事件,在本文中担钮,我們將介紹來(lái)保持tableView代碼清潔和良好的結(jié)構(gòu)橱赠。

UITableViewController vs. UIViewController

蘋果提供了UITableViewController作為tableView的專用視圖控制器類。UITableViewController實(shí)現(xiàn)了非常實(shí)用的功能箫津,可以幫助我們避免一遍又一遍的重復(fù)的寫相同的代碼模板狭姨。另一方面,UITableViewController被限制為只能管理一個(gè)tableView苏遥,而且其實(shí)充滿整個(gè)屏幕的饼拍,在很多情況下,這正是我們所需要的田炭,如果不是的話师抄,有辦法解決這個(gè)問(wèn)題,我們將在下面講解

UITableViewController的特點(diǎn)

UITableViewController在第一次展示的時(shí)候會(huì)加載tableView的數(shù)據(jù)教硫。更具體的叨吮,它會(huì)幫你切換tableView的編輯模式辆布,響應(yīng)鍵盤通知以及一些小的任務(wù),比如閃現(xiàn)側(cè)邊的滑動(dòng)提示條和清除選中時(shí)的背景色茶鉴。為了讓這些特性生效锋玲,當(dāng)你在子類中覆寫類似 viewWillAppear:或者 viewDidAppear:等事件方法時(shí),需要調(diào)用 super 版本涵叮。

UITableViewController相對(duì)于普通的UIViewController有一個(gè)優(yōu)點(diǎn)惭蹂,就是它支持Apple實(shí)現(xiàn)的“下拉刷新”功能,目前唯一使用UIRefreshControl的方式就是在UITableViewController中割粮,雖然可以努力讓它在其他地方工作(見(jiàn)此處)盾碗,但是可能在iOS的下一個(gè)版本就不支持了。

這些要素加一起舀瓢,為我們提供了大部分 Apple 所定義的標(biāo)準(zhǔn) table view 交互行為廷雅,如果你的應(yīng)用恰好符合這些標(biāo)準(zhǔn),那么直接使用 table view controllers 來(lái)避免寫那些死板的代碼是個(gè)很好的方法氢伟。

UITableViewController的限制

UITableViewController的view屬性永遠(yuǎn)是一個(gè)tableView,如果你稍后決定在tableView旁邊展示一些其他的視圖(比如地圖)榜轿,如果不是依賴其他的黑科技幽歼,別的就沒(méi)有辦法了朵锣。

如果你在代碼中使用的是xib文件來(lái)定義一個(gè)界面,那么會(huì)很簡(jiǎn)單的遷移的標(biāo)準(zhǔn)的UIViewController甸私。如果你用的storyboards诚些,那么這個(gè)過(guò)程會(huì)涉及到幾個(gè)步驟。除非重新創(chuàng)建皇型,否則你并不能在 storyboards 中將 UITableViewController 改成一個(gè)標(biāo)準(zhǔn)的 UIViewController诬烹。這意味著你必須將所有內(nèi)容拷貝到新的 view controller,然后再重新連接一遍弃鸦。

最后绞吁,你需要把遷移后丟失的 UITableViewController 的特性給補(bǔ)回來(lái)。大多數(shù)都是viewWillAppear:或viewDidAppear:中簡(jiǎn)單的一條語(yǔ)句唬格。切換編輯模式需要實(shí)現(xiàn)一個(gè) action 方法家破,用來(lái)切換 tableView 的editing屬性。大多數(shù)工作來(lái)自重新創(chuàng)建對(duì)鍵盤的支持购岗。

在選擇這條路之前汰聋,其實(shí)還有一個(gè)更輕松的選擇,它可以通過(guò)分離我們需要關(guān)心的功能(關(guān)注點(diǎn)分離)喊积,讓你獲得額外的好處:
使用Child View Controllers

和完全拋棄 UITableViewController 不同烹困,你還可以將它作為 child view controller 添加到其他 view controller 中(關(guān)于此話題的文章)。這樣乾吻,parent view controller 在管理其他的你需要的新加的界面元素的同時(shí)髓梅,UITableViewController 還可以繼續(xù)管理它的tableView拟蜻。

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

如果你使用這個(gè)解決方案,你就必須在 child view controller 和 parent view controller 之間建立消息傳遞的渠道枯饿。比如瞭郑,如果用戶選擇了一個(gè) table view 中的 cell,parent view controller 需要知道這個(gè)事件來(lái)推入其他 view controller鸭你。根據(jù)使用習(xí)慣屈张,通常最清晰的方式是為這個(gè) table view controller 定義一個(gè) delegate protocol,然后到 parent view controller 中去實(shí)現(xiàn)袱巨。

@protocol DetailsViewControllerDelegate
- (void)didSelectPhotoAttributeWithKey:(NSString *)key;
@end
@interface PhotoViewController () <DetailsViewControllerDelegate>
@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 之間的消息傳遞帶來(lái)了額外的開(kāi)銷,但是作為回報(bào)愉老,代碼封裝和分離非常清晰司训,有更好的復(fù)用性。根據(jù)實(shí)際情況的不同围详,這既可能讓事情變得更簡(jiǎn)單觉至,也可能會(huì)更復(fù)雜,需要讀者自行斟酌和決定咒林。

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

當(dāng)處理 table views 的時(shí)候熬拒,有許多各種各樣的任務(wù),這些任務(wù)穿梭于 models垫竞,controllers 和 views 之間澎粟。為了避免讓 view controllers 做所有的事,我們將盡可能地把這些任務(wù)劃分到合適的地方欢瞪,這樣有利于閱讀活烙、維護(hù)和測(cè)試。
這里描述的技術(shù)是文章更輕量的 View Controllers 中的概念的延伸遣鼓,請(qǐng)參考這篇文章來(lái)理解如何重構(gòu) data source 和 model 的邏輯啸盏。結(jié)合 table views,我們來(lái)具體看看如何在 view controllers 和 views 之間分離關(guān)注點(diǎn)骑祟。

搭建 Model 對(duì)象和 Cells 之間的橋梁

有時(shí)我們需要將想顯示的 model 層中的數(shù)據(jù)傳到 view 層中去顯示回懦。由于我們同時(shí)也希望讓 model 和 view 之間明確分離,所以通常把這個(gè)任務(wù)轉(zhuǎn)移到 table view 的 data 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;
}

- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
    [super setSelected:selected animated:animated];
    
    // Configure the view for the selected state
}

但是這樣的代碼會(huì)讓 data source 變得混亂曾我,因?yàn)樗?data source 暴露了 cell 的設(shè)計(jì)粉怕。最好分解出來(lái),放到 cell 類的一個(gè) 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 方法就變得簡(jiǎn)單了。

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

在我們的示例代碼中,table view 的 data source 已經(jīng)分解到單獨(dú)的類中了稚晚,它用一個(gè)設(shè)置 cell 的 block 來(lái)初始化崇堵。這時(shí),這個(gè) block 就變得這樣簡(jiǎn)單了:

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

讓 Cells 可復(fù)用

有時(shí)多種 model 對(duì)象需要用同一類型的 cell 來(lái)表示客燕,這種情況下鸳劳,我們可以進(jìn)一步讓 cell 可以復(fù)用。首先也搓,我們給 cell 定義一個(gè) protocol赏廓,需要用這個(gè) cell 顯示的對(duì)象必須遵循這個(gè) protocol。然后簡(jiǎn)單修改 category 中的設(shè)置方法傍妒,讓它可以接受遵循這個(gè) protocol 的任何對(duì)象幔摸。這些簡(jiǎn)單的步驟讓 cell 和任何特殊的 model 對(duì)象之間得以解耦,讓它可適應(yīng)不同的數(shù)據(jù)類型颤练。

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

如果你想自定義 table views 默認(rèn)的高亮或選擇行為既忆,你可以實(shí)現(xiàn)兩個(gè) delegate 方法,把點(diǎn)擊的 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;
}

然而患雇,這兩個(gè) delegate 方法的實(shí)現(xiàn)又基于了 view controller 知曉 cell 實(shí)現(xiàn)的具體細(xì)節(jié)。如果我們想替換或重新設(shè)計(jì) cell宇挫,我們必須改寫 delegate 代碼苛吱。View 的實(shí)現(xiàn)細(xì)節(jié)和 delegate 的實(shí)現(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

總的來(lái)說(shuō)又谋,我們?cè)谂Π?view 層和 controller 層的實(shí)現(xiàn)細(xì)節(jié)分離開(kāi)拼缝。delegate 肯定得清楚一個(gè) view 該顯示什么狀態(tài)娱局,但是它不應(yīng)該了解如何修改 view 結(jié)構(gòu)或者給某些 subviews 設(shè)置某些屬性以獲得正確的狀態(tài)。所有這些邏輯都應(yīng)該封裝到 view 內(nèi)部咧七,然后給外部提供一個(gè)簡(jiǎn)單的 API衰齐。

控制多個(gè) Cell 類型

如果一個(gè) table view 里面有多種類型的 cell,data source 方法很快就難以控制了继阻。在我們示例程序中耻涛,photo details table 有兩種不同類型的 cell:一種用于顯示幾個(gè)星,另一種用來(lái)顯示一個(gè)鍵值對(duì)瘟檩。為了劃分處理不同 cell 類型的代碼抹缕,data source 方法簡(jiǎn)單地通過(guò)判斷 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 提供了易于使用的編輯特性卓研,允許你對(duì) cell 進(jìn)行刪除或重新排序。這些事件都可以讓 table view 的 data source 通過(guò) delegate 方法得到通知。因此奏赘,通常我們能在這些 delegate 方法中看到對(duì)數(shù)據(jù)的進(jìn)行修改的操作寥闪。
修改數(shù)據(jù)很明顯是屬于 model 層的任務(wù)。Model 應(yīng)該為諸如刪除或重新排序等操作暴露一個(gè) API磨淌,然后我們可以在 data source 方法中調(diào)用它疲憋。這樣,controller 就可以扮演 view 和 model 之間的協(xié)調(diào)者梁只,而不需要知道 model 層的實(shí)現(xiàn)細(xì)節(jié)缚柳。并且還有額外的好處,model 的邏輯也變得更容易測(cè)試搪锣,因?yàn)樗辉俸?view controllers 的任務(wù)混雜在一起了喂击。

總結(jié)

Table view controllers(以及其他的 controller 對(duì)象!)應(yīng)該在 model 和 view 對(duì)象之間扮演協(xié)調(diào)者和調(diào)解者的角色淤翔。它不應(yīng)該關(guān)心明顯屬于 view 層或 model 層的任務(wù)翰绊。你應(yīng)該始終記住這點(diǎn),這樣 delegate 和 data source 方法會(huì)變得更小巧旁壮,最多包含一些簡(jiǎn)單的樣板代碼监嗜。
這不僅減少了 table view controllers 那樣的大小和復(fù)雜性,而且還把業(yè)務(wù)邏輯和 view 的邏輯放到了更合適的地方抡谐。Controller 層的里里外外的實(shí)現(xiàn)細(xì)節(jié)都被封裝成了簡(jiǎn)單的 API裁奇,最終,它變得更加容易理解麦撵,也更利于團(tuán)隊(duì)協(xié)作刽肠。

擴(kuò)展閱讀

Table View Programming Guide
Cocoa Core Competencies: Controller Object

原文Clean table view code

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市免胃,隨后出現(xiàn)的幾起案子音五,更是在濱河造成了極大的恐慌,老刑警劉巖羔沙,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件躺涝,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡扼雏,警方通過(guò)查閱死者的電腦和手機(jī)坚嗜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)诗充,“玉大人苍蔬,你說(shuō)我怎么就攤上這事『眩” “怎么了碟绑?”我有些...
    開(kāi)封第一講書人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我蜈敢,道長(zhǎng)辜荠,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任抓狭,我火速辦了婚禮伯病,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘否过。我一直安慰自己午笛,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布苗桂。 她就那樣靜靜地躺著药磺,像睡著了一般。 火紅的嫁衣襯著肌膚如雪煤伟。 梳的紋絲不亂的頭發(fā)上癌佩,一...
    開(kāi)封第一講書人閱讀 51,301評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音便锨,去河邊找鬼围辙。 笑死,一個(gè)胖子當(dāng)著我的面吹牛放案,可吹牛的內(nèi)容都是我干的姚建。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼吱殉,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼掸冤!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起友雳,我...
    開(kāi)封第一講書人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤稿湿,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后沥阱,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體缎罢,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年考杉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片舰始。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡崇棠,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出丸卷,到底是詐尸還是另有隱情枕稀,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站萎坷,受9級(jí)特大地震影響凹联,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜哆档,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一蔽挠、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧瓜浸,春花似錦澳淑、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至雇寇,卻和暖如春氢拥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背锨侯。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工兄一, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人识腿。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓出革,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親渡讼。 傳聞我的和親對(duì)象是個(gè)殘疾皇子骂束,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

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