iOS 架構元件:讓複雜 TableView 優雅起來
一、傳統方式的弊端
UITableView
是出場率極高的檢視元件,開發者通過實現 <UITableViewDataSource>
和 <UITableViewDelegate>
協議方法來配置佈局邏輯,面向協議設計模式在蘋果的程式碼設計中很常見,它能適應大部分的業務場景且足夠靈活。
UITableView
相關的協議方法充分體現了單一職責原則,這種方式優點很多,比如某一時刻元件只需要關心當前需要的資料,避免了多餘的計算,同時也可以讓資料及時釋放減小記憶體峰值。
然而當某一個介面結構比較複雜多元且展示順序可動態變化的時候,開發者往往需要寫大量的 if/else/else if
或 switch
分支語句來區分不同 section/row
的檢視型別及其佈局,由於 UITableView
相關協議方法的職責單一性,這種分支語句會重複出現在多個協議方法裡面。
顯然這種場景下, UITableView
變得不那麼優雅。
二、常規優化思路
理所當然的,大家很容易想到使用一箇中間類來將佈局一個 Cell 分散的資料集中起來管理:
@interface CellLayout : NSObject @property (nonatomic, strong) Class cellClass; @property (nonatomic, assign) CGFloat cellHeight; @property (nonatomic, strong) AnyModel *cellModel; ... @end
然後在 UITableView
各個協議方法裡從 NSArray<CellLayout *> layoutArray;
陣列中拿到 CellLayout
物件配置就行了,如此,開發者只需要關心如何構建 layoutArray
陣列,避免了在協議方法中寫過多的分支語句。
這種思路就做了兩件事:
-
提供一個包含某個 Cell 所有佈局資訊的中間類。
-
在中間類確定的情況下,
和
協議方法返回值只需要依據對應中間類的某個屬性,簡潔清晰。
筆者思考過後,花了些時間做了一個小元件 (https: //github.com/indulgeIn/YBHandyTableView) ,它能讓你輕鬆的實現這種優化方案,核心操作就是用陣列來替代協議方法為 UITableView
配置資料。
當然,這麼做有它的侷限性,後文再來分析。
三、元件架構設計
經過前面的分析,元件要做的事情有兩個,一個是設計一箇中間類,一個是封裝 <UITableViewDataSource>
和 <UITableViewDelegate>
協議方法的實現。
核心思路
按照常規的思路,可能會想到設計一個通用的中間類,就像之前說的 CellLayout
,然後利用繼承的特性來為 CellLayout
新增額外的屬性。這樣確實能達到目的,不過這樣帶來了較為嚴重的耦合,需要開發者一開始就知道他必須寫一個類來繼承自你的 CellLayout
,若本身業務中需要繼承另外一個類就很蛋疼了(畢竟 OC 不支援多繼承);再者,若某一天想要剔除這種方案可能會很麻煩, CellLayout
設計得越臃腫、包含的業務越多將越難剝離。
並且,一個 CellLayout
是解決不了問題的,因為配置 UITableView
可能需要 UITableViewCell
的一些資料,也需要一些通用的方法來告知 UITableViewCell
何時配置資料重新整理UI,也就意味著按照這種邏輯,還需要寫一個 BaseTableViewCell
……
顯然,這種方式並不優雅,也違背了依賴倒置原則。
筆者的做法是將這個“中間類”抽象出來,作為兩個協議: YBHTCellProtocol
和 YBHTCellConfigProtocol
,這兩個協議包含了佈局 UITableView
所需的資料,當然可以結合自己的業務擴充這兩個協議。 YBHTCellProtocol
由自定義的 UITableViewCell
來實現; YBHTCellConfigProtocol
隨意開發者用什麼類來實現,通常情況下,使用包含 UITableViewCell
所需資料的 Model
來實現是最快捷的做法(可看Demo中的使用案例)。
保證深度定製性
考慮到一個問題, UITableView
相關協議方法非常多,若為 YBHTCellProtocol
和 YBHTCellConfigProtocol
拓展所有的配置將會需要大量的程式碼,可能有些得不償失。
所以筆者使用多代理 ( YBHandyTableViewProxy
) 來保證元件使用方深度定製的需求,也是為了避免某些特殊情況下,使用該元件的業務模組能快速的拓展之前沒有的功能:
- (void)ybht_addDelegate:(id<UITableViewDelegate>)delegate; - (void)ybht_addDataSource:(id<UITableViewDataSource>)dataSource;
當然這樣做會有隱患,所以建議讀者朋友若想使用該元件先了解它的原理,該元件的程式碼不多也不高深,相信只要感興趣的朋友能很快理解。
四、元件的弊端
元件的配置方式很簡單:
NSArray<id<YBHTCellConfigProtocol>> tmpArr = ...; [anyTableView.ybht_rowArray addObjectsFromArray:tmpArr]; [anyTableView reloadData];
正如程式碼所見,需要傳入的是實現 YBHTCellConfigProtocol
協議的例項,同時需要對應的 UITableViewCell
實現 YBHTCellProtocol
協議(可對比 UML 類圖)。
取個例子,若你在 UIViewController
裡面寫了一個 UITableView
,然後使用該元件配置資料,可以明確的是元件將 <UITableViewDataSource>
和 <UITableViewDelegate>
協議封裝起來, UIViewController
和你定製的那些 UITableViewCell
已經沒有了耦合,也就意味著,它們之間的互動將不能直接進行。
那麼,它們如何間接的互動呢?
-
YBHandyTableViewIMP
是元件實現和
協議的類,那麼將
UIViewController
物件傳入到該類就能實現與UITableViewCell
的互動,但是由於YBHandyTableViewIMP
和UITableViewCell
不直接依賴而是都依賴於YBHTCellProtocol
協議,這為定製性的互動帶來了困難。 -
從另一個方面思考問題,從元件的使用方法可知,
UIViewController
和id
之間是有關聯的,而id
與UITableViewCell
是有關聯的,所以可以通過id
將UIViewController
傳遞到UITableViewCell
中,然後進行互動。 -
基於響應鏈的傳遞路徑來攔截事件。這種方式比較巧,但是卻始終感覺不是那麼穩妥,它的好處是處理
UITableViewCell
的互動事件完全可以不經過該元件就能完成。
最後,筆者建議使用第二種方式。
不過不管哪種方式來說都不太優雅了,在業務開發中應該多考慮一下, UITableViewCell
中會不會有大量的事件需要傳遞到最外層的業務,比如跳轉介面、網路請求等就可以直接在 UITableViewCell
裡面呼叫。若大量的互動是必然的(或者說是為了滿足業務架構規範),那就放棄“偷懶”,專門設計一個適合業務的方式吧。
五、結語
本文是筆者做的一個小實踐的思路分享,需要明白的是,一個程式碼設計並非能滿足所有的業務,特別是這種和具體業務緊密相連的元件。在一開始筆者還滿懷希望,覺得這個元件的場景很大,後來發現還是有侷限性。
元件總是會讓粒度變大,當你追求更小粒度的時候你會發現:我去,好像這個元件沒有了意義:joy:。
設計有取捨,沒有萬能的方案。
GitHub 地址:YBHandyTableView ( https: //github.com/indulgeIn/YBHandyTableView)
推薦閱讀
在看就點點吧