iOS “獲取驗證碼”按鈕的倒計時功能


iOS 的倒計時有多種實現細節,Cocoa Touch 為我們提供了 NSTimer 類和 GCD 的dispatch_source_set_timer方法去更加方便的使用計時器。我們也可以很容易的的各種 UI 控件上添加倒計時功能,你只需

iOS 的倒計時有多種實現細節,Cocoa Touch 為我們提供了 NSTimer 類和 GCD 的dispatch_source_set_timer 方法去更加方便的使用計時器。我們也可以很容易的的各種 UI 控件上添加倒計時功能,你只需要定時刷新一次界面,給控件文本屬性重新賦新值即可,但在實際項目中,可能並沒有你想的這么簡單美好。

我們不妨設想一下這樣的情景:

主界面上有一個注冊按鈕,你點擊按鈕 push 到下一級頁面,這個頁面讓你輸入手機號並有一個獲取驗證碼的按鈕。你填完號碼,再點擊“獲取驗證碼”按鈕,然后按鈕上的文字開始了 60 秒的倒計時。20 秒之后你 pop 回上一級頁面,那么現在的頁面應該被銷毀了,10 秒后再次 push 到這個注冊頁面,那么倒計時按鈕上的文字應該是【獲取驗證碼】還是 【30 秒后重試】?

合理交互應該是:按鈕正在從 30 秒開始,繼續倒計時,直到秒數為 0 然后按鈕文字再次變為【獲取驗證碼】,並且倒計時過程中,按鈕不可點擊。否則倒計時限制將不再有意義。

有 2 個細節需要注意:

  • 界面銷毀以后,計時器還在繼續計時
  • 重新創建界面后,如果該按鈕的計時器存在,則應當立即繼續倒計時。

我們要做的就是 獲取該按鈕對應對的計時器 。

有人會說,何必如此麻煩,直接將這個頁面或者這個按鈕寫成單例不就得了?是的,單例可以輕松解決這個問題,但是這種設計模式切不可濫用,假如你的 App 有20 個頁面需要獲取驗證碼按鈕,那豈不是得生成 20 個單例的 View controller ?要知道,你並不是經常需要這些頁面。如果把按鈕設計成單例,那更不可取,一但你修改了一個按鈕,其他地方的按鈕必受牽連,引發不可估計的后果。

我的一個方案是, 生成一個全局的計時器管理類,它負責為每一個需要倒計時功能的按鈕分配一個計時器,按鈕和計時器通過一個 key 相互綁定。按鈕被 dealloc 之后,計時器任然存在,直至計時結束。按鈕重新生成時,計時器管理類會根據 key 決定該按鈕是否需要繼續倒計時 。

Do it!

首先需要思考,這個計時器管理類應該是是什么樣子?它的具體功能又是什么?我給它命名為 WLButtonCountdownManager ,它是一個全局類,可用單例設計(1 個單例類比 20 個單例頁面划算得多)。它負責分配計時器並將其與按鈕綁定,所以它需要有一個容器屬性來存儲計時器,並且還要知道,容器里是否已經有計時器在跑了。那么 WLButtonCountdownManager 的頭文件大概類似於這樣:

NS_ASSUME_NONNULL_BEGIN
@interface WLButtonCountdownManager : NSObject

/**
 *  獲取單例
 *
 *  @return 該類的唯一實例
 */
+ (instancetype)defaultManager;

/**
 *  開始倒計時,如果倒計時管理器里具有相同的key,則直接開始回調。
 *
 *  @param aKey         任務key,用於標示唯一性
 *  @param timeInterval 倒計時總時間,受操作系統后台時間限制,倒計時時間規定不得大於 120 秒.
 *  @param countingDown 倒計時時,會多次回調,提供當前秒數
 *  @param finished     倒計時結束時調用,提供當前秒數,值恆為 0
 */
- (void)scheduledCountDownWithKey:(NSString *)aKey
                     timeInterval:(NSTimeInterval)timeInterval
                     countingDown:(nullable void (^)(NSTimeInterval leftTimeInterval))countingDown
                         finished:(nullable void (^)(__unused NSTimeInterval finalTimeInterval))finished;

/**
 *  查詢倒計時任務是否存在
 *
 *  @param akey 任務key
 *  @param task 任務
 *  @return YES - 存在, NO - 不存在
 */
- (BOOL)coundownTaskExistWithKey:(NSString *)akey task:(NSOperation * _Nullable * _Nullable)task;


@end
NS_ASSUME_NONNULL_END

[*注]這里不得不提一下,對於單例的寫法,各位看官仁者見仁智者見智。一個嚴謹的單例至少需要滿足以下條件:

  1. 全局唯一性 
    ①不可通過 alloc 再次分配資源 
    ②線程安全
  2. 不可繼承

很多人忽略了第一條的第一點和第二條。

About timer

關於計時器,我使用子線程每秒睡眠一次進行模擬,計時操作精度要求並不高,且線程池也利於管理。

第一步,子類化一個 NSOperation 的類,名為 WLCountdownTask :

@interface WLCountdownTask : NSOperation
/**
 *  計時中回調
 */
@property (copy, nonatomic) void (^countingDownBlcok)(NSTimeInterval timeInterval);
/**
 *  計時結束后回調
 */
@property (copy, nonatomic) void (^finishedBlcok)(NSTimeInterval timeInterval);
/**
 *  計時剩余時間
 */
@property (assign, nonatomic) NSTimeInterval leftTimeInterval;
/**
 *  后台任務標識,確保程序進入后台依然能夠計時
 */
@property (assign, nonatomic) UIBackgroundTaskIdentifier taskIdentifier;
@end


- (void)main {
    self.taskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:nil];

    while (--_leftTimeInterval > 0) {
        dispatch_async(dispatch_get_main_queue(), ^{
            if (_countingDownBlcok) _countingDownBlcok(_leftTimeInterval);
        });

        [NSThread sleepForTimeInterval:1];
    }

    dispatch_async(dispatch_get_main_queue(), ^{
        if (_finishedBlcok) {
            _finishedBlcok(0);
        }
    });

    if (self.taskIdentifier != UIBackgroundTaskInvalid) {
        [[UIApplication sharedApplication] endBackgroundTask:self.taskIdentifier];
        self.taskIdentifier = UIBackgroundTaskInvalid;
    }
}

@end

下一步,WLButtonCountdownManager 擁有一個線程池(也叫並發操作隊列,規定隊列中最多只允許存在 20 個並發線程),每分配一個計時器(即創建一個子線程)就將其放入池子中,計時器跑完以后會自動從池子里銷毀。

在創建計時任務之前,Manager 從池子里檢索是否有相同 key 的計時任務,如果任務存在,直接回調計時操作。否則,新建一個標識為 key 的任務。

- (void)scheduledCountDownWithKey:(NSString *)aKey
                     timeInterval:(NSTimeInterval)timeInterval
                     countingDown:(void (^)(NSTimeInterval))countingDown
                         finished:(void (^)(NSTimeInterval))finished
{
    if (timeInterval > 120) {
        NSCAssert(NO, @"受操作系統后台時間限制,倒計時時間規定不得大於 120 秒.");
    }

    if (_pool.operations.count >= 20)  // 最多 20 個並發線程
        return;

    WLCountdownTask *task = nil;
    if ([self coundownTaskExistWithKey:aKey task:&task]) {
        task.countingDownBlcok = countingDown;
        task.finishedBlcok     = finished;
        if (countingDown) {
            countingDown(task.leftTimeInterval);
        }
    } else {
        task                   = [[WLCountdownTask alloc] init];
        task.name              = aKey;
        task.leftTimeInterval  = timeInterval;
        task.countingDownBlcok = countingDown;
        task.finishedBlcok     = finished;
        [_pool addOperation:task];
    }
}


- (BOOL)coundownTaskExistWithKey:(NSString *)akey
                            task:(NSOperation *__autoreleasing  _Nullable *)task
{
    __block BOOL taskExist = NO;
    [_pool.operations enumerateObjectsUsingBlock:^(__kindof NSOperation * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([obj.name isEqualToString:akey]) {
            if (task) *task = obj;
            taskExist = YES;
            *stop     = YES;
        }
    }];

    return taskExist;
}

Last

至此,解決方案到此就結束了


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM