需求
App中有很多頁面地方要發(fā)送驗(yàn)證碼,涉及到驗(yàn)證碼的地方肯定會(huì)有倒計(jì)時(shí)功能。產(chǎn)品要求發(fā)送驗(yàn)證碼以后,在倒計(jì)時(shí)結(jié)束之前不重復(fù)發(fā)送驗(yàn)證碼。
第一步
首先實(shí)現(xiàn)倒計(jì)時(shí)功能,以登錄界面為例,用戶輸入手機(jī)號(hào)以后,需要點(diǎn)擊按鈕發(fā)送驗(yàn)證碼,發(fā)送驗(yàn)證碼成功以后,會(huì)調(diào)用下面方法,實(shí)現(xiàn)按鈕倒計(jì)時(shí)功能
- (void)timerCountDownWithType:(BOUCountDownType)countDownType {
_countDonwnType = countDownType;
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);
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];
if (interval <= 0) {
dispatch_source_cancel(_timer);
dispatch_async(dispatch_get_main_queue(), ^{
//倒計(jì)時(shí)結(jié)束,改變按鈕狀態(tài),做相關(guān)操作
[self.meeageButton setTitle:@"重新獲取" forState:UIControlStateNormal];
[self.meeageButton setEnabled:YES];
});
}
else {
dispatch_async(dispatch_get_main_queue(), ^{
//倒計(jì)時(shí)中 interval是倒計(jì)時(shí)描述,可以用此值更新按鈕文字
[self.meeageButton setTitle:@(interval).stringValue forState:UIControlStateNormal];
[self.meeageButton setEnabled:NO];
});
}
});
dispatch_resume(_timer);
}
上面方法實(shí)現(xiàn)了按鈕的倒計(jì)時(shí)功能,但是有個(gè)問題,如果不點(diǎn)擊返回按鈕,離開當(dāng)前頁面的話,那么倒計(jì)時(shí)正常,但是當(dāng)返回上層界面,再次進(jìn)入本頁面后,倒計(jì)時(shí)按鈕會(huì)重置。用戶可以重新發(fā)送驗(yàn)證碼,但是上次的倒計(jì)時(shí)時(shí)間還未到,這與產(chǎn)品需求不符合,所以上面的方案需要調(diào)整。
第二步
經(jīng)過思考以后,決定將倒計(jì)時(shí)功能單獨(dú)封裝到一個(gè)類中,避免頻繁書寫重復(fù)代碼。
1.新建BOUTimerManager類,繼承自NSObject
由于有多個(gè)頁面需要實(shí)現(xiàn)倒計(jì)時(shí)功能,為了區(qū)分倒計(jì)時(shí)所屬頁面,定義以下枚舉類型:
typedef NS_ENUM(NSInteger, BOUCountDownType) {
BOUCountDownTypeLogin,//登錄界面
BOUCountDownTypeFindPassword,//忘記密碼界面
BOUCountDownTypeRegister,//注冊界面
BOUCountDownTypeModifyPhone,//修改手機(jī)號(hào)界面
};
在BOUTimerManager.h文件中定義以下方法:
+ (instancetype)shareInstance;//此方法實(shí)現(xiàn)單例
- (void)timerCountDownWithType:(BOUCountDownType)countDownType;//調(diào)用此方法開始倒計(jì)時(shí),根據(jù)傳入的type值判斷開始哪個(gè)頁面的倒計(jì)時(shí)。
- (void)cancelTimerWithType:(BOUCountDownType)countDownType;//調(diào)用此方法取消倒計(jì)時(shí),根據(jù)傳入的type值判斷取消的是哪個(gè)頁面的倒計(jì)時(shí)。
在倒計(jì)時(shí)過程中,響應(yīng)界面需要根據(jù)是倒計(jì)時(shí)中或者倒計(jì)時(shí)完成處理相關(guān)頁面邏輯,我在這里使用發(fā)送通知的方法,在倒計(jì)時(shí)過程中和倒計(jì)時(shí)完成時(shí)發(fā)送通知,頁面注冊通知以后可以接收到倒計(jì)時(shí)狀態(tài),所以在BOUTimerManager.h還需定義以下內(nèi)容:
#define kLoginCountDownCompletedNotification @"kLoginCountDownCompletedNotification"
#define kFindPasswordCountDownCompletedNotification @"kFindPasswordCountDownCompletedNotification"
#define kRegisterCountDownCompletedNotification @"kRegisterCountDownCompletedNotification"
#define kModifyPhoneCountDownCompletedNotification @"kModifyPhoneCountDownCompletedNotification"
#define kLoginCountDownExecutingNotification @"kLoginCountDownExecutingNotification"
#define kFindPasswordCountDownExecutingNotification @"kFindPasswordCountDownExecutingNotification"
#define kRegisterCountDownExecutingNotification @"kRegisterCountDownExecutingNotification"
#define kModifyPhoneCountDownExecutingNotification @"kModifyPhoneCountDownExecutingNotification"
BOUTimerManager.h全部內(nèi)容如下:
#import <Foundation/Foundation.h>
#define kLoginCountDownCompletedNotification @"kLoginCountDownCompletedNotification"
#define kFindPasswordCountDownCompletedNotification @"kFindPasswordCountDownCompletedNotification"
#define kRegisterCountDownCompletedNotification @"kRegisterCountDownCompletedNotification"
#define kModifyPhoneCountDownCompletedNotification @"kModifyPhoneCountDownCompletedNotification"
#define kLoginCountDownExecutingNotification @"kLoginCountDownExecutingNotification"
#define kFindPasswordCountDownExecutingNotification @"kFindPasswordCountDownExecutingNotification"
#define kRegisterCountDownExecutingNotification @"kRegisterCountDownExecutingNotification"
#define kModifyPhoneCountDownExecutingNotification @"kModifyPhoneCountDownExecutingNotification"
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, BOUCountDownType) {
BOUCountDownTypeLogin,
BOUCountDownTypeFindPassword,
BOUCountDownTypeRegister,
BOUCountDownTypeModifyPhone,
};
@interface BOUTimerManager : NSObject
DEF_SINGLETON(BOUTimerManager);
- (void)timerCountDownWithType:(BOUCountDownType)countDownType;
- (void)cancelTimerWithType:(BOUCountDownType)countDownType;
@end
NS_ASSUME_NONNULL_END
BOUTimerManager.m實(shí)現(xiàn)全部內(nèi)容如下:
#import "BOUTimerManager.h"
#define kMaxCountDownTime 60//倒計(jì)時(shí)時(shí)間,可自定義
@interface BOUTimerManager ()
@property (nonatomic, assign) BOUCountDownType countDonwnType;
@property (nonatomic, nullable, strong) dispatch_source_t loginTimer;//登錄界面倒計(jì)時(shí)timer
@property (nonatomic, nullable, strong) dispatch_source_t findPwdTimer;//找回密碼界面倒計(jì)時(shí)timer
@property (nonatomic, nullable, strong) dispatch_source_t registerTimer;//注冊界面倒計(jì)時(shí)timer
@property (nonatomic, nullable, strong) dispatch_source_t modifyPhoneTimer;//修改手機(jī)號(hào)界面倒計(jì)時(shí)timer
@end
@implementation BOUTimerManager
IMP_SINGLETON(BOUTimerManager);
- (void)timerCountDownWithType:(BOUCountDownType)countDownType {
_countDonwnType = countDownType;
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);
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];
if (interval <= 0) {
dispatch_source_cancel(_timer);
dispatch_async(dispatch_get_main_queue(), ^{
if ([_timer isEqual:self.loginTimer]) {
[[NSNotificationCenter defaultCenter] postNotificationName:kLoginCountDownCompletedNotification object:@(interval)];
} else if ([_timer isEqual:self.findPwdTimer]) {
[[NSNotificationCenter defaultCenter] postNotificationName:kFindPasswordCountDownCompletedNotification object:@(interval)];
} else if ([_timer isEqual:self.registerTimer]) {
[[NSNotificationCenter defaultCenter] postNotificationName:kRegisterCountDownCompletedNotification object:@(interval)];
} else if ([_timer isEqual:self.modifyPhoneTimer]) {
[[NSNotificationCenter defaultCenter] postNotificationName:kModifyPhoneCountDownCompletedNotification object:@(interval)];
}
});
}
else {
dispatch_async(dispatch_get_main_queue(), ^{
if ([_timer isEqual:self.loginTimer]) {
[[NSNotificationCenter defaultCenter] postNotificationName:kLoginCountDownExecutingNotification object:@(interval)];
} else if ([_timer isEqual:self.findPwdTimer]) {
[[NSNotificationCenter defaultCenter] postNotificationName:kFindPasswordCountDownExecutingNotification object:@(interval)];
} else if ([_timer isEqual:self.registerTimer]) {
[[NSNotificationCenter defaultCenter] postNotificationName:kRegisterCountDownExecutingNotification object:@(interval)];
} else if ([_timer isEqual:self.modifyPhoneTimer]) {
[[NSNotificationCenter defaultCenter] postNotificationName:kModifyPhoneCountDownExecutingNotification object:@(interval)];
}
});
}
});
if (self.countDonwnType == BOUCountDownTypeLogin) {
self.loginTimer = _timer;
} else if (self.countDonwnType == BOUCountDownTypeFindPassword) {
self.findPwdTimer = _timer;
} else if (self.countDonwnType == BOUCountDownTypeRegister) {
self.registerTimer = _timer;
} else if (self.countDonwnType == BOUCountDownTypeModifyPhone) {
self.modifyPhoneTimer = _timer;
}
dispatch_resume(_timer);
}
- (void)cancelTimerWithType:(BOUCountDownType)countDownType {
switch (countDownType) {
case BOUCountDownTypeLogin:
if (self.loginTimer) {
dispatch_source_cancel(self.loginTimer);
self.loginTimer = nil;
}
break;
case BOUCountDownTypeRegister:
if (self.registerTimer) {
dispatch_source_cancel(self.registerTimer);
self.registerTimer = nil;
}
break;
case BOUCountDownTypeModifyPhone:
if (self.registerTimer) {
dispatch_source_cancel(self.modifyPhoneTimer);
self.registerTimer = nil;
}
break;
case BOUCountDownTypeFindPassword:
if (self.registerTimer) {
dispatch_source_cancel(self.findPwdTimer);
self.registerTimer = nil;
}
break;
default:
break;
}
}
@end
DEF_SINGLETON是單例聲明的宏定義,IMP_SINGLETON是單例實(shí)現(xiàn)的宏定義
#undef DEF_SINGLETON
#define DEF_SINGLETON( __class ) \
+ (__class *)sharedInstance;
#undef IMP_SINGLETON
#define IMP_SINGLETON( __class ) \
+ (__class *)sharedInstance \
{ \
static dispatch_once_t once; \
static __class * __singleton__; \
dispatch_once( &once, ^{ __singleton__ = [[__class alloc] init]; } ); \
return __singleton__; \
}
2.控制器中處理邏輯,以登錄界面為例
在- (instancetype)init和- (instancetype)initWithCoder:(NSCoder *)coder方法中注冊倒計(jì)時(shí)通知事件
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(loginTimerCountDownCompleted) name:kLoginCountDownCompletedNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(loginTimerCountDownExecutingWithTimeOut:) name:kLoginCountDownExecutingNotification object:nil];
#pragma mark - NSNotification 處理倒計(jì)時(shí)事件
- (void)loginTimerCountDownExecutingWithTimeOut:(NSNotification *)notification {
NSInteger timeOut = [notification.object integerValue];
NSString *timeStr = [NSString stringWithFormat:@"(%.2ld)重新獲取",(long)timeOut];
self.btnCountDown.selected = YES;//此處的 self.topView.btnCountDown換成自己的button
[self.btnCountDown setTitle:timeStr forState:UIControlStateNormal];//此處的 self.topView.btnCountDown換成自己的button
[self.btnCountDown setTitleColor:ZYC_COLOR_WITH_HEX(0x999999) forState:UIControlStateNormal];
self.btnCountDown.userInteractionEnabled = NO;
}
- (void)loginTimerCountDownCompleted {
self.btnCountDown.selected = NO;//此處的 self.topView.btnCountDown換成自己的button
[self.btnCountDown setTitle:@"獲取驗(yàn)證碼" forState:UIControlStateNormal];//此處的 self.topView.btnCountDown換成自己的button
[self.btnCountDown setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];//此處的 self.topView.btnCountDown換成自己的button
self.btnCountDown.userInteractionEnabled = YES;//此處的 self.topView.btnCountDown換成自己的button
}
在dealloc方法中銷毀注冊通知
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
輸入手機(jī)號(hào)碼以后,點(diǎn)擊發(fā)送驗(yàn)證碼,后臺(tái)接口返回成功以后,開始倒計(jì)時(shí)
- (void)sendSMSRequestWithPhone:(NSString *)phoneNum sender:(UIButton *)sender {
//模擬網(wǎng)絡(luò)請求
//...
//開始倒計(jì)時(shí)
[[BOUTimerManager sharedInstance] timerCountDownWithType:BOUCountDownTypeLogin];
}
點(diǎn)擊登錄按鈕,后臺(tái)接口返回以后,取消登錄界面驗(yàn)證碼倒計(jì)時(shí)
- (void)clickLoginAction:(UIButton *)sender {
//模擬網(wǎng)絡(luò)請求
//...
//取消登錄界面倒計(jì)時(shí)
[[BOUTimerManager sharedInstance] cancelTimerWithType:BOUCountDownTypeLogin];
}
結(jié)尾
demo地址 https://github.com/latacat/iOS_Demos/tree/main/AuthCodeTest