您好,登錄后才能下訂單哦!
這篇文章主要介紹“怎么寫好一個UITableView”,在日常操作中,相信很多人在怎么寫好一個UITableView問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”怎么寫好一個UITableView”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
如果你覺得 `UITableViewDelegate` 和 `UITableViewDataSource` 這兩個協議中有大量方法每次都是復制粘貼,實現起來大同小異;如果你覺得發起網絡請求并解析數據需要一大段代碼,加上刷新和加載后簡直復雜度爆表,如果你想知道為什么下面的代碼可以滿足上述所有要求:
在討論解耦之前,我們要弄明白 MVC 的核心:控制器(以下簡稱 C)負責模型(以下簡稱 M)和視圖(以下簡稱 V)的交互。
這里所說的 M,通常不是一個單獨的類,很多情況下它是由多個類構成的一個層。最上層的通常是以 `Model` 結尾的類,它直接被 C 持有。`Model` 類還可以持有兩個對象:
1. Item:它是實際存儲數據的對象。它可以理解為一個字典,和 V 中的屬性一一對應
2. Cache:它可以緩存自己的 Item(如果有很多)
常見的誤區:
1. 一般情況下數據的處理會放在 M 而不是 C(C 只做不能復用的事)
2. 解耦不只是把一段代碼拿到外面去。而是關注是否能合并重復代碼, 并且有良好的拖展性。
在 C 中,我們創建 `UITableView` 對象,然后將它的數據源和代理設置為自己。也就是自己管理著 UI 邏輯和數據存取的邏輯。在這種架構下,主要存在這些問題:
1. 違背 MVC 模式,現在是 V 持有 C 和 M。
2. C 管理了全部邏輯,耦合太嚴重。
3. 其實絕大多數 UI 相關都是由 Cell 而不是 `UITableView` 自身完成的。
為了解決這些問題,我們首先弄明白,數據源和代理分別做了那些事。
它有兩個必須實現的代理方法:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section; - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
簡單來說,只要實現了這個兩個方法,一個簡單的 `UITableView` 對象就算是完成了。
除此以外,它還負責管理 `section` 的數量,標題,某一個 `cell` 的編輯和移動等。
代理主要涉及以下幾個方面的內容:
1. cell、headerView 等展示前、后的回調。
2. cell、headerView 等的高度,點擊事件。
最常用的也是兩個方法:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath; - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
提醒:絕大多數代理方法都有一個 `indexPath` 參數
最簡單的思路是單獨把數據源拿出來作為一個對象。
這種寫法有一定的解耦作用,同時可以有效減少 C 中的代碼量。然而總代碼量會上升。我們的目標是減少不必要的代碼。
比如獲取每一個 `section` 的行數,它的實現邏輯總是高度類似。然而由于數據源的具體實現方式不統一,所以每個數據源都要重新實現一遍。
首先我們來思考一個問題,數據源作為 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
其中的 `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,這些都是數據源不應該做的事。
這兩個問題的解決方案如下:
1. 定義一個協議,父類返回基類 `Cell`,子類視情況返回合適的類型。
2. 為 `Cell` 添加一個 `setObject` 方法,用于解析 Item 并更新 UI。
經過這一番折騰,好處是相當明顯的:
1. 子類的數據源只需要實現 `cellClassForObject` 方法即可。原來的數據源方法已經在父類中被統一實現了。
2. 每一個 Cell 只要寫好自己的 `setObject` 方法,然后坐等自己被創建,被調用這個方法即可。
3. 子類通過 `objectForRowAtIndexPath` 方法可以快速獲取 item,不用重寫。
對照 demo(SHA-1:6475496),感受一下效果。
我們以之前所說的,代理協議中常用的兩個方法為例,看看怎么進行優化與解耦。
首先是計算高度,這個邏輯并不一定在 C 完成,由于涉及到 UI,所以由 Cell 負責實現即可。而計算高度的依據就是 Object,所以我們給基類的 Cell 加上一個類方法:
+ (CGFloat)tableView:(UITableView*)tableView rowHeightForObject:(KtTableViewBaseItem *)object;
另外一類問題是以處理點擊事件為代表的代理方法, 它們的主要特點是都有 `indexPath` 參數用來表示位置。然而實際在處理過程中,我們并不關系位置,關心的是這個位置上的數據。
因此,我們對代理方法做一層封裝,使得 C 調用的方法中都是帶有數據參數的。因為這個數據對象可以從數據源拿到,所以我們需要能夠在代理方法中獲取到數據源對象。
為了實現這一點, 最好的辦法就是繼承 `UITableView`:
@protocol KtTableViewDelegate<UITableViewDelegate> @optional - (void)didSelectObject:(id)object atIndexPath:(NSIndexPath*)indexPath; - (UIView *)headerViewForSectionObject:(KtTableViewSectionObject *)sectionObject atSection:(NSInteger)section; // 將來可以有 cell 的編輯,交換,左滑等回調 // 這個協議繼承了UITableViewDelegate ,所以自己做一層中轉,VC 依然需要實現某 @end @interface KtBaseTableView : UITableView<UITableViewDelegate> @property (nonatomic, assign) id<KtTableViewDataSource> ktDataSource; @property (nonatomic, assign) id<KtTableViewDelegate> ktDelegate; @end
cell 高度的實現如下,調用數據源的方法獲取到數據:
- (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]; }
通過對 `UITableViewDelegate` 的封裝(其實主要是通過 `UITableView` 完成),我們獲得了以下特性:
1. C 不用關心 Cell 高度了,這個由每個 Cell 類自己負責
2. 如果數據本身存在數據源中,那么在代理協議中它可以被傳給 C,免去了 C 重新訪問數據源的操作。
3. 如果數據不存在于數據源,那么代理協議的方法會被正常轉發(因為自定義的代理協議繼承自 `UITableViewDelegate`)
對照 demo(SHA-1:ca9b261),感受一下效果。
在上面的兩次封裝中,其實我們是把 `UITableView` 持有原生的代理和數據源,改成了 `KtTableView` 持有自定義的代理和數據源。并且默認實現了很多系統的方法。
到目前為止,看上去一切都已經完成了,然而實際上還是存在一些可以改進的地方:
1. 目前仍然不是 MVC 模式!
2. C 的邏輯和實現依然可以進一步簡化
基于以上考慮, 我們實現一個 `UIViewController` 的子類,并且把數據源和代理封裝到 C 中。
@interface KtTableViewController : UIViewController<KtTableViewDelegate, KtTableViewControllerDelegate> @property (nonatomic, strong) KtBaseTableView *tableView; @property (nonatomic, strong) KtTableViewDataSource *dataSource; @property (nonatomic, assign) UITableViewStyle tableViewStyle; // 用來創建 tableView - (instancetype)initWithStyle:(UITableViewStyle)style; @end
為了確保子類創建了數據源,我們把這個方法定義到協議里,并且定義為 `required`。
現在我們梳理一下經過改造的 `TableView` 該怎么用:
1. 首先你需要創建一個繼承自 `KtTableViewController` 的視圖控制器,并且調用它的 `initWithStyle` 方法。
`objc KTMainViewController *mainVC = [[KTMainViewController alloc] initWithStyle:UITableViewStylePlain];`
2. 在子類 VC 中實現 `createDataSource` 方法,實現數據源的綁定。
```objc
* (void)createDataSource { self.dataSource = [[KtMainTableViewDataSource alloc] init]; // 這 一步創建了數據源 } ```
3. 在數據源中,需要指定 cell 的類型。
```objc
* (Class)tableView:(UITableView *)tableView cellClassForObject:(KtTableViewBaseItem *)object { return [KtMainTableViewCell class]; } ```
4. 在 Cell 中,需要通過解析數據,來更新 UI 并返回自己的高度。
objc
* (CGFloat)tableView:(UITableView *)tableView rowHeightForObject:(KtTableViewBaseItem *)object { return 60; } // Demo 中沿用了父類的 setObject 方法。 ```
到目前為止,我們實現了對 `UITableView` 以及相關協議、方法的封裝,使它更容易使用,避免了很多重復、無意義的代碼。
在使用時,我們需要創建一個控制器,一個數據源,一個自定義 Cell,它們正好是基于 MVC 模式的。因此,可以說在封裝與解耦方面,我們已經做的相當好了,即使再花大力氣,也很難有明顯的提高。
但關于 `UITableView` 的討論遠遠沒有結束,我列出了以下需要解決的問題
1. 在這種設計下,數據的回傳不夠方便,比如 cell 的給 C 發消息。
2. 下拉刷新與上拉加載如何集成
3. 網絡請求的發起,與解析數據如何集成
關于第一個問題,其實是普通的 MVC 模式中 V 和 C 的交互問題,可以在 Cell(或者其他類) 中添加 weak 屬性達到直接持有的目的,也可以定義協議。
問題二和三是另一大塊話題,網絡請求大家都會實現,但如何優雅的集成進框架,保證代碼的簡單和可拓展,就是一個值得深入思考,研究的問題了。接下來我們就重點討論網絡請求。
一個 iOS 的網絡層框架該如何設計?這是一個非常寬泛,也超出我能力范圍之外的問題。業內已有一些優秀的,成熟的思路和解決方案,由于能力,角色所限,我決定從一個普通開發者而不是架構師的角度來說說,一個普通的、簡單的網絡層該如何設計。我相信再復雜的架構,也是由簡單的設計演化而來的。
對于絕大多數小型應用來說,集成 `AFNetworking` 這樣的網絡請求框架就足以應付 99% 以上的需求了。但是隨著項目的擴大,或者用長遠的眼光來考慮,直接在 VC 中調用具體的網絡框架(下面以 `AFNetworking` 為例),至少存在以下問題:
1. 一旦日后 `AFNetworking` 停止維護,而且我們需要更換網絡框架,這個成本將無法想象。所有的 VC 都要改動代碼,而且絕大多數改動都是雷同的。
這樣的例子真實存在,比如我們的項目中就依然使用早已停止維護的 `ASIHTTPRequest`,可以預見,這個框架遲早要被替換。
2. 現有的框架可能無法實現我們的需求。以 `ASIHTTPRequest` 為例,它的底層用 `NSOperation` 來表示每一個網絡請求。眾所周知,一個 `NSOperation` 的取消,并不是簡單調用 `cancel` 方法就可以的。在不修改源碼的前提下,一旦它被放入隊列,其實是無法取消的。
3. 有時候我們的需求僅僅是進行網絡請求,還會對這個請求進行各種自定義的拓展。比如我們可能要統計請求的發起和結束時間,從而計算網絡請求,數據解析的步驟的耗時。有時候,我們希望設計一個通用組件,并且支持由各個業務部門去自定義具體的規則。比如可能不同的部門,會為 HTTP 請求添加不同的頭部。
4. 網絡請求還有可能有其他廣泛需要添加的需求,比如請求失敗時的彈窗,請求時的日志記錄等等。
參考當前代碼(SHA-1:a55ef42)感受一下沒有任何網絡層時的設計。
其實解決方案非常簡單:
所有的計算機問題,都可以通過添加中間層來解決
讀者可以自行思考,為什么添加中間層可以解決上述三個問題。
對于一個網絡框架來說,我認為主要有三個方面值得去設計:
1. 如何請求
2. 如何回調
3. 數據解析
一個完整的網絡請求一般由以上三個模塊組成,我們逐一分析每個模塊實現時的注意事項:
### 發起請求
發起請求時,一般有兩種思路,第一種是把所有要配置的參數寫到同一個方法中,借用 [與時俱進,HTTP/2下的iOS網絡層架構設計](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 }
這種寫法的好處在于所有參數一目了然,而且簡單易用,每次都調用這個方法即可。但是缺點也很明顯,隨著參數和調用次數的增多,網絡請求的代碼很快多到爆炸。
另一組方法則是將 API 設置成一個對象,把要傳入的參數作為這個對象的屬性。在發起請求時,只要設置好對象的相關屬性,然后調用一個簡單的方法即可。
@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
根據前文提到的 Model 和 Item 的概念,那么應該可以想到:**這個用于訪問網絡的 API 對象,其實是作為 Model 的一個屬性**。
Model 負責對外暴露必要的屬性和方法,而具體的網絡請求則由 API 對象完成,同時 Model 也應該持有真正用來存儲數據的 Item。
一次網絡請求的返回結果應該是一個 JSON 格式的字符串,通過系統的或者一些開源框架可以將它轉換成字典。
接下來我們需要使用 runtime 相關的方法,將字典轉換成 Item 對象。
最后,Model 需要將這個 Item 賦值給自己的屬性,從而完成整個網絡請求。
如果從全局角度來說,我們還需要一個 Model 請求完成的回調,這樣 VC 才能有機會做相應的處理。
考慮到 Block 和 Delegate 的優缺點,我們選擇用 Block 來完成回調。
這一部分主要是利用 runtime 將字典轉換成 Item,它的實現并不算難,但是如何隱藏好實現細節,使上層業務不用過多關心,則是我們需要考慮的問題。
我們可以定義一個基類的 Item,并且為它定義一個 `parseData` 函數:
// KtBaseItem.m - (void)parseData:(NSDictionary *)data { // 解析 data 這個字典,為自己的屬性賦值 // 具體的實現請見后面的文章 }
首先,我們封裝一個 `KtBaseServerAPI` 對象,這個對象的主要目的有三個:
1. 隔離具體的網絡庫的實現細節,為上層提供一個穩定的的接口
2. 可以自定義一些屬性,比如網絡請求的狀態,返回的數據等,方便的調用
3. 處理一些公用的邏輯,比如網絡耗時統計
具體的實現請參考 Git 提交歷史:SHA-1:76487f7
Model 主要需要負責發起網絡請求,并且處理回調,來看一下基類的 Model 如何定義:
@interface KtBaseModel // 請求回調 @property (nonatomic, copy) KtModelBlock completionBlock; //網絡請求 @property (nonatomic,retain) KtBaseServerAPI *serverApi; //網絡請求參數 @property (nonatomic,retain) NSDictionary *params; //請求地址 需要在子類init中初始化 @property (nonatomic,copy) NSString *address; //model緩存 @property (retain,nonatomic) KtCache *ktCache;
它通過持有 API 對象完成網絡請求,可以定制自己的存儲邏輯,控制請求方式的選擇(長、短鏈接,JSON或protobuf)。
Model 應該對上層暴露一個非常簡單的調用接口,因為假設一個 Model 對應一個 URL,其實每次請求只需要設置好參數,就可以調用合適的方法發起請求了。
由于我們不能預知請求何時結束,所以需要設置請求完成時的回調,這也需要作為 Model 的一個屬性。
基類的 Item 主要是負責 property name 到 json path 的映設,以及 json 數據的解析。最核心的字典轉模型實現如下:
- (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
在實際使用時,首先要創建子類的 Modle 和 Item。子類的 Model 應該持有 Item 對象,并且在網絡請求回調時,將 API 中攜帶的 JSON 數據賦值給 Item 對象。
這個 JSON 轉對象的過程在基類的 Item 中實現,子類的 Item 在創建時,需要指定屬性名和 JSON 路徑之間的對應關系。
對于上層來說,它需要生成一個 Model 對象,設置好它的路徑以及回調,這個回調一般是網絡請求返回時 VC 的操作,比如調用 `reloadData` 方法。這時候的 VC 可以確定,網絡請求的數據就存在 Model 持有的 Item 對象中。
具體代碼參考 Git 提交歷史:SHA-1:8981e28
很多應用的 `UITableview` 都具有下拉刷新和上拉加載的功能,在實現這個功能時,我們主要考慮兩點:
1. 隱藏底層的實現細節,對外暴露穩定易用的接口
2. Model 和 Item 如何實現
第一點已經是老生常談,參考 SHA-1 61ba974 就可以看到如何實現一個簡單的封裝。
重點在于對于 Model 和 Item 的改造。
這個 Item 沒有什么別的作用,就是定義了一個屬性 `pageNumber`,這是需要與服務端協商的。Model 將會根據這個屬性這個屬性判斷有沒有全部加載完。
// 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` 無疑是非常低效的,因為每次參數都可能不同,計算總數據量是一項非常耗時的工作。因此在實際使用中,客戶端可以和 Server 約定,返回的結果中帶有 `isHasNext` 字段。通過這個字段,我們一樣可以判斷是否加載到最后一頁。
它持有一個 `ListItem` 對象, 對外暴露一組加載方法,并且定義了一個協議 `KtBaseListModelProtocol`,這個協議中的方法是請求結束后將要執行的方法。
@protocol KtBaseListModelProtocol <NSObject> @required - (void)refreshRequestDidSuccess; - (void)loadRequestDidSuccess; - (void)didLoadLastPage; - (void)handleAfterRequestFinish; // 請求結束后的操作,刷新tableview或關閉動畫等。 @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 端發生數據的增刪時,只傳 `nextPage` 這個參數是不能滿足要求的。兩次獲取的頁面并非完全沒有交集,很有可能他們具有重復元素,所以 Model 還應該肩負起去重的任務。為了簡化問題,這里就不完整實現了。
它實現了 `ListMode` 中定義的協議,提供了一些通用的方法,而具體的業務邏輯則由子類實現。
#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`,然后實現 `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]; // 這一步創建了數據源 } - (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]; } }
其他的判斷,比如請求結束時關閉動畫,最后一頁提示沒有更多數據,下拉刷新和上拉加載觸發的方法等公共邏輯已經被父類實現了。
到此,關于“怎么寫好一個UITableView”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。