如果你覺得 UITableViewDelegate 和 UITableViewDataSource 這兩個協(xié)議中有大量方法每次都是復制粘貼,實現(xiàn)起來大同小異告嘲;如果你覺得發(fā)起網絡請求并解析數據需要一大段代碼错维,加上刷新和加載后簡直復雜度爆表,如果你想知道為什么下面的代碼可以滿足上述所有要求:
解耦后的VC
MVC
在討論解耦之前状蜗,我們要弄明白 MVC 的核心:控制器(以下簡稱 C)負責模型(以下簡稱 M)和視圖(以下簡稱 V)的交互需五。
這里所說的 M,通常不是一個單獨的類轧坎,很多情況下它是由多個類構成的一個層宏邮。最上層的通常是以 Model 結尾的類,它直接被 C 持有缸血。Model 類還可以持有兩個對象:
- Item:它是實際存儲數據的對象蜜氨。它可以理解為一個字典,和 V 中的屬性一一對應
- Cache:它可以緩存自己的 Item(如果有很多)
常見的誤區(qū): - 一般情況下數據的處理會放在 M 而不是 C(C 只做不能復用的事)
- 解耦不只是把一段代碼拿到外面去捎泻。而是關注是否能合并重復代碼飒炎, 并且有良好的拖展性。
原始版
在 C 中笆豁,我們創(chuàng)建 UITableView 對象郎汪,然后將它的數據源和代理設置為自己。也就是自己管理著 UI 邏輯和數據存取的邏輯闯狱。在這種架構下煞赢,主要存在這些問題:
- 違背 MVC 模式,現(xiàn)在是 V 持有 C 和 M哄孤。
- C 管理了全部邏輯照筑,耦合太嚴重。
- 其實絕大多數 UI 相關都是由 Cell 而不是 UITableView 自身完成的瘦陈。
為了解決這些問題凝危,我們首先弄明白,數據源和代理分別做了那些事晨逝。
數據源
它有兩個必須實現(xiàn)的代理方法:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
簡單來說蛾默,只要實現(xiàn)了這個兩個方法,一個簡單的 UITableView 對象就算是完成了咏花。
除此以外趴生,它還負責管理 section 的數量阀趴,標題,某一個 cell 的編輯和移動等苍匆。
代理
代理主要涉及以下幾個方面的內容:
- cell刘急、headerView 等展示前、后的回調浸踩。
- cell叔汁、headerView 等的高度,點擊事件检碗。
最常用的也是兩個方法:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
提醒:絕大多數代理方法都有一個 indexPath 參數
優(yōu)化數據源
最簡單的思路是單獨把數據源拿出來作為一個對象据块。
這種寫法有一定的解耦作用,同時可以有效減少 C 中的代碼量折剃。然而總代碼量會上升另假。我們的目標是減少不必要的代碼。
比如獲取每一個 section 的行數怕犁,它的實現(xiàn)邏輯總是高度類似边篮。然而由于數據源的具體實現(xiàn)方式不統(tǒng)一,所以每個數據源都要重新實現(xiàn)一遍奏甫。
SectionObject
首先我們來思考一個問題戈轿,數據源作為 M,它持有的 Item 長什么樣阵子?答案是一個二維數組思杯,每個元素保存了一個 section 所需要的全部信息。因此除了有自己的數組(給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 數組,應該存儲了每個 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
父類實現(xiàn)代碼
規(guī)定好了統(tǒng)一的數據存儲格式以后,我們就可以考慮在基類中完成某些方法了攘须。以 - (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 的類型于宙,自然也就無法調用 alloc 方法。除此以外悍汛,cell 除了創(chuàng)建捞魁,還需要設置 UI,這些都是數據源不應該做的事离咐。
這兩個問題的解決方案如下:
- 定義一個協(xié)議谱俭,父類返回基類 Cell奉件,子類視情況返回合適的類型。
- 為 Cell 添加一個 setObject 方法昆著,用于解析 Item 并更新 UI县貌。
優(yōu)勢
經過這一番折騰,好處是相當明顯的:
- 子類的數據源只需要實現(xiàn) cellClassForObject 方法即可凑懂。原來的數據源方法已經在父類中被統(tǒng)一實現(xiàn)了煤痕。
- 每一個 Cell 只要寫好自己的 setObject 方法,然后坐等自己被創(chuàng)建接谨,被調用這個方法即可摆碉。
- 子類通過 objectForRowAtIndexPath 方法可以快速獲取 item,不用重寫脓豪。
對照 demo(SHA-1:6475496)巷帝,感受一下效果。
優(yōu)化代理
我們以之前所說的扫夜,代理協(xié)議中常用的兩個方法為例楞泼,看看怎么進行優(yōu)化與解耦。
首先是計算高度历谍,這個邏輯并不一定在 C 完成现拒,由于涉及到 UI,所以由 Cell 負責實現(xiàn)即可望侈。而計算高度的依據就是 Object印蔬,所以我們給基類的 Cell 加上一個類方法:
+ (CGFloat)tableView:(UITableView*)tableView rowHeightForObject:(KtTableViewBaseItem *)object;
另外一類問題是以處理點擊事件為代表的代理方法, 它們的主要特點是都有 indexPath 參數用來表示位置脱衙。然而實際在處理過程中侥猬,我們并不關系位置,關心的是這個位置上的數據捐韩。
因此退唠,我們對代理方法做一層封裝,使得 C 調用的方法中都是帶有數據參數的荤胁。因為這個數據對象可以從數據源拿到瞧预,所以我們需要能夠在代理方法中獲取到數據源對象。
為了實現(xiàn)這一點仅政, 最好的辦法就是繼承 UITableView:
@protocol KtTableViewDelegate<UITableViewDelegate>
@optional
- (void)didSelectObject:(id)object atIndexPath:(NSIndexPath*)indexPath;
- (UIView *)headerViewForSectionObject:(KtTableViewSectionObject *)sectionObject atSection:(NSInteger)section;
// 將來可以有 cell 的編輯垢油,交換,左滑等回調
// 這個協(xié)議繼承了UITableViewDelegate 圆丹,所以自己做一層中轉滩愁,VC 依然需要實現(xiàn)某
@end
@interface KtBaseTableView : UITableView<UITableViewDelegate>
@property (nonatomic, assign) id<KtTableViewDataSource> ktDataSource;
@property (nonatomic, assign) id<KtTableViewDelegate> ktDelegate;
@end
cell 高度的實現(xiàn)如下,調用數據源的方法獲取到數據:
- (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 完成)辫封,我們獲得了以下特性:
- C 不用關心 Cell 高度了硝枉,這個由每個 Cell 類自己負責
- 如果數據本身存在數據源中廉丽,那么在代理協(xié)議中它可以被傳給 C,免去了 C 重新訪問數據源的操作妻味。
- 如果數據不存在于數據源正压,那么代理協(xié)議的方法會被正常轉發(fā)(因為自定義的代理協(xié)議繼承自 UITableViewDelegate)
對照 demo(SHA-1:ca9b261),感受一下效果弧可。
更加 MVC蔑匣,更加簡潔
在上面的兩次封裝中,其實我們是把 UITableView 持有原生的代理和數據源棕诵,改成了 KtTableView 持有自定義的代理和數據源裁良。并且默認實現(xiàn)了很多系統(tǒng)的方法。
到目前為止校套,看上去一切都已經完成了价脾,然而實際上還是存在一些可以改進的地方:
- 目前仍然不是 MVC 模式!
- C 的邏輯和實現(xiàn)依然可以進一步簡化
基于以上考慮笛匙, 我們實現(xiàn)一個 UIViewController 的子類侨把,并且把數據源和代理封裝到 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)建了數據源妹孙,我們把這個方法定義到協(xié)議里秋柄,并且定義為 required。
成果與目標
現(xiàn)在我們梳理一下經過改造的 TableView 該怎么用:
- 首先你需要創(chuàng)建一個繼承自 KtTableViewController 的視圖控制器蠢正,并且調用它的 initWithStyle 方法骇笔。
objc KTMainViewController *mainVC = [[KTMainViewController alloc] initWithStyle:UITableViewStylePlain]; - 在子類 VC 中實現(xiàn) createDataSource 方法,實現(xiàn)數據源的綁定嚣崭。
* (void)createDataSource { self.dataSource = [[KtMainTableViewDataSource alloc] init]; // 這 一步創(chuàng)建了數據源 } ```
1.在數據源中笨触,需要指定 cell 的類型。
* (Class)tableView:(UITableView *)tableView cellClassForObject:(KtTableViewBaseItem *)object { return [KtMainTableViewCell class]; }
1.在 Cell 中雹舀,需要通過解析數據芦劣,來更新 UI 并返回自己的高度。
* (CGFloat)tableView:(UITableView *)tableView rowHeightForObject:(KtTableViewBaseItem *)object { return 60; } // Demo 中沿用了父類的 setObject 方法说榆。
還有什么要優(yōu)化的
到目前為止虚吟,我們實現(xiàn)了對 UITableView 以及相關協(xié)議、方法的封裝签财,使它更容易使用稍味,避免了很多重復荠卷、無意義的代碼。
在使用時掂碱,我們需要創(chuàng)建一個控制器,一個數據源蚁堤,一個自定義 Cell醉者,它們正好是基于 MVC 模式的。因此披诗,可以說在封裝與解耦方面撬即,我們已經做的相當好了,即使再花大力氣呈队,也很難有明顯的提高剥槐。
但關于 UITableView 的討論遠遠沒有結束,我列出了以下需要解決的問題
- 在這種設計下宪摧,數據的回傳不夠方便粒竖,比如 cell 的給 C 發(fā)消息。
- 下拉刷新與上拉加載如何集成
- 網絡請求的發(fā)起几于,與解析數據如何集成
關于第一個問題蕊苗,其實是普通的 MVC 模式中 V 和 C 的交互問題,可以在 Cell(或者其他類) 中添加 weak 屬性達到直接持有的目的沿彭,也可以定義協(xié)議朽砰。
問題二和三是另一大塊話題,網絡請求大家都會實現(xiàn)膝蜈,但如何優(yōu)雅的集成進框架锅移,保證代碼的簡單和可拓展,就是一個值得深入思考饱搏,研究的問題了非剃。接下來我們就重點討論網絡請求。
為何創(chuàng)建網絡層
一個 iOS 的網絡層框架該如何設計推沸?這是一個非常寬泛备绽,也超出我能力范圍之外的問題。業(yè)內已有一些優(yōu)秀的鬓催,成熟的思路和解決方案肺素,由于能力,角色所限宇驾,我決定從一個普通開發(fā)者而不是架構師的角度來說說倍靡,一個普通的、簡單的網絡層該如何設計课舍。我相信再復雜的架構塌西,也是由簡單的設計演化而來的他挎。
對于絕大多數小型應用來說,集成 AFNetworking 這樣的網絡請求框架就足以應付 99% 以上的需求了捡需。但是隨著項目的擴大办桨,或者用長遠的眼光來考慮,直接在 VC 中調用具體的網絡框架(下面以 AFNetworking 為例)站辉,至少存在以下問題:
- 一旦日后 AFNetworking 停止維護呢撞,而且我們需要更換網絡框架,這個成本將無法想象饰剥。所有的 VC 都要改動代碼殊霞,而且絕大多數改動都是雷同的。
這樣的例子真實存在捐川,比如我們的項目中就依然使用早已停止維護的 ASIHTTPRequest脓鹃,可以預見,這個框架遲早要被替換古沥。 - 現(xiàn)有的框架可能無法實現(xiàn)我們的需求瘸右。以 ASIHTTPRequest 為例,它的底層用 NSOperation 來表示每一個網絡請求岩齿。眾所周知太颤,一個 NSOperation 的取消,并不是簡單調用 cancel 方法就可以的盹沈。在不修改源碼的前提下龄章,一旦它被放入隊列,其實是無法取消的乞封。
- 有時候我們的需求僅僅是進行網絡請求做裙,還會對這個請求進行各種自定義的拓展。比如我們可能要統(tǒng)計請求的發(fā)起和結束時間肃晚,從而計算網絡請求锚贱,數據解析的步驟的耗時。有時候关串,我們希望設計一個通用組件拧廊,并且支持由各個業(yè)務部門去自定義具體的規(guī)則。比如可能不同的部門晋修,會為 HTTP 請求添加不同的頭部吧碾。
- 網絡請求還有可能有其他廣泛需要添加的需求,比如請求失敗時的彈窗墓卦,請求時的日志記錄等等倦春。
參考當前代碼(SHA-1:a55ef42)感受一下沒有任何網絡層時的設計。
如何設計網絡層
其實解決方案非常簡單:
所有的計算機問題,都可以通過添加中間層來解決
讀者可以自行思考溅漾,為什么添加中間層可以解決上述三個問題山叮。
三大模塊
對于一個網絡框架來說,我認為主要有三個方面值得去設計:
- 如何請求
- 如何回調
- 數據解析
一個完整的網絡請求一般由以上三個模塊組成添履,我們逐一分析每個模塊實現(xiàn)時的注意事項:
發(fā)起請求
發(fā)起請求時,一般有兩種思路脑又,第一種是把所有要配置的參數寫到同一個方法中暮胧,借用 與時俱進,HTTP/2下的iOS網絡層架構設計 一文中的代碼表示:
+ (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 設置成一個對象哮笆,把要傳入的參數作為這個對象的屬性来颤。在發(fā)起請求時,只要設置好對象的相關屬性稠肘,然后調用一個簡單的方法即可福铅。
@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 格式的字符串,通過系統(tǒng)的或者一些開源框架可以將它轉換成字典歉胶。
接下來我們需要使用 runtime 相關的方法汛兜,將字典轉換成 Item 對象。
最后跨扮,Model 需要將這個 Item 賦值給自己的屬性序无,從而完成整個網絡請求。
如果從全局角度來說衡创,我們還需要一個 Model 請求完成的回調帝嗡,這樣 VC 才能有機會做相應的處理。
考慮到 Block 和 Delegate 的優(yōu)缺點璃氢,我們選擇用 Block 來完成回調哟玷。
數據解析
這一部分主要是利用 runtime 將字典轉換成 Item,它的實現(xiàn)并不算難,但是如何隱藏好實現(xiàn)細節(jié)巢寡,使上層業(yè)務不用過多關心喉脖,則是我們需要考慮的問題。
我們可以定義一個基類的 Item抑月,并且為它定義一個 parseData
函數:
// KtBaseItem.m
- (void)parseData:(NSDictionary *)data {
// 解析 data 這個字典树叽,為自己的屬性賦值
// 具體的實現(xiàn)請見后面的文章
}
封裝 API 對象
首先,我們封裝一個 KtBaseServerAPI 對象谦絮,這個對象的主要目的有三個:
- 隔離具體的網絡庫的實現(xiàn)細節(jié)题诵,為上層提供一個穩(wěn)定的的接口
- 可以自定義一些屬性,比如網絡請求的狀態(tài)层皱,返回的數據等性锭,方便的調用
- 處理一些公用的邏輯,比如網絡耗時統(tǒng)計
具體的實現(xiàn)請參考 Git 提交歷史:SHA-1:76487f7
Model 與 Item
BaseModel
Model 主要需要負責發(fā)起網絡請求叫胖,并且處理回調草冈,來看一下基類的 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,其實每次請求只需要設置好參數你踩,就可以調用合適的方法發(fā)起請求了诅岩。
由于我們不能預知請求何時結束,所以需要設置請求完成時的回調带膜,這也需要作為 Model 的一個屬性吩谦。
BaseItem
基類的 Item 主要是負責 property name 到 json path 的映設,以及 json 數據的解析膝藕。最核心的字典轉模型實現(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 應該持有 Item 對象,并且在網絡請求回調時,將 API 中攜帶的 JSON 數據賦值給 Item 對象变骡。
這個 JSON 轉對象的過程在基類的 Item 中實現(xiàn)轨域,子類的 Item 在創(chuàng)建時拍棕,需要指定屬性名和 JSON 路徑之間的對應關系。
對于上層來說,它需要生成一個 Model 對象,設置好它的路徑以及回調俺陋,這個回調一般是網絡請求返回時 VC 的操作,比如調用 reloadData 方法。這時候的 VC 可以確定腊状,網絡請求的數據就存在 Model 持有的 Item 對象中诱咏。
具體代碼參考 Git 提交歷史:SHA-1:8981e28
下拉刷新
很多應用的 UITableview 都具有下拉刷新和上拉加載的功能,在實現(xiàn)這個功能時缴挖,我們主要考慮兩點:
1
隱藏底層的實現(xiàn)細節(jié)袋狞,對外暴露穩(wěn)定易用的接口
2
Model 和 Item 如何實現(xiàn)
第一點已經是老生常談,參考 SHA-1 61ba974 就可以看到如何實現(xiàn)一個簡單的封裝映屋。
重點在于對于 Model 和 Item 的改造硕并。
ListItem
這個 Item 沒有什么別的作用,就是定義了一個屬性 pageNumber秧荆,這是需要與服務端協(xié)商的。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 字段甘有。通過這個字段,我們一樣可以判斷是否加載到最后一頁葡缰。
ListModel
它持有一個 ListItem 對象亏掀, 對外暴露一組加載方法,并且定義了一個協(xié)議 KtBaseListModelProtocol泛释,這個協(xié)議中的方法是請求結束后將要執(zhí)行的方法滤愕。
@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 端發(fā)生數據的增刪時,只傳 nextPage 這個參數是不能滿足要求的裙顽。兩次獲取的頁面并非完全沒有交集付燥,很有可能他們具有重復元素,所以 Model 還應該肩負起去重的任務锦庸。為了簡化問題机蔗,這里就不完整實現(xiàn)了。
RefreshTableViewController
它實現(xiàn)了 ListMode 中定義的協(xié)議,提供了一些通用的方法萝嘁,而具體的業(yè)務邏輯則由子類實現(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)建了數據源
}
- (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];
}
}
其他的判斷咱枉,比如請求結束時關閉動畫卑硫,最后一頁提示沒有更多數據,下拉刷新和上拉加載觸發(fā)的方法等公共邏輯已經被父類實現(xiàn)了蚕断。
具體代碼見 Git 提交歷史:SHA-1:0555db2
寫在結尾
網絡請求的設計架構到此就全部結束了欢伏,它還有很多值的拓展的地方。還是那句老話亿乳,沒有通用的架構硝拧,只有最適合業(yè)務的架構。
我的 Demo 為了方便演示和閱讀葛假,通常都是先實現(xiàn)底層的類和方法障陶,然后再由上層調用。但實際上這種做法在實際開發(fā)中是不現(xiàn)實的聊训。我們總是在發(fā)現(xiàn)大量冗余抱究,無意義的代碼后,才開始設計架構带斑。
因此在我看來鼓寺,真正的架構過程是當業(yè)務發(fā)生變更(通常是變復雜了)時,我們開始應該思考當前哪些操作是可以省略的(由父類或代理實現(xiàn))遏暴,最上層應該以何種方式調用底層的服務侄刽。一旦設計好了最上層的調用方式,就可以逐步向底層實現(xiàn)了朋凉。
由于本人水平也有限州丹,本文的架構并不優(yōu)秀,希望在深入理解設計模式杂彭,積累更多經驗后墓毒,再與大家分享收獲。