先查看效果:
我们的目的是简单的调用一句代码的方法来完成按钮的倒数计时效果:
@IBAction func btnCodeClicked(_ sender: UIButton) {
sender.countDown(5)
}
下面我们看看这个方法是怎么实现的:
extension UIButton {
func countDown(_ interval: TimeInterval) {
info["originalTitle"] = titleLabel?.text
info["originalColor"] = backgroundColor ?? UIColor()
tag = Int(interval)
//set status
isEnabled = false
setTitle(String(format: "%d s", tag), for: .normal)
backgroundColor = .lightGray
let timer = Timer.safe_scheduledTimerWithTimeInterval(1, closure: {
var newTitle = ""
if self.tag <= 0 {
newTitle = self.info["originalTitle"] as? String ?? ""
let t = self.info["timer"] as? Timer
if t != nil {
t?.invalidate()
self.info["timer"] = nil
}
self.isEnabled = true
self.backgroundColor = self.info["originalColor"] as? UIColor ?? UIColor()
} else {
self.tag -= 1
newTitle = String(format: "%d s", self.tag)
}
DispatchQueue.main.async {
self.setTitle(newTitle, for: .normal)
}
}, repeats: true)
info["timer"] = timer
}
}
随便一个.swift文件里面增加一个extension,这里给UIButton使用了关联属性info,它的类型是[String: Any],后面会给出方法,用它存储相关数据(初始的标题、背景颜色等),用button的tag作为倒计时的时间。在timer中使用了自定义的初始化方法,这个方法可以避免controller引用timer造成的强引用循环,后面给出方法。在timer的action中更新按钮的表现形式。
下面给出关联属性的实现方式:
private var key: Void?
extension NSObject {
var info: [String: Any] {
set {
objc_setAssociatedObject(self, &key, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
get {
var obj = objc_getAssociatedObject(self, &key) as? [String: Any]
if obj?.count ?? 0 <= 0 {
objc_setAssociatedObject(self, &key, [String: Any](), .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
obj = objc_getAssociatedObject(self, &key) as? [String: Any]
}
return obj!
}
}
}
属性扩展的使用方法我是在extension中增加了计算属性返回NSObject的关联属性,为了让它是一个确定有值的类型,我在get的时候进行了判断空处理,AnyObject的判空是使用.count来判断。如果为空就set并get获得新值返回。
下面给出timer的实现:
// 破除timer引用循环
public typealias TimerExcuteClosure = @convention(block) () -> ()
extension Timer {
private class TimerActionBlockWrapper : NSObject {
var block : TimerExcuteClosure
init(block: @escaping TimerExcuteClosure) {
self.block = block
}
}
public class func safe_scheduledTimerWithTimeInterval(_ ti:TimeInterval, closure: @escaping TimerExcuteClosure, repeats yesOrNo: Bool) -> Timer {
return self.scheduledTimer(timeInterval: ti, target: self, selector: #selector(Timer.excuteTimerClosure(_:)), userInfo: TimerActionBlockWrapper(block: closure), repeats: true)
}
@objc private class func excuteTimerClosure(_ timer: Timer) {
if let action = timer.userInfo as? TimerActionBlockWrapper {
action.block()
}
}
}
原理是使用中间类来避免timer的addTarget直接指向timer的引用类
运行效果是点击按钮进入倒数,按钮无法点击,直到时间结束,按钮恢复原来的颜色和标题。