UITableView 組件化

源起

在 iOS 開發(fā)中,UITableView 可以說是最常用的控件。幾行代碼,實現(xiàn)對應方法,系統(tǒng)就會給你呈現(xiàn)一個 60 幀無比流暢的列表,讓初學者成就感爆棚。然而隨著開發(fā)的深入,我們就會慢慢覺察到當前的 UITableView 實現(xiàn)會有這樣或那樣的問題。

  • 繁瑣的重用流程

幾乎所有 TableView Adapter 中都有如下的代碼 registerClass(Nib):forCellReuseIdentifier 進行 cell 重用的注冊,后續(xù)又需要使用 dequeueReusableCellWithIdentifier: 獲取對應 cell。蘋果的這套重用機制對于開發(fā)者來說相當簡單友好,但寫多了難免覺得重復乏味。同時如何給 cell 設置一個有意義且不重復的 reuseIdentifier 又會成為眾多強迫癥程序員的煩惱之一。

  • 不安全的 model 和 cell 映射關系

隨著業(yè)務深入,一個 UITableView 往往會包含多種 model,對應不同形式的 cell,那么建立 model 和 cell 的映射關系就會非常蛋疼,無論是if else,switch,還是 map<model,cell> 都不是那么的優(yōu)雅,每當 model 類型有所增刪,開發(fā)者往往需要心驚膽戰(zhàn)地檢查各處實現(xiàn)方法里是否進行了正確的處理。

  • 單調(diào)的優(yōu)化過程

業(yè)務繼續(xù)深入,為了保證相關代碼整潔,易于拓展和性能高效,除了維護 model 和 cell 關系(ModelCellMap)外,我們往往需要引入各種類做職責分離:DataSource 管理數(shù)據(jù)源,LayoutManager 負責排版和提供預計算高度能力,CellHeightCache 提供高度緩存,Interactor 提供事件路由和處理等等,這樣可以一定程度減輕代碼膨脹的問題。但也不是完美的:套路都是類似的,即使你熟練掌握了這些所謂的設計原則,在實際操作中仍有大量的重復代碼。

  • 數(shù)據(jù)源和 UI 不綁定

當 model 變化時,我們往往需要通過當前 model 位置反推出 cell 在 UITableView 中的位置(即 indexPath),然后做相應的更新處理,反之亦然。但這部分工作無非是數(shù)組遍歷,尋找 index,重復且繁瑣,稍有不慎還有出錯導致崩潰的可能。

組件化方案

為了解決如上問題,同時也受到 IGListKit 和 React.js 的啟發(fā),M80TableViewComponent 提出了一種組件化的解決方案,實現(xiàn)類似 React.js 的 “單向數(shù)據(jù)綁定” 功能,同時將大量的重復計算歸納在組件內(nèi)部,上層使用者只需要根據(jù)當前業(yè)務創(chuàng)建相應組件并組合使用即可。

基礎組件

為了實現(xiàn)整個 UITableView 的流程, M80TableViewComponent 引入三個基礎組件:

  • M80TableViewComponent
  • M80TableViewSectionComponent
  • M80TableViewCellComponent

顧名思義,他們分別對應 UITableView,Section 和 UITableViewCell。用前端技術做類比的話,M80TableViewComponent 就是我們定義的 VirtualDOM,而 UITableView 則是真正的 DOM。前者記錄虛擬的層次結構,后者仍負責最終的渲染。具體關系參考下圖:

簡單使用

定義組件

一個簡單的 M80TableViewComponent 定義如下

這是一個用于文本列表顯示的組件,只實現(xiàn)最基本組件協(xié)議

  • 當前組件對應何種 UITableViewCell: - (Class)cellClass
  • 當前組件對應 UITableViewCell 高度是多少: - (CGFloat)height
  • 如何通過當前組件配置 UITableViewCell: - (void)configure:(UITableViewCell *)cell

和 UITableView 聯(lián)動

定義完組件后,我們只需要按照順序?qū)⒔M件加入父組件中,即可完成和 UITableView 的綁定。

具體效果詳見 Example Project

特性

看完上述的使用方式后,你很可能將 M80TableViewComponent 當成一種固定數(shù)據(jù)源組裝方式而已,并沒有其他新意。但事實上,除了充當固定結構數(shù)據(jù)源外,它還有如下優(yōu)勢

單向綁定

當我們使用組件時,一旦當前 M80TableViewComponent 和 UITableView 關聯(lián),后續(xù)針對 M80TableViewComponent 的所有操作都會實時反應到 UITableView 之上,包括對 cell component 的移除,刷新,插入,以及 section component 的插入,移除和刷新。我們不再需要繁瑣地通過 controller 同時操作 view 和 model 以保證其一致性,只需要單純操作 component 即可:component 將根據(jù)自身層次結構計算出對應的 UI 層次結構,在修改 component 內(nèi)部結構的同時也會自動獲取到對應的 cell 對象進行修改。這樣做的好處是上層開發(fā)只需要關注 component 即可,而不再關心 indexPath 相關的計算過程,從而規(guī)避繁復的 indexPath 計算及計算錯誤導致的崩潰。

靈活組裝功能

使用 M80TableViewComponent 可以輕易支持多種不同類型的數(shù)據(jù)模型,同時由于我們將復用層次從 vc/tableview 下降到 cell/section component 層次,也更方便了在不同場景下的組合使用。

自動重用

每一個 M80TableViewCellComponent 在第一次被使用時都會通過 M80TableViewComponentRegister 根據(jù)上下文信息自動綁定 reuseIdentifier 和 cellClass 的關系,完成 cell 的重用。默認使用當前 cell component 的類名作為 reuseIdentifier,既能保證不與其他 cell 重名,又省去了取名之苦。

高度優(yōu)化和局部刷新

在 iOS 中比較蛋疼的事情是如何判斷兩個對象相等:在不使用 runtime 的場景下,往往需要業(yè)務層添加大量冗余代碼用于支持對象比較,而使用了 runtime 又會對業(yè)務侵入過多。在 M80TableViewComponent 中我們使用了一種不基于 runtime 且比較輕量的方法:

所有的 M80TableViewCellComponent 都遵循 M80ListDiffable 協(xié)議,以用于組件內(nèi)部的一致性判斷:

  • (NSString *)diffableHash;

默認情況下,每個 cell component 在初始化時都會有自己唯一的 cellIdentifier 作為 diffableHash。

以此為出發(fā)點,我們就可以進行如下場景的優(yōu)化。

  • 自動 cell 高度緩存
  • 通過 ListDiff 算法實現(xiàn)的 section 局部刷新

當開啟高度緩存選項時,M80TableViewComponent 計算 cell 高度后會自動記錄 diffableHash 和 height 的對應關系。后續(xù)再次刷新將自動獲取對應高度而無需再次計算。當一個 cell 有多重狀態(tài),需要在不同狀態(tài)下展示不同高度時,則可以通過業(yè)務狀態(tài)返回不同的 diffableHash 進行高度切換。除了高度緩存外,M80TableViewComponent 也提供了一種預計算高度的機制,在組裝完 cell component 后,只需要簡單調(diào)用基類方法 measure 就可以直接完成預計算。

而適用局部刷新時,cell component 的 diffableHash 將做為唯一標識:old components 和 new components 根據(jù) diffableHash 被 hash 到不同桶內(nèi),沖突桶中的 component 標記為 move,不沖突桶中的 component 則為 add/remove。詳細算法可參考 M80ListDiff 函數(shù)。在合適的場景下,使用 ListDiff 進行 section 的重新載入,而不是人工計算各種變化信息后進行逐一操作,能夠在保證性能的前提下,簡化開發(fā)過程和良好的界面表現(xiàn)。

使用貼士

不同于以往構建 UITableView 的常見用法,使用 M80TableViewComponent 推薦所有操作都針對 component 進行。

  • 涉及單個 cell 的操作,直接使用 cell component 本身的方法,如 remove,reload 方法。
  • 涉及單個 section 內(nèi)多個 cell 變化,可以考慮每次重新 setComponents 或調(diào)用 reloadUsingListDiff 進行局部刷新。
  • 涉及到多 section 多 cell 變化,則可以重新組裝所有 component。一方面這樣做比較簡單,不容易出錯。另一方面 component 只是 viewmodel,在真正刷新前的批量操作并不會有過多性能問題。
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,621評論 1 32
  • UITableViewCell 父類是UIView UITableView的每一行都是一個UITableViewC...
    翻這個墻閱讀 6,796評論 0 1
  • 掌握 設置UITableView的dataSource、delegate UITableView多組數(shù)據(jù)和單組數(shù)據(jù)...
    JonesCxy閱讀 1,390評論 0 2
  • UITableView內(nèi)置了兩種樣式:UITableViewStylePlain,UITableViewStyle...
    Windv587閱讀 429評論 0 1
  • 很久沒有打開簡書寫寫看看了。 一直和孩子說寫作文不一定要真實發(fā)生的事情,文章來源于生活,但是要高于...
    我來自遠方閱讀 188評論 0 1

友情鏈接更多精彩內(nèi)容