UITableView SingleCodePath

1. 起因

2. 設(shè)計(jì)與實(shí)現(xiàn)

3. 拓展


1. 起因

List 是開(kāi)發(fā)中最常見(jiàn)的一種控件骡男,由于業(yè)務(wù)迭代頻繁蕾域,所以宙橱,列表的使用會(huì)更多。但是匆赃,列表中會(huì)有許多重復(fù)的邏輯。比如今缚,數(shù)據(jù)源和操作事件的回調(diào)等算柳。將這些通用的代碼邏輯抽象出來(lái),不但有利于規(guī)范代碼路徑姓言,同時(shí)也是為 controller 減負(fù)的手段之一埠居。我們目前項(xiàng)目中用到了 DJTableView 做這件事情查牌。但是,相對(duì)于 Swift 項(xiàng)目來(lái)說(shuō)滥壕,DJTableView 的實(shí)現(xiàn)方式和接口調(diào)用上都不十分友好纸颜。并且,使用到目前發(fā)現(xiàn)了一些問(wèn)題绎橘。

<1> 只是對(duì) tableView 各種系統(tǒng)方法進(jìn)行了一層封裝胁孙,并不關(guān)心實(shí)際的數(shù)據(jù)傳遞刷新,將所有的行為都交給使用者称鳞。
<2> 會(huì)多一層 data -> row 的封裝涮较,對(duì)于數(shù)據(jù)源數(shù)量很大時(shí),會(huì)創(chuàng)建很多這樣的封裝冈止。比如狂票,讀取很多相冊(cè)中的圖片。
<3> 過(guò)度依賴(lài)?yán)^承

Github上處理 List 比較流行的應(yīng)該是 IGListKit熙暴,主要實(shí)現(xiàn)是有一個(gè) adapter闺属,將自定義 list 和當(dāng)前 controller 注冊(cè)給它,再將 controller 注冊(cè)為數(shù)據(jù)源周霉,通過(guò)代理回調(diào)數(shù)據(jù)掂器。這里,在回調(diào)方法中俱箱,需要返回繼承自 sectionController 的子類(lèi)国瓮,在子類(lèi)中,有一系列方法需要重寫(xiě)狞谱。對(duì)于 cell 只需要實(shí)現(xiàn)數(shù)據(jù) protocol乃摹,就會(huì)在合適的時(shí)機(jī)被回調(diào)更新 cell。IGListKit 無(wú)論從代碼邏輯還是接口封裝都做的很棒跟衅,也始終貫徹面向協(xié)議的編程孵睬。但也存在一些問(wèn)題。

<1> 沒(méi)有支持 tableView issues #584
<2> 對(duì) swift value type 只能通過(guò) wrapper 的方式實(shí)現(xiàn) issues #35
<3> 沒(méi)有發(fā)現(xiàn)對(duì) swift 中形為 [[ListDiffable]] 的支持与斤,語(yǔ)法轉(zhuǎn)換后只能是 [ListDiffable]肪康。

以上兩者盡管在接口上都對(duì)系統(tǒng)的 tableView 或 collectionView 有了完全性的封裝,IGListKit 還專(zhuān)門(mén)針對(duì) Swift 提供了支持撩穿。但是磷支,Swift 是強(qiáng)大的。因此食寡,針對(duì)此問(wèn)題雾狈,我嘗試用 more swift 的方式解決一下。

在 Swift 中更加鼓勵(lì) Protocol + Value Type 的方式抵皱,使用 Protocol 應(yīng)該是目前用組合代替繼承的最佳實(shí)踐善榛。關(guān)于繼承的一些可能的問(wèn)題辩蛋,我引用 WWDC 2015 - 408 Protocol-Oriented Programming in Swift 中的描述來(lái)簡(jiǎn)單闡述。

Inheritance Intrusive
- One superclass
- Single Inheritance weight gain - bloated
- No retroactive modeling - define not extension
- Superclass may have stored properties
   - You must accept them
   - Initialization burden
   - Don’t break superclass invariants
- Know what / how to override (and when not to)

關(guān)于值類(lèi)型的種種好處移盆,像是線程安全悼院,通過(guò)寫(xiě)時(shí)復(fù)制提供良好性能,便于編譯器進(jìn)一步優(yōu)化等咒循。

這次探索主要的靈感也來(lái)自于 WWDC 2016 - 419 Protocol and Value Oriented Programming in UIKit Apps 中的 single code path 概念据途,強(qiáng)調(diào)的是唯一路徑進(jìn)行modelview的更新,增強(qiáng)代碼的可維護(hù)性和可拓展性叙甸,也便于定位 bug颖医。而且 protocol 的設(shè)計(jì)更加傾向于限制某些行為的路徑,讓大家在這些行為上達(dá)成共識(shí)裆蒸,這樣在跨業(yè)務(wù)合作開(kāi)發(fā)時(shí)熔萧,能減少很多閱讀別人代碼帶來(lái)的負(fù)擔(dān),也更利于整個(gè) app 各種行為的統(tǒng)一僚祷。像 UI 組件化做的也就是類(lèi)似的事情佛致。


2. 設(shè)計(jì)與實(shí)現(xiàn)

<1> 針對(duì)特定的行為,抽象 protocol
<2> 提供對(duì) tableView 的統(tǒng)一管理
<3> Demo 接入

I 部分

Protocol Reference
ListDiffable - 唯一 id 與 判等方法
DataProtocol - 定義 data
ListProtocol - 定義 view
SingleCodePathProtocol - 定義更新 data 與 view 的唯一路徑

II 部分

Protocol Reference
ListGodContext - dequeueReusableCell
ListSectionProtocol - 定義 dataSource 與 delegate 的各種行為
ListGodDataSource - 獲取 data 與 cell.type

定義了 ListGod 作為 tableView 的 dataSource 與 delegate久妆,統(tǒng)一抽象對(duì) tableView 的數(shù)據(jù)管理與事件回調(diào)晌杰。這里跷睦,將 section 抽象為 ListSectionProtocol筷弦,由 struct ListSections 統(tǒng)一管理,ListSections 實(shí)現(xiàn)了 subscript抑诸、ExpressibleByArrayLiteral烂琴、Sequence、Collection蜕乡,用于獲得標(biāo)準(zhǔn)庫(kù)的各種便利方法奸绷。

ListGod 實(shí)現(xiàn)了 DataListProtocol,所以在實(shí)現(xiàn)了 SingleCodePathProtocolListGodContext 之后可以直接使用默認(rèn)實(shí)現(xiàn)层玲。

ListGodContext 想要獲取 reusableCell号醉,需要保證 cell 是用 identifier 注冊(cè)過(guò)的,因此有了 IdentifierProtocol辛块。最后在 extension UITableView 中提供注冊(cè)和 dequeue 的泛型方法畔派。

SingleCodePathProtocol 想要統(tǒng)一 model 與 view 的更新,首先需要保證 model 和 view 的一一對(duì)應(yīng)润绵,這在構(gòu)建 tableView 的時(shí)候已經(jīng)構(gòu)建好了线椰。之后,需要計(jì)算出 oldModel 與 newModel 之間的 diff尘盼,這個(gè) diff 是數(shù)據(jù)變化的最小集合憨愉,再通過(guò) view 提供的接口更新數(shù)據(jù)烦绳,這整個(gè)過(guò)程都被統(tǒng)一在 SingleCodePath 中。對(duì)于 tableView配紫,映射的更新對(duì)象是 IndexPath径密,相應(yīng)的行為是 插入、刪除躺孝、更新睹晒、移動(dòng)。這里括细,我引用了 IGListDiffKit 來(lái)計(jì)算 IndexPathDiff伪很。最初的版本是直接使用 IGListDiffable,但是后來(lái)因?yàn)槠鋵?duì)值類(lèi)型支持的缺失奋单,所以用 swift 重寫(xiě)了這個(gè)算法锉试。

IGListDiffKit

<1>
有序用
ListDiffing
mInserts: old Data 中未出現(xiàn)
mDeletes: new Data 中未出現(xiàn)
mUpdates: 對(duì)于 index 和 originalIndex,在 new old Data 中 key 相同览濒,但指向的對(duì)象不同
mMoves: 對(duì)于相同 key 的 data呆盖,在 new old 中 index 不同

無(wú)序用
Set
inserted = to.subtracting(from)
deleted = from.subtracting(to)

<2> 原理



圖還是比較自解釋的。除去邊界判斷贷笛,主要流程是:
i. 順序遍歷 newData应又,構(gòu)建 VectorNew<Record>,增加 newCounter乏苦,push(N)
ii. 逆序遍歷 oldData株扛,構(gòu)建 VectorOld<Record>,增加 oldCounter汇荐,push(originalIndex)
iii. 順序遍歷 VectorNew<Record>洞就,與 oldData[originalIndex] 判斷 Updated,
VectorNew<Record>.record.index = originalIndex掀淘,
VectorOld<Record>.record.index = i
iv. 順序遍歷 VectorOld<Record>旬蟋,mDeletes
v. 順序遍歷 VectorNew<Record>,mInserts革娄,mUpdates倾贰,mMoves

<3> 性能
容器選用: unordered_map & vector
函數(shù)調(diào)用: C - struct func
時(shí)間復(fù)雜度: O(n)

III 部分

給 listGod 對(duì)應(yīng)的 tableView 和 data,并通過(guò)實(shí)現(xiàn)了 ListSectionProtocol 的 ListSection 返回自定義的 Cell.Type拦惋。在 cell 中用回調(diào)的 data 填充完后匆浙。只需要在數(shù)據(jù)變化時(shí),調(diào)用 reloadDiffableData()架忌。就可以免去管理 tableView 的繁瑣及唯一刷新 data->view 的路徑吞彤。

這里有三個(gè)例子,一個(gè)來(lái)自 IGListKit,兩個(gè)來(lái)自 session 220 2019饰恕,都可以用 listGod 無(wú)縫對(duì)接挠羔。


3. 拓展

Layout protocol
State protocol
Generic diff algorithm - IGListKit (issues #694)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市埋嵌,隨后出現(xiàn)的幾起案子破加,更是在濱河造成了極大的恐慌,老刑警劉巖雹嗦,帶你破解...
    沈念sama閱讀 222,252評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件范舀,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡了罪,警方通過(guò)查閱死者的電腦和手機(jī)锭环,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)泊藕,“玉大人辅辩,你說(shuō)我怎么就攤上這事⊥拊玻” “怎么了玫锋?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,814評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)讼呢。 經(jīng)常有香客問(wèn)我撩鹿,道長(zhǎng),這世上最難降的妖魔是什么悦屏? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,869評(píng)論 1 299
  • 正文 為了忘掉前任节沦,我火速辦了婚禮,結(jié)果婚禮上窜管,老公的妹妹穿的比我還像新娘散劫。我一直安慰自己稚机,他們只是感情好幕帆,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,888評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著赖条,像睡著了一般失乾。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上纬乍,一...
    開(kāi)封第一講書(shū)人閱讀 52,475評(píng)論 1 312
  • 那天碱茁,我揣著相機(jī)與錄音,去河邊找鬼仿贬。 笑死纽竣,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蜓氨,決...
    沈念sama閱讀 41,010評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼聋袋,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了穴吹?” 一聲冷哼從身側(cè)響起幽勒,我...
    開(kāi)封第一講書(shū)人閱讀 39,924評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎港令,沒(méi)想到半個(gè)月后啥容,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,469評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡顷霹,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,552評(píng)論 3 342
  • 正文 我和宋清朗相戀三年咪惠,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片淋淀。...
    茶點(diǎn)故事閱讀 40,680評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡硝逢,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出绅喉,到底是詐尸還是另有隱情渠鸽,我是刑警寧澤,帶...
    沈念sama閱讀 36,362評(píng)論 5 351
  • 正文 年R本政府宣布柴罐,位于F島的核電站徽缚,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏革屠。R本人自食惡果不足惜凿试,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,037評(píng)論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望似芝。 院中可真熱鬧那婉,春花似錦、人聲如沸党瓮。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,519評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)寞奸。三九已至呛谜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間枪萄,已是汗流浹背隐岛。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,621評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留瓷翻,地道東北人聚凹。 一個(gè)月前我還...
    沈念sama閱讀 49,099評(píng)論 3 378
  • 正文 我出身青樓割坠,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親妒牙。 傳聞我的和親對(duì)象是個(gè)殘疾皇子韭脊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,691評(píng)論 2 361