最近在做項目時發(fā)現(xiàn)列表分頁加載數(shù)據(jù)體驗并不是很好牧愁,第一個想到解決此問題的方案就是預(yù)加載,便在網(wǎng)上找了一些相關(guān)的資料還有 demo 試過之后發(fā)現(xiàn)效果不是很好而且好多細節(jié)都沒有處理好外莲,便想著自己寫個 demo 實現(xiàn)預(yù)加載猪半,因為平時項目比較忙寫這個demo 都是平時下班時間寫一點,寫了蠻長時間的偷线。今天把 demo 中實現(xiàn)的一些細節(jié)和學(xué)習(xí)資料分享給大家磨确。沒有耐心讀的同學(xué)可以直接翻到底即可下載 demo
來瞅一眼效果圖,一圖勝千言声邦。
依賴三方庫:
AFNetworking
用于網(wǎng)絡(luò)請求
MJRefresh
用于刷新
YYKit
用于模型轉(zhuǎn)換乏奥,也可以直接用YYModel
或者MJExtension
ReactiveObjC
用于數(shù)據(jù)的傳輸 (大家平時用的應(yīng)該都是 block 去傳遞數(shù)據(jù)吧,當寫完這個 demo 的時候也用 block 去實現(xiàn)了網(wǎng)絡(luò)請求傳遞數(shù)據(jù)后來又刪掉了亥曹,因為 block 在傳遞數(shù)據(jù)的時候沒有 reactive 優(yōu)雅邓了。如果有同學(xué)對此三方庫不了解,可自行實現(xiàn)數(shù)據(jù)傳遞部分)
MBProgressHUD
不需要的可以移除掉
項目中常見問題:
相信你所做的項目中有很多是用 Tableview 或 Collectionview 所展示的數(shù)據(jù)列表媳瞪,做列表展示時會一些常見問題:
每次滑動到底部時都要去加載下一頁的數(shù)據(jù)骗炉,每次都是菊花轉(zhuǎn)啊轉(zhuǎn)用戶體驗并不是很好,如何下拉刷新時加載菊花上拉加載時不加載菊花(MBProgressHUD)蛇受。
-
每次都要定義一個全局的
currentPage
和totlePage
來計算當前頁是否小于總頁句葵,而且需要不斷的從接口拿取頁碼數(shù)據(jù),比如這種數(shù)據(jù):"page":{ "totalResultSize":27, "totalPageSize":3, "pageSize":10, "currentPage":1, }
每次網(wǎng)絡(luò)請求時總需要把頁碼信息拿出來用來判斷是否發(fā)起網(wǎng)絡(luò)請求,這樣寫很繁瑣有木有
每次網(wǎng)絡(luò)請求的時候要判斷是否是刷新還是獲取新數(shù)據(jù)來對接受數(shù)據(jù)的數(shù)組來做移除或添加操作乍丈。獲取數(shù)據(jù)后刷新UI是不是有卡頓的現(xiàn)象剂碴,總說數(shù)據(jù)和刷新 UI 要分開操作,刷新 UI 要放到主線程去做轻专,可是你真的是這么做的嗎忆矛?你的數(shù)據(jù)處理真的是放到子線程去的嗎?
-
用
MJRefresh
去刷新列表的時候你是否是這么操作的铭若?結(jié)束刷新操作不是應(yīng)該放在獲取到數(shù)據(jù)之后才做的嗎洪碳?self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{ //通過這個狀態(tài)來判斷對接受數(shù)據(jù)的數(shù)組來做移除或添加操作 weakSelf.addData = NO; //將頁碼重置 weakSelf.currentPage = 1; //發(fā)起網(wǎng)絡(luò)請求 [weakSelf loadDiscoverData]; //立刻結(jié)束刷新狀態(tài),即關(guān)閉菊花 [weakSelf.tableView.mj_header endRefreshing]; }];
解決問題
帶著這些問題我們來慢慢找尋一下解決辦法叼屠。
- 如何每次網(wǎng)絡(luò)能請求兩頁數(shù)據(jù)瞳腌,當滑動到列表的某一位置來發(fā)起網(wǎng)絡(luò)請求。你可以去網(wǎng)上搜一下相關(guān)的關(guān)鍵詞镜雨,差不多會得到兩種結(jié)果①利用 scrollview 的代理來計算內(nèi)容高度嫂侍;②利用indexPath的下標與數(shù)據(jù)源判斷是否發(fā)起網(wǎng)絡(luò)請求。是的荚坞,我用的就是第二種挑宠,不需要繁瑣的計算。
由于發(fā)起了兩次網(wǎng)絡(luò)請求颓影,列表會刷新兩次各淀,如果網(wǎng)絡(luò)條件不是很好的情況下頁面刷新不及時會卡一下,由于用的是MBProgressHUD
做的提示诡挂,當每次請求的時候會在屏幕中間加載旋轉(zhuǎn)菊花碎浇,每次發(fā)起網(wǎng)絡(luò)請求菊花旋轉(zhuǎn)時間比請求一次的時候要長。針對這一情況璃俗,我在網(wǎng)絡(luò)請求工具中設(shè)置了是否顯示菊花奴璃,關(guān)于我封裝的網(wǎng)絡(luò)請求介紹可以看這篇文章,里面有詳細的使用介紹城豁。
在網(wǎng)絡(luò)請求使用類MNetConfig
中加入了isHidenHUD
,這與下拉和上拉狀態(tài)取反達到異曲同工之妙苟穆。
/** *是否顯示HUD,默認顯示*/
@property (nonatomic, assign) BOOL isHidenHUD;
- 如何不用傳頁碼參數(shù)來判斷當前數(shù)據(jù)是第幾頁數(shù)據(jù),如何獲取到?jīng)]有更多數(shù)據(jù)的狀態(tài)唱星。
我將網(wǎng)絡(luò)請求和數(shù)據(jù)的處理從控制器中抽離出來即MVVM
中的VM
雳旅,具體關(guān)于MVVM
設(shè)計模式請自行查詢,這里就不做過多闡述间聊。我通過對NSObject
類進行了Category
岭辣,抽離出一個專門處理網(wǎng)絡(luò)請求數(shù)據(jù)的類NSObject+MRequestAdd.h
來看一下我針對網(wǎng)絡(luò)請求設(shè)置了哪些屬性:
/**
* 數(shù)據(jù)數(shù)組
*/
@property (nonatomic, strong) NSMutableArray *dataArray;
/**
* 原始請求數(shù)據(jù)
*/
@property (nonatomic, strong) id orginResponseObject;
/**
* 當前頁碼
*/
@property (nonatomic, assign) NSInteger currentPage;
/**
* 是否請求中
*/
@property (nonatomic, assign) BOOL isRequesting;
/**
* 是否數(shù)據(jù)加載完
*/
@property (nonatomic, assign) BOOL isNoMoreData;
-(RACSignal *)singalForSingleRequestWithSet:(MNetConfig *)setting;
如果你寫過類的Category
會發(fā)現(xiàn)分類是不允許property
的。
如果你寫一個屬性會在.m
中出現(xiàn)黃色警告甸饱,要么將這個屬性標記為@dynamic
要么實現(xiàn)set
和get
方法,就算實現(xiàn)了 set 和 get 方法在調(diào)用的時候也可能報錯。
Property 'test' requires method 'setTest:' to be defined
- use @dynamic or provide a method implementation in this category
這時就需要用到runtime
了叹话,可能你感覺這個東西很虛無縹緲而且不好懂(像我這種初學(xué)者)如果你資料看多了也就慢慢懂了偷遗,下面給同學(xué)們普及一點點 runtime 的一些知識,本人理解的比較淺如有哪里不對的地方請留言給我驼壶。
runtime 能為類做些什么:
為現(xiàn)有的類添加私有變量氏豌,比如在網(wǎng)絡(luò)請求時來判斷網(wǎng)絡(luò)請求是否加載完成
@property (nonatomic, assign) BOOL isNoMoreData;
-
為現(xiàn)有的類添加共有屬性供外部訪問。
@property (nonatomic, strong) NSMutableArray *dataArray;
為 KVO 創(chuàng)建一個關(guān)聯(lián)的觀察者热凹,這個屬性我還沒有用到泵喘,具體怎么用我也不是很清楚,這個用法也是資料說的般妙。
第一點與第二點的區(qū)別無非是一個公有和私有的區(qū)別纪铺,從本質(zhì)創(chuàng)建上并么有太大區(qū)別,我在項目中用的最多的也是這兩點碟渺。
創(chuàng)建 runtime 屬性:你可以在#import <objc/runtime.h>
找到它們
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,id _Nullable value, objc_AssociationPolicy policy)
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
objc_removeAssociatedObjects(id _Nonnull object)
從字面上你應(yīng)該可以猜到了鲜锚,
objc_setAssociatedObject
是一個 set 方法,重寫 set 方法相信大家都寫過苫拍,這個用法與之類似芜繁,是用于給對象添加關(guān)聯(lián)對象
objc_getAssociatedObject
獲取關(guān)聯(lián)對象
objc_removeAssociatedObjects
移除一個對象的所有關(guān)聯(lián)對象
objc_setAssociatedObject
在objc_setAssociatedObject
會涉及到四個參數(shù),分別是object
绒极、key
骏令、value
、policy
object垄提,要關(guān)聯(lián)的對象即 self
key 榔袋,這個 key 值必須保證是一個對象級別的唯一常量與創(chuàng)建 tablviewcell 所創(chuàng)建的 ID 類似;一般來說塔淤,有以下三種推薦的 key 值:① 聲明
static const char * key_m_dataArray = "key_m_dataArray";
使用&key_m_dataArray
作為 key 值這個是需要加&符號獲取地址;② 聲明static const void * key_m_dataArray = "key_m_dataArray"
摘昌,使用 key_m_dataArray 作為 key 值;③ 用 selector 高蜂,使用 getter 方法的名稱作為 key 值聪黎。因為它省掉了一個變量名,非常優(yōu)雅地解決了命名問題备恤。value 即當前屬性的值
-
policy 關(guān)聯(lián)策略
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) { OBJC_ASSOCIATION_ASSIGN = 0,//弱引用對象 OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, //強引用對象且為非原子操作 OBJC_ASSOCIATION_COPY_NONATOMIC = 3,//復(fù)制關(guān)聯(lián)對象且為非原子操作 OBJC_ASSOCIATION_RETAIN = 01401,//強引用對象且為原子操作 OBJC_ASSOCIATION_COPY = 01403//復(fù)制關(guān)聯(lián)對象為原子操作 }; 將前三種翻譯過來即: @property (nonatomic, assign) @property (nonatomic, strong) @property (nonatomic, copy)
關(guān)聯(lián)對象的五種關(guān)聯(lián)策略與屬性的限定符非常類似稿饰,在絕大多數(shù)情況下,我們都會使用 OBJC_ASSOCIATION_RETAIN_NONATOMIC
的關(guān)聯(lián)策略露泊,這可以保證我們持有關(guān)聯(lián)對象喉镰。
關(guān)于Associated Objects
是如何實現(xiàn)以及如何儲存數(shù)據(jù)和關(guān)聯(lián)對象建議你看一下這篇文章或許對你有幫助。關(guān)于 runtime 的一些知識就介紹那么多惭笑,現(xiàn)在我對 runtime 只是會用一些簡單的屬性而更深層次的用法我也在探索中侣姆。
回到NSObject+MRequestAdd
這個類中來生真,看一下內(nèi)部實現(xiàn)。
通過-(RACSignal *)singalForSingleRequestWithSet:(MNetConfig *)setting;
這個方法來進行網(wǎng)絡(luò)請求捺宗,而每次網(wǎng)絡(luò)請求是通過下面的方法來實現(xiàn)的
- (RACSignal *)baseSingleRequestWithSet:(MNetConfig *)setting{
RACReplaySubject *subject = [RACReplaySubject subject];
//判斷當前網(wǎng)絡(luò)狀態(tài)柱蟀,是否已經(jīng)在請求數(shù)據(jù)或者沒有更多數(shù)據(jù)時返回 error 狀態(tài),表示沒有數(shù)據(jù)或請求失敗
if (![self isSatisfyLoadMoreRequest]&&!setting.isRefresh) {
[subject sendError:nil];
return subject;
}
//避免某些接口只有 page 一個參數(shù)蚜厉,所以需要初始化一個 parameter 來存放 page 字段
if (!setting.paramet) {
setting.paramet = [NSMutableDictionary dictionary];
}
if (setting.isRefresh) {
self.currentPage = 0;
}
self.currentPage ++;
if (setting.keyOfPage) {
[setting.paramet setValue:@(self.currentPage) forKey:setting.keyOfPage];
}
//每一次網(wǎng)絡(luò)請求都是YES长已,請求完畢就為 NO
self.isRequesting = YES;
[[MNetRequestModel netRequestSeting:setting] subscribeNext:^(id _Nullable x) {
self.isRequesting = NO;
[subject sendNext:x];
} error:^(NSError * _Nullable error) {
self.isRequesting = NO;
//如果當前請求失敗,因為都是在原來頁碼上進行++昼牛,所以這里需要--來回退頁碼术瓮。
if (self.currentPage > 0) {
self.currentPage--;
}
[subject sendError:error];
} completed:^{
[subject sendCompleted];
}];
return subject;
}
- (BOOL)isSatisfyLoadMoreRequest{
return (!self.isNoMoreData&&!self.isRequesting);
}
再來看一下.h 文件放出的接口的實現(xiàn)
- (RACSignal *)singalForSingleRequestWithSet:(MNetConfig *)setting{
RACReplaySubject *subject = [RACReplaySubject subject];
//每次調(diào)用的即是上面所寫的方法,每次有新數(shù)據(jù)時才會走網(wǎng)絡(luò)請求贰健,如果沒有走 error 狀態(tài)胞四,即沒有數(shù)據(jù)表示已無更多數(shù)據(jù)
[[self baseSingleRequestWithSet:setting] subscribeNext:^(id _Nullable x) {
//每一次請求到的源數(shù)據(jù)
self.orginResponseObject = x;
//利用 runtime 創(chuàng)建的屬性來初始化
if (!self.dataArray) {
self.dataArray = @[].mutableCopy;
}
if (setting.isRefresh) {
[self.dataArray removeAllObjects];
}
//定位到要解析的數(shù)據(jù)位置,用“/”做拆分
NSArray *separateKeyArray = [setting.modelLocalPath componentsSeparatedByString:@"/"];
for (NSString *sepret_key in separateKeyArray) {
x = x[sepret_key];
}
//每一次網(wǎng)絡(luò)請求獲取的模型數(shù)據(jù)
NSArray *dataArray = [NSArray modelArrayWithClass:NSClassFromString(setting.modelNameOfArray) json:x];
//如果當前請求到的數(shù)據(jù)為空霎烙,說明網(wǎng)絡(luò)出錯或者沒有更多數(shù)據(jù)
if (dataArray.count == 0) {
self.isNoMoreData = YES;
[subject sendError:nil];
} else {
//只有有數(shù)據(jù)時才進行 sendnext撬讽,即傳遞數(shù)據(jù)
self.isNoMoreData = NO;
[self.dataArray addObjectsFromArray:dataArray];
[subject sendNext:self.dataArray];
}
} error:^(NSError * _Nullable error) {
[subject sendError:error];
}completed:^{
[subject sendCompleted];
}];
return subject;
}
當前操作就完美解決了每次都要去處理接口中的 page 信息問題⌒看一下如何請求:
- (RACSignal *)siganlForTopicDataIsReload:(BOOL)isReload{
RACReplaySubject *subject = [RACReplaySubject subject];
MNetConfig *seting = [MNetConfig new];
seting.hostUrl = Test_Page_URL;
// seting.paramet = @{};//如果有頁碼參數(shù),不要寫到字典,將頁碼參數(shù)寫到下方
seting.modelLocalPath = @"entity/topics";//數(shù)據(jù)定位,即 entity 下的 topics 對應(yīng)的數(shù)據(jù)
seting.keyOfPage = @"page.currentPage";//頁碼寫這
seting.modelNameOfArray = @"MHYTestModel";//要顯示列表對應(yīng)的數(shù)據(jù)模型
seting.isRefresh = isReload;//是否刷新
seting.isHidenHUD = !isReload;//上拉刷新顯示 HUD 上拉更多不顯示 HUD
// seting.cashSeting = MCacheSave;// 是否進行本地緩存
// seting.cashTime = 4;//設(shè)置緩存時間為4分鐘,默認3分鐘
// seting.isCashMoreData = YES;//進行多頁數(shù)據(jù)緩存
seting.jsonValidator = @{@"entity":[NSDictionary class],
@"entity":@{@"topics":[NSArray class]}
};//檢測 entity 是否為字典類型,檢測 entity 下 topics 字段是否為數(shù)組
[[self singalForSingleRequestWithSet:seting]subscribeNext:^(id _Nullable x) {
[subject sendNext:x];
} error:^(NSError * _Nullable error) {
[subject sendError:error];
} completed:^{
[subject sendCompleted];
}];
return subject;
}
獲取到的數(shù)據(jù)如何正在子線程去處理呢游昼?
我在每一個網(wǎng)絡(luò)請求中加入了一個線程,可以看一下MNetRequestModel.m
這個文件尝蠕,demo 中所有的網(wǎng)絡(luò)請求最終的請求都是它來完成的烘豌。
dispatch_async (dispatch_get_global_queue
(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//獲取數(shù)據(jù)發(fā)送 next,處理數(shù)據(jù)
[subject sendNext];
dispatch_async (dispatch_get_main_queue(), ^{
//再發(fā)送完成信號看彼,來刷新 UI
[subject sendCompleted];
});
});
這樣就將數(shù)據(jù)處理和 UI 刷新分開廊佩。
回過頭來看一下解決了哪些問題:刷新菊花顯示問題;處理頁碼問題靖榕;刷新 UI 問題标锄,和 mj 停止刷新時機問題(即在發(fā)送 comply 后停止刷新),以上羅列的問題都解決了茁计。
其實這樣做還有一些問題:如果非列表數(shù)據(jù)請求因為在所有網(wǎng)絡(luò)請求中加入了線程料皇,有一些信息是在 next 中獲取的比如接口中的message
信息,這些信息需要給用戶來展示星压,如果在 next 中調(diào)用MBProgressHUD
的 show 方法是崩潰的践剂,因為MBProgressHUD
的菊花必須要的主線程中才可以調(diào)用。我是這樣處理的娜膘,為NSString
類寫一個分類逊脯,里面的方法大概就這么寫:
-(void)showSucceed;
-(void)showSucceed{
dispatch_async(dispatch_get_main_queue(), ^{
[MBProgressHUD showSuccess:self];
});
}
如果信息和 UI 必須放在一起刷新,比如UIButton
的狀態(tài)和文字改變竣贪,這時必須在當前文件中來創(chuàng)建中間替換變量再刷新 UI军洼。
還有一個最大的問題就是每次獲取到的數(shù)據(jù)都是總數(shù)據(jù)巩螃。如果要對模型做計算處理比如通過模型計算 cell 中的控件的 frame,第一頁獲取10條數(shù)據(jù)匕争,對模型做10次計算牺六,再請求10條數(shù)據(jù),此時應(yīng)該處理請求下來的10條數(shù)據(jù)汗捡,而數(shù)據(jù)處理是在 next 中完成的,next 中為總數(shù)據(jù)即20條數(shù)據(jù)畏纲,這樣模型的計算就進行了20次扇住,隨著頁面的增加計算量越來越大。這個問題我暫時還沒想到好的解決辦法盗胀,如果你有好的解決辦法請私信我艘蹋。
創(chuàng)建UITableView
的 runtime 屬性
寫一個UITableView的分類UITableView+MPreload
,創(chuàng)建倆個屬性:
/** tableview數(shù)據(jù) */
@property (nonatomic, strong) NSMutableArray *dataArray;
/** 預(yù)加載回調(diào)*/
@property (nonatomic, copy) PreloadBlock m_preloadBlock;
一個常量:
/** 預(yù)加載觸發(fā)的數(shù)量 */
static NSInteger const PreloadMinCount = 3;
和一個公開方法:
- (void)preloadDataWithCurrentIndex:(NSInteger)currentIndex;
- (void)preloadDataWithCurrentIndex:(NSInteger)currentIndex{
NSInteger totalCount = self.dataArray.count;
//判斷當前行數(shù)是否滿足預(yù)加載的條件
if ([self isSatisfyPreloadDataWithTotalCount:totalCount currentIndex:currentIndex]&&self.m_preloadBlock) {
//通過 block 來調(diào)用網(wǎng)絡(luò)請求
self.m_preloadBlock();
}
}
- (BOOL)isSatisfyPreloadDataWithTotalCount:(NSInteger)totalCount currentIndex:(NSInteger)currentIndex{
//如果預(yù)加載觸發(fā)的數(shù)量為3票灰,總數(shù)據(jù)為10條即第7行觸發(fā)預(yù)加載
return ((currentIndex == totalCount - PreloadMinCount) && (currentIndex >= PreloadMinCount));
}
更具體的代碼請看 demo 中UITableView+MPreload
文件
捋一下整體思路:
抽出一個專門做數(shù)據(jù)請求的類TestDataModel
繼承與NSObject
類型女阀,對NSObject
利用 runtime 特性進行擴展屬性得到每次請求到的總數(shù)據(jù)dataArray
,這樣TestDataModel
類所創(chuàng)建的對象都可以擁有當前屬性屑迂,然后再利用 runtime 特性對UITableView
進行擴展分別是兩個屬性dataArray
浸策、m_preloadBlock
一個方法- (void)preloadDataWithCurrentIndex:(NSInteger)currentIndex
這樣每滑動 tablview 就會調(diào)用該方法灿椅,通過判斷是否進行預(yù)加載行為拢军,預(yù)加載請求的數(shù)據(jù)通過對象再給 tablview 的dataArray
。其實思路很簡單,runtime擴展所需要的屬性和方法,然后有機的結(jié)合調(diào)用山上,這樣彼此循環(huán)調(diào)用就能創(chuàng)建一個無限循環(huán)的列表了手报。具體方法及細節(jié)見 demo 點我下載
學(xué)習(xí)參考資料: