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)行model
與view
的更新,增強(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)了 SingleCodePathProtocol 與 ListGodContext 之后可以直接使用默認(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)