1. 程式人生 > >iOS 一個更完善的Swift倒計時按鈕(附後臺許可權申請)

iOS 一個更完善的Swift倒計時按鈕(附後臺許可權申請)

一個更完善的Swift倒計時按鈕(附後臺許可權申請)

如今越來越多app使用手機號碼作為使用者名稱,其中總是要涉及到驗證碼的傳送。

倒計時按鈕實現關鍵點:

  1. 當前檢視控制器銷燬後倒計時計數的再恢復
  2. 不同頁面可能使用同一個倒計時計數
  3. 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進入後臺後的倒計時

這點就和我們的倒計時按鈕沒有關係了。有兩種實現方案

  1. 記錄進入後臺與回到前臺的間隔時間
  2. 申請後臺執行許可權

本文介紹一下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
    }
}