簡潔的表格檢視程式碼 無腦意譯

Cruise_Chan發表於2014-11-05

前言

表格檢視在iOS應用開發中是極具多變的模組,會有很多相關得此類程式碼,包含了提供資料,更新檢視,響應行為事件,響應使用者選擇等等功能。本文主要介紹如何編寫一個簡潔優良架構的表格類程式碼。

UITableViewController對比UIViewController

表格檢視控制器是專門用來服務於表格檢視。其實現了大把有用的特性從而避免不斷去敲一些既定需求的程式碼。相反方面,表格檢視控制器被嚴格限制於只管理滿屏的表格檢視。但是,在許多場景已經夠用。若不夠用,也有一些方法可以解決,下面會介紹這些方法。

表格檢視控制器特性

表格檢視控制器當第一次出現得時候為你載入表格檢視資料。更專業一點的說,它為你連線表格檢視的編輯模式,響應鍵盤事件,重新整理滾動標誌圖,反選。為了能成功的實現這些效果,對你來說非常重要的是你在自定義的子類中重寫檢視事件方法時候需要呼叫父類的檢視事件方法(比如viewWillAppear:viewDidAppear:)。

對比標準的檢視控制器,表格檢視控制器有一個只有其才擁有的賣點那就是支援蘋果拉動式重新整理的實現。現在唯一有文件化宣告並使用UIRefreshControl。有其他一些方式去實現這種功能,但是如果大蘋果更新了iOS了可能就失效了。。。

表格檢視控制器的侷限性

表格檢視通常情況自然是表格檢視控制器的成員變數。如果你決定除了表格檢視外還在你的螢幕上顯示其他的東東,那你面對糟糕的約束時候你就不夠走運了。

如果你定義你的介面無論是使用程式碼亦或是使用XIB則非常弄容易的轉換成標準檢視控制器。但是一旦你使用了SB,那就需要涉及到多一些的步驟。使用SB的時候你不能夠在不重新建立的前提下改變表格檢視控制器成標準檢視控制器。這意味著你不得不拷貝所有得內容到新的檢視控制器並重新將所有東西連線好。

最後,你還需要加回所有表格試圖控制器所具有因為轉換遺漏的特性。大多數是很簡單的一句程式碼比如在viewWillAppear或者viewDidAppear。切換編輯狀態需要實現一個動作方法,該方法改變表格檢視的編輯狀態editing屬性。工作量最大的在於編寫鍵盤事件響應的支援。

在你這麼做之前,這裡有個一個很好的轉換方法??

子檢視控制器

與徹底取代表格檢視控制器不同,你也可以以子檢視控制器的形式新增它。這時候就可以讓表格試圖控制器關注它該關注的東西了,而父試圖控制器則關注剩餘的介面。

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

如果你用上述實現模式,你需要為父子檢視控制器建立一個通訊通道。舉個例子,如果使用者選中一個cell那麼父試圖控制器需要知道該事件並且推入另外一個試圖控制器。根據使用情形,通常最簡潔的方式是為表格檢視控制器定義一個代理協議,代理方法具體實現由父檢視控制器搞定。

@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

正如你所看到的,這種構建確實可以得到一個解耦和更好複用的效果但是同時也造成額外的通訊代價。根據特殊的使用情形,這樣可以根據具體需要來簡化或者複雜化。這是你所要思考跟決定的。

解耦思想

處理表格檢視的時候會因為涉及到因為資料,控制器,檢視之間的互動的深度不同會有不同的變種。為了避免程式碼理到處都是這些業務邏輯,我們需要儘量在正確的地方編寫出這部分邏輯程式碼。這樣可以增強可讀性,可維護性以及可測試性。

連線資料物件與Cells

我們需要根據資料來源來展示我們的cells,一般做法是讓表格檢視的資料來源來完成這個任務:

- (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的類的category分離cell根據業務初始化的程式碼:

@implementation PhotoCell (ConfigureForPhoto)

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

@end

這樣子的好處是使我們的資料來源方法十分精簡:

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

我們還可以通過塊設定來進一步精簡我們的程式碼:

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

Cells複用

在許多場景我們有多種得資料物件需要使用一些相同型別得cell展示,我們可以進一步提升cell的複用效果。要做到這些,我們首先要定一個cell需要遵守的協議來展示我們的cell,從而我們可以簡單改變cell的category中的配置方法中遵守該協議的任何物件。這些簡單地步驟可以讓cell從特定的資料物件中解耦出來並且可以使其適用於不同的資料型別。

在Cell中處理Cell的狀態

如果我們想多處理一些超出標準高亮、選擇的表格檢視操作。我們需要實現下面兩個代理方法來讓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;
}

然後,這兩個代理的實現依賴於它們知道特定的cell實現。如果我們需要交換cell或者重現用另外的方式實現,我們不得不去適配代理方法。檢視的具體實現完全以代理實現為根本。換言之,我們需要把邏輯搬到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

通常來說我們極力推薦將檢視層的實現與控制器層的實現做到分割。代理需要知道檢視可能處於的不同狀態,但是不能去越權修改如何修改檢視樹結構或者子檢視的屬性來獲取想要的狀態。所有得邏輯需要在檢視內封裝好,對外暴露的介面需要保證簡潔。

處理多Cell型別

若在你的表格檢視有多種不同的cell型別,那麼資料來源方法可以幫你很容易的搞定這類問題。在我們的栗子APP中的圖片詳情表格我們擁有兩種不同的Cell型別:一種用來展示星級,另外一種用來展示鍵值對Cell。為了將處理不同cell型別的程式碼分離出去,資料來源方法根據不同的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
{
    // ...
}

表格檢視編輯

表格檢視提供了一種便捷的編輯方式,允許使用者改變cell順序以及刪除cell。在這些事件中,表格檢視的資料來源通過代理方法得到修改。因此,我們通常主要在這些代理方法中看到資料修改的程式碼實現。

修改資料其實是資料層應該乾的活。資料層需要暴露出API去刪除或者排序,從而我們可以呼叫這些資料來源方法。這樣子,控制器可以扮演一個檢視與資料協調人的角色而不用去管資料層具體實現細節。至於更多的優點是在於資料層可以更容易被測試,因為它不用跟檢視控制器的其他業務邏輯混雜在一起。

總結

表格檢視控制器需要儘可能的承當資料與檢視中間人的角色。它們不應該狗拿耗子去管理本應屬於檢視層或者資料層所要關注的任務。如果你始終記住這點,那麼代理以及資料來源方法將可以簡化到只包含簡單的模板。

這樣做不僅僅淡出的降低了表格檢視控制的程式碼量與複雜度,更把一些業務邏輯以及檢視邏輯放在了恰當得地方。實現的具體細節被封裝到了API裡面,最終達到程式碼可讀性優良以及更好的呼叫協作!

相關文章