一个更完善的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
}
}