验证码倒计时 不随页面释放而停止

今天我们的Android说你做的验证码发送是倒计时60S,那界面回退上级页面后还在倒计时吗。我说没有,他说那这样等于倒计时作用不大。其实并不是作用不大,而是正常的合理逻辑就是应该界面消失,倒计时还在继续,当用户在倒计时未走完时再次进入页面,还在继续倒计时。

我一开始做的最简单的,就是倒计时随着视图的消失而被释放从而停止。下面就是最简单的
声明@property (nonatomic,strong) dispatch_source_t timer;

-(void)timeStart
{
    __weak __typeof(self) weakSelf = self;
    __block int timeout = 60;
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    dispatch_source_set_timer(_timer, dispatch_walltime(NULL, 0), 1.0*NSEC_PER_SEC, 0);
    dispatch_source_set_event_handler(_timer, ^{
        if (timeout <= 0)
        {
            dispatch_source_cancel(_timer);
            dispatch_async(dispatch_get_main_queue(), ^{
                weakSelf.getCodeBtn.userInteractionEnabled = YES;
                weakSelf.getCodeBtn.titleLabel.font = [UIFont systemFontOfSize:12];
                [weakSelf.getCodeBtn setTitle:l10n(@"get_codeAgin") forState:UIControlStateNormal];
                [weakSelf.getCodeBtn setTitleColor:kNavcBackGroudColor forState:UIControlStateNormal];
            });
        }
        else
        {
            dispatch_async(dispatch_get_main_queue(), ^{
                [weakSelf.getCodeBtn setTitle:[NSString stringWithFormat:@"%ds",timeout] forState:UIControlStateNormal];
            });
            timeout--;
        }
    });
    dispatch_resume(_timer);
}

但是上面这种并满足不了。
主界面上有一个注册按钮,你点击按钮 push 到下一级页面,这个页面让你输入手机号并有一个获取验证码的按钮。你填完号码,再点击“获取验证码”按钮,然后按钮上的文字开始了 60 秒的倒计时。20 秒之后你 pop 回上一级页面,那么现在的页面应该被销毁了,10 秒后再次 push 到这个注册页面,那么倒计时按钮上的文字应该是【获取验证码】还是 【30 秒后重试】?

当然有人会说,何必如此麻烦,直接将这个页面或者这个按钮写成单例不就得了?是的,单例可以轻松解决这个问题,但是这种设计模式切不可滥用,假如你的 App 有20 个页面需要获取验证码按钮,那岂不是得生成 20 个单例的 View controller ?要知道,你并不是经常需要这些页面。如果把按钮设计成单例,那更不可取,一但你修改了一个按钮,其他地方的按钮必受牵连,引发不可估计的后果。

话不多说呈上代码
首先需要思考,这个计时器管理类应该是是什么样子?它的具体功能又是什么?我给它命名为 WLButtonCountdownManager ,它是一个全局类,可用单例设计(1 个单例类比 20 个单例页面划算得多)。它负责分配计时器并将其与按钮绑定,所以它需要有一个容器属性来存储计时器,并且还要知道,容器里是否已经有计时器在跑了。
创建个定时器工厂管理类
验证码倒计时 不随页面释放而停止_第1张图片
TimerManager.h

#import "CountDownTask.h"

@interface TimerManager : NSObject
/**
 *  获取单例
 *
 *  @return 该类的唯一实例
 */
+ (instancetype)defaultManager;
/**
 *  开始倒计时,如果倒计时管理器里具有相同的key,则直接开始回调。
 *
 *  @param aKey         任务key,用于标示唯一性
 *  @param timeInterval 倒计时总时间,受操作系统后台时间限制,倒计时时间规定不得大于 120 秒.
 *  @param countingDown 倒计时时,会多次回调,提供当前秒数
 *  @param finished     倒计时结束时调用,提供当前秒数,值恒为 0
 *  @param number       记录进入页面是否点击点击了按钮

 */
- (void)scheduledCountDownWithKey:(NSString *)aKey
                     timeInterval:(NSTimeInterval)timeInterval
                     countingDown:(nullable void (^)(NSTimeInterval leftTimeInterval))countingDown
                         finished:(nullable void (^)(__unused NSTimeInterval finalTimeInterval))finished
                           number:(NSInteger)number;

/**
 *  查询倒计时任务是否存在
 *
 *  @param akey 任务key
 *  @param task 任务
 *  @return YES - 存在, NO - 不存在
 */
- (BOOL)coundownTaskExistWithKey:(NSString *)akey task:(NSOperation * _Nullable * _Nullable)task;
@end

拥有一个线程池(也叫并发操作队列,规定队列中最多只允许存在 20 个并发线程),每分配一个计时器(即创建一个子线程)就将其放入池子中,计时器跑完以后会自动从池子里销毁。

在创建计时任务之前,Manager 从池子里检索是否有相同 key 的计时任务,如果任务存在,直接回调计时操作。否则,新建一个标识为 key 的任务。
TimerManager.m


@interface TimerManager()
@property (nonatomic, strong) NSOperationQueue *pool;
@end
@implementation TimerManager

+(instancetype)defaultManager{

    static TimerManager *manager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        manager = [[TimerManager alloc]init];
    });
    return manager;
}

- (void)scheduledCountDownWithKey:(NSString *)aKey
                     timeInterval:(NSTimeInterval)timeInterval
                     countingDown:(void (^)(NSTimeInterval))countingDown
                         finished:(void (^)(NSTimeInterval))finished
                           number:(NSInteger)number  
{
    if (timeInterval > 120) {
        NSCAssert(NO, @"受操作系统后台时间限制,倒计时时间规定不得大于 120 秒.");
    }

    if (_pool.operations.count >= 20){// 最多 20 个并发线程
        [SVProgressHUD showErrorWithStatus:@"操作频繁,请稍后再试!"];
        return;
    }

    CountDownTask *task = nil;
    //判断是否存在该定时器
    if ([self coundownTaskExistWithKey:aKey task:&task] == YES) {
        task.countingDownBlcok = countingDown;
        task.finishedBlcok     = finished;
        if (countingDown) {
            countingDown(task.leftTimeInterval);
        }
    } else if(([self coundownTaskExistWithKey:aKey task:&task] == NO) && (number != 0)){//不存在并且是首次进页面不倒计时。点击btn再进行倒计时
        task                   = [[CountDownTask alloc] init];
        task.name              = aKey;
        task.leftTimeInterval  = timeInterval;
        task.countingDownBlcok = countingDown;
        task.finishedBlcok     = finished;
        _pool = [[NSOperationQueue alloc] init];
        [_pool addOperation:task];
    }
}


- (BOOL)coundownTaskExistWithKey:(NSString *)akey
                            task:(NSOperation *__autoreleasing  _Nullable *)task
{
    __block BOOL taskExist = NO;
    [_pool.operations enumerateObjectsUsingBlock:^(__kindof NSOperation * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([obj.name isEqualToString:akey]) {
            if (task) *task = obj;
            taskExist = YES;
            *stop     = YES;
        }
    }];
    return taskExist;
}
@end

CountDownTask.h


@interface CountDownTask : NSOperation
/**
 *  计时中回调
 */
@property (copy, nonatomic) void (^countingDownBlcok)(NSTimeInterval timeInterval);
/**
 *  计时结束后回调
 */
@property (copy, nonatomic) void (^finishedBlcok)(NSTimeInterval timeInterval);
/**
 *  计时剩余时间
 */
@property (assign, nonatomic) NSTimeInterval leftTimeInterval;
/**
 *  后台任务标识,确保程序进入后台依然能够计时
 */
@property (assign, nonatomic) UIBackgroundTaskIdentifier taskIdentifier;
@end

CountDownTask.m

@implementation CountDownTask



- (void)main {
    self.taskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:nil];

    while (--_leftTimeInterval > 0) {
        dispatch_async(dispatch_get_main_queue(), ^{
            if (_countingDownBlcok) _countingDownBlcok(_leftTimeInterval);
        });

        [NSThread sleepForTimeInterval:1];
    }

    dispatch_async(dispatch_get_main_queue(), ^{
        if (_finishedBlcok) {
            _finishedBlcok(0);
        }
    });

    if (self.taskIdentifier != UIBackgroundTaskInvalid) {
        [[UIApplication sharedApplication] endBackgroundTask:self.taskIdentifier];
        self.taskIdentifier = UIBackgroundTaskInvalid;
    }
}
@end

接下来就是调用了
写一个类方法,全局通用。

//定时器
+ (void)startTimer:(UIButton *)button key:(NSString *)aKey number:(NSInteger)number{
    [[TimerManager defaultManager] scheduledCountDownWithKey:aKey timeInterval:60 countingDown:^(NSTimeInterval leftTimeInterval) {
        [button setTitle:[NSString stringWithFormat:@"%.fs",leftTimeInterval] forState:UIControlStateNormal];
    } finished:^(NSTimeInterval finalTimeInterval) {
        button.userInteractionEnabled = YES;
        button.titleLabel.font = [UIFont systemFontOfSize:12];
        [button setTitle:@"获取验证码" forState:UIControlStateNormal];
        [button setTitleColor:typeFaceSC3377FF forState:UIControlStateNormal];
    } number:number];
}

再btn创建的时候调用 number传入0
再btn点击事件调用 number传入非零数值均可。

才疏学浅,希望可以帮助到大家,有不足的地方多多指教。

你可能感兴趣的:(ios)