今天我们的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 个单例页面划算得多)。它负责分配计时器并将其与按钮绑定,所以它需要有一个容器属性来存储计时器,并且还要知道,容器里是否已经有计时器在跑了。
创建个定时器工厂管理类
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传入非零数值均可。
才疏学浅,希望可以帮助到大家,有不足的地方多多指教。