提高 TableView 的整潔度

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

UITableViewController vs. UIViewController

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

TableViewControllers 的特性

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

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

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

TableViewControllers 的限制

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

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

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

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

使用 ChildViewControllers

和完全拋棄 tableViewController 不同,你還可以將它作為 childviewcontroller 添加到其他 viewController 中。這樣,parentViewController 在管理其他的你需要的新加的界面元素的同時,tableViewController 還可以繼續管理它的 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];
}

如果你使用這個解決方案,你就必須在 childViewController 和 parentViewController 之間建立消息傳遞的渠道。比如,如果用戶選擇了一個 tableView 中的 cell,parentViewController 需要知道這個事件來推入其他 viewController。根據使用習慣,通常最清晰的方式是為這個 tableViewController 定義一個 delegate protocol,然后到 parentViewController 中去實現。

@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

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

分離關注點(Separating Concerns)

當處理 tableViews 的時候,有許多各種各樣的任務,這些任務穿梭于 models,controllers 和 views 之間。為了避免讓 viewControllers 做所有的事,我們將盡可能地把這些任務劃分到合適的地方,這樣有利于閱讀、維護和測試。我們來具體看看如何在 viewControllers 和 views 之間分離關注點。

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

有時我們需要將想顯示的 model 層中的數據傳到 view 層中去顯示。由于我們同時也希望讓 model 和 view 之間明確分離,所以通常把這個任務轉移到 tableView 的 datasource 中去處理:

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

但是這樣的代碼會讓 datasource 變得混亂,因為它向 datasource 暴露了 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;
}

在我們的示例代碼中,tableView 的 datasource 已經分解到獨立的類中,它用一個設置 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 的狀態

如果你想自定義 tableViews 默認的高亮或選擇行為,你可以實現兩個 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 方法的實現又基于了 viewController 知曉 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,datasource 方法很快就難以控制了。在我們示例程序中,photoDetailsTable 有兩種不同類型的 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 的任務混雜在一起了。

此文章原文鏈接自己的個人博客: www.koalaliu.com ,因簡書平臺規范性以及用戶量,搬至簡書。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容