UITableViewController vs. UIViewController

Table view 是 iOS 應用程序中非常通用的組件。許多代碼和 table view 都有直接或間接的關系,隨便舉幾個例子,比如提供數據、更新 table view,控制它的行為以及響應選擇事件。在這篇文章中,我們將會展示保持 table view 相關代碼的整潔和良好組織的技術。

Apple 提供了UITableViewController作為 table views 專屬的 view controller 類。Table view controllers 實現了一些非常有用的特性,來幫你避免一遍又一遍地寫那些死板的代碼!但是話又說回來,table view controller 只限于管理一個全屏展示的 table view。大多數情況下,這就是你想要的,但如果不是,還有其他方法來解決這個問題,就像下面我們展示的那樣。

Table View Controllers 的特性

Table view controllers 會在第一次顯示 table view 的時候幫你加載其數據。另外,它還會幫你切換 table view 的編輯模式、響應鍵盤通知、以及一些小任務,比如閃現側邊的滑動提示條和清除選中時的背景色。為了讓這些特性生效,當你在子類中覆寫類似viewWillAppear:或者viewDidAppear:等事件方法時,需要調用 super 版本。

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

這些要素加一起,為我們提供了大部分 Apple 所定義的標準 table view 交互行為,如果你的應用恰好符合這些標準,那么直接使用 table view controllers 來避免寫那些死板的代碼是個很好的方法。

Table View Controllers 的限制

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

如果你是用代碼或 .xib 文件來定義的界面,那么遷移到一個標準 view controller 將會非常簡單。但是如果你使用了 storyboards,那么這個過程要多包含幾個步驟。除非重新創建,否則你并不能在 storyboards 中將 table view controller 改成一個標準的 view controller。這意味著你必須將所有內容拷貝到新的 view controller,然后再重新連接一遍。

最后,你需要把遷移后丟失的 table view controller 的特性給補回來。大多數都是viewWillAppear:或viewDidAppear:中簡單的一條語句。切換編輯模式需要實現一個 action 方法,用來切換 table view 的editing屬性。大多數工作來自重新創建對鍵盤的支持。

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

使用 Child View Controllers

和完全拋棄 table view controller 不同,你還可以將它作為 child view controller 添加到其他 view controller 中(關于此話題的文章)。這樣,parent view controller 在管理其他的你需要的新加的界面元素的同時,table view controller 還可以繼續管理它的 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 controller 和 parent view controller 之間建立消息傳遞的渠道。比如,如果用戶選擇了一個 table view 中的 cell,parent view controller 需要知道這個事件來推入其他 view controller。根據使用習慣,通常最清晰的方式是為這個 table view controller 定義一個 delegate protocol,然后到 parent view controller 中去實現。

//

@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

//

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

分離關注點(Separating Concerns)

當處理 table views 的時候,有許多各種各樣的任務,這些任務穿梭于 models,controllers 和 views 之間。為了避免讓 view controllers 做所有的事,我們將盡可能地把這些任務劃分到合適的地方,這樣有利于閱讀、維護和測試。

這里描述的技術是文章更輕量的 View Controllers中的概念的延伸,請參考這篇文章來理解如何重構 data source 和 model 的邏輯。結合 table views,我們來具體看看如何在 view controllers 和 views 之間分離關注點。

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

有時我們需要將想顯示的 model 層中的數據傳到 view 層中去顯示。由于我們同時也希望讓 model 和 view 之間明確分離,所以通常把這個任務轉移到 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;

}

//


但是這樣的代碼會讓 data source 變得混亂,因為它向 data source 暴露了 cell 的設計。最好分解出來,放到 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 view 的 data source 已經分解到單獨的類中了,它用一個設置 cell 的 block 來初始化。這時,這個 block 就變得這樣簡單了:

TableViewCellConfigureBlock block = ^(PhotoCell *cell, Photo *photo) {

[cell configureForPhoto:photo];

};

讓 Cells 可復用

有時多種 model 對象需要用同一類型的 cell 來表示,這種情況下,我們可以進一步讓 cell 可以復用。首先,我們給 cell 定義一個 protocol,需要用這個 cell 顯示的對象必須遵循這個 protocol。然后簡單修改 category 中的設置方法,讓它可以接受遵循這個 protocol 的任何對象。這些簡單的步驟讓 cell 和任何特殊的 model 對象之間得以解耦,讓它可適應不同的數據類型。

在 Cell 內部控制 Cell 的狀態

如果你想自定義 table views 默認的高亮或選擇行為,你可以實現兩個 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 方法的實現又基于了 view controller 知曉 cell 實現的具體細節。如果我們想替換或重新設計 cell,我們必須改寫 delegate 代碼。View 的實現細節和 delegate 的實現交織在一起了。我們應該把這些細節移到 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 層的實現細節分離開。delegate 肯定得清楚一個 view 該顯示什么狀態,但是它不應該了解如何修改 view 結構或者給某些 subviews 設置某些屬性以獲得正確的狀態。所有這些邏輯都應該封裝到 view 內部,然后給外部提供一個簡單的 API。

控制多個 Cell 類型

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

- (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 進行刪除或重新排序。這些事件都可以讓 table view 的 data source 通過delegate 方法得到通知。因此,通常我們能在這些 delegate 方法中看到對數據的進行修改的操作。

修改數據很明顯是屬于 model 層的任務。Model 應該為諸如刪除或重新排序等操作暴露一個 API,然后我們可以在 data source 方法中調用它。這樣,controller 就可以扮演 view 和 model 之間的協調者,而不需要知道 model 層的實現細節。并且還有額外的好處,model 的邏輯也變得更容易測試,因為它不再和 view controllers 的任務混雜在一起了。

總結

Table view controllers(以及其他的 controller 對象!)應該在 model 和view 對象之間扮演協調者和調解者的角色。它不應該關心明顯屬于 view 層或 model 層的任務。你應該始終記住這點,這樣 delegate 和 data source 方法會變得更小巧,最多包含一些簡單的樣板代碼。

這不僅減少了 table view controllers那樣的大小和復雜性,而且還把業務邏輯和 view 的邏輯放到了更合適的地方。Controller 層的里里外外的實現細節都被封裝成了簡單的 API,最終,它變得更加容易理[看這里][13]解,也更利于團隊協作

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,701評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,691評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,694評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,026評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,193評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,719評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,442評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,668評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,846評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,394評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容