iOS技巧学习_JMiOS开发(OC)

Cell上的倒计时显示

2016-06-27  本文已影响658人  天空中的球

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

Dayly Deal 的倒计时

此时,简单分析下这个需求,第一就是倒计时的实现,第二就是加入到UICollectionView 中。想想其实也蛮简单的,但是要注意细节的地方不少哦

一、倒计时的实现

首先计时器这块,我第一个会想到是用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、进入后台后,是否会继续生效。

分析并尝试解决:

而且注意我们是用一个定时器去管理,而不是说有多少个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 三者的区别,


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
然而并没有很好的起到作用,此时换一种说法就是如何让这一块的Label 不被复用,怎么办呢?接着想。。。

** 这个问题是 由于我只针对 专门的cell 用了定时器,然而由于复用导致了其中cell 不断的被干掉,而定时器显示那块假如是干掉了,那个`setCountDownModel:` 方法不会执行,当其出现时在执行,所以就慢了一些也就导致了一闪,所以最后再优化成获取时间后再刷新时间的方式。 **


>** 记住销毁 NSTime **

由于这样设置的情况下,如果不销毁的情况下,它会一直存在,对于很多时候只是很有问题的,所以一定要记得销毁。

同时注意这个countDownManager类里面写 Deolloc 基本是没法被调用的,所以这个类在有些情况,其实我们**不抽出来,直接写在ViewControlelr中**也是OK的,另外那个时间要保持更新,当然是需要后台随时也返回一个新的时间是最好的。

**如果时间是固定的**,注意销毁的位置在哪里

> 总的来说,这是一个对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
上一篇下一篇

猜你喜欢

热点阅读