我们在开发中一定会遇到连续点击按钮触发事件的需求,button的快速重复点击会带来预想不到的结果,甚至直接导致crash。那么是否有一种处理方法解决这个问题呢?答案是肯定的,我们可以用一种更优雅的方式来解决button的连续重复点击问题,那就是Runtime。
Runtime
在解决button连续点击问题的时候需要用到runtime中的关联对象(Associated Objects)和方法交叉(Method Swizzling)。
Associated Objects
private struct AssociatedKey {
static var defaultDurationTime: TimeInterval = 1.0 // 默认时间间隔
static var clickDurationTime = "clickDurationTime"
static var isIgnoreEvent = "isIgnoreEvent"
}
这里先定义一个runtime关联属性的结构体,接下来我们进行属性的关联:
/// 点击事件时间间隔
var clickDurationTime: TimeInterval {
set {
objc_setAssociatedObject(self, &AssociatedKey.clickDurationTime, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
get {
if let time = objc_getAssociatedObject(self, &AssociatedKey.clickDurationTime) as? TimeInterval {
return time
}
return AssociatedKey.defaultDurationTime
}
}
/// 是否忽略连续点击事件
var isIgnoreEvent: Bool {
get {
if let event = objc_getAssociatedObject(self, &AssociatedKey.isIgnoreEvent) as? Bool {
return event
}
return false
}
set {
objc_setAssociatedObject(self, &AssociatedKey.isIgnoreEvent, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
利用runtime关联属性之后,在自定义的点击方法内进行判断,是否需要延迟响应点击。然后进行方法交叉
Method Swizzling
我们先来看看UIButton的点击事件方法,然后在这个方法里处理连续点击的问题:
public func sendAction(action: Selector, to target: AnyObject?, forEvent event: UIEvent?)
所以我们需要重写交叉方法:
/// Swizzled Method
@objc private func my_sendAction(_ action: Selector, to target: Any?, for event: UIEvent?) {
clickDurationTime = clickDurationTime == 0 ? AssociatedKey.defaultDurationTime : clickDurationTime
// 判断是否忽略连续点击事件
// 如果不忽略false则关闭用户交互,点击间隔时间后打开用户交互
// 如果忽略true则直接调用原有方法
if !isIgnoreEvent {
isUserInteractionEnabled = false
delayTask(clickDurationTime) { [weak self] in
self?.isUserInteractionEnabled = true
}
}
my_sendAction(action, to: target, for: event)
}
接下来就是方法交叉:
static func initializeMethod() {
if self !== UIButton.self {
return
}
DispatchQueue.once(token: "UIButtonClickDurationTime") {
let originalSelector = #selector(sendAction)
let swizzledSelector = #selector(my_sendAction)
let originalMethod = class_getInstanceMethod(UIButton.self, originalSelector)
let swizzledMethod = class_getInstanceMethod(UIButton.self, swizzledSelector)
// 运行时为类添加我们自己写的my_sendAction(_:to:forEvent:)
let didAddMethod = class_addMethod(UIButton.self, originalSelector, method_getImplementation(swizzledMethod!), method_getTypeEncoding(swizzledMethod!))
if didAddMethod {
// 如果添加成功,则交换方法
class_replaceMethod(UIButton.self, swizzledSelector, method_getImplementation(originalMethod!), method_getTypeEncoding(originalMethod!))
} else {
// 如果添加失败,则交换方法的具体实现
method_exchangeImplementations(originalMethod!, swizzledMethod!)
}
}
}
在Swift中和OC不一样的是,load类方法在Swift中是不会被runtime调用的,所以我们必须确保方法交叉在一个dispatch_once中,这样就是安全的。而在Swift中dispatch_once可以这样来写:
extension DispatchQueue {
private static var _onceTracker = [String]()
public static func once(token: String, block: () -> ()) {
objc_sync_enter(self)
defer {
objc_sync_exit(self)
}
if _onceTracker.contains(token) {
return
}
_onceTracker.append(token)
block()
}
}
由于在Swift中runtime不会调用load类方法,所以我们需要放在Appdelegate中去做,在程序启动的时候进行方法交叉。
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
UIButton.initializeMethod()
return true
}
这样就解决了UIButton连续重复点击的问题。