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至此,解決方案到此就結束了 |
