整潔的 Table View 代碼

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

UITableViewController vs. UIViewController

Apple 提供了 UITableViewController 做爲 table views 專屬的 view controller 類。Table view controllers 實現了一些很是有用的特性,來幫你避免一遍又一遍地寫那些死板的代碼!可是話又說回來,table view controller 只限於管理一個全屏展現的 table view。大多數狀況下,這就是你想要的,但若是不是,還有其餘方法來解決這個問題,就像下面咱們展現的那樣。ios

對於Swift,有更好的看法,想要更好的探討,能夠進入iOS技術羣,一塊兒探討交流

點擊此處進交流羣 有技術的來閒聊 沒技術的來學習

Table View Controllers 的特性

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

Table view controllers 相對於標準 view controllers 的一個特別的好處是它支持 Apple 實現的「下拉刷新」。目前,文檔中惟一的使用 UIRefreshControl 的方式就是經過 table view controller ,雖然經過努力在其餘地方也能讓它工做(見此處),但極可能在下一次 iOS 更新的時候就不行了。app

這些要素加一塊兒,爲咱們提供了大部分 Apple 所定義的標準 table view 交互行爲,若是你的應用剛好符合這些標準,那麼直接使用 table view controllers 來避免寫那些死板的代碼是個很好的方法。iphone

Table View Controllers 的限制

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

若是你是用代碼或 .xib 文件來定義的界面,那麼遷移到一個標準 view controller 將會很是簡單。可是若是你使用了 storyboards,那麼這個過程要多包含幾個步驟。除非從新建立,不然你並不能在 storyboards 中將 table view controller 改爲一個標準的 view controller。這意味着你必須將全部內容拷貝到新的 view controller,而後再從新鏈接一遍。學習

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

在選擇這條路以前,其實還有一個更輕鬆的選擇,它能夠經過分離咱們須要關心的功能(關注點分離),讓你得到額外的好處:ui

使用 Child View Controllers

和徹底拋棄 table view controller 不一樣,你還能夠將它做爲 child view controller 添加到其餘 view controller 中(關於此話題的文章)。這樣,parent view controller 在管理其餘的你須要的新加的界面元素的同時,table view controller 還能夠繼續管理它的 table view。spa

- (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,最終,它變得更加容易理解,也更利於團隊協做。

擴展閱讀

小編這裏有大量的書籍和麪試資料哦(點擊下載

原文Clean table view code

相關文章
相關標籤/搜索