我们现在的很多应用中都需要用户注册之后登陆,而其方式,在移动端多是通过手机号注册和登陆,那么获取验证码的操作就必不可少,包括一些有提现功能的应用,需要在提现时验证用户手机号,都是其应用的场景;
今天刚好有个朋友问到了这里,我就看了一下之前写的代码,那段代码又乱又杂,实现方式上也大可优化一下,之前只是为自己项目定制的,正好借这个机会重新梳理一番;
该功能的实现前前后后我改了3遍,走的弯路这里就不写了,直接来分析当前的实现方案:
首先,我们列一下需求:
1)点击“获取验证码”按钮时,实现验证码倒计时120秒,期间按钮不可点击,走完120秒时候,按钮重新回到“获取验证码”按钮可点击的状态;
2)程序运行的情况下,只要120秒没走完,那么每次进入到该界面时,都需要继续进行倒计时;
3)若用户将程序进程杀掉,重新启动应用时,进入该界面,仍需继续进行倒计时;
基本的要求就是这样的,其中比较重要的有这样几点:
1)按钮存在两种状态的切换,一种是倒计时状态(每秒都会变化)不可点击;一种是“获取验证码状态” 可点击;
2)无论什么情况下,只要验证码没走完,进入该界面,就必须接着让他走完,这里涉及到一开始显示的倒计时数的判断和计算;
3)在倒计时的过程中,我们需要每秒回调刷新界面显示;
基于此,代码的实现方案如下:(为了方便 我就直接粘代码了)
新建LLValidCodeManager类,用于实现该功能;
.h文件内容如下:
#define VALIDCODE_MAX_TIMECOUNT 120
typedef enum {
LLGETVALIDCODE_TYPE_REGISTER = 0, //注册
LLGETVALIDCODE_TYPE_FORGETPASSWORD, //忘记密码
LLGETVALIDCODE_TYPE_WITHDRAW, //提现
LLGETVALIDCODE_TYPE_
}LLGETVALIDCODE_TYPE;
@interface LLValidCodeManager : NSObject{
NSTimer * registTimer;//定时器
}
typedef void (^EverySecondBlock)( BOOL isUsing , long long currentTimeCounts);
@property (nonatomic,copy)EverySecondBlock block;
@property (nonatomic , assign) long long currentTimeCounts; //已记录的时间增量(单位:秒)
@property (nonatomic , assign) BOOL isUsing; //是否在使用中标识
@property (nonatomic , strong) NSString * validCodeTypeStr; //获取验证码类型的字符串标示(用于本地存储)
/**
* 初始化方法(必须调用)
*/
-(instancetype)init;
/**
* 定时器启动 则每秒回调
*/
-(void)beginTimerWithValidCodeType:(LLGETVALIDCODE_TYPE)type EverySecondBlock:(EverySecondBlock)block;
/**
* 同步定时状态操作 若定时器需要启动 则启动定时器 每秒回调
*/
-(void)assignTimerUsingStateWithValidCodeType:(LLGETVALIDCODE_TYPE)type EverySecondBlock:(EverySecondBlock)block;
.m文件内容如下:
-(instancetype)init{
self = [super init];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(APPWillBecomeActive)
name:UIApplicationWillEnterForegroundNotification object:nil];
}
return self;
}
-(void)dealloc{
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillEnterForegroundNotification object:nil];
}
-(void)beginTimerWithValidCodeType:(LLGETVALIDCODE_TYPE)type EverySecondBlock:(EverySecondBlock)block{ self.block = block;
[self assignValidCodeTypeStr:type];
[self beginTimer];
}
-(void)assignTimerUsingStateWithValidCodeType:(LLGETVALIDCODE_TYPE)type EverySecondBlock:(EverySecondBlock)block{
self.block = block;
[self assignValidCodeTypeStr:type];
[self assignTimerUsingState];
}
-(void)assignValidCodeTypeStr:(LLGETVALIDCODE_TYPE)type{
if (type == LLGETVALIDCODE_TYPE_REGISTER) {
self.validCodeTypeStr = @"LLGETVALIDCODE_TYPE_REGISTER";
}else if (type == LLGETVALIDCODE_TYPE_FORGETPASSWORD){
self.validCodeTypeStr = @"LLGETVALIDCODE_TYPE_FORGETPASSWORD";
}else if (type == LLGETVALIDCODE_TYPE_WITHDRAW){
self.validCodeTypeStr = @"LLGETVALIDCODE_TYPE_WITHDRAW";
}
}
#pragma mark -启动定时器 和 同步定时器状态
-(void)beginTimer
{
long long currentDate = [[NSDate getCurrentTime] longLongValue]/1000;
//保存开始时间
[[NSUserDefaults standardUserDefaults] setValue:[NSNumber numberWithLongLong:currentDate] forKey:self.validCodeTypeStr];
[[NSUserDefaults standardUserDefaults] synchronize];
[self ll_InitTimer];
}
-(void)assignTimerUsingState{
if (self.isUsing) {
return;
}else{//程序重新启动 或者 没在使用
long long currentDate = [[NSDate getCurrentTime] longLongValue]/1000;
long long beginDate = [[[NSUserDefaults standardUserDefaults] valueForKey:self.validCodeTypeStr] longLongValue];
if ((currentDate - beginDate) >= VALIDCODE_MAX_TIMECOUNT) {//倒计时已结束
return;
}else if ((currentDate - beginDate) >0){//倒计时还未结束
_currentTimeCounts = currentDate - beginDate;
[self ll_InitTimer];
return;
}else{
//等于0的情况 小于0时可能手机时间已被修改
return ;
}
}
}
//定时调用方法的执行内容
- (void)timerAction
{
_currentTimeCounts += 1;
//倒计时完毕
if (_currentTimeCounts >= VALIDCODE_MAX_TIMECOUNT) {//倒计时时间的宏 当前设置为120s
_currentTimeCounts = 0;
[self ll_CancelTimer];
}
self.block(self.isUsing , _currentTimeCounts);
}
#pragma mark -初始化定时器和取消定时器
-(void)ll_InitTimer{
[self ll_CancelTimer];
self.isUsing = YES;
registTimer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
// 将timer加入到当前的run loop中,并将run loop mode设置为UITrackingRunLoopMode或NSRunLoopCommonModes,这样,即使用户触摸了屏幕,也不会导致timer暂停界面刷新.
[[NSRunLoop currentRunLoop] addTimer:registTimer forMode:UITrackingRunLoopMode];
[registTimer fire];
}
-(void)ll_CancelTimer{
self.isUsing = NO;
if (registTimer) {//废除定时器
if ([registTimer isValid]) {
[registTimer invalidate];
}
registTimer = nil;
}
}
#pragma mark -监听进入后台 以及 从后台进入前台的通知
//一进入前台 若当前定时器在使用 则判断是否需要重新启动定时器
-(void)APPWillBecomeActive
{
if (self.isUsing) {
long long currentDate = [[NSDate getCurrentTime] longLongValue]/1000;
long long beginDate = [[[NSUserDefaults standardUserDefaults] valueForKey:self.validCodeTypeStr] longLongValue];
_currentTimeCounts = currentDate - beginDate;
[registTimer fire];
}
}
其中关于当前时间的获取,可用如下方法:
+(NSString *)getCurrentTime
{
//获取当前时间的秒数
NSTimeInterval time = [[NSDate date] timeIntervalSince1970];
long long date = (long long)(time *1000);//精确到毫秒
NSString * timeStr = [NSString stringWithFormat:@"%lld",date];
return timeStr;
}
在使用中需要在控件视图添加时,调用.h中第二个有block的方法,进行当前获取验证码类型对应的按钮状态同步;
在点击“获取验证码”按钮时,调用.h中第一个有block的方法,进行定时器的启动;
相应的block中我们可以进行按钮的两种状态的切换,相应的代码示例如下:
if (isUsing) {
[self.getValidCode setTitle:[NSString stringWithFormat:@"%llds",VALIDCODE_MAX_TIMECOUNT - currentTimeCounts] forState:UIControlStateNormal];
self.getValidCode.userInteractionEnabled = NO;
[self.getValidCode setTitleColor:COLOR_RGB_HEX(0x999999) forState:UIControlStateNormal];
}else{
[self.getValidCode setTitle:@"获取验证码" forState:UIControlStateNormal];
self.getValidCode.userInteractionEnabled = YES;
[self.getValidCode setTitleColor:COLOR_RGB_HEX(0x666666) forState:UIControlStateNormal];
}
代码加完了,接下来解释一下,这部分代码是如何满足上述需求的:
在实现时,我们需要注意一点,不要将按钮的显示控制混合到这个功能实现类中,那么刷新按钮显示,回调就一定要有,我们设置的block就是这个作用,传递的两个参数意义也比较明显,一个标示定时器是不是再走,对应按钮状态的切换,另一个标示定时器已经记录的时间增量,用于界面的显示;
定时器每秒都会执行相应的方法,对应的每秒都要进行回调,这就说明了第一点和第三点;
再说第二点:
我们将每次点击“获取验证码”按钮时的时间记录在本地,在以下几种情况根据当前的时间与本地的时间差 与 倒计时最大值的比较,判断出是否需要新的定时器(如果已有定时器直接启动就行,没有的话,实例化对象,初始化新的定时器进行倒计时):
1)从后台切回前台的情况;
2)退出当前界面 或 进行界面切换的情况;
3)杀掉程序重新启动的情况;
终于说完了,希望我说的还明白。。。
这个功能,相信不少的同仁都有实现过,我的只是实现的一种,我也相信会有更好的方案;如果大家有用到,在调试过程遇到了问题,可以给我留言。
这段代码是我和我的同事一起实现得出来的方案,感谢我的同事,是这篇blog成为可能。