iOS 一個更完善的Swift倒計時按鈕(附後臺許可權申請)
阿新 • • 發佈:2018-12-16
一個更完善的Swift倒計時按鈕(附後臺許可權申請)
如今越來越多app使用手機號碼作為使用者名稱,其中總是要涉及到驗證碼的傳送。
倒計時按鈕實現關鍵點:
- 當前檢視控制器銷燬後倒計時計數的再恢復
- 不同頁面可能使用同一個倒計時計數
- app進入後臺後的倒計時
明確了上面幾個問題,接下來程式碼編寫就簡單了
符合設計模式,我們將倒計時按鈕實現拆分為兩個類:
WynCountdownButton
: 繼承UIButton,對外開放
WynCountdownController
: 倒計時控制模組,與WynCountdownButton
下面針對3個實現關鍵點做實現設計
1. 當前檢視控制器銷燬後倒計時計數的再恢復
- 按鈕、控制器分離。按鈕生命週期隨所處的檢視控制器。控制器隨倒計時的開始與結束,做初始化與銷燬。
- 按鈕與控制器一一對應
class WynCountdownButton: UIButton {
private weak var controller: WynCountdownController
...
}
/// 全域性的常量或變數都是延遲計算的,跟延遲儲存屬性相似,但全域性的常量或變數不需要標記‘lazy’特性。
/// 全域性變數持有控制器
private var wynCountdownControllers: [String: WynCountdownController] = [:]
2.不同頁面可能使用同一個倒計時計數
class WynCountdownController {
/// 通過identifier來取得控制器例項
static func shared(withIdentifier identifier: String) -> WynCountdownController {
if let c = wynCountdownControllers[identifier] {
return c
} else {
let c = WynCountdownController()
c.identifier = identifier
objc_sync_enter(wynCountdownControllers)
wynCountdownControllers[identifier] = c
objc_sync_exit(wynCountdownControllers)
return c
}
}
/// 限制只能通過shared(withIdentifier:)方法來例項化Controller
private init() {}
...
}
3. app進入後臺後的倒計時
這點就和我們的倒計時按鈕沒有關係了。有兩種實現方案
- 記錄進入後臺與回到前臺的間隔時間
- 申請後臺執行許可權
本文介紹一下2,申請後臺執行許可權的方法。 只需在AppDelegate
中新增以下程式碼
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
...
var backgroundTaskId: UIBackgroundTaskIdentifier?
func applicationDidEnterBackground(_ application: UIApplication) {
let sharedApp = UIApplication.shared
backgroundTaskId = sharedApp.beginBackgroundTask(expirationHandler: { [unowned sharedApp] in
sharedApp.endBackgroundTask(self.backgroundTaskId!)
self.backgroundTaskId = UIBackgroundTaskIdentifier.invalid
})
}
...
}
若採用此方案,推薦加入一個標識,判斷當前是否需要申請後臺執行權
下面是完整程式碼,暫未傳到Github
####使用方法
let cBtn = WynCountdownButton(sec: 30, type: .system, identifier: "HistoryAndFavoriteBtn")
cBtn.frame = CGRect(x: 100, y: 250, width: 120, height: 44)
cBtn.setTitle("點選獲取驗證碼", for: .normal)
cBtn.attributedTitleForCountingClosure = { (btn, sec) in
return NSAttributedString(string: "剩餘\(sec)秒", attributes:[.foregroundColor: UIColor.random()])
}
cBtn.didCountdownBeginClosure = { (btn) in
kIsBgTaskEnable = true
}
cBtn.didCountdownFinishClosure = { (btn) in
print("Finished")
kIsBgTaskEnable = false
}
view.addSubview(cBtn)
WynCountdownButton.swift
import UIKit
class WynCountdownButton: UIButton {
/// 必須設定
@IBInspectable
public var identifier: String! {
didSet {
controller = WynCountdownController.shared(withIdentifier: identifier)
}
}
/// 倒計時長
@IBInspectable
public var sec: Int = 60
/// 自定義倒計時時顯示的文字(每秒回撥一次)
public var titleForCountingClosure: ((UIButton, Int) -> String)?
public var attributedTitleForCountingClosure: ((UIButton, Int) -> NSAttributedString)?
/// 倒計時開始、結束回撥
public var didCountdownBeginClosure: ((UIButton) -> Void)?
public var didCountdownFinishClosure: ((UIButton) -> Void)?
/* ============================================================ */
// MARK: - Initilize
/* ============================================================ */
/// 初始化倒計時按鈕
///
/// - Parameters:
/// - sec: 倒計時長(秒)
/// - identifier: 唯一標示,用於恢復倒計時
/// - type: 按鈕型別
convenience init(sec: Int, type: ButtonType, identifier: String = "identifier") {
self.init(type: type)
self.sec = sec
self.identifier = identifier
self.controller = WynCountdownController.shared(withIdentifier: identifier)
if controller.isTicking {
self.start()
}
}
/* ============================================================ */
// MARK: - Public func
/* ============================================================ */
/// 開始倒計時
@objc public func start() {
isEnabled = false
let tickingHandler: ((Timer, Int) -> Void) = { [weak self](timer, currentVal) in
guard let strongSelf = self else { return }
strongSelf.handleTicking(currentVal: currentVal)
}
if controller.isTicking {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
// 延時以等待設定TitleForCountingClosure
self.handleTicking(currentVal: self.controller.currentVal)
self.controller.tick = tickingHandler
}
} else {
controller.begin(sec: sec, tickClosure: tickingHandler)
}
didCountdownBeginClosure?(self)
}
/* ============================================================ */
// MARK: - Private properties & function
/* ============================================================ */
private weak var controller: WynCountdownController! {
didSet {
if controller.isTicking {
self.start()
}
}
}
private func handleTicking(currentVal: Int) {
if currentVal > 0 {
if let closure = titleForCountingClosure {
setTitle(closure(self, currentVal), for: .disabled)
return
}
if let closure = attributedTitleForCountingClosure {
setAttributedTitle(closure(self, currentVal), for: .disabled)
return
}
setTitle("\(currentVal)", for: .disabled)
} else {
didCountdownFinishClosure?(self)
isEnabled = true
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.addTarget(self, action: #selector(start), for: .touchUpInside)
}
/// IB方式建立
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.addTarget(self, action: #selector(start), for: .touchUpInside)
/// 檢查未填寫identifier
if identifier != nil && identifier != "" {
self.controller = WynCountdownController.shared(withIdentifier: identifier)
}
}
}
WynCountdownController.swift
/// 全域性的常量或變數都是延遲計算的,跟延遲儲存屬性相似,但全域性的常量或變數不需要標記‘lazy’特性。
private var wynCountdownControllers: [String: WynCountdownController] = [:]
class WynCountdownController {
/// 通過identifier來取得控制器例項
static func shared(withIdentifier identifier: String) -> WynCountdownController {
if let c = wynCountdownControllers[identifier] {
return c
} else {
let c = WynCountdownController()
c.identifier = identifier
objc_sync_enter(wynCountdownControllers)
wynCountdownControllers[identifier] = c
objc_sync_exit(wynCountdownControllers)
return c
}
}
/// 只能通過shared(withIdentifier:)方法來例項化Controller
private init() {}
private var identifier: String!
private var timer: Timer?
/// 當前倒計時時間
public var currentVal: Int = 60
/// 當前狀態
public var isTicking = false
/// 1秒1回撥
public var tick: ((Timer, Int) -> Void)!
private func newTimer() {
timer = Timer(fire: Date(), interval: 1, repeats: true) { [weak self](timer) in
guard let strongSelf = self else { return }
strongSelf.currentVal -= 1
strongSelf.tick(timer, strongSelf.currentVal)
if strongSelf.currentVal <= 0 {
strongSelf.isTicking = false
timer.invalidate()
strongSelf.timer = nil
objc_sync_enter(wynCountdownControllers)
wynCountdownControllers[strongSelf.identifier] = nil
objc_sync_exit(wynCountdownControllers)
}
}
}
public func begin(sec: Int, tickClosure: @escaping (Timer, Int) -> Void) {
currentVal = sec
tick = tickClosure
newTimer()
RunLoop.current.add(self.timer!, forMode: .common)
isTicking = true
}
deinit {
self.timer?.invalidate()
self.timer = nil
}
}