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连续重复点击的问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    阳明AI阅读 16,074评论 3 119
  • 句中单词首字母大写 确保字符串的每个单词首字母都大写,其余部分小写。 像'the'和'of'这样的连接符同理。
    yyggfffg阅读 1,490评论 0 0
  • 终于找到一个可以自己偷偷说话的地方。取名叫心不广,是因为怀念一个叫广的男人,他走了三年三个月,我很难过。 不知道,...
    心不广阅读 1,706评论 0 0
  • 哪里来的不速之客?打扰了我清静悠然的心境。它哪里知道我的心刚刚还在九十年之前的那个别离的车站,感受着当年莹哥...
    青青子衿悠悠我心_玉壶阅读 1,607评论 0 0
  • DOM 事件 什么是事件 事件的三个特点 小 A 订阅/关注/监听了 XXX XXX 发生了变化 小 A 得到通知...
    ZombieBrandg阅读 2,854评论 0 0

友情链接更多精彩内容