Cell上的倒计时显示

作者: 天空中的球 | 来源:发表于2016-06-27 20:50 被阅读658次

    需求是这样的,对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

    相关文章

      网友评论

      • 小伙eryayayayaya:倒计时结束程序就崩溃了,什么情况??
        天空中的球:@小伙eryayayayaya 刚才在 Demo 中已经更新了一下,我刚才想了下之前实现的想想还是有些问题,建议你是看看整体实现思路,找借鉴点,看看是否对你有启发 :smile:
        小伙eryayayayaya:@天空中的球 那个continue具体加在哪里?
        天空中的球:@小伙eryayayayaya 之前测试代码中 循环里少加了一个 continue;
        你提到的刷新问题,直接刷新那个 Cell 就好啦啊,确定其 IndexPath 就 OK 了的
      • 小伙eryayayayaya:如果倒计时结束后在刷新这个cell怎么办?在哪里写? 我有个需需求是在tableViewCell上有个倒计时(每个cell上都有倒计时,而且时间不一样),倒计时结束后就刷新该行cell,但是有时候刷新,不会显示最新的数据,怎么破
      • 马铃薯蜀黍:怎么只做了两个倒计时 ..
        天空中的球:@马铃薯蜀黍 需求是这样的,但是近期发现那样写有问题,思路是对的,但有细节处理的不好,如有优化欢迎告知,:smile:
      • 什么的黑夜:时间到0了你是怎么进行刷新操作的
        什么的黑夜:您好,可以给我一份代码吗
        什么的黑夜:@天空中的球 多谢,有这样想过,明天试试:smile:
        天空中的球:@什么的黑夜 当时间到0 的时候,额外再给一个事件,刷新其当个 IndexPath,已更新。
      • 小强七号:你这个NSTimer在scrollview滑动的时候不会静止?
        天空中的球:不会啊,run loop 那块已经转换啦啊

      本文标题:Cell上的倒计时显示

      本文链接:https://www.haomeiwen.com/subject/wpkndttx.html