溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

怎么寫好一個UITableView

發(fā)布時間:2021-11-15 12:49:00 來源:億速云 閱讀:145 作者:iii 欄目:移動開發(fā)

這篇文章主要介紹“怎么寫好一個UITableView”,在日常操作中,相信很多人在怎么寫好一個UITableView問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”怎么寫好一個UITableView”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!

如果你覺得 `UITableViewDelegate` 和 `UITableViewDataSource` 這兩個協(xié)議中有大量方法每次都是復制粘貼,實現(xiàn)起來大同小異;如果你覺得發(fā)起網(wǎng)絡(luò)請求并解析數(shù)據(jù)需要一大段代碼,加上刷新和加載后簡直復雜度爆表,如果你想知道為什么下面的代碼可以滿足上述所有要求:

怎么寫好一個UITableView

MVC

在討論解耦之前,我們要弄明白 MVC 的核心:控制器(以下簡稱 C)負責模型(以下簡稱 M)和視圖(以下簡稱 V)的交互。

這里所說的 M,通常不是一個單獨的類,很多情況下它是由多個類構(gòu)成的一個層。最上層的通常是以 `Model` 結(jié)尾的類,它直接被 C 持有。`Model` 類還可以持有兩個對象:

1.  Item:它是實際存儲數(shù)據(jù)的對象。它可以理解為一個字典,和 V 中的屬性一一對應(yīng)

2.  Cache:它可以緩存自己的 Item(如果有很多)

常見的誤區(qū):

1.  一般情況下數(shù)據(jù)的處理會放在 M 而不是 C(C 只做不能復用的事)

2.  解耦不只是把一段代碼拿到外面去。而是關(guān)注是否能合并重復代碼, 并且有良好的拖展性。

原始版

在 C 中,我們創(chuàng)建 `UITableView` 對象,然后將它的數(shù)據(jù)源和代理設(shè)置為自己。也就是自己管理著 UI 邏輯和數(shù)據(jù)存取的邏輯。在這種架構(gòu)下,主要存在這些問題:

1.  違背 MVC 模式,現(xiàn)在是 V 持有 C 和 M。

2.  C 管理了全部邏輯,耦合太嚴重。

3.  其實絕大多數(shù) UI 相關(guān)都是由 Cell 而不是 `UITableView` 自身完成的。

為了解決這些問題,我們首先弄明白,數(shù)據(jù)源和代理分別做了那些事。

數(shù)據(jù)源

它有兩個必須實現(xiàn)的代理方法:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;

簡單來說,只要實現(xiàn)了這個兩個方法,一個簡單的 `UITableView` 對象就算是完成了。

除此以外,它還負責管理 `section` 的數(shù)量,標題,某一個 `cell` 的編輯和移動等。

代理

代理主要涉及以下幾個方面的內(nèi)容:

1.  cell、headerView 等展示前、后的回調(diào)。

2.  cell、headerView 等的高度,點擊事件。

最常用的也是兩個方法:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;

提醒:絕大多數(shù)代理方法都有一個 `indexPath` 參數(shù)

優(yōu)化數(shù)據(jù)源

最簡單的思路是單獨把數(shù)據(jù)源拿出來作為一個對象。

這種寫法有一定的解耦作用,同時可以有效減少 C 中的代碼量。然而總代碼量會上升。我們的目標是減少不必要的代碼。

比如獲取每一個 `section` 的行數(shù),它的實現(xiàn)邏輯總是高度類似。然而由于數(shù)據(jù)源的具體實現(xiàn)方式不統(tǒng)一,所以每個數(shù)據(jù)源都要重新實現(xiàn)一遍。

SectionObject

首先我們來思考一個問題,數(shù)據(jù)源作為 M,它持有的 Item 長什么樣?答案是一個二維數(shù)組,每個元素保存了一個 `section` 所需要的全部信息。因此除了有自己的數(shù)組(給cell用)外,還有 section 的標題等,我們把這樣的元素命名為 `SectionObject`:

@interface KtTableViewSectionObject : NSObject
@property (nonatomic, copy) NSString *headerTitle; // UITableDataSource 協(xié)議中的 titleForHeaderInSection 方法可能會用到
@property (nonatomic, copy) NSString *footerTitle; // UITableDataSource 協(xié)議中的 titleForFooterInSection 方法可能會用到
@property (nonatomic, retain) NSMutableArray *items;
- (instancetype)initWithItemArray:(NSMutableArray *)items;
@end

Item

其中的 `items` 數(shù)組,應(yīng)該存儲了每個 cell 所需要的 `Item`,考慮到 `Cell` 的特點,基類的 `BaseItem` 可以設(shè)計成這樣:

@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

父類實現(xiàn)代碼

規(guī)定好了統(tǒng)一的數(shù)據(jù)存儲格式以后,我們就可以考慮在基類中完成某些方法了。以 `- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section` 方法為例,它可以這樣實現(xiàn):

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    if (self.sections.count > section) {
        KtTableViewSectionObject *sectionObject = [self.sections objectAtIndex:section];
        return sectionObject.items.count;
    }
    return 0;
}

比較困難的是創(chuàng)建 `cell`,因為我們不知道 `cell` 的類型,自然也就無法調(diào)用 `alloc` 方法。除此以外,`cell` 除了創(chuàng)建,還需要設(shè)置 UI,這些都是數(shù)據(jù)源不應(yīng)該做的事。

這兩個問題的解決方案如下:

1.  定義一個協(xié)議,父類返回基類 `Cell`,子類視情況返回合適的類型。

2.  為 `Cell` 添加一個 `setObject` 方法,用于解析 Item 并更新 UI。

優(yōu)勢

經(jīng)過這一番折騰,好處是相當明顯的:

1.  子類的數(shù)據(jù)源只需要實現(xiàn) `cellClassForObject` 方法即可。原來的數(shù)據(jù)源方法已經(jīng)在父類中被統(tǒng)一實現(xiàn)了。

2.  每一個 Cell 只要寫好自己的 `setObject` 方法,然后坐等自己被創(chuàng)建,被調(diào)用這個方法即可。

3.  子類通過 `objectForRowAtIndexPath` 方法可以快速獲取 item,不用重寫。

對照 demo(SHA-1:6475496),感受一下效果。

優(yōu)化代理

我們以之前所說的,代理協(xié)議中常用的兩個方法為例,看看怎么進行優(yōu)化與解耦。

首先是計算高度,這個邏輯并不一定在 C 完成,由于涉及到 UI,所以由 Cell 負責實現(xiàn)即可。而計算高度的依據(jù)就是 Object,所以我們給基類的 Cell 加上一個類方法:

+ (CGFloat)tableView:(UITableView*)tableView rowHeightForObject:(KtTableViewBaseItem *)object;

另外一類問題是以處理點擊事件為代表的代理方法, 它們的主要特點是都有 `indexPath` 參數(shù)用來表示位置。然而實際在處理過程中,我們并不關(guān)系位置,關(guān)心的是這個位置上的數(shù)據(jù)。

因此,我們對代理方法做一層封裝,使得 C 調(diào)用的方法中都是帶有數(shù)據(jù)參數(shù)的。因為這個數(shù)據(jù)對象可以從數(shù)據(jù)源拿到,所以我們需要能夠在代理方法中獲取到數(shù)據(jù)源對象。

為了實現(xiàn)這一點, 最好的辦法就是繼承 `UITableView`:

@protocol KtTableViewDelegate<UITableViewDelegate>
@optional
- (void)didSelectObject:(id)object atIndexPath:(NSIndexPath*)indexPath;
- (UIView *)headerViewForSectionObject:(KtTableViewSectionObject *)sectionObject atSection:(NSInteger)section;
// 將來可以有 cell 的編輯,交換,左滑等回調(diào)
// 這個協(xié)議繼承了UITableViewDelegate ,所以自己做一層中轉(zhuǎn),VC 依然需要實現(xiàn)某
@end
@interface KtBaseTableView : UITableView<UITableViewDelegate>
@property (nonatomic, assign) id<KtTableViewDataSource> ktDataSource;
@property (nonatomic, assign) id<KtTableViewDelegate> ktDelegate;
@end

cell 高度的實現(xiàn)如下,調(diào)用數(shù)據(jù)源的方法獲取到數(shù)據(jù):

- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath {
    id<KtTableViewDataSource> dataSource = (id<KtTableViewDataSource>)tableView.dataSource;
    KtTableViewBaseItem *object = [dataSource tableView:tableView objectForRowAtIndexPath:indexPath];
    Class cls = [dataSource tableView:tableView cellClassForObject:object];
    return [cls tableView:tableView rowHeightForObject:object];
}

優(yōu)勢

通過對 `UITableViewDelegate` 的封裝(其實主要是通過 `UITableView` 完成),我們獲得了以下特性:

1.  C 不用關(guān)心 Cell 高度了,這個由每個 Cell 類自己負責

2.  如果數(shù)據(jù)本身存在數(shù)據(jù)源中,那么在代理協(xié)議中它可以被傳給 C,免去了 C 重新訪問數(shù)據(jù)源的操作。

3.  如果數(shù)據(jù)不存在于數(shù)據(jù)源,那么代理協(xié)議的方法會被正常轉(zhuǎn)發(fā)(因為自定義的代理協(xié)議繼承自 `UITableViewDelegate`)

對照 demo(SHA-1:ca9b261),感受一下效果。

 更加 MVC,更加簡潔

在上面的兩次封裝中,其實我們是把 `UITableView` 持有原生的代理和數(shù)據(jù)源,改成了 `KtTableView` 持有自定義的代理和數(shù)據(jù)源。并且默認實現(xiàn)了很多系統(tǒng)的方法。

到目前為止,看上去一切都已經(jīng)完成了,然而實際上還是存在一些可以改進的地方:

1.  目前仍然不是 MVC 模式!

2.  C 的邏輯和實現(xiàn)依然可以進一步簡化

基于以上考慮, 我們實現(xiàn)一個 `UIViewController` 的子類,并且把數(shù)據(jù)源和代理封裝到 C 中。

@interface KtTableViewController : UIViewController<KtTableViewDelegate, KtTableViewControllerDelegate>
@property (nonatomic, strong) KtBaseTableView *tableView;
@property (nonatomic, strong) KtTableViewDataSource *dataSource;
@property (nonatomic, assign) UITableViewStyle tableViewStyle; // 用來創(chuàng)建 tableView
- (instancetype)initWithStyle:(UITableViewStyle)style;
@end

為了確保子類創(chuàng)建了數(shù)據(jù)源,我們把這個方法定義到協(xié)議里,并且定義為 `required`。

成果與目標

現(xiàn)在我們梳理一下經(jīng)過改造的 `TableView` 該怎么用:

1.  首先你需要創(chuàng)建一個繼承自 `KtTableViewController` 的視圖控制器,并且調(diào)用它的 `initWithStyle` 方法。

    `objc KTMainViewController *mainVC = [[KTMainViewController alloc] initWithStyle:UITableViewStylePlain];`

2.  在子類 VC 中實現(xiàn) `createDataSource` 方法,實現(xiàn)數(shù)據(jù)源的綁定。

    ```objc

    *   (void)createDataSource { self.dataSource = [[KtMainTableViewDataSource alloc] init]; // 這 一步創(chuàng)建了數(shù)據(jù)源 } ```

3.  在數(shù)據(jù)源中,需要指定 cell 的類型。

    ```objc

    *   (Class)tableView:(UITableView *)tableView cellClassForObject:(KtTableViewBaseItem *)object { return [KtMainTableViewCell class]; } ```

4.  在 Cell 中,需要通過解析數(shù)據(jù),來更新 UI 并返回自己的高度。

    objc

    *   (CGFloat)tableView:(UITableView *)tableView rowHeightForObject:(KtTableViewBaseItem *)object { return 60; } // Demo 中沿用了父類的 setObject 方法。 ```

還有什么要優(yōu)化的

到目前為止,我們實現(xiàn)了對 `UITableView` 以及相關(guān)協(xié)議、方法的封裝,使它更容易使用,避免了很多重復、無意義的代碼。

在使用時,我們需要創(chuàng)建一個控制器,一個數(shù)據(jù)源,一個自定義 Cell,它們正好是基于 MVC 模式的。因此,可以說在封裝與解耦方面,我們已經(jīng)做的相當好了,即使再花大力氣,也很難有明顯的提高。

但關(guān)于 `UITableView` 的討論遠遠沒有結(jié)束,我列出了以下需要解決的問題

1.  在這種設(shè)計下,數(shù)據(jù)的回傳不夠方便,比如 cell 的給 C 發(fā)消息。

2.  下拉刷新與上拉加載如何集成

3.  網(wǎng)絡(luò)請求的發(fā)起,與解析數(shù)據(jù)如何集成

關(guān)于第一個問題,其實是普通的 MVC 模式中 V 和 C 的交互問題,可以在 Cell(或者其他類) 中添加 weak 屬性達到直接持有的目的,也可以定義協(xié)議。

問題二和三是另一大塊話題,網(wǎng)絡(luò)請求大家都會實現(xiàn),但如何優(yōu)雅的集成進框架,保證代碼的簡單和可拓展,就是一個值得深入思考,研究的問題了。接下來我們就重點討論網(wǎng)絡(luò)請求。

為何創(chuàng)建網(wǎng)絡(luò)層

一個 iOS 的網(wǎng)絡(luò)層框架該如何設(shè)計?這是一個非常寬泛,也超出我能力范圍之外的問題。業(yè)內(nèi)已有一些優(yōu)秀的,成熟的思路和解決方案,由于能力,角色所限,我決定從一個普通開發(fā)者而不是架構(gòu)師的角度來說說,一個普通的、簡單的網(wǎng)絡(luò)層該如何設(shè)計。我相信再復雜的架構(gòu),也是由簡單的設(shè)計演化而來的。

對于絕大多數(shù)小型應(yīng)用來說,集成 `AFNetworking` 這樣的網(wǎng)絡(luò)請求框架就足以應(yīng)付 99% 以上的需求了。但是隨著項目的擴大,或者用長遠的眼光來考慮,直接在 VC 中調(diào)用具體的網(wǎng)絡(luò)框架(下面以 `AFNetworking` 為例),至少存在以下問題:

1.  一旦日后 `AFNetworking` 停止維護,而且我們需要更換網(wǎng)絡(luò)框架,這個成本將無法想象。所有的 VC 都要改動代碼,而且絕大多數(shù)改動都是雷同的。

    這樣的例子真實存在,比如我們的項目中就依然使用早已停止維護的 `ASIHTTPRequest`,可以預(yù)見,這個框架遲早要被替換。

2.  現(xiàn)有的框架可能無法實現(xiàn)我們的需求。以 `ASIHTTPRequest` 為例,它的底層用 `NSOperation` 來表示每一個網(wǎng)絡(luò)請求。眾所周知,一個 `NSOperation` 的取消,并不是簡單調(diào)用 `cancel` 方法就可以的。在不修改源碼的前提下,一旦它被放入隊列,其實是無法取消的。

3.  有時候我們的需求僅僅是進行網(wǎng)絡(luò)請求,還會對這個請求進行各種自定義的拓展。比如我們可能要統(tǒng)計請求的發(fā)起和結(jié)束時間,從而計算網(wǎng)絡(luò)請求,數(shù)據(jù)解析的步驟的耗時。有時候,我們希望設(shè)計一個通用組件,并且支持由各個業(yè)務(wù)部門去自定義具體的規(guī)則。比如可能不同的部門,會為 HTTP 請求添加不同的頭部。

4.  網(wǎng)絡(luò)請求還有可能有其他廣泛需要添加的需求,比如請求失敗時的彈窗,請求時的日志記錄等等。

參考當前代碼(SHA-1:a55ef42)感受一下沒有任何網(wǎng)絡(luò)層時的設(shè)計。

如何設(shè)計網(wǎng)絡(luò)層

其實解決方案非常簡單:

所有的計算機問題,都可以通過添加中間層來解決

讀者可以自行思考,為什么添加中間層可以解決上述三個問題。

三大模塊

對于一個網(wǎng)絡(luò)框架來說,我認為主要有三個方面值得去設(shè)計:

1.  如何請求

2.  如何回調(diào)

3.  數(shù)據(jù)解析

一個完整的網(wǎng)絡(luò)請求一般由以上三個模塊組成,我們逐一分析每個模塊實現(xiàn)時的注意事項:

### 發(fā)起請求

發(fā)起請求時,一般有兩種思路,第一種是把所有要配置的參數(shù)寫到同一個方法中,借用 [與時俱進,HTTP/2下的iOS網(wǎng)絡(luò)層架構(gòu)設(shè)計](http://www.jianshu.com/p/a9bca62d8dab) 一文中的代碼表示:

+ (void)networkTransferWithURLString:(NSString *)urlString
                       andParameters:(NSDictionary *)parameters
                              isPOST:(BOOL)isPost
                        transferType:(NETWORK_TRANSFER_TYPE)transferType
                   andSuccessHandler:(void (^)(id responseObject))successHandler
                   andFailureHandler:(void (^)(NSError *error))failureHandler {
                           // 封裝AFN
                   }

這種寫法的好處在于所有參數(shù)一目了然,而且簡單易用,每次都調(diào)用這個方法即可。但是缺點也很明顯,隨著參數(shù)和調(diào)用次數(shù)的增多,網(wǎng)絡(luò)請求的代碼很快多到爆炸。

另一組方法則是將 API 設(shè)置成一個對象,把要傳入的參數(shù)作為這個對象的屬性。在發(fā)起請求時,只要設(shè)置好對象的相關(guān)屬性,然后調(diào)用一個簡單的方法即可。

@interface DRDBaseAPI : NSObject
@property (nonatomic, copy, nullable) NSString *baseUrl;
@property (nonatomic, copy, nullable) void (^apiCompletionHandler)(_Nonnull id responseObject,  NSError * _Nullable error);
- (void)start;
- (void)cancel;
...
@end

根據(jù)前文提到的 Model 和 Item 的概念,那么應(yīng)該可以想到:**這個用于訪問網(wǎng)絡(luò)的 API 對象,其實是作為 Model 的一個屬性**。

Model 負責對外暴露必要的屬性和方法,而具體的網(wǎng)絡(luò)請求則由 API 對象完成,同時 Model 也應(yīng)該持有真正用來存儲數(shù)據(jù)的 Item。

如何回調(diào)

一次網(wǎng)絡(luò)請求的返回結(jié)果應(yīng)該是一個 JSON 格式的字符串,通過系統(tǒng)的或者一些開源框架可以將它轉(zhuǎn)換成字典。

接下來我們需要使用 runtime 相關(guān)的方法,將字典轉(zhuǎn)換成 Item 對象。

最后,Model 需要將這個 Item 賦值給自己的屬性,從而完成整個網(wǎng)絡(luò)請求。

如果從全局角度來說,我們還需要一個 Model 請求完成的回調(diào),這樣 VC 才能有機會做相應(yīng)的處理。

考慮到 Block 和 Delegate 的優(yōu)缺點,我們選擇用 Block 來完成回調(diào)。

[數(shù)據(jù)解析](https://bestswifter.com/how-to-create-an-uitableview/#)

這一部分主要是利用 runtime 將字典轉(zhuǎn)換成 Item,它的實現(xiàn)并不算難,但是如何隱藏好實現(xiàn)細節(jié),使上層業(yè)務(wù)不用過多關(guān)心,則是我們需要考慮的問題。

我們可以定義一個基類的 Item,并且為它定義一個 `parseData` 函數(shù):

// KtBaseItem.m
- (void)parseData:(NSDictionary *)data {
    // 解析 data 這個字典,為自己的屬性賦值
    // 具體的實現(xiàn)請見后面的文章
}

封裝 API 對象

首先,我們封裝一個 `KtBaseServerAPI` 對象,這個對象的主要目的有三個:

1.  隔離具體的網(wǎng)絡(luò)庫的實現(xiàn)細節(jié),為上層提供一個穩(wěn)定的的接口

2.  可以自定義一些屬性,比如網(wǎng)絡(luò)請求的狀態(tài),返回的數(shù)據(jù)等,方便的調(diào)用

3.  處理一些公用的邏輯,比如網(wǎng)絡(luò)耗時統(tǒng)計

具體的實現(xiàn)請參考 Git 提交歷史:SHA-1:76487f7

Model 與 Item

BaseModel

Model 主要需要負責發(fā)起網(wǎng)絡(luò)請求,并且處理回調(diào),來看一下基類的 Model 如何定義:

@interface KtBaseModel
// 請求回調(diào)
@property (nonatomic, copy) KtModelBlock completionBlock;
//網(wǎng)絡(luò)請求
@property (nonatomic,retain) KtBaseServerAPI *serverApi;
//網(wǎng)絡(luò)請求參數(shù)
@property (nonatomic,retain) NSDictionary *params;
//請求地址 需要在子類init中初始化
@property (nonatomic,copy)   NSString *address;
//model緩存
@property (retain,nonatomic) KtCache *ktCache;

它通過持有 API 對象完成網(wǎng)絡(luò)請求,可以定制自己的存儲邏輯,控制請求方式的選擇(長、短鏈接,JSON或protobuf)。

Model 應(yīng)該對上層暴露一個非常簡單的調(diào)用接口,因為假設(shè)一個 Model 對應(yīng)一個 URL,其實每次請求只需要設(shè)置好參數(shù),就可以調(diào)用合適的方法發(fā)起請求了。

由于我們不能預(yù)知請求何時結(jié)束,所以需要設(shè)置請求完成時的回調(diào),這也需要作為 Model 的一個屬性。

BaseItem

基類的 Item 主要是負責 property name 到 json path 的映設(shè),以及 json 數(shù)據(jù)的解析。最核心的字典轉(zhuǎn)模型實現(xiàn)如下:

- (void)parseData:(NSDictionary *)data {
    Class cls = [self class];
    while (cls != [KtBaseItem class]) {
        NSDictionary *propertyList = [[KtClassHelper sharedInstance] propertyList:cls];
        for (NSString *key in [propertyList allKeys]) {
            NSString *typeString = [propertyList objectForKey:key];
            NSString* path = [self.jsonDataMap objectForKey:key];
            id value = [data objectAtPath:path];
            [self setfieldName:key fieldClassName:typeString value:value];
        }
        cls = class_getSuperclass(cls);
    }
}

完整代碼參考 Git 提交歷史:SHA-1:77c6392

如何使用

在實際使用時,首先要創(chuàng)建子類的 Modle 和 Item。子類的 Model 應(yīng)該持有 Item 對象,并且在網(wǎng)絡(luò)請求回調(diào)時,將 API 中攜帶的 JSON 數(shù)據(jù)賦值給 Item 對象。

這個 JSON 轉(zhuǎn)對象的過程在基類的 Item 中實現(xiàn),子類的 Item 在創(chuàng)建時,需要指定屬性名和 JSON 路徑之間的對應(yīng)關(guān)系。

對于上層來說,它需要生成一個 Model 對象,設(shè)置好它的路徑以及回調(diào),這個回調(diào)一般是網(wǎng)絡(luò)請求返回時 VC 的操作,比如調(diào)用 `reloadData` 方法。這時候的 VC 可以確定,網(wǎng)絡(luò)請求的數(shù)據(jù)就存在 Model 持有的 Item 對象中。

具體代碼參考 Git 提交歷史:SHA-1:8981e28

下拉刷新

很多應(yīng)用的 `UITableview` 都具有下拉刷新和上拉加載的功能,在實現(xiàn)這個功能時,我們主要考慮兩點:

1.  隱藏底層的實現(xiàn)細節(jié),對外暴露穩(wěn)定易用的接口

2.  Model 和 Item 如何實現(xiàn)

第一點已經(jīng)是老生常談,參考 SHA-1 61ba974 就可以看到如何實現(xiàn)一個簡單的封裝。

重點在于對于 Model 和 Item 的改造。

ListItem

這個 Item 沒有什么別的作用,就是定義了一個屬性 `pageNumber`,這是需要與服務(wù)端協(xié)商的。Model 將會根據(jù)這個屬性這個屬性判斷有沒有全部加載完。

// In .h
@interface KtBaseListItem : KtBaseItem
@property (nonatomic, assign) int pageNumber;
@end
// In .m
- (id)initWithData:(NSDictionary *)data {
    if (self = [super initWithData:data]) {
        self.pageNumber = [[NSString stringWithFormat:@"%@", [data objectForKey:@"page_number"]] intValue];
    }
    return self;
}

對于 Server 來說,如果每次都返回 `page_number` 無疑是非常低效的,因為每次參數(shù)都可能不同,計算總數(shù)據(jù)量是一項非常耗時的工作。因此在實際使用中,客戶端可以和 Server 約定,返回的結(jié)果中帶有 `isHasNext` 字段。通過這個字段,我們一樣可以判斷是否加載到最后一頁。

ListModel

它持有一個 `ListItem` 對象, 對外暴露一組加載方法,并且定義了一個協(xié)議 `KtBaseListModelProtocol`,這個協(xié)議中的方法是請求結(jié)束后將要執(zhí)行的方法。

@protocol KtBaseListModelProtocol <NSObject>
@required
- (void)refreshRequestDidSuccess;
- (void)loadRequestDidSuccess;
- (void)didLoadLastPage;
- (void)handleAfterRequestFinish; // 請求結(jié)束后的操作,刷新tableview或關(guān)閉動畫等。
@optional
- (void)didLoadFirstPage;
@end
@interface KtBaseListModel : KtBaseModel
@property (nonatomic, strong) KtBaseListItem *listItem;
@property (nonatomic, weak) id<KtBaseListModelProtocol> delegate;
@property (nonatomic, assign) BOOL isRefresh; // 如果為是,表示刷新,否則為加載。
- (void)loadPage:(int)pageNumber;
- (void)loadNextPage;
- (void)loadPreviousPage;
@end

實際上,當 Server 端發(fā)生數(shù)據(jù)的增刪時,只傳 `nextPage` 這個參數(shù)是不能滿足要求的。兩次獲取的頁面并非完全沒有交集,很有可能他們具有重復元素,所以 Model 還應(yīng)該肩負起去重的任務(wù)。為了簡化問題,這里就不完整實現(xiàn)了。

RefreshTableViewController

它實現(xiàn)了 `ListMode` 中定義的協(xié)議,提供了一些通用的方法,而具體的業(yè)務(wù)邏輯則由子類實現(xiàn)。

#pragma -mark KtBaseListModelProtocol
- (void)loadRequestDidSuccess {
    [self requestDidSuccess];
}
- (void)refreshRequestDidSuccess {
    [self.dataSource clearAllItems];
    [self requestDidSuccess];
}
- (void)handleAfterRequestFinish {
    [self.tableView stopRefreshingAnimation];
    [self.tableView reloadData];
}
- (void)didLoadLastPage {
    [self.tableView.mj_footer endRefreshingWithNoMoreData];
}
#pragma -mark KtTableViewDelegate
- (void)pullUpToRefreshAction {
    [self.listModel loadNextPage];
}
- (void)pullDownToRefreshAction {
    [self.listModel refresh];
}

實際使用

在一個 VC 中,它只需要繼承 `RefreshTableViewController`,然后實現(xiàn) `requestDidSuccess` 方法即可。下面展示一下 VC 的完整代碼,它超乎尋常的簡單:

- (void)viewDidLoad {
    [super viewDidLoad];
    [self createModel];
    // Do any additional setup after loading the view, typically from a nib.
}
- (void)createModel {
    self.listModel = [[KtMainTableModel alloc] initWithAddress:@"/mooclist.php"];
    self.listModel.delegate = self;
}
- (void)createDataSource {
    self.dataSource = [[KtMainTableViewDataSource alloc] init]; // 這一步創(chuàng)建了數(shù)據(jù)源
}
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}
- (void)requestDidSuccess {
    for (KtMainTableBookItem *book in ((KtMainTableModel *)self.listModel).tableViewItem.books) {
        KtTableViewBaseItem *item = [[KtTableViewBaseItem alloc] init];
        item.itemTitle = book.bookTitle;
        [self.dataSource appendItem:item];
    }
}

其他的判斷,比如請求結(jié)束時關(guān)閉動畫,最后一頁提示沒有更多數(shù)據(jù),下拉刷新和上拉加載觸發(fā)的方法等公共邏輯已經(jīng)被父類實現(xiàn)了。

到此,關(guān)于“怎么寫好一個UITableView”的學習就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續(xù)學習更多相關(guān)知識,請繼續(xù)關(guān)注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI