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
    }
}

猜你喜欢

转载自blog.csdn.net/imhwn/article/details/85015257
今日推荐