Cell上的倒计时显示
Dayly Deal 的倒计时需求是这样的,对UICollectionView中的前两个Cell 加入一个倒计时器的显示,如下图:
此时,简单分析下这个需求,第一就是倒计时的实现,第二就是加入到UICollectionView 中。想想其实也蛮简单的,但是要注意细节的地方不少哦
- 1、倒计时的实现。
- 2、倒计时在 UICollectionCell 上完好的展现以及控制好它。
一、倒计时的实现
首先计时器这块,我第一个会想到是用NSTimer定时器,还是用GCD定时器,或者说CADisplayLink定时呢。经过粗略的比较,GCD定时器可能更好,但此处还是选择 NSTimer 。
* NSTimer是必须要在run loop已经启用的情况下使用的,否则无效。
而只有主线程是默认启动run loop的。
我们不能保证自己写的方法不会被人在异步的情况下调用到,所以有时使用NSTimer不是很保险的。
同时 NSTime 的坑比较多,循环应用和 RunLoop 那块的坑都可以开专题啦,但话又说回来可以好好深入下这部分。
* 而CADisplayLink相对来说比较适合做界面的不停重绘。
* NStimer是在RunLoop的基础上执行的,然而RunLoop是在GCD基础上实现的,所以说GCD可算是更加高级。
同时,顺便简单了解下GCD 定时器的实现
//设置间隔还是2秒
uint64_t interval = 2 * NSEC_PER_SEC;
//设置一个专门执行timer回调的GCD队列
dispatch_queue_t queue = dispatch_queue_create("my queue", 0);
//设置Timer
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//使用dispatch_source_set_timer函数设置timer参数
dispatch_source_set_timer(_timer, dispatch_time(DISPATCH_TIME_NOW, 0), interval, 0);
//设置回调
dispatch_source_set_event_handler(_timer, ^() {
NSLog(@"Timer %@", [NSThread currentThread]);
});
//dispatch_source默认是Suspended状态,通过dispatch_resume函数开始它
dispatch_resume(_timer);
二、倒计时在 UICollectionCell 上完好的展现。
初次问题整合:
1、第一次进入的时候,会出现延时的问题,先空白1秒的样子。
2、UICollectionView 向下滑动后再返回的时候,计时器的 view 会出现短暂的空白时间。
3、由于复用,Cell 被回收杀死后,会重新倒计时。
4、进入后台后,是否会继续生效。
分析并尝试解决:
- 1、就是在呈现之前如何先获取到那个时间,以及在view呈现的同时,如何将倒计时的时间显示出来;
- 2、时间上就是cell 被回收后,那个CountBackView重新清空了。此处相当于会重新显示其创建时默认的值,因为之前默认是不填的,后来改为@“00”,后面就一直显示00啦。同时还有一个问题,有时在短暂的0.5秒之间,往后滑动后面返回后依然会出现
- 3、实际上就是需要将 倒计时事件单独抽离出来,之前是和cell 放在一起,cell 杀死了,它就自然也就没了,所以可以写一个CountDownManager 类专门管理倒计时,然后和其一起用。
- 4,是没有什么问题的,因为我们要考虑到一点,一般我们的这个时间点是从后台获取的,当真正杀死 app 后,又会自动从服务器那边获取,而平常的跳转进入 app 是 OK 的。
而且注意我们是用一个定时器去管理,而不是说有多少个cell 需要显示就创建多少个Cell。
先展示一张大致的图片:
大致效果图下面我通过代码来说明问题:
#import <UIKit/UIKit.h>
#pragma mark 倒计时Cell
@class CountDownShowModel;
@interface CountDownCollectionViewCell : UICollectionViewCell
@property (nonatomic, assign) BOOL isHaveCountDownTime; // 是否拥有那个倒计时
@property (nonatomic, strong) CountDownShowModel *countDownModel; // 时、分、秒的model
@end
#pragma mark 倒计时 小时,分钟,秒 Model
@interface CountDownShowModel : NSObject
@property (nonatomic, copy) NSString *hour;
@property (nonatomic, copy) NSString *minute;
@property (nonatomic, copy) NSString *second;
@end
#pragma mark 传值的 Model(indexPath\time)
@interface CountDownSendValueModel : NSObject
@property (nonatomic, strong) NSIndexPath *indexPath;
@property (nonatomic, assign) NSInteger lastTime;
@end
#pragma mark 倒计时管理类
typedef void (^GetTheTimeBlock)(NSIndexPath *indexPath);
@interface CountDownManager : NSObject
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, strong) NSMutableArray<CountDownSendValueModel *> *modelArray; // 需要传入的数组
@property (nonatomic, copy) GetTheTimeBlock getTheTimeBlock;
@property (nonatomic, weak) UICollectionView *collectionView;
- (void)setCountDownBegin;
@end
#import "CountDownCollectionViewCell.h"
#pragma mark CountDownCollectionViewCell @interface
@interface CountDownCollectionViewCell ()
@property (nonatomic, strong) UIView *countDownView;
@property (nonatomic, strong) UILabel *hourLabel;
@property (nonatomic, strong) UILabel *minuteLabel;
@property (nonatomic, strong) UILabel *secondLabel;
@end
#pragma mark CountDownCollectionViewCell @implementation
@implementation CountDownCollectionViewCell
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
_countDownView = [[UIView alloc] init];
_countDownView.hidden = YES;
_countDownView.backgroundColor = [UIColor orangeColor];
[self addSubview:_countDownView];
[_countDownView mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.bottom.trailing.equalTo(@0);
make.height.mas_equalTo(@40);
}];
_hourLabel = [self makeCustomLabel];
[_countDownView addSubview:_hourLabel];
[_hourLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(@5);
make.bottom.mas_equalTo(@(-5));
make.width.mas_equalTo(@30);
}];
_minuteLabel = [self makeCustomLabel];
[_countDownView addSubview:_minuteLabel];
[_minuteLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.bottom.width.equalTo(_hourLabel);
make.centerX.equalTo(_countDownView.mas_centerX);
make.leading.equalTo(_hourLabel.mas_trailing).offset(5);
}];
_secondLabel = [self makeCustomLabel];
[_countDownView addSubview:_secondLabel];
[_secondLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.bottom.width.equalTo(_minuteLabel);
make.leading.equalTo(_minuteLabel.mas_trailing).offset(5);
}];
}
return self;
}
- (void)setIsHaveCountDownTime:(BOOL)isHaveCountDownTime {
self.countDownView.hidden = !isHaveCountDownTime;
}
- (UILabel *)makeCustomLabel {
UILabel *label = [[UILabel alloc] init];
label.backgroundColor = [UIColor whiteColor];
label.textAlignment = NSTextAlignmentCenter;
label.textColor = [UIColor blackColor];
label.text = @"00";
return label;
}
- (void)setCountDownModel:(CountDownShowModel *)countDownModel {
self.hourLabel.text = countDownModel.hour;
self.minuteLabel.text = countDownModel.minute;
self.secondLabel.text = countDownModel.second;
}
@end
#pragma mark CountDownShowModel
@implementation CountDownShowModel
@end
#pragma mark 传值的 CountDownSendValueModel
@implementation CountDownSendValueModel
@end
#pragma mark CountDownManager @implementation
@implementation CountDownManager {
int _overTimeCount; // 去掉的次数
NSUInteger _countOfIndex; // 总的次数
NSMutableArray<CountDownSendValueModel *> *_array;
CountDownCollectionViewCell *_countDownCell;
}
- (void)setModelArray:(NSMutableArray<CountDownSendValueModel *> *)modelArray {
_array = modelArray;
_overTimeCount = 0;
}
- (void)setCountDownBegin {
_countOfIndex = _array.count;
dispatch_async(dispatch_get_main_queue(), ^{
[self refreshTheTime];
_timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(refreshTheTime) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
});
}
- (void)refreshTheTime {
NSInteger timeout;
for (CountDownSendValueModel *model in _array.reverseObjectEnumerator) {
// 获取我们指定的倒计时时间
timeout = model.lastTime;
// NSLog(@"lastTime === %lu",timeout);
_countDownCell = (CountDownCollectionViewCell *)[self.collectionView cellForItemAtIndexPath:model.indexPath];
// 真正开始算时间
NSInteger days = (int)(timeout/(3600*24));
NSInteger hours = (int)((timeout-days*24*3600)/3600);
NSInteger minute = (int)(timeout-days*24*3600-hours*3600)/60;
NSInteger second = timeout-days*24*3600-hours*3600-minute*60;
CountDownShowModel *countDownModel = [[CountDownShowModel alloc] init];
if (hours < 10) {
countDownModel.hour = [NSString stringWithFormat:@"0%ld",hours];
}else{
countDownModel.hour = [NSString stringWithFormat:@"%ld",hours];
}
if (minute < 10) {
countDownModel.minute = [NSString stringWithFormat:@"0%ld",minute];
}else{
countDownModel.minute = [NSString stringWithFormat:@"%ld",minute];
}
if (second < 10) {
countDownModel.second = [NSString stringWithFormat:@"0%ld",second];
}else{
countDownModel.second = [NSString stringWithFormat:@"%ld",second];
}
_countDownCell.countDownModel = countDownModel;
if (timeout == 0) {
countDownModel.hour = @"00";
countDownModel.minute = @"00";
countDownModel.second = @"00";
if (self.getTheTimeBlock) {
self.getTheTimeBlock(model.indexPath);
}
_overTimeCount++;
// 删除这个已经计时结束的Model,并加1
[_array removeObject:model];
}
// 当所有结束的时候,将_time 清空
if (_overTimeCount == _countOfIndex) {
[_timer invalidate];
_timer = nil;
}
timeout--;
model.lastTime = timeout;
}
}
- (void)dealloc {
[_timer invalidate];
_timer = nil;
NSLog(@"CountDownManager Dealloc");
}
@end
VC 中的使用,注意要传入的NSIndexPath和lastTime 的不同和搭配。
@property (nonatomic, strong) NSMutableArray *indexArray;
@property (nonatomic, strong) CountDownManager *countDownManager;
@property (nonatomic, strong) CountDownShowModel *countDownShowModel;
- (void)makeShowCountDownTime {
self.countDownManager.modelArray = [self makeCustomModelArray];
[self.countDownManager setCountDownBegin];
}
- (NSMutableArray *)makeCustomModelArray {
// 假设需要 要更新的数组
NSMutableArray * modelArray = [NSMutableArray array];
[self.indexArray removeAllObjects];
self.indexArray = [NSMutableArray arrayWithArray:@[[NSIndexPath indexPathForRow:0 inSection:0],[NSIndexPath indexPathForRow:1 inSection:0]]];
NSArray *timeArray = @[@"5",@"86200"];
for (int i = 0; i < 2; i++){
CountDownSendValueModel *model = [[CountDownSendValueModel alloc] init];
model.indexPath = self.indexArray[i];
model.lastTime = [timeArray[i] integerValue];
[modelArray addObject:model];
}
return modelArray;
}
- (CountDownManager *)countDownManager {
if (!_countDownManager) {
_countDownManager = [[CountDownManager alloc] init];
__weak typeof(self) weakSelf = self;
_countDownManager.getTheTimeBlock = ^(CountDownShowModel *model, NSIndexPath *indexPath) {
__strong typeof (self) strongSelf = weakSelf;
[strongSelf.indexArray removeObject:indexPath];
[strongSelf.collectionView reloadItemsAtIndexPaths:@[indexPath]];
};
}
return _countDownManager;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
CountDownCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kCollectionViewIden forIndexPath:indexPath];
cell.backgroundColor = [UIColor colorWithRed:(arc4random()%255)/255.0 green:(arc4random()%255)/255.0 blue:(arc4random()%255)/255.0 alpha:1.0];
cell.isHaveCountDownTime = NO;
for (NSIndexPath *tempIndexPath in self.indexArray) {
if (tempIndexPath == indexPath){
cell.isHaveCountDownTime = YES;
}
}
return cell;
}
上面我用了两个Model,和一个Manager ,用model是为了更好的方便传值,用Manager 是为了更好的管理计时器这块。
并注意UITrackingRunLoopMode,NSRunLoopCommonModes,NSDefaultRunLoopMode 三者的区别,
-
UITrackingRunLoopMode
: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响(操作 UI 界面的情况下运行) -
NSRunLoopCommonModes
:这是一个占位用的 Mode,不是一种真正的 Mode (RunLoop无法启动该模式,设置这种模式下,默认和操作 UI 界面时线程都可以运行,但无法改变 RunLoop 同时只能在一种模式下运行的本质) -
NSDefaultRunLoopMode
: App 的默认 Mode,通常主线程是在这个 Mode 下运行(默认情况下运行)
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 在默认模式下添加的 timer 当我们拖拽 scrollerView 的时候,不会运行 run 方法
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 在 UI 跟踪模式下添加 timer 当我们拖拽 scrollerView 的时候,run 方法才会运行
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
// timer 可以运行在两种模式下,相当于上面两句代码写在一起
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
这三个对比参考了这位同学提供的例子:[RunLoop运行循环机制](http://www.jianshu.com/p/0be6be50e461)
#####问题再次整合
1、如何在进入页面的同一时刻立马显示出数值来,**不会有延迟效果**
2、在滑动UICollectionView 的时候,当最上面的View 被回收后,怎样保证显示的时候不会有断层。(就是闪一下默认显示的,再显示我们需要的)
>**延时问题**
经过测试发现,时间刚好UI初始化成功到时间改变是在 1秒左右的
11:03:32.852 TestWork[3844:97056] make label
11:03:33.858 TestWork[3844:97056] realy change countDown
再进一步分析, 真正产生时间间隔的地方
11:05:47.270 TestWork[4023:100079] set countDown
11:05:48.295 TestWork[4023:100079] refreshTheTime
毕竟这个事件是需要1秒之后才会产生的
[NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(refreshTheTime) userInfo:nil repeats:YES];
所以解决这个方法的办法就是在这个方法执行之前,先执行一次这个方法就好啦。
[self refreshTheTime]
_timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(refreshTheTime) userInfo:nil repeats:YES];
> **向下滑动返回时闪一下默认显示的值**
在我们向下滑动之后,立马向上返回的时候会出现闪一下默认显示的,经过测试发现并不是重新创建的Label,它是label的重复使用导致的。
向下滑的时候,那部分的Label 就被放到复用池中去,然后向上返回后,又从复用池出来了,此时lable 上面的text自然是空的。第一反应,我是改动cell 中的 prepareForReuse
- (void)prepareForReuse {
self.hourLabel.text = self.tempCountDownModel.hour;
self.minuteLabel.text = self.tempCountDownModel.minute;
self.secondLabel.text = self.tempCountDownModel.second;
}
然而并没有很好的起到作用,此时换一种说法就是如何让这一块的Label 不被复用,怎么办呢?接着想。。。
** 这个问题是 由于我只针对 专门的cell 用了定时器,然而由于复用导致了其中cell 不断的被干掉,而定时器显示那块假如是干掉了,那个`setCountDownModel:` 方法不会执行,当其出现时在执行,所以就慢了一些也就导致了一闪,所以最后再优化成获取时间后再刷新时间的方式。 **
>** 记住销毁 NSTime **
由于这样设置的情况下,如果不销毁的情况下,它会一直存在,对于很多时候只是很有问题的,所以一定要记得销毁。
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[_countDownManager.timer invalidate];
_countDownManager.timer = nil;
_countDownManager = nil;
} - (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self makeShowCountDownTime];
}
同时注意这个countDownManager类里面写 Deolloc 基本是没法被调用的,所以这个类在有些情况,其实我们**不抽出来,直接写在ViewControlelr中**也是OK的,另外那个时间要保持更新,当然是需要后台随时也返回一个新的时间是最好的。
**如果时间是固定的**,注意销毁的位置在哪里
- (void)dealloc {
[_countDownManager.timer invalidate];
_countDownManager.timer = nil;
_countDownManager = nil;
}
> 总的来说,这是一个对NSTimer很好的了解过程,毕竟NSTimer的坑还是蛮多的。
PS: 后期整理下成为 [Demo](https://github.com/YangPeiqiu/CountDownTime), 欢迎一起探讨。
#####备注参考:
https://github.com/zhengwenming/countDown
http://js.sunansheng.com/p/544e2e24eda2
http://www.jianshu.com/p/0be6be50e461