这次主要分享一下自己在做验证码倒计时的时候的实现思路,不想看文字的直接到结尾可以下载项目代码。
下面就要开始我的表演了。
先放一张UI图:
说一下业务逻辑吧:
点击"获取验证码"按钮, 请求后台接口,成功后按钮变暗,并且开始倒计时。
需要注意的点:(我能想到的两个点)
1. 在倒计时的时候,返回上一个页面,再次进入这个页面的时候,倒计时仍在继续,并且是能够衔接上的(这里的衔接指的是,已经去掉了退出返回这一操作的时间,在这个基础上继续倒计时)
2. App进入后台,用户看了收到的验证码,再次返回App进行验证码的填写,这个倒计时也是能够衔接上的(同样是在去掉这一系列操作的时间,在这个基础上继续倒计时)
倒计时的实现方法
通常倒计时,会想到NSTimer,使用定时器是最简单的实现倒计时的方法(我并没有使用NSTimer,因为发现存在一些问题。没有直接讲解最终的实现方法是想分享一下我的思考过程)
通常的写法如下:在控制器中定义一个NSTimer,然后在按钮的点击事件中创建定时器
// 点击"获取验证码"按钮
- (void)buttonCLickAction {
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(countDown) userInfo:nil repeats:YES];
self.timer = timer;
}
//倒计时方法
- (void)countDown {
int leftTime = 30
if (leftTime > 0) {
NSLog(@"%@", [NSString stringWithFormat:@"单例倒计时:剩余 %ds", _leftTime]);
leftTime--;
}else {
[self.timer invalidate];
self.timer = nil;
}
}
这样写存在一些问题:
- 退出控制器的时候,如果不销毁定时器,会导致循环引用。如果销毁定时器,再进来的时候时间是衔接不上的
- 定时器创建的时候是加入默认的运行循环,App进入后台之后,定时器就停止了,再次进入App,时间也是衔接不上的
下面就以上问题进行一个一个的解决:
问题1:
需要在控制器被销毁的时候,计时器仍然能够继续运行,那么控制器就不能引用NSTimer了,我们可以换一个对象来引用NSTimer。
在返回上一个控制器,当前控制器被销毁的情况下,定时器依然需要运行,那么定时器的拥有者是在这一过程中一直存在的,最容易想到的就是使用单例了,让单例来成为NSTimer的拥有者,紧接着就是要考虑,如何把单例中的时间传递给控制器——通过代理,让控制器成为单例的代理,而且代理都是使用weak修饰的,不会导致运行循环,内存泄露。
先贴一部分代码:
#import <Foundation/Foundation.h>
@protocol XCTimerManagerDelegate<NSObject>
/* 返回剩余时间 */
- (void)timerManagerCountDown:(int)timeout;
@end
@interface XCTimerManager : NSObject
/* 代理 */
@property (nonatomic, weak) id<XCTimerManagerDelegate> delegate;
/* 倒计时剩余的时间 */
@property (nonatomic, assign) int leftTime;
/* 单例 */
+ (instancetype)sharedTimerManager;
/* 使用NSTimer测试 (不推荐使用NSTimer) */
- (void)countDownUseNSTimer;
@end
#import "XCTimerManager.h"
#define kMaxCountDownTime 30
@implementation XCTimerManager
{
NSTimer *_countdownTimer;
}
/* 单例 */
+ (instancetype)sharedTimerManager {
static XCTimerManager *_instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [[XCTimerManager alloc] init];
});
return _instance;
}
/**** 以下利用NSTimer实现倒计时 (不推荐使用NSTimer, 程序进入后台之后,倒计时就停止了,程序回到前台时,时间是接着停止的时候继续) ********/
- (void)countDownUseNSTimer {
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(countDown) userInfo:nil repeats:YES];
_countdownTimer = timer;
_leftTime = kMaxCountDownTime;
}
- (void)countDown {
if ([self.delegate respondsToSelector:@selector(timerManagerCountDown:)]) {
[self.delegate timerManagerCountDown:_leftTime];
}
if (_leftTime > 0) {
NSLog(@"%@", [NSString stringWithFormat:@"单例倒计时:剩余 %ds", _leftTime]);
_leftTime--;
}else {
[_countdownTimer invalidate];
_countdownTimer = nil;
}
}
/****************************************** 以上利用NSTimer实现倒计时 ****************************************************/
@end
// 部分控制器中的代码,方便理解
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor colorWithRed:242/255.0 green:242/255.0 blue:242/255.0 alpha:1.0];
self.navigationItem.title = @"忘记密码";
// 设置单例的代理
[XCTimerManager sharedTimerManager].delegate = self;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
/*
之所以在 viewWillAppear 中添加判断
因为如果不添加这个判断,剩余秒数通过代理传递给控制器之后,控制器才能控制按钮的状态
"获取验证码"按钮 会有一个 由"获取验证码" -> "重新发送" 的变化过程
*/
XCTimerManager *manager = [XCTimerManager sharedTimerManager];
if (manager.leftTime > 0) {
self.codeButton.enabled = NO;
self.codeButton.backgroundColor = [UIColor lightGrayColor];
[self.codeButton setTitle:[NSString stringWithFormat:@"重新发送(%d]s)", manager.leftTime] forState:UIControlStateNormal];
}
}
/* 点击获取验证码 */
- (void)clickCodeButton {
/* 这里模拟一下请求后台的过程 */
[MBProgressHUD showHUDAddedTo:self.view animated:YES];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[NSThread sleepForTimeInterval:1.0];
dispatch_async(dispatch_get_main_queue(), ^{
[MBProgressHUD hideHUDForView:self.view animated:YES];
self.codeButton.enabled = NO;
self.codeButton.backgroundColor = [UIColor lightGrayColor];
[[XCTimerManager sharedTimerManager] countDownUseNSTimer];
});
});
}
#pragma mark - XCTimerManagerDelegate
- (void)timerManagerCountDown:(int)timeout {
dispatch_async(dispatch_get_main_queue(), ^{
if (timeout > 0) { // 倒计时未结束
[self.codeButton setTitle:[NSString stringWithFormat:@"重新发送(%ds)", timeout] forState:UIControlStateNormal];
}else { // 倒计时结束
[self.codeButton setTitle:@"获取验证码" forState:UIControlStateNormal];
self.codeButton.backgroundColor = [UIColor orangeColor];
self.codeButton.enabled = YES;
}
});
}
运行的效果gif:
gif中显示,已经解决了:
1、返回上一层控制器的时候,当前控制器是销毁了的
2、再次进入控制器的时候,时间的衔接上的
但是也暴露了问题:
1、点击"获取验证码"之后,按钮显示变灰,然后才会显示倒计时
2、App进入后台之后,在进入前台时间没有衔接上
针对第一个问题,我替换了单例中代理执行代码的位置,并没有解决问题,至于第二个问题,我搜索了一下网上,可以设置后台运行模式为音频,然后在Appdelegate中码代码,但是发现有人因为这个问题被拒了,所以后面就不在讨论使用NSTimer来实现了,详细的请见👇链接。
罗里吧嗦的讲了这么多,原来这里才是正真实现的方法,浪费了大家这么多时间,主要就是为了分享我的思考过程
其实定时器并不是只有一个NSTimer,我们也可以使用GCD来实现.详细的,可以看一下下面这篇文章,我如果详细解释的话估计也是照抄别人的,所以还是要尊重一下别人的劳动成果。
后面的实现就很简单了,把NSTimer替换掉就好了, 同时多加一个取消的方法用于实际开发中,用户信息都填写完之后,将定时器取消。
/* 倒计时方法 */
- (void)timeCountDown {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
_currentTimer = _timer;
dispatch_source_set_timer(_timer, dispatch_walltime(NULL, 0), 1.0 * NSEC_PER_SEC, 0);
NSTimeInterval seconds = kMaxCountDownTime;
NSDate *endTime = [NSDate dateWithTimeIntervalSinceNow:seconds];
dispatch_source_set_event_handler(_timer, ^{
int interval = [endTime timeIntervalSinceNow];
self->_leftTime = interval;
if (interval > 0) {
NSLog(@"%@", [NSString stringWithFormat:@"单例倒计时:剩余 %ds", interval]);
}else {
dispatch_source_cancel(_timer);
}
if ([self.delegate respondsToSelector:@selector(timerManagerCountDown:)]) {
[self.delegate timerManagerCountDown:interval];
}
});
dispatch_resume(_timer);
}
- (void)cancelTimer {
dispatch_source_cancel(_currentTimer);
_leftTime = 0;
NSLog(@"取消倒计时");
}
最后放一个gif看下效果:
完整代码轻点👇
一位小码农TimeCountDownDemo