打造輕量級 ViewController 之抽離 DataSource/Delegate

發表於2016-05-05

前言

UITableView/UICollectionView 是我們開發中使用最為頻繁的兩個控制元件。關於其使用的實踐網上已經有很多優秀的總結了,所以我不打算再囉嗦了。今天要討論的問題基於 objc.io 的一遍文章 Lighter View Controllers,此文講述如何通過抽取頻繁出現的配置型別的程式碼到專門的一個 DataSource/Delegate 裡面來為 Controller 瘦身。我們從中受到了啟發,由於文中給出的 demo 不具有通用性,所以打算寫一個比較全面的封裝來組織 DataSource/Delegate 的程式碼。

我們先看一下平時都是怎麼使用 UITableView 的,一般我們需要做這樣幾件事:

  • 註冊需要使用的 cell 的樣式到 UITableView
  • 實現 UITableViewDataSource 的幾個方法來告訴 UITableView 什麼地方怎麼如何顯示 cell
  • 實現 UITableViewDelegate 來告訴 UITableView 具體每個 cell 的高度,以及處理點選事件等

一般情況大致做的就這些。通常的做法就是直接設定當前 controllertableView 的資料來源和事件回撥委託。這樣會造成很多 controller 有大致一致的程式碼。經驗告訴我們,大面積出現類似的程式碼就可以考慮把這些程式碼抽取出來封裝成一個更加通用的組織形式了。也就是我們要做的事情,其實就是對 UITableViewDataSourceUITableViewDelegate 的進一步拆分和封裝。

思考一下我們看到的 tableView 都包含哪些部分,展示了哪些元素。從 Apple 提供的 API 中我們可以看出大致包含 tableViewHeader/tableViewFooterSectionHeaderView/SectionFooterViewSectionHeaderTitle/SectionFooterTitlesectionIndex 以及最重要的 Cell。如果我們把這些東西都對映成為一個資料型別,然後直接讓 tableView 去取對應的部分資料然後渲染到介面上不就好了麼,每個頁面我們就不再關心如何去實現 UITableViewDataSource/UITableViewDelegate ,只需要告知必要的資訊,其餘重複性極高的事情就交給封裝的程式碼來做了,就像在配置介面一樣,真正實現「你們做 iOS 的不就是把服務端的資料顯示在介面上就好了麼」。

廢話了這麼多,直接上我們的解決方案吧!原始碼已經放到 GitHub 上了。下面主要說一下怎麼用。

程式碼組織

程式碼主要分為以下幾部分:

  • TCDataSourceProtocol: 對 UITableViewUICollectionView 按照介面劃分為幾個配置不同介面的模組,實現者根據需求實現各自的協議,來 “配置” 介面。
  • TCDataSourceDataSource 的基類,所有 UITableViewUICollectionView 的資料來源的基類,其中已經預設實現了重複率高的程式碼,其實就是對 UITableViewDataSource/UICollectionViewDataSource 的實現。還實現了 UITableviewMove/Edit 操作的輔助方法。UICollectionViewMove 操作輔助方法等。
  • TCDelegateDelegate 的基類,所有 UITableViewUICollectionView 的委託的基類,其中實現了與 UIScrollView 相關的一部分功能,比如 Cell的圖片懶載入。為子類實現一些輔助方法,比如基於 Autolayout 自動計算 Cell/SectionHeaderView/SectionFooterView 行高的輔助方法。
  • TCSectionDataMetricUITableView/UICollectionView 各個分組的元件的資料封裝。包含 SectionHeaderView/SectionFooterView, SectionHeaderTitle/SectionFooterTitle 以及 Cell 等的資料。
  • TCGlobalDataMetric:對整個 UITableView/UICollectionView 各個元件的資料的封裝。其作為一個容器,裡面包含若干個 TCSectionDataMetric

基本使用

下面直接以我工作的通用樣板來說明如何使用,一個場景的檔案目錄大致像這樣:

  • ProductViewController(基於 UITableView)
  • ProductViewModel(採用 RAC 來處理網路層邏輯)
  • ProductDataSource
  • ProductDelegate
  • Views
  • Model

基於這樣的架構,Controller 檔案程式碼一般保持在 200 到 300 行之間,其他檔案行數則更少。這樣一來程式碼清晰了,邏輯自然也比較容易釐清,修改功能也容易多了。至於維護那種開啟檔案一看就是上千行程式碼的情況,我的內心是崩潰的。

言歸正傳,來看一下相關類中的關鍵程式碼是怎樣的?

ProductViewController 中,初始化 DataSourceDelegate 並關聯到 tableView

ProductDataSource 需要繼承自 TCDataSource

ProductDelegate 原始碼大致如下:

最後介面都配置好了,你需要為配置好的介面提供資料。也就是 ProductViewModel 中做的事情,從伺服器獲取資料,並組裝成框架需要的資料結構,也就是 TCGlobalDataMetric 大致表示如下:

最後更新資料來源中的資料並過載 TableView 即可展示所有的介面了。

關於 Cell 的高度,你可以自己實現 delegate 的高度相關的方法,或者簡單的返回輔助方法。如下所示

需要注意的是,採用這種方式,你需要在 celllayoutSubviews 裡面指定多行文字的 preferredMaxLayoutWidth,或許是我哪裡處理錯了,但這樣才能正確計算行高。

如果你需要實現的只是簡單的介面展示,那麼以上就已經完全滿足需求了。

但是如果只提供這些功能,恐怕封裝的優勢就不是那麼明顯了,請接著看。

如何實現其他功能

如何提供 Section Title

  • 設定 tableViewstyle.Grouped
  • 每個 sectionTCSectionDataMetric 初始化的時候提供 title

如何提供 Section header/footer view

擴充套件 ProductDataSource 讓其遵守 TCTableViewHeaderFooterViewibility 協議

delegate 裡面提供 header/footer view,為了防止與 section title 衝突,所以預設未實現,你需要動手呼叫輔助方法,如下所示。 如果你使用 Autolayout,你還可使用輔助方法來計算 header/footer view 的高度。

如何提供編輯選項

如果你需要插入、刪除 Cell,只需要實現 TCTableViewEditable 協議即可。

同時你需要實現 UITabelViewDelegate 的方法來指定編輯模式,不實現預設為刪除操作。

如何提供移動操作

由上面的規律,你應該知道。只需要實現某個協議就可以了。對於 UICollectionView 需要 iOS9+ 才能該協議才會生效,所以如果你需要重新排序的功能,GitHub 有你需要的實現

如何提供索引功能

索引功能由資料來配置,在初始化 TCSectionDataMetric 的時候,帶上 index title 即可,與 section header/footer title 類似。

懶載入圖片

如果你需要該功能,在配置 cell 資料的時候不要設定圖片,在這個方法裡面來設定圖片的資料,即可實現圖片的懶載入功能。

以上提到的都是基於 UITableView 的例子,UICollectionView 原理類似。 你可以實現 TCCollectionSupplementaryViewibility,為 UICollectionView 提供類似 header/footer view 的效果 當然,懶載入圖片也是可以使用的。

回頭看看

為什麼要自己造 TCGlobalDataMetricTCSectionDataMetric

因為像 Lighter View Controllers demo 中的方式, 直接使用陣列只能表示單個分組,或者使用二維陣列來表示多個分組。這樣會讓人很疑惑。也無法將 header/footer title/view 的資料組合到 與 cell 平級的資料中,資料也分散在不同的地方。所以我們的方式是將整個 tableview 所需要的所有的資料都放到一起,就成了你看到的 TCGlobalDataMetricTCSectionDataMetric。這樣就可以實現由資料來驅動介面的效果。你需要做的就是按照 UI 效果來組裝整個 tableView/collectionView 的資料即可。

為什麼需要基類 ProductDataSource 而不是直接基於協議

試想一下,有個提供資料的協議 DataSourceProtocol,然後我們預設實現 tableViewdataSource 相關程式碼。如下所示:

然後我們在使用的時候只需要讓我們自己的資料來源實現該協議,由於已經提供了預設實現,所以我們其實什麼實現程式碼都不用寫了。

這樣不是更加靈活,更加面向協議,更加 swiftly。 嗯。設想是美好的,但是現實總是會嘲笑我們圖樣圖森破。至於為什麼不行,請檢視參考資料中的連結一探究竟。

為什麼所有的資料都是 TCDataType(aka AnyObject) 型別的

嗯。這也是框架中做的不好的地方。這麼做的原因是每個 cell 所需要的資料型別可能不一樣。 如果都一樣的話,那麼很明顯我們可以採用泛型方式在為 cell 配置資料的時候解析出具體的資料型別,一旦這樣做了,就不具有通用性了。 那為什麼採用 AnyObject 呢,而不是 AnyAny 表示的範圍更加大。 由於 tableView(_:, sectionForSectionIndexTitle:, atIndex:) -> Int 方法中會用到 indexOf, 該方法接受一個實現了 Equatable 協議的引數。或者自己提供一個 closure 來告訴它如何判斷你提供的元素是否相等。 為了不讓使用者自己提供該方法的實現,我們選擇了系統預設實現該協議的範圍儘可能大的型別 AnyObject。 所以在使用資料(設定 cell 資料)的時候,你需要轉換成對應的具體型別。

我還沒有用上 Swift

噢。那你可以看看類似封裝的 Objective-c 版本。(之前也是用的 OC 版本的,新專案啟動就翻譯成了 swift…)

宣告

最後,需要特別宣告。作者水平有限,程式碼只代表個人的思考,不保證絕對正確。希望能夠拋磚引玉,有更好的見解還望不吝賜教。 如果能夠對讀者有所幫助,那就再好不過了。

感謝 WayJoey 兩位小夥伴的鼓勵和幫助。

參考資料

相關文章