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 [*注]這里不得不提一下,對於單例的寫法,各位看官仁者見仁智者見智。一個嚴謹的單例至少需要滿足以下條件:
很多人忽略了第一條的第一點和第二條。 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至此,解決方案到此就結束了 |