Cell上的倒计时显示

需求是这样的,对UICollectionView中的前两个Cell 加入一个倒计时器的显示,如下图:

Dayly Deal 的倒计时

此时,简单分析下这个需求,第一就是倒计时的实现,第二就是加入到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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容