LPDMvvmKit 系列之 UITableView 的改造

發表於2016-12-27

閱讀本文需要對ReactiveCocoa足夠了解,也可以參閱圖解ReactiveCocoa基本函式

Cocoa Touch Framework無疑是一個很好的框架,特別是對動畫的支援,在我接觸過的框架中可能是最好的(當然我接觸的框架可能比較少),但是就UITableView來說確實存在很多吐槽點,從我個人理解的角度做些分析,嘗試去解決這些吐槽點,並給到的解決方案。

UITableView列舉濫用

列舉從來都是為了可擴充套件而存在的,UITableView中對UITableViewStyle的使用堪稱濫用,先看看這個列舉的定義,列舉項的命名不夠直觀,原始碼的註釋也得不到有效資訊,

再看看如下文件的說明,基本明確了設計者的本意,UITableViewStyle想要區分的是頁首或頁尾(section headers or footers)是否浮動,接下來做個剖析:

UITableView的初始化函式

  • UITableViewStyle作為初始化函式的引數的不合理性,大多數的UIView及其子類都是一樣風格的初始化函式,到了UITableView這裡就顯得有點另類,設計者將 UITableViewStyle放到初始化函式中作為引數,無非就是不希望style在UITableView初始化之後被改變,可能原因是UITableView滑動的過程中style被改變了,不管之前是否存在浮動的頁首或頁尾,改變之後對UI的呈現可能是比較突兀的,另外這種變更可能並沒有實際意義;
  • UITableViewStyle的存在不合理,當一個列舉只存在兩個選項時,很多時候會考慮使用BOOL來表示,可讀性也不差,比如這裡用isSectionGrouped,可能時不需要看注視或者文件就可以理解了;
  • UITableViewStylePlain的命名不合理,我們知道UITableView總是會分section,
    Plain從其語義和StoryBoard預設值的顯示可以聯想UITableViewStylePlain可能是想表示只有一個section的情況,那麼所謂的頁首或頁尾是否浮動其實就沒有太大意義,如果頁首或頁尾不需要浮動其實就是一個Cell了,因為最終效果都是一樣的,反過來假設需要多個section,但是頁首或頁尾都不需要浮動,那麼這些頁首或頁尾其實用Cell來表示是不是更好呢!

綜上得出結論:UITableViewStyle是不該用。

UITableViewCell列舉亂用

UITableViewCell存在好幾個列舉的亂用,亂用表示不該用的時候用了。

UITableViewCellStyle的亂用

UITableViewCell的初始化方法中同樣也帶上了UITableViewCellStyle,先看程式碼

如果說UITableView設計者覺得就只存在兩種style,那麼UITableViewCell設計中加入UITableViewCellStyle就顯得完全是亂用了。一樣的道理,列舉從來就不是為了擴充套件而存在,UITableViewCell做為cell的基類,擴充套件是必須的,不可能所有的cell都長的跟UITableViewCellStyle中定義的幾個列舉項所分類的完全一樣,所以這個設計是有多噁心啊。

再看看UITableViewCellStyle的各個列舉項的命名,簡直是殘暴啊,UITableViewCellStyleValue1,UITableViewCellStyleValue2這些是什麼鬼哦,再看看註釋,分別說明Used in Settings和Used in Phone/Contacts,這就很明顯了,這些實現完全就是系統元件用到了這樣的實現,然後直接做為api開放出來的,並沒有做很好的抽象,在初始化函式中加入UITableViewCellStyle,汙染了初始化函式,限制了擴充套件,每每在寫一個UITableViewCell的子類時,總是有一種莫名的哀傷,UITableViewCellStyle做為引數存在唯一的作用就是多寫了點程式碼,然後沒有任何意義。這些cell style所表示的cell完全應該通過子類化來實現的,所以UITableViewCellStyle的亂用是有點慘不忍睹的。

UITableViewCellSeparatorStyle的亂用

怎麼說也不應該存在這樣一個列舉,CellSeparatorStyle這裡針對不同的UITableViewStyle而設計的,不管是何種style,應該只需要isShowCellSeparatorLine這樣一個BOOL值表示是否需要顯示邊框,如果是UITableViewStyleGrouped這種style,可能需要額外的一個isCellSeparatorLineEtched,如果根據前面的假設,頁首或頁尾都是預設浮動的話,這樣設計是很合理的。

當一個列舉各項的命名過於詭異時,這個列舉的存在實際上是要好好考慮下的,所以UITableViewCellSeparatorStyle也是典型的亂用。

UITableViewCell對以下列舉的使用也是有待商榷的

UITableViewCellSelectionStyle想表示cell選中的樣式,這裡大概是通過這種方式來提高几種預設值,因為CellSelectionStyle還是可以定製的,但是UITableViewCellSelectionStyleDefault放在最後UITableViewCellSelectionStyleNone放在最開始,到底誰是default哦;

UITableViewCellFocusStyle這個列舉的存在難道僅僅是為了無病呻吟嗎?

UITableView委託亂用

UITableViewDelegate,UITableViewDataSource,包括剛引入的UITableViewDataSourcePrefetching,這幾個delegate的設計好像是缺少了些設計,更像是為了解決問題而寫程式碼,作為一個基礎框架,實在是不可取的。

UITableViewDelegate設計之重

這幾個委託函式,都是與Cell、頁首、頁尾相關的,但是全都集中在UITableViewDelegate這個委託中,且命名都是類似,當一個protocol在定義時存在過多的@optional委託函式時,這個protocol的設計本身就是不合理的,應該拆分成更細的protocol,我們應該時在必要的時候選擇相應的protocol,而不是實現存在的@optional委託函式,然後UITableViewDelegate這個protocol本身所有的委託函式都是@optional,這是真的不合理,如果是我們來設計Cell、頁首、頁尾實際上都是應該UIView,且存在諸多共同點(參考UICollectionView的設計,Cell、頁首、頁尾就存在一個共同的基類UICollectionReusableView),應該設計一個UIReusableView,(UICollectionReusableView也可以不需要了)其中存在如下方法,這些方法可以在子類中重寫

且應該設計一個UIReusableViewDelegate,其包括如下委託函式

UIReusableView存在UIReusableViewDelegate的一個delegate,前面所提到的那六個委託函式,實際上應該在Cell、頁首、頁尾各自需要的時候實現UIReusableViewDelegate。

綜上,UITableViewDelegate實際上是太重了。

UITableViewDelegate職責之亂

下面這些委託函式,實際上應該存在UITableViewDataSource中。頁首、頁尾的資料來源跟cell的資料來源應該是平等的存在,不應該是說不常用了,我就放到UITableViewDelegate中,本來就應該放在UITableViewDataSource,不必須用的可以optional修飾下也還說得過去。

UITableViewDataSource設計之重

經過前面的梳理,那麼UITableViewDataSource中應該包括以下這些函式

跟前面提到UITableViewDelegate設計之重一個道理,Cell、頁首、頁尾的DataSource也是應該分開的,在需要的時候實現對應的DataSource,需要定義額外的一個列舉UIReusableViewType

然後對頁首、頁尾就有UIReusableViewDataSource,其中的委託函式如下:

單獨的針對cell,有UITableViewCellDataSource,其中的委託函式如下:

至於UITableViewDataSourcePrefetching就不應該出現,為了優化滾動幀率,拆東牆補西牆之舉。從開發者的角度,最簡單的做法就是把整個的資料來源給到,剩下的就應該是UITableView自身去實現了,資料都有了,想要什麼預載入都是框架自身的事情了,減少對開發者的依賴,更是減少api的耦合度,對外暴露的介面越多越不好。

改造之路在何方?

前面在吐槽的時候,每每會給出自認為更合理的設計,然而並沒有什麼卵用,既有程式碼是無法修改的,那改造之路又在何方呢?不能改變既有程式碼,那麼只能是將這麼東西儘可能的封裝起來,Objective-C語言還提供了一個蠻有意思的編譯期常量NS_UNAVAILABLE,可以在編譯期禁用父類的方法,算是不完美中的完全吧,我們可以禁用掉一些不合理的類成員,來達到一個比較好的封裝效果。

UITableView列舉濫用的解決

UITableView可以禁用被列舉汙染的初始化函式,重寫預設的initWithFrame初始化函式並預設設style為UITableViewStyleGrouped,參考類 LPDTableView暫時並沒有重寫初始化函式,目前認為無傷大雅。

UITableViewCell列舉亂用的解決

UITableViewCell無法禁用被列舉汙染的初始化函式,因為重用時會呼叫到,參考類 LPDTableViewCell,選擇無視UITableViewCellStyle,並將已存在的幾種cellStyle都擴充套件成對應的子類,LPDTableViewDefaultCell,LPDTableViewValue1Cell,LPDTableViewValue2Cell,LPDTableViewSubtitleCell命名還是保留一致,畢竟大家都已經習慣了這種醜。

UITableView委託亂用的解決

既然無法改造既有的UITableView,可以從另外一個側面來解決。

UITableView如何資料驅動

引入MVVM的思想,為UITableView新增對應的ViewModel,有了ViewModel,則可以引入資料驅動的方式,當我們需要為Cell、頁首、頁尾提供DataSource時,只需要呼叫LPDTableViewModelProtocl中的方法就好了,介面的粒度已經比較細了,但可能不是最合理的組合,相關的函式都在下面:

UITableView委託轉RACSignal

引入ReactiveCocoa中的RACSignal,將UITableViewDelegate中的委函式都轉成訊號,當我們需要實現某一個委託函式,只需要訂閱對應的RACSignal即可,不訂閱沒有任何副作用。

Cell、頁首、頁尾也存在相應的ViewModel

Cell、頁首、頁尾跟其ViewModel之間需要遵守約定好的命名規則,如此會自動匹配。另外Cell、頁首、頁尾預設都是重用的,同一型別reuseIdentifier一樣,重用相關的函式就都在 LPDTableViewFactory。這個類中了當我們關心DataSource或者Delegate時,我們只需要跟對應的ViewModel互動即可,將Cell、頁首、頁尾解耦合。

LPDTableSectionViewModelProtocol

這個protocol的實現類LPDTableSectionViewModel,只是在ViewModel層抽象出來,這樣才好完善ViewModel層的實現,並不存在對應的SectionView。

關於height

cell,header,footer的viewmodel中都有對應的height欄位,需要根據viewmodel的model欄位在bindingTo:viewModel函式中設定height值,可以針對model做height的快取。

改造之後的例子

載入tableview的資料

新增一個cell

批量新增cell

刪除一個cell

Cell的didSelectRowAtIndexPathSignal

具體請下載lpd-tableview-kit,看看其中的demo。

相關文章