iOS验证码倒计时实现分享

   这次主要分享一下自己在做验证码倒计时的时候的实现思路,不想看文字的直接到结尾可以下载项目代码。
下面就要开始我的表演了。
iOS验证码倒计时实现分享_第1张图片

先放一张UI图:

iOS验证码倒计时实现分享_第2张图片
获取验证码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;
    }
}

这样写存在一些问题:

  1. 退出控制器的时候,如果不销毁定时器,会导致循环引用。如果销毁定时器,再进来的时候时间是衔接不上的
  2. 定时器创建的时候是加入默认的运行循环,App进入后台之后,定时器就停止了,再次进入App,时间也是衔接不上的

下面就以上问题进行一个一个的解决:
问题1:
   需要在控制器被销毁的时候,计时器仍然能够继续运行,那么控制器就不能引用NSTimer了,我们可以换一个对象来引用NSTimer。
   在返回上一个控制器,当前控制器被销毁的情况下,定时器依然需要运行,那么定时器的拥有者是在这一过程中一直存在的,最容易想到的就是使用单例了,让单例来成为NSTimer的拥有者,紧接着就是要考虑,如何把单例中的时间传递给控制器——通过代理,让控制器成为单例的代理,而且代理都是使用weak修饰的,不会导致运行循环,内存泄露。
先贴一部分代码:

#import 


@protocol XCTimerManagerDelegate

/* 返回剩余时间 */
- (void)timerManagerCountDown:(int)timeout;

@end

@interface XCTimerManager : NSObject

/* 代理 */
@property (nonatomic, weak) id 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:
NSTimerDemo.gif

gif中显示,已经解决了:
1、返回上一层控制器的时候,当前控制器是销毁了的
2、再次进入控制器的时候,时间的衔接上的
但是也暴露了问题:
1、点击"获取验证码"之后,按钮显示变灰,然后才会显示倒计时
2、App进入后台之后,在进入前台时间没有衔接上

针对第一个问题,我替换了单例中代理执行代码的位置,并没有解决问题,至于第二个问题,我搜索了一下网上,可以设置后台运行模式为音频,然后在Appdelegate中码代码,但是发现有人因为这个问题被拒了,所以后面就不在讨论使用NSTimer来实现了,详细的请见链接。

NSTimer后台运行计时

iOS验证码倒计时实现分享_第3张图片
有图有真相(截取于某篇文章)

罗里吧嗦的讲了这么多,原来这里才是正真实现的方法,浪费了大家这么多时间,主要就是为了分享我的思考过程

其实定时器并不是只有一个NSTimer,我们也可以使用GCD来实现.详细的,可以看一下下面这篇文章,我如果详细解释的话估计也是照抄别人的,所以还是要尊重一下别人的劳动成果。

Dispatch Source方法实现定时器功能

   后面的实现就很简单了,把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看下效果:


iOS验证码倒计时实现分享_第4张图片
最后的效果

完整代码轻点
一位小码农TimeCountDownDemo

你可能感兴趣的:(iOS验证码倒计时实现分享)