前言:
在开发中遇到这么一个需求,一个列表页展示很多商品数据,这些商品数据的信息是在实时变动的,可以简单的说是一个TableView的Cell里每个Cell是一个商品,每个商品的数据通过接口轮训数据并
实时展示显示的数据,同时还要保证离开这个页面后停止无意义的数据变换及轮询,减少不必要的服务器带宽
知识点:Cell重用、定时器、通知、网络请求、倒计时、轮询可见部分
过程:
查找了很多资料,在参考了 iOS 在cell中使用倒计时的处理方法这篇博文后找了思路
1、由于Cell太多,不能每个Cell都创建一个Timer刷定时器,而且也不好控制让离开改某个View时停止定时器,在进入该页面后又重新开始定时器,所以需要用到的页面,在对应的Controller里使用一个定时器统一管理
// .h
@interface LHCountDownManager : NSObject
// 倒计时通知名字
@property (nonatomic,copy) NSString *countDownNotifyKey;
/// 时间差(单位:秒)
@property (nonatomic, assign) NSInteger timeInterval;
/// 开始倒计时
- (void)start;
/// 刷新倒计时
- (void)reload;
/// 停止倒计时
- (void)invalidate;
+ (instancetype)managerWithCountDownKey:(NSString *)countDownKey;
@end
// .m
#import "LHCountDownManager.h"
@interface LHCountDownManager()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation LHCountDownManager
+ (instancetype)managerWithCountDownKey:(NSString *)countDownKey
{
LHCountDownManager *manager = [LHCountDownManager new];
manager.countDownNotifyKey = countDownKey;
return manager;
}
- (void)start {
// 启动定时器
[self timer];
}
- (void)reload {
// 刷新只要让时间差为0即可
_timeInterval = 0;
}
- (void)invalidate {
[self.timer invalidate];
self.timer = nil;
self.timeInterval = 0;
}
- (void)timerAction {
// 时间差+1
self.timeInterval ++;
// 发出通知--可以将时间差传递出去,或者直接通知类属性取
[[NSNotificationCenter defaultCenter] postNotificationName:self.countDownNotifyKey object:nil userInfo:@{@"TimeInterval" : @(self.timeInterval)}];
// 对最大值进行限制
if (self.timeInterval >= NSUIntegerMax) {
self.timeInterval = 0;
}
// 对最小值进行限制
if (self.timeInterval <= 0) {
self.timeInterval = 0;
}
}
- (NSTimer *)timer {
if (_timer == nil) {
_timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
}
return _timer;
}
@end
2、由统一管理定时器的类进行按一定时间倒计时发通知,然后由有需要变动的Cell进行接收通知并处理,如果Cell接收到的通知后进行对Cell管理的model的值如果有变化,进行倒计时显示
#pragma mark - ******** 倒计时通知处理
- (void)setCountDownNotifyName:(NSString *)countDownNotifyName
{
// 如果以前设置的倒计时通知有变化,去除以前的通知
if (_countDownNotifyName != nil && ![_countDownNotifyName isEqualToString:countDownNotifyName]) {
[[NSNotificationCenter defaultCenter] removeObserver:self name:_countDownNotifyName object:nil];
}
// 如果现在设置的通知与之前的不一致,添加通知
if (countDownNotifyName !=nil && ![_countDownNotifyName isEqualToString:countDownNotifyName]) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(countDownChange:) name:countDownNotifyName object:nil];
}
_countDownNotifyName = countDownNotifyName;
}
#pragma mark 显示倒计时减一
- (void)countDownChange:(NSNotification *)notify
{
// 倒计时减一
self.model.goodsTime --;
NSInteger countDown = self.model.goodsTime;
/// 当倒计时到了进行回调
if (countDown <= 0) {
countDown = 0;
}
NSString *cuountDownText = [NSString stringWithFormat:@"%02zd:%02zd:%02zd", countDown/3600, (countDown/60)%60, countDown%60];
self.timeLbl.text = cuountDownText;
}
#pragma mark - ******** 轮询价格变化处理
- (void)setPriceChangeNotifyName:(NSString *)priceChangeNotifyName
{
// 如果以前设置的倒计时通知有变化,去除以前的通知
if (_priceChangeNotifyName != nil && ![_priceChangeNotifyName isEqualToString:priceChangeNotifyName]) {
[[NSNotificationCenter defaultCenter] removeObserver:self name:_priceChangeNotifyName object:nil];
}
// 如果现在设置的通知与之前的不一致,添加通知
if (priceChangeNotifyName !=nil && ![_priceChangeNotifyName isEqualToString:priceChangeNotifyName]) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(priceChange:) name:priceChangeNotifyName object:nil];
}
_priceChangeNotifyName = priceChangeNotifyName;
}
#pragma mark 内容经过轮询后显示最新内容
- (void)priceChange:(NSNotification *)notify
{
NSDictionary *userInfo = notify.userInfo;
NSString *goodsId = userInfo[@"goodsId"];
// goodsId等于空或不是当前Cell显示的model,直接不处理
if (goodsId == nil || [goodsId isEqualToString:@""] || ![goodsId isEqualToString:self.model.goodsId]) {
return;
}
NSString *currentNamePrice = [NSString stringWithFormat:@"%@出价¥%.2f",self.model.goodsBuyer,self.model.goodsPrice];
// 如果购买者或者购买的价格没有变化不处理
if (currentNamePrice == nil || [self.oldNamePrice isEqualToString:currentNamePrice]) {
return;
}
// 更新数据
self.peoplePriceLbl.text = currentNamePrice;
NSInteger countDown = self.model.goodsTime;
/// 当倒计时到了进行回调
if (countDown <= 0) {
countDown = 0;
}
NSString *cuountDownText = [NSString stringWithFormat:@"%02zd:%02zd:%02zd", countDown/3600, (countDown/60)%60, countDown%60];
self.timeLbl.text = cuountDownText;
self.oldNamePrice = currentNamePrice;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - ******** 设置数据
- (void)setModel:(LHProductModel *)model
{
_model = model;
[self.productImg sd_setImageWithURL:[NSURL URLWithString:model.goodsImage] placeholderImage:[UIImage imageNamed:@"nahan"]];
self.nameLbl.text = model.goodsName;
NSString *currentNamePrice = [NSString stringWithFormat:@"%@出价¥%.2f",self.model.goodsBuyer,self.model.goodsPrice];
self.peoplePriceLbl.text = currentNamePrice;
self.oldNamePrice = currentNamePrice;
NSInteger countDown = model.goodsTime;
/// 当倒计时到了进行回调
if (countDown <= 0) {
countDown = 0;
}
NSString *cuountDownText = [NSString stringWithFormat:@"%02zd:%02zd:%02zd", countDown/3600, (countDown/60)%60, countDown%60];
self.timeLbl.text = cuountDownText;
}
3、如何轮询只显示的Cell的数据,我的办法是轮询也统一放在倒计时管理里处理,然后再Cell的 _setModel里把旧的model移除需要轮询的队列,把新要显示的model加入要轮询的队列,然后管理定时器的类会定时轮询这些model,这样一个定时器管理类就能把倒计时显示和最少量轮询同时完成
3.1 、管理一个保持轮询中的数组pollingListM
3.2、 在每次重新请求了数据后将pollingListM清空
3.3、在cellForRowAtIndexPath方法里将被替换不显示的model移除需要轮询的数组,将接近显示的model放入轮询数组,然后通过定时器在列表里轮询列表数组
#import "LHHomeViewController.h"
#import "LHProductTableViewCell.h"
#import "ProductService.h"
#import "LHCountDownManager.h"
#import "LHGoodsDetailController.h"
#define kHomeCountDownTimerNotifiyKey @"kHomeCountDownTimerNotifiyKey"
#define kHomeProductChangeNotifiyKey @"kHomeProductChnageNotifiyKey"
@interface LHHomeViewController ()
@property (nonatomic,strong) UITableView * tableView;
@property (nonatomic,strong) ProductService * service;
@property (nonatomic,strong) LHCountDownManager * countDownManager;
@property (nonatomic,strong) NSMutableArray * pollingListM;
@end
@implementation LHHomeViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self.view addSubview:self.tableView];
self.view.backgroundColor = [UIColor whiteColor];
[self _loadFirstPage];
[self _addNotifications];
}
- (void)_loadFirstPage
{
__weak typeof(self) weakSelf = self;
[self.service loadFirstPageProductListWithBlock:^(BOOL success, NSString *msg, BOOL hasMore) {
if (success) {
[weakSelf.pollingListM removeAllObjects];
// 有重新加载了数据就要将轮询数组里的数据全清空一遍
[weakSelf.tableView reloadData];
}
}];
}
- (void)_addNotifications
{
// 添加定时器变化
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(countDownChange:)
name:kHomeCountDownTimerNotifiyKey
object:nil];
}
#pragma mark - ******** 轮询所有保存在轮询数组里的内容
- (void)countDownChange:(NSNotification *)notify
{
// 2秒轮询一次
if (self.countDownManager.timeInterval % 2 == 0) {
NSString *ids = [[self.pollingListM valueForKeyPath:@"goodsId"] componentsJoinedByString:@","];
NSLog(@"变量的数组:%@",ids);
// 对所有轮询数组里的数据进行轮询请求
for (LHProductModel *model in self.pollingListM) {
// 这个方法只改变model的值,不重新创建model
[self.service pollingModelWithModel:model block:^(BOOL success, id msg) {
// 如果请求成功,发送通知修改显示内容
if (success && [msg isKindOfClass:[LHProductModel class]]) {
[[NSNotificationCenter defaultCenter] postNotificationName:kHomeProductChangeNotifiyKey object:nil userInfo:@{@"goodsId":((LHProductModel *)msg).goodsId?:@""}];
}
}];
}
}
}
#pragma mark - ******** 进入页面或离开页面开启关闭定时器
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.countDownManager start];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self.countDownManager invalidate];
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#pragma mark - ******** UITableViewDataSource && UITabelViewDelegate
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSInteger count = self.service.dataList.count;
return count;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return 130;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
LHProductModel *model = self.service.dataList[indexPath.row];
LHProductTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"LHProductTableViewCell"];
// 将重用Cell之前保存的model从轮询数组里去除
if (cell.model != nil) {
[self.pollingListM removeObject:cell.model];
}
// 将接近要显示的Cell保存的model放到轮询数组里用于轮询
if (model != nil) {
[self.pollingListM addObject:model];
}
// 设置让Cell可以处理倒计时的通知
cell.countDownNotifyName = kHomeCountDownTimerNotifiyKey;
cell.priceChangeNotifyName = kHomeProductChangeNotifiyKey;
// 将数据赋值给Cell显示
cell.model = model;
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[tableView deselectRowAtIndexPath:indexPath animated:YES];
});
LHProductModel *model = self.service.dataList[indexPath.row];
LHGoodsDetailController *vc = [LHGoodsDetailController new];
vc.model = model;
vc.hidesBottomBarWhenPushed = YES;
[self.navigationController pushViewController:vc animated:YES];
}
#pragma mark - ******** getter && setter
- (UITableView *)tableView
{
if (_tableView == nil) {
_tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
_tableView.dataSource = self;
_tableView.delegate = self;
[_tableView registerNib:[UINib nibWithNibName:@"LHProductTableViewCell" bundle:nil] forCellReuseIdentifier:@"LHProductTableViewCell"];
}
return _tableView;
}
- (ProductService *)service
{
if (_service == nil) {
_service = [ProductService new];
}
return _service;
}
- (LHCountDownManager *)countDownManager
{
if (_countDownManager == nil) {
_countDownManager = [LHCountDownManager managerWithCountDownKey:kHomeCountDownTimerNotifiyKey];
}
return _countDownManager;
}
- (NSMutableArray *)pollingListM
{
if (_pollingListM == nil) {
_pollingListM = [NSMutableArray array];
}
return _pollingListM;
}
@end
具体项目demo