iOS验证码倒计时实现分享

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

先放一张UI图:

获取验证码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 <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:
NSTimerDemo.gif

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

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

NSTimer后台运行计时

有图有真相(截取于简书某篇文章)

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

其实定时器并不是只有一个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看下效果:


最后的效果

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容