如何寫好一個UITableView(上)

bestswifter發表於2016-04-18

本文是直播分享的簡單文字整理,視訊地址:優酷YouTube
Demo 地址:KtTableView

MVC

討論解耦之前,我們要弄明白 MVC 的核心:控制器(以下簡稱 C)負責模型(以下簡稱 M)和檢視(以下簡稱 V)的互動。

這裡所說的 M,通常不是一個單獨的類,很多情況下它是由多個類構成的一個層。最上層的通常是以 Model 結尾的類,它直接被 C 持有。Model 類還可以持有兩個物件:

  1. Item:它是實際儲存資料的物件。它可以理解為一個字典,和 V 中的屬性一一對應
  2. Cache:它可以快取自己的 Item(如果有很多)

常見的誤區:

  1. 一般情況下資料的處理會放在 M 而不是 C(C 只做不能複用的事)
  2. 解耦不只是把一段程式碼拿到外面去。而是關注是否能合併重複程式碼, 並且有良好的拖展性。

原始版

在 C 中,我們建立 UITableView 物件,然後將它的資料來源和代理設定為自己。也就是自己管理著 UI 邏輯和資料存取的邏輯。在這種架構下,主要存在這些問題:

  1. 違背 MVC 模式,現在是 V 持有 C 和 M。
  2. C 管理了全部邏輯,耦合太嚴重。
  3. 其實絕大多數 UI 相關都是由 Cell 而不是 UITableView 自身完成的。

為了解決這些問題,我們首先弄明白,資料來源和代理分別做了那些事。

資料來源

它有兩個必須實現的代理方法:

簡單來說,只要實現了這個兩個方法,一個簡單的 UITableView 物件就算是完成了。

除此以外,它還負責管理 section 的數量,標題,某一個 cell 的編輯和移動等。

代理

代理主要涉及以下幾個方面的內容:

  1. cell、headerView 等展示前、後的回撥。
  2. cell、headerView 等的高度,點選事件。

最常用的也是兩個方法:

提醒:絕大多數代理方法都有一個 indexPath 引數

優化資料來源

最簡單的思路是單獨把資料來源拿出來作為一個物件。

這種寫法有一定的解耦作用,同時可以有效減少 C 中的程式碼量。然而總程式碼量會上升。我們的目標是減少不必要的程式碼。

比如獲取每一個 section 的行數,它的實現邏輯總是高度類似。然而由於資料來源的具體實現方式不統一,所以每個資料來源都要重新實現一遍。

SectionObject

首先我們來思考一個問題,資料來源作為 M,它持有的 Item 長什麼樣?答案是一個二維陣列,每個元素儲存了一個 section 所需要的全部資訊。因此除了有自己的陣列(給cell用)外,還有 section 的標題等,我們把這樣的元素命名為 SectionObject

Item

其中的 items 陣列,應該儲存了每個 cell 所需要的 Item,考慮到 Cell 的特點,基類的 BaseItem 可以設計成這樣:

父類實現程式碼

規定好了統一的資料儲存格式以後,我們就可以考慮在基類中完成某些方法了。以 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 方法為例,它可以這樣實現:

比較困難的是建立 cell,因為我們不知道 cell 的型別,自然也就無法呼叫 alloc 方法。除此以外,cell 除了建立,還需要設定 UI,這些都是資料來源不應該做的事。

這兩個問題的解決方案如下:

  1. 定義一個協議,父類返回基類 Cell,子類視情況返回合適的型別。
  2. Cell 新增一個 setObject 方法,用於解析 Item 並更新 UI。

優勢

經過這一番折騰,好處是相當明顯的:

  1. 子類的資料來源只需要實現 cellClassForObject 方法即可。原來的資料來源方法已經在父類中被統一實現了。
  2. 每一個 Cell 只要寫好自己的 setObject 方法,然後坐等自己被建立,被呼叫這個方法即可。
  3. 子類通過 objectForRowAtIndexPath 方法可以快速獲取 item,不用重寫。

對照 demo(SHA-1:6475496),感受一下效果。

優化代理

我們以之前所說的,代理協議中常用的兩個方法為例,看看怎麼進行優化與解耦。

首先是計算高度,這個邏輯並不一定在 C 完成,由於涉及到 UI,所以由 Cell 負責實現即可。而計算高度的依據就是 Object,所以我們給基類的 Cell 加上一個類方法:

另外一類問題是以處理點選事件為代表的代理方法, 它們的主要特點是都有 indexPath 引數用來表示位置。然而實際在處理過程中,我們並不關係位置,關心的是這個位置上的資料。

因此,我們對代理方法做一層封裝,使得 C 呼叫的方法中都是帶有資料引數的。因為這個資料物件可以從資料來源拿到,所以我們需要能夠在代理方法中獲取到資料來源物件。

為了實現這一點, 最好的辦法就是繼承 UITableView

cell 高度的實現如下,呼叫資料來源的方法獲取到資料:

優勢

通過對 UITableViewDelegate 的封裝(其實主要是通過 UITableView 完成),我們獲得了以下特性:

  1. C 不用關心 Cell 高度了,這個由每個 Cell 類自己負責
  2. 如果資料本身存在資料來源中,那麼在代理協議中它可以被傳給 C,免去了 C 重新訪問資料來源的操作。
  3. 如果資料不存在於資料來源,那麼代理協議的方法會被正常轉發(因為自定義的代理協議繼承自 UITableViewDelegate

對照 demo(SHA-1:ca9b261),感受一下效果。

更加 MVC,更加簡潔

在上面的兩次封裝中,其實我們是把 UITableView 持有原生的代理和資料來源,改成了 KtTableView 持有自定義的代理和資料來源。並且預設實現了很多系統的方法。

到目前為止,看上去一切都已經完成了,然而實際上還是存在一些可以改進的地方:

  1. 目前仍然不是 MVC 模式!
  2. C 的邏輯和實現依然可以進一步簡化

基於以上考慮, 我們實現一個 UIViewController 的子類,並且把資料來源和代理封裝到 C 中。

為了確保子類建立了資料來源,我們把這個方法定義到協議裡,並且定義為 required

成果與目標

現在我們梳理一下經過改造的 TableView 該怎麼用:

  1. 首先你需要建立一個繼承自 KtTableViewController 的檢視控制器,並且呼叫它的 initWithStyle 方法。
  2. 在子類 VC 中實現 createDataSource 方法,實現資料來源的繫結。
  3. 在資料來源中,需要指定 cell 的型別。
  4. 在 Cell 中,需要通過解析資料,來更新 UI 並返回自己的高度。

下一步做什麼?

關於 TableView 的討論遠遠沒有結束,我列出了以下需要解決的問題

  1. 在這種設計下,資料的回傳不夠方便,比如 cell 的給 C 發訊息。
  2. 下拉重新整理與上拉載入如何整合
  3. 網路請求的發起,與解析資料如何整合

關於第一個問題,其實是普通的 MVC 模式中 V 和 C 的互動問題,可以在 Cell(或者其他類) 中新增 weak 屬性達到直接持有的目的,也可以定義協議。

問題二和三是另一大塊話題,網路請求大家都會實現,但如何優雅的整合進框架,保證程式碼的簡單和可擴充,就是一個值得深入思考,研究的問題了。我會在下次有空的時候和大家分享這個問題。

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

任選一種支付方式

如何寫好一個UITableView(上) 如何寫好一個UITableView(上)

相關文章