关于首页倒计时处理一些细节
关于首页倒计时处理一些细节
下面是效果图
这个模块是展示促销商品模块的:
需求有下面的几点:
- 上面是频道栏目 可以左右滑动进行切换
- 下面是促销商品的列表
- 商品栏目数目为2 为了后期兼容做成可以左右滚动
- 当两个其中只有一个已经停止就把停止的商品显示 DEAL ENDED
- 当两个都已经停止就去除对应的栏目
本来想把定时器做到 Cell 里面或者上面显示时间的控件里面 开始做的时候没发现什么问题。后来切换了频道,发现其他频道已经两个结束了竟然没有删除对应的栏目
经过思考,导致这个问题出现的原因是
促销商品展示的 Cell 是重用的,开始的时候其他栏目是没有赋值的。导致是不能收到已经停止的消息的,自然也就没办法从列表里面进行移除
解决的方案就是对数据源进行各自的监听,当数据源显示时间已经停止的时候,就移除对应的数据源,重新刷新界面。
新建一个基类用于管理倒计时GBSaleTimeModel
因为倒计时只分为还没有开始 正在进行 已经结束三种时间段,我们新建一个 ENUM 类型来标识
/**
销售的三种状态
- GBSaleTimeTypeNoStart: 还没有开始
- GBSaleTimeTypeStarting: 开始销售中
- GBSaleTimeTypeEnded: 已经结束
*/
typedef NS_ENUM(NSUInteger, GBSaleTimeType) {
GBSaleTimeTypeNoStart,
GBSaleTimeTypeStarting,
GBSaleTimeTypeEnded
};
新建一个 typedef用户 Block 进行回调
/**
当定时器值改变的时候就回调
@param model GBSaleTimeModel
*/
typedef void(^GBSaleTimeValueChangeCompletionHandle)(GBSaleTimeModel *model);
让外部可以知道当前的数据源处于什么状态,我们新建一个只读的变量用于让外接知道当前数据源处于什么状态。
/**
当前销售状态
*/
@property (nonatomic, assign, readonly) GBSaleTimeType saleTimeType;
我们是只读的属性 我们实现一个 Get 方法
- (GBSaleTimeType)saleTimeType {
NSTimeInterval nowTimeInterval = [[NSDate date] timeIntervalSince1970];
if (nowTimeInterval < self.startTimeInterval) {
return GBSaleTimeTypeNoStart;
} else if (nowTimeInterval < self.endTimeInterval) {
return GBSaleTimeTypeStarting;
} else {
return GBSaleTimeTypeEnded;
}
}
因为是获取距离现在时间的状态,自然要获取的是现在时间距离1970的时间戳,比较时间戳。
如果现在时间戳小于开始的时间戳 标识还没有开始销售
如果现在的时间戳大于等于开始时间戳并且小于结束的时间戳 标识正在销售
如果现在的时间戳大于等于结束的时间戳 标识已经结束销售
因为开始的时间和结束的时间是后段给的 我们无法知道所有新建两个属性 只读让子类实现 get 方法
/**
开始时间 子类重写 get 方法
*/
@property (nonatomic, assign, readonly) NSTimeInterval startTimeInterval;
/**
结束时间 子类重写 get 方法
*/
@property (nonatomic, assign, readonly) NSTimeInterval endTimeInterval;
新建一个只读的属性 让外部可以知道现在时间距离开始 或者 结束 时间段 方便做倒计时
/**
时间段 如果还没有开始就是现在时间和开始时间的间距 如果是已经开始就等于现在时间和结束时间的间距 如果是已经结束就等于0
*/
@property (nonatomic, assign, readonly) NSTimeInterval nowTimePeriod;
- (NSTimeInterval)nowTimePeriod {
NSTimeInterval nowTimeInterval = [[NSDate date] timeIntervalSince1970];
if (self.saleTimeType == GBSaleTimeTypeNoStart) {
return self.startTimeInterval - nowTimeInterval;
} else if (self.saleTimeType == GBSaleTimeTypeStarting) {
return self.endTimeInterval - nowTimeInterval;
} else {
return 0;
}
}
我们判断目前的状态 如果是还没有开始就 获取距离开始的时间
如果是正在销售 就获取距离结束的时间
如果是已经销售结束 就赋值等于0
我们新增一个注册监听的方法 让外界监听销售状态和改变倒计时状态
/**
注册监听时间改变的回调
@param completionHandle 回调
*/
- (void)registerSaleTimeValueChangedCompletionHandle:(GBSaleTimeValueChangeCompletionHandle)completionHandle;
- (void)registerSaleTimeValueChangedCompletionHandle:(GBSaleTimeValueChangeCompletionHandle)completionHandle {
if (!_completionHandleList) {
_completionHandleList = [NSMutableArray array];
}
if (completionHandle) {
[_completionHandleList addObject:completionHandle];
}
[self valueChnaged];
if (!_saleTimer && self.endTimeInterval > self.startTimeInterval && self.startTimeInterval > 0) {
_saleTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(valueChnaged) userInfo:nil repeats:YES];
}
}
为什么要把 Block 添加到数组里面 ?
因为要做一个功能 就是让多个人进行监听同一个对象的回调 这个也直接导致下面的一个问题的出现
为什么要判断 Block 存在再添加呢?
因为如果外部调用方法不实现 block 就会直接的崩溃
为什么要在定时器之前还调用一下valueChnaged值改变的方法呢?
因为可能用户注册的时候 倒计时已经停止 或者 不满足定时器开启的条件 外接就无法得到对应的状态 会出现一些问题无法修复
- (void)valueChnaged {
for (int i = 0; i < _completionHandleList.count; i ++) {
GBSaleTimeValueChangeCompletionHandle completionHandle = _completionHandleList[i];
completionHandle(self);
}
if (self.saleTimeType == GBSaleTimeTypeEnded) {
[_saleTimer invalidate];
[_completionHandleList removeAllObjects];
}
}
因为要监听还没有显示的模块信息 所以我们在给整个模块赋值的时候 进行遍历销售的商品进行监听倒计时
如果其中的一组已经全部停止 就删除对应的频道
- (void)checkSaleItem:(BOOL)needRegister {
NSMutableArray *nowSaleModels = [NSMutableArray array];
@weakify(self);
[_headerModel.homeSaleModels enumerateObjectsUsingBlock:^(GBHomeSaleModel * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
@strongify(self);
NSMutableArray *hotSaleGoodList = [NSMutableArray array];
[obj.hotSaleGoodList enumerateObjectsUsingBlock:^(GBHomeSaleItemModel * _Nonnull obj1, NSUInteger idx1, BOOL * _Nonnull stop) {
@strongify(self);
if (obj1.saleTimeType != GBSaleTimeTypeEnded) {
// 如果还没有结束才显示
[hotSaleGoodList addObject:obj1];
if (needRegister) {
[obj1 registerSaleTimeValueChangedCompletionHandle:^(GBSaleTimeModel *model) {
@strongify(self);
if (model.saleTimeType == GBSaleTimeTypeEnded) {
[self checkSaleItem:NO];
[self registerTableViewGroup];
[self.tableView reloadData];
}
}];
}
}
}];
if (hotSaleGoodList.count > 0) {
[nowSaleModels addObject:obj];
}
}];
_headerModel.homeSaleModels = nowSaleModels;
}
为什么要在方法添加一个是否需要注册的参数呢?
因为 我们在倒计时结束的时候 重新走了一次本方法 进行数据的筛选。 如果我们每次都注册 导致如果结束的时候回调就会死循环 如果在添加之前判断时候结束 也是可以的
为什么要做销售商品数组大于零 就添加对应的频道?
因为之前做的是 如果商品已经销售停止 就删除对应的元素 让界面只显示正在销售的
后来产品说只显示一个元素界面会不好看 就改成了如果两个都销售停止才删除对应的频道 如果只有一个就让已经销售停止的展示 DEAL ENDED
我们在销售商品的试图 Cell 上面赋值数据源的地方进行监听倒计时
[itemModel registerSaleTimeValueChangedCompletionHandle:^(GBSaleTimeModel *model) {
@strongify(self);
if (self.itemModel != model) {
return ;
}
self.saleTimeLabel.timeInterval = model.nowTimePeriod;
NSString *startLabelText;
if (model.saleTimeType == GBSaleTimeTypeNoStart) {
startLabelText = @"Starts in";
} else {
startLabelText = @"Ended in";
}
BOOL isHideDealEnded = model.saleTimeType == GBSaleTimeTypeEnded;
UIColor *startTimeColor = isHideDealEnded ? GB_COLOR_RGB(153, 153, 153, 1.0) : GB_COLOR_RGB(51, 51, 51, 1.0);
self.startTimeLabel.textColor = startTimeColor;
self.dealEndedView.hidden = !isHideDealEnded;
self.startTimeLabel.text = startLabelText;
}];
这个方法存在 一个严重的问题就是 当切换频道 就会cell 重用 新的数据源就会重新的注册 block
但是之前注册的 block 还是存在在内存里面就会出现显示的倒计时的时间不正确
if (self.itemModel != model) {
return ;
}
这一句代码就是为了判断如果回调的 block 如果不是现在显示的数据源回调就不会往下继续走
其实还有一个更好的方法 就是注册的时候保留 block 对象 cell 进行刷新的时候 移除之前注册的
这样不仅解决了数据源显示错误问题 而且可以让之前注册 block 不会回调 减少性能消耗