UITableView 組件化

源起

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

  • 繁瑣的重用流程

幾乎所有 TableView Adapter 中都有如下的代碼 registerClass(Nib):forCellReuseIdentifier 進(jìn)行 cell 重用的注冊(cè),后續(xù)又需要使用 dequeueReusableCellWithIdentifier: 獲取對(duì)應(yīng) cell。蘋(píng)果的這套重用機(jī)制對(duì)于開(kāi)發(fā)者來(lái)說(shuō)相當(dāng)簡(jiǎn)單友好,但寫(xiě)多了難免覺(jué)得重復(fù)乏味。同時(shí)如何給 cell 設(shè)置一個(gè)有意義且不重復(fù)的 reuseIdentifier 又會(huì)成為眾多強(qiáng)迫癥程序員的煩惱之一。

  • 不安全的 model 和 cell 映射關(guān)系

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

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

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

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

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

組件化方案

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

基礎(chǔ)組件

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

  • M80TableViewComponent
  • M80TableViewSectionComponent
  • M80TableViewCellComponent

顧名思義,他們分別對(duì)應(yīng) UITableView,Section 和 UITableViewCell。用前端技術(shù)做類(lèi)比的話,M80TableViewComponent 就是我們定義的 VirtualDOM,而 UITableView 則是真正的 DOM。前者記錄虛擬的層次結(jié)構(gòu),后者仍負(fù)責(zé)最終的渲染。具體關(guān)系參考下圖:

簡(jiǎn)單使用

定義組件

一個(gè)簡(jiǎn)單的 M80TableViewComponent 定義如下

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

  • 當(dāng)前組件對(duì)應(yīng)何種 UITableViewCell: - (Class)cellClass
  • 當(dāng)前組件對(duì)應(yīng) UITableViewCell 高度是多少: - (CGFloat)height
  • 如何通過(guò)當(dāng)前組件配置 UITableViewCell: - (void)configure:(UITableViewCell *)cell

和 UITableView 聯(lián)動(dòng)

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

具體效果詳見(jiàn) Example Project

特性

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

單向綁定

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

靈活組裝功能

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

自動(dòng)重用

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

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

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

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

  • (NSString *)diffableHash;

默認(rèn)情況下,每個(gè) cell component 在初始化時(shí)都會(huì)有自己唯一的 cellIdentifier 作為 diffableHash。

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

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

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

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

使用貼士

不同于以往構(gòu)建 UITableView 的常見(jiàn)用法,使用 M80TableViewComponent 推薦所有操作都針對(duì) component 進(jìn)行。

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

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

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

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