表檢視是一個非常萬能的iOS應用程式構建模組。因此,有很多與表檢視直接或間接相關的程式碼,包括提供資料、更新表檢視、控制其行為和選擇做出的反應,這僅僅是幾個例子。在這篇文章裡,我們將會介紹一些整潔而結構良好的程式碼。
UITableViewController VS UIViewControler
蘋果提供了UITableViewController作為表檢視的專用檢視控制器。Table view controllers實現了一些非常有用的特性來幫助你避免寫重複的程式碼。另一方面,Table view controllers僅限於管理一個全屏顯示的表檢視。然而,在大多數情況下,這就可以滿足你的需要了。如果不滿足的話,我們將會在下面講解解決它的方法。
Table View Controllers的特性
Table View Controllers將會在它第一次顯示的時候幫你載入表檢視的資料。更具體地說,它可以幫助切換表檢視的編輯模式,對鍵盤的通知做出反應,比如滾動重新整理和清除選項這類的小功能。重要的是,你可以自定義子類來重寫這些可以被稱為萬能的檢視時間方法來實現這些特性。
Table View Controllers有一個獨特的賣點超越了標準檢視控制器的獨特賣點,它支援蘋果的z“下拉更新”的功能目前,這是唯一通過使用一個表控制器來控制重新整理記錄的方法。還有一些其他的方法來使它生效,但是在下一次iOS中的更新卻不是那麼的容易
所有的這些原理提供了大量的表檢視控制器的介面像蘋果已經定義的那樣,如果你APP符合這些標準,堅持使用表檢視控制器是一個避免重寫模版程式碼的好辦法。
Table View控制器的限制
表檢視控制器的檢視屬性總是被設定在一個表檢視上。如果你以後決定想在螢幕上顯示除了檢視表以外的東西(比如地圖),假如你不想依賴於笨拙的補丁那你就有的慘了。
如果你已經定義在程式碼裡定義了介面或者在使用.xib檔案,那麼轉換到一個標準的檢視控制器就會相當容易了。如果你使用指令碼的話這個轉換過程會涉及到更多一些的步驟。用指令碼,你需要通過重新建立來把一個表檢視控制器轉變為一個標準的表檢視控制器。這就意味這你必須把所有的內容複製到這個新的試圖控制器中然後再重新建立。最後,你需要重新把轉變過程中丟失的表檢視控制器的功能新增一遍。大部分都是viewWillAppear或viewDidAppear裡簡單的單行語句。切換編輯狀態需要一個點選表檢視的編輯屬性方法來執行。大部分的工作在於重新建立鍵盤支援。
在你繼續走這個路線之前,這裡有一個關注點分離附加好處的簡單替代方法。
子檢視控制器
並非為了完全擺脫表檢視管理器,你也可以將其作為一個子檢視控制器新增到另一個檢視控制器(見本文關於檢視管理器的控制)。然後表檢視管理器只需要繼續管理這個表檢視而父檢視管理器可以處理任何額外你需要的介面元素。
1 2 3 4 5 6 7 8 9 10 11 12 |
- (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]; } |
如果你選擇這個解決方法的話,你需要建立一個子檢視和父檢視之間的通訊通道。例如,為了能夠push另一個檢視進來,父檢視需要知道table view的cell被選中了。鑑於這個使用場景,最乾淨的方法就是為table view控制器定義一個代理協議,可以在父檢視中實現這個協議。
如果你要使用這個解決方法,你必須建立一個從子類到父類的通訊通道。例如,如果使用者選擇了一個表檢視的單元格,父檢視控制器需要接收到訊息來推動另一個檢視控制器。根據例項,通常最簡潔的方法是為這個表檢視控制器定義一個委託協議,然後你在父檢視管理器中實現。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@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 |
正如你看的那樣,這種結構會在檢視控制器之間的通訊中伴隨一些其他的額外開銷來換取乾淨的關注點分離和更好的重用性.根據具體的用例,最終使事情比必需要更簡單或者更復雜,這是你需要考慮和決定的。
分離關注點
當處理表檢視時有各種不同的任務關於模型,控制器和檢視的跨越邊界問題。為了防止檢視控制器成為存放這些任務的地方,我們會檢視講這些任務單獨的放在更合適的地方。這有助於程式碼的可讀性,維護性和測試性。
在輕檢視控制器的文章裡講述了詳細的概念和擴充套件技術。如何把我們的資料來源和模型邏輯引入。在表檢視的環境下,我們將專門看看如何分離檢視控制器和檢視的關注點的問題。
橋接模型物件和單元之間的差距
在某種程度上,我們必須交出想要在檢視層顯示的資料。由於我們想要維持一個模型和檢視之間清晰的分離點,經常把這個任務放到表檢視的資料來源裡。
1 2 3 4 5 6 7 8 9 |
- (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; } |
這個程式碼將資料來源和cell的設計邏輯繫結在了一起。我們最好將這個在cell的類別類裡重構一下。
1 2 3 4 5 6 7 8 9 10 |
@implementation PhotoCell (ConfigureForPhoto) - (void)configureForPhoto:(Photo *)photo { self.photoTitleLabel.text = photo.name; NSString* date = [self.dateFormatter stringFromDate:photo.creationDate]; self.photoDateLabel.text = date; } @end |
這種情況下,我們的資料來源程式碼就變得非常簡單了。
1 2 3 4 5 6 7 |
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { PhotoCell *cell = [tableView dequeueReusableCellWithIdentifier:PhotoCellIdentifier]; [cell configureForPhoto:[self itemAtIndexPath:indexPath]]; return cell; } |
在示例程式碼中,初始化cell的時候時候block的方式,table檢視的資料來源已經分離到了一個單獨的控制物件中,在這個例子中,這個block就像下面這樣
1 2 3 |
TableViewCellConfigureBlock block = ^(PhotoCell *cell, Photo *photo) { [cell configureForPhoto:photo]; }; |
使cell可重用
在這種有多個資料模型使用同一個cell型別展示的情況下,我們甚至可以進用一步就可以達到cell重用的效果。首先,我們定義一個所有需要使用這個cell型別展示資料的物件都需要實現的協議。然後,我們修改一些cell類別中的配置方法,使它可以接受任何遵循上述協議的物件。這兩個簡單的步驟將cell和資料模型分離並且使cell可以接受不同的資料型別。
在cell中處理cell的狀態
如果我們想做一些與預設情況下不同的table view的高亮和選中狀態,我們需要實現兩個代理方法來實現將cell修改成我們想要的狀態。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
- (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; } |
然而,這兩個方法需要依賴於知道cell是如何佈局的,如果我們想要換一個cel或者重新設計cell,我們同樣需要修改這段代理程式碼。view的設計細節就和代理交織在一起了,我們應該將這段邏輯放到cell中。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@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層的實現細節和控制器層的實現細節分離開來。代理可以知道view的狀態變化,但是不應該知道如何修改view的樹狀結構以及它的子檢視應該設成什麼狀態,所有這些狀態都應該封裝在view中,然後提供給外部一個訪問的介面。
處理多種cell型別
如果在一個table view中有多種cell型別,資料來源就要變得失控了。在我們的示例app中,我們的照片詳情表格有兩種不同型別的cell:一個顯示評分,另一個就是一般的顯示鍵-值的cell。為了將顯示不同cell型別的程式碼分離,資料來源方法裡就是簡單呼叫不同型別cell的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
- (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 controllers (和其他控制器物件)大部分情況下都應該起著模型和檢視物件之間的協調中介作用,他們不應該關心模型或者檢視的具體實現細節。如果你記住這點,那麼代表和資料來源方法就變得更簡單和更容易維護的樣板程式碼了。
這樣不僅會降低了Table view controllers 的程式碼規模和複雜性,而且使模型邏輯程式碼和檢視程式碼放在了更合適的地方。控制器上下之間的實現細節都被隱藏在簡單的API中,最終使得程式碼更容易理解和協同工作。