先展示一张效果图
###1.重复发送网络请求的处理办法 当点击了左侧的TableView将发送网络请求给服务器,右侧的TableView将返回的数据装载到右侧的Cell中,但是有时用户点了左侧的A cell又点击了B cell,再点击A Cell时会重复发送网络请求给服务器,这样即浪费了用户流量,而且网络不好时用户体验也非常差。
解决办法: 给左侧cell的模型增加一个成员属性,用来保存该类别对应的用户数据。 一下我们以YGRecommendCategory类为例,在YGRecommendCategory类的.h文件中增加该可变数组
/** 保存该类别对应的用户数据 */
@property (nonatomic, strong) NSMutableArray *users;
复制代码
在.m文件中重写users的getter方法,采用懒加载,不用关心该属性什么时候创建,只创建一次,提升系能
- (NSMutableArray *)users
{
if (!_users) { // 如果users没用值,则创建,否则直接返回_users值
_users = [NSMutableArray array];
}
return _users;
}
复制代码
在TableView控制器的代理方法- (void)tableView: didSelectRowAtIndexPath:中,将每次返回的用户数据存储到这个users大数组中,方便下次直接读取。因为每次点击Cell都会调用这个方法,所以在这个方法中将返回的数据保存起来
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// 取出左边选中行对应的用户模型数据
YGRecommendCategory *c = self.categories[indexPath.row];
if (c.users.count) { // 如果users中有对应的值,直接加载数据,不再发送网络请求
// 刷新右侧的TableView,显示最新的拿到的数据
[self.rightTableView reloadData];
} else {
// 发送请求给服务器, 加载右侧的数据
NSMutableDictionary *params = [NSMutableDictionary dictionary];
params[@"a"] = @"list";
params[@"c"] = @"subscribe";
params[@"category_id"] = @(c.id);
[[AFHTTPSessionManager manager] GET:@"http://api.budejie.com/api/api_open.php" parameters:params success:^(NSURLSessionDataTask *task, id responseObject) {
// 将返回的字典数组转为模型数组
NSArray *users = [YGRecommendUser mj_objectArrayWithKeyValuesArray:responseObject[@"list"]];
// 将服务器返回的右侧cell数据添加到users这个可变大数组中方便下次直接读取
[c.users addObjectsFromArray:users];
// 刷新右边的表格
[self.rightTableView reloadData];
} failure:^(NSURLSessionDataTask *task, NSError *error) {
YGLog(@"%@", error);
}];
}
}
复制代码
###2 加载多页数据 当右侧Cell返回数据较多时,一页显示不完,我们就需要让用户往上拖拽刷新,加载更多数据,刷新数据的方法自己写较为复杂,需要监听TableView的滚动、拖拽等,这里我采用MJRefresh的第三方框架来实现。 解决办法: 当控制器加载完毕后,首先在右侧控制器的.m文件中添加两个头尾刷新控件,同时写两个方法- (void)loadMoreUsers 和 - (void)loadNewUsers,用来实现往上拖拽和往下拖拽刷新需要做的操作
// 添加头部控件
self.rightTableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingTarget:self refreshingAction:@selector(loadNewUsers)];
// 添加尾部刷新控件
self.rightTableView.mj_footer = [MJRefreshAutoNormalFooter footerWithRefreshingTarget:self refreshingAction:@selector(loadMoreUsers)];
// 默认设置footer控件隐藏,当拖拽时才出现
self.rightTableView.mj_footer.hidden = YES;
复制代码
点击切换左侧Cell时,来决定右侧footer控件出现或隐藏,由于需要经常判断footer的状态,所以封装一个- (void)checkFooterState函数
- (void)checkFooterState
{
// 取出左侧Cell选中的模型
YGRecommendCategory *c = self.categories[self.leftTableView.indexPathForSelectedRow.row];
// 每次刷新右边数据时, 都控制footer显示或者隐藏
// 当users大数组中没有数据时,隐藏footer控件
self.rightTableView.mj_footer.hidden = (c.users.count == 0);
if (c.users.count == c.total) { // 全部数据加载完毕
[self.rightTableView.mj_footer endRefreshingWithNoMoreData]; // 提示用户所有数据已经加载完毕
} else { // 还没有加载完毕
[self.rightTableView.mj_footer endRefreshing]; // 结束刷新,等待用户拖拽刷新
}
}
复制代码
在- (NSInteger)tableView: numberOfRowsInSection:代理方法中监测footer的状态,用来判断显示或隐藏。因为每次点击左侧的Cell都会调用这个方法,在一开始就要判断右侧的footer显示怎样的状态。这个很重要!!!不然直接导致后面右侧数据的footer控件状态显示不正确
[self checkFooterStatus];
复制代码
在- (void)loadMoreUsers函数中实现发送网络请求的代码,同时将返回的数据保存到YGRecommendCategory类的users大数组中
// 添加到当前对应的用户数据中
[category.users addObjectsFromArray:user];
复制代码
然后添加检测底部控件状态代码
[self checkFooterStatus];
复制代码
在YGRecommendCategory类的.h文件中再添加两个成员属性用来保存服务器返回的右侧Cell的总数total和已经加载出来的当前页current_page
/**
* 总数
*/
@property (assign, nonatomic) NSInteger total;
/**
* 当前页码
*/
@property (assign, nonatomic) NSInteger current_page;
复制代码
然后在- (void)loadMoreUsers方法中将服务器返回的总数保存给total变量
c.total = [responseObject[@"total" integerValue]];
复制代码
在- (void)tableView: didSelectRowAtIndexPath:方法的else判断中(没有数据,需要发送网络请求),将当前页设置为1
c.current_page = 1; // 设置当前页码为1
复制代码
并添加服务器页码参数
params[@"page"] = @(c.current_page); //先默认请求服务器第一页
复制代码
这时,我们在- (void)loadMoreUsers方法中加载更多数据时,将当前页码++
params[@"page"] = @(++category.current_page); // 将当前页加1后发送给服务器加载更多数据
复制代码
添加检测底部控件代码
[self checkFooterStatus];
复制代码
但是有一个问题,用户可能会有这样一种操作:
######用户往上拖拽加载更多数据后,又往下拖拽加载最新数据,再往上拖拽加载更多数据。
这样在加载更多数据时就会重复加载已有数据,因为我是将获取到的数据扔到大数组users中,所以还需要在- (void)loadNewUsers方法中清除users数组中所有值,下次加载新数据时,重新开始获取。
[c.users removeAllObjects];
复制代码
其中c 和 category 都是YGRecommendCategory类new出的对象。
###3.网络延迟带来的一些细节问题
- #####用户点击了左侧cell的A,在点击B后,如果网络延迟,在B的cell数据回来之前,界面还在显示A类的数据 解决办法: 在- (void)tableView: didSelectRowAtIndexPath:该方法的条件判断中,在else加入一行。如果users数组中没有数据,那么发送网络请求之前先刷新右侧的TableView
[self.rightTableView reloadData];
复制代码
- #####当用点击左侧Cell时,右侧应该默认进入下拉刷新状态,这样用户体验会好,否则如果网络不好,数据没回来之前右侧TableView将是一篇空白 解决办法: 为右侧TableView集成头部刷新控件header,同时实现一个加载最新数据的方法- (void)loadNewUsers
// 添加头部控件
self.rightTableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingTarget:self refreshingAction:@selector(loadNewUsers)];
复制代码
然后在- (void)tableView: didSelectRowAtIndexPath:方法的else判断中加进入往下拖拽刷新
[self.rightTableView.mj_header beginRefreshing];
复制代码
将发送网络请求的代码放在- (void)loadNewUsers方法中,一旦进入下来刷新,将自动调用该方法,发送网络请求 数据一旦回来,则需要结束header刷新
[self.rightTableView.mj_header endRefreshing];
复制代码
如果下拉刷新失败,同样也要在失败block中加入结束刷新代码。
- #####用户点击了A类Cell后,header控件自动刷新完,将数据展示出来,用户往上拽,footer控件加载更多数据,但有时,其实一页已经展示完毕 解决办法: 当header刷新完数据,同时判断footer显示“加载更多数据”或“全部数据加载完毕”,节省用户流量和期望,所以在- (void)loadNewUsers方法中加入检查header控件状态的代码
[self checkFooterStatus];
复制代码
- #####在网络不好的情况下,如果用户点了左侧的某个Cell发送了网络请求,但是数据回来之前,用户又点击左侧其他Cell,这样就会连发多个请求给服务器,这样当数据返回后,用户也不知道现在显示出来的数据是哪次点击的内容 解决办法: 给控制器增加一个成员属性params,用来保存每次服务器请求参数,将每次请求的参数字典赋值给params保存,如果之前发送后返回的参数和现在保存的params参数不符,则让之前的请求不要刷新右边的TableView,这样就能保证最后一次点击,展示出来的数据是最后一次,之前请求回来的数据不会干扰。 首先,增加控制器属性
/**
* 请求参数
*/
@property (strong, nonatomic) NSMutableDictionary *params;
复制代码
然后将每次请求参数保存起来
self.params = params; //将参数赋值给成员属性保存
复制代码
然后在GET请求的成功block中增加判断,如果上次的params和现在保存的params不同,即说明现在返回的数据是之前发送的请求,则不予刷新右边TableView,直接return。但是要将返回的数据保存起来供以后使用。
//如果两次请求不同,直接return
if (self.params != params) return;
复制代码
- #####发送网络请求的GET方法调用非常频繁,如果直接利用[AFHTTPSessionManager manager]拿到当前manager会非常消耗内存。 解决办法:给控制增加一个manager的成员属性,用来保存当前manager,重写getter方法采用懒加载,保证每次发请求都是这个manager,这样只需创建一次。 控制器增加属性
/**
* AFHTTPSessionManager的manager
*/
@property (strong, nonatomic) AFHTTPSessionManager *manager;
复制代码
然后将代码中所有的[AFHTTPSessionManager manager] 替换成self.manager即可。
- #####网络差时,用户点击了左侧Cell,数据还没返回时,用户失去耐心,停止等待,直接点击左上角返回,销毁控制器。这样当数据返回后,会调用self的成员属性,但发现控制器已经销毁,所以程序会奔溃 解决办法: manager对象有一个属性operationQueue,用来保存所有请求任务,拿到这个队列,调cancelAllOperations方法销毁所有任务。
// 重写控制器dealloc方法
-(void)dealloc
{
//停止所有请求
[self.manager.operationQueue cancelAllOperations];
}
复制代码