本文授權轉載,作者:bestswifter(簡書)
本文是直播分享的簡單文字整理,視頻地址:優酷、YouTube
Demo 地址:KtTableView
MVC
討論解耦之前,我們要弄明白 MVC 的核心:控制器(以下簡稱 C)負責模型(以下簡稱 M)和視圖(以下簡稱 V)的交互。
這裡所說的 M,通常不是一個單獨的類,很多情況下它是由多個類構成的一個層。最上層的通常是以 Model 結尾的類,它直接被 C 持有。Model 類還可以持有兩個對象:
Item:它是實際存儲數據的對象。它可以理解為一個字典,和 V 中的屬性一一對應
Cache:它可以緩存自己的 Item(如果有很多)
常見的誤區:
一般情況下數據的處理會放在 M 而不是 C(C 只做不能復用的事)
解耦不只是把一段代碼拿到外面去。而是關注是否能合並重復代碼, 並且有良好的拖展性。
原始版
在 C 中,我們創建 UITableView 對象,然後將它的數據源和代理設置為自己。也就是自己管理著 UI 邏輯和數據存取的邏輯。在這種架構下,主要存在這些問題:
違背 MVC 模式,現在是 V 持有 C 和 M。
C 管理了全部邏輯,耦合太嚴重。
其實絕大多數 UI 相關都是由 Cell 而不是 UITableView 自身完成的。
為了解決這些問題,我們首先弄明白,數據源和代理分別做了那些事。
數據源
它有兩個必須實現的代理方法:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section; - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
簡單來說,只要實現了這個兩個方法,一個簡單的 UITableView 對象就算是完成了。
除此以外,它還負責管理 section 的數量,標題,某一個 cell 的編輯和移動等。
代理
代理主要涉及以下幾個方面的內容:
cell、headerView 等展示前、後的回調。
cell、headerView 等的高度,點擊事件。
最常用的也是兩個方法:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath; - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
提醒:絕大多數代理方法都有一個 indexPath 參數
優化數據源
最簡單的思路是單獨把數據源拿出來作為一個對象。
這種寫法有一定的解耦作用,同時可以有效減少 C 中的代碼量。然而總代碼量會上升。我們的目標是減少不必要的代碼。
比如獲取每一個 section 的行數,它的實現邏輯總是高度類似。然而由於數據源的具體實現方式不統一,所以每個數據源都要重新實現一遍。
SectionObject
首先我們來思考一個問題,數據源作為 M,它持有的 Item 長什麼樣?答案是一個二維數組,每個元素保存了一個 section 所需要的全部信息。因此除了有自己的數組(給cell用)外,還有 section 的標題等,我們把這樣的元素命名為 SectionObject:
@interface KtTableViewSectionObject : NSObject @property (nonatomic, copy) NSString *headerTitle; // UITableDataSource 協議中的 titleForHeaderInSection 方法可能會用到 @property (nonatomic, copy) NSString *footerTitle; // UITableDataSource 協議中的 titleForFooterInSection 方法可能會用到 @property (nonatomic, retain) NSMutableArray *items; - (instancetype)initWithItemArray:(NSMutableArray *)items; @end
Item
其中的 items 數組,應該存儲了每個 cell 所需要的 Item,考慮到 Cell 的特點,基類的 BaseItem 可以設計成這樣:
@interface KtTableViewBaseItem : NSObject @property (nonatomic, retain) NSString *itemIdentifier; @property (nonatomic, retain) UIImage *itemImage; @property (nonatomic, retain) NSString *itemTitle; @property (nonatomic, retain) NSString *itemSubtitle; @property (nonatomic, retain) UIImage *itemAccessoryImage; - (instancetype)initWithImage:(UIImage *)image Title:(NSString *)title SubTitle:(NSString *)subTitle AccessoryImage:(UIImage *)accessoryImage; @end
父類實現代碼
規定好了統一的數據存儲格式以後,我們就可以考慮在基類中完成某些方法了。以 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 方法為例,它可以這樣實現:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if (self.sections.count > section) { KtTableViewSectionObject *sectionObject = [self.sections objectAtIndex:section]; return sectionObject.items.count; } return 0; }
比較困難的是創建 cell,因為我們不知道 cell 的類型,自然也就無法調用 alloc 方法。除此以外,cell 除了創建,還需要設置 UI,這些都是數據源不應該做的事。
這兩個問題的解決方案如下:
定義一個協議,父類返回基類 Cell,子類視情況返回合適的類型。
為 Cell 添加一個 setObject 方法,用於解析 Item 並更新 UI。
優勢
經過這一番折騰,好處是相當明顯的:
子類的數據源只需要實現 cellClassForObject 方法即可。原來的數據源方法已經在父類中被統一實現了。
每一個 Cell 只要寫好自己的 setObject 方法,然後坐等自己被創建,被調用這個方法即可。
子類通過 objectForRowAtIndexPath 方法可以快速獲取 item,不用重寫。
對照 demo(SHA-1:6475496),感受一下效果。
優化代理
我們以之前所說的,代理協議中常用的兩個方法為例,看看怎麼進行優化與解耦。
首先是計算高度,這個邏輯並不一定在 C 完成,由於涉及到 UI,所以由 Cell 負責實現即可。而計算高度的依據就是 Object,所以我們給基類的 Cell 加上一個類方法:
+ (CGFloat)tableView:(UITableView*)tableView rowHeightForObject:(KtTableViewBaseItem *)object;
另外一類問題是以處理點擊事件為代表的代理方法, 它們的主要特點是都有 indexPath 參數用來表示位置。然而實際在處理過程中,我們並不關系位置,關心的是這個位置上的數據。
因此,我們對代理方法做一層封裝,使得 C 調用的方法中都是帶有數據參數的。因為這個數據對象可以從數據源拿到,所以我們需要能夠在代理方法中獲取到數據源對象。
為了實現這一點, 最好的辦法就是繼承 UITableView:
@protocol KtTableViewDelegate@optional - (void)didSelectObject:(id)object atIndexPath:(NSIndexPath*)indexPath; - (UIView *)headerViewForSectionObject:(KtTableViewSectionObject *)sectionObject atSection:(NSInteger)section; // 將來可以有 cell 的編輯,交換,左滑等回調 // 這個協議繼承了UITableViewDelegate ,所以自己做一層中轉,VC 依然需要實現某 @end @interface KtBaseTableView : UITableView@property (nonatomic, assign) id ktDataSource; @property (nonatomic, assign) id ktDelegate; @end
cell 高度的實現如下,調用數據源的方法獲取到數據:
- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath { id dataSource = (id)tableView.dataSource; KtTableViewBaseItem *object = [dataSource tableView:tableView objectForRowAtIndexPath:indexPath]; Class cls = [dataSource tableView:tableView cellClassForObject:object]; return [cls tableView:tableView rowHeightForObject:object]; }
優勢
通過對 UITableViewDelegate 的封裝(其實主要是通過 UITableView 完成),我們獲得了以下特性:
C 不用關心 Cell 高度了,這個由每個 Cell 類自己負責
如果數據本身存在數據源中,那麼在代理協議中它可以被傳給 C,免去了 C 重新訪問數據源的操作。
如果數據不存在於數據源,那麼代理協議的方法會被正常轉發(因為自定義的代理協議繼承自 UITableViewDelegate)
對照 demo(SHA-1:ca9b261),感受一下效果。
更加 MVC,更加簡潔
在上面的兩次封裝中,其實我們是把 UITableView 持有原生的代理和數據源,改成了 KtTableView 持有自定義的代理和數據源。並且默認實現了很多系統的方法。
到目前為止,看上去一切都已經完成了,然而實際上還是存在一些可以改進的地方:
目前仍然不是 MVC 模式!
C 的邏輯和實現依然可以進一步簡化
基於以上考慮, 我們實現一個 UIViewController 的子類,並且把數據源和代理封裝到 C 中。
@interface KtTableViewController : UIViewController@property (nonatomic, strong) KtBaseTableView *tableView; @property (nonatomic, strong) KtTableViewDataSource *dataSource; @property (nonatomic, assign) UITableViewStyle tableViewStyle; // 用來創建 tableView - (instancetype)initWithStyle:(UITableViewStyle)style; @end
為了確保子類創建了數據源,我們把這個方法定義到協議裡,並且定義為 required。
成果與目標
現在我們梳理一下經過改造的 TableView 該怎麼用:
首先你需要創建一個繼承自 KtTableViewController 的視圖控制器,並且調用它的 initWithStyle 方法。
KTMainViewController *mainVC = [[KTMainViewController alloc] initWithStyle:UITableViewStylePlain];
在子類 VC 中實現 createDataSource 方法,實現數據源的綁定。
- (void)createDataSource { self.dataSource = [[KtMainTableViewDataSource alloc] init]; // 這 一步創建了數據源 }
在數據源中,需要指定 cell 的類型。
- (Class)tableView:(UITableView *)tableView cellClassForObject:(KtTableViewBaseItem *)object { return [KtMainTableViewCell class]; }
在 Cell 中,需要通過解析數據,來更新 UI 並返回自己的高度。
+ (CGFloat)tableView:(UITableView *)tableView rowHeightForObject:(KtTableViewBaseItem *)object { return 60; } // Demo 中沿用了父類的 setObject 方法。
下一步做什麼?
關於TableView的討論遠遠沒有結束,我列出了以下需要解決的問題
在這種設計下,數據的回傳不夠方便,比如 cell 的給 C 發消息。
下拉刷新與上拉加載如何集成
網絡請求的發起,與解析數據如何集成
關於第一個問題,其實是普通的 MVC 模式中 V 和 C 的交互問題,可以在 Cell(或者其他類) 中添加 weak 屬性達到直接持有的目的,也可以定義協議。
問題二和三是另一大塊話題,網絡請求大家都會實現,但如何優雅的集成進框架,保證代碼的簡單和可拓展,就是一個值得深入思考,研究的問題了。我會在下次有空的時候和大家分享這個問題。