Swift中UIButton连续点击的处理

我们在开发中一定会遇到连续点击按钮触发事件的需求,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连续重复点击的问题。

你可能感兴趣的:(Swift中UIButton连续点击的处理)