本文是直播分享的簡單文字整理,視訊地址:優酷、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
自身完成的。
為了解決這些問題,我們首先弄明白,資料來源和代理分別做了那些事。
資料來源
它有兩個必須實現的代理方法:
1 2 |
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section; - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath; |
簡單來說,只要實現了這個兩個方法,一個簡單的 UITableView
物件就算是完成了。
除此以外,它還負責管理 section
的數量,標題,某一個 cell
的編輯和移動等。
代理
代理主要涉及以下幾個方面的內容:
- cell、headerView 等展示前、後的回撥。
- cell、headerView 等的高度,點選事件。
最常用的也是兩個方法:
1 2 |
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath; - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath; |
提醒:絕大多數代理方法都有一個 indexPath
引數
優化資料來源
最簡單的思路是單獨把資料來源拿出來作為一個物件。
這種寫法有一定的解耦作用,同時可以有效減少 C 中的程式碼量。然而總程式碼量會上升。我們的目標是減少不必要的程式碼。
比如獲取每一個 section
的行數,它的實現邏輯總是高度類似。然而由於資料來源的具體實現方式不統一,所以每個資料來源都要重新實現一遍。
SectionObject
首先我們來思考一個問題,資料來源作為 M,它持有的 Item 長什麼樣?答案是一個二維陣列,每個元素儲存了一個 section
所需要的全部資訊。因此除了有自己的陣列(給cell用)外,還有 section 的標題等,我們把這樣的元素命名為 SectionObject
:
1 2 3 4 5 6 7 8 9 10 |
@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
可以設計成這樣:
1 2 3 4 5 6 7 8 9 10 11 |
@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
方法為例,它可以這樣實現:
1 2 3 4 5 6 7 |
- (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 加上一個類方法:
1 |
+ (CGFloat)tableView:(UITableView*)tableView rowHeightForObject:(KtTableViewBaseItem *)object; |
另外一類問題是以處理點選事件為代表的代理方法, 它們的主要特點是都有 indexPath
引數用來表示位置。然而實際在處理過程中,我們並不關係位置,關心的是這個位置上的資料。
因此,我們對代理方法做一層封裝,使得 C 呼叫的方法中都是帶有資料引數的。因為這個資料物件可以從資料來源拿到,所以我們需要能夠在代理方法中獲取到資料來源物件。
為了實現這一點, 最好的辦法就是繼承 UITableView
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@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 高度的實現如下,呼叫資料來源的方法獲取到資料:
1 2 3 4 5 6 7 8 |
- (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 中。
1 2 3 4 5 6 7 8 9 |
@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
方法。
1KTMainViewController *mainVC = [[KTMainViewController alloc] initWithStyle:UITableViewStylePlain]; - 在子類 VC 中實現
createDataSource
方法,實現資料來源的繫結。
123- (void)createDataSource {self.dataSource = [[KtMainTableViewDataSource alloc] init]; // 這 一步建立了資料來源} - 在資料來源中,需要指定 cell 的型別。
123- (Class)tableView:(UITableView *)tableView cellClassForObject:(KtTableViewBaseItem *)object {return [KtMainTableViewCell class];} - 在 Cell 中,需要通過解析資料,來更新 UI 並返回自己的高度。
1234+ (CGFloat)tableView:(UITableView *)tableView rowHeightForObject:(KtTableViewBaseItem *)object {return 60;}// Demo 中沿用了父類的 setObject 方法。
下一步做什麼?
關於 TableView
的討論遠遠沒有結束,我列出了以下需要解決的問題
- 在這種設計下,資料的回傳不夠方便,比如 cell 的給 C 發訊息。
- 下拉重新整理與上拉載入如何整合
- 網路請求的發起,與解析資料如何整合
關於第一個問題,其實是普通的 MVC 模式中 V 和 C 的互動問題,可以在 Cell(或者其他類) 中新增 weak 屬性達到直接持有的目的,也可以定義協議。
問題二和三是另一大塊話題,網路請求大家都會實現,但如何優雅的整合進框架,保證程式碼的簡單和可擴充,就是一個值得深入思考,研究的問題了。我會在下次有空的時候和大家分享這個問題。
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式