你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 如何寫好一個UITableView(上)

如何寫好一個UITableView(上)

編輯:IOS開發基礎

20150526121043_76948550.jpg

本文授權轉載,作者: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 屬性達到直接持有的目的,也可以定義協議。

問題二和三是另一大塊話題,網絡請求大家都會實現,但如何優雅的集成進框架,保證代碼的簡單和可拓展,就是一個值得深入思考,研究的問題了。我會在下次有空的時候和大家分享這個問題。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved