UIButton防止连续点击

1.需求

曾经有一个app摆在我的面前,然后我对每个按钮进行疯狂连续点击,结果出现了不可描述的BUG,其实这个app就是我们自己开发的,所以修复这个BUG迫在眉睫.

2.原理

在button的响应方法里断个点,查看调用栈每次都有走过这个方法:
- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event
所以基本思路就是,能够在每次sendAction的时候都判断一下时间戳,如果和上次send的时间戳相隔过短,则终止send,否则放开让它继续send.
但是涉及到一个核心问题:
- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event是系统方法,我改不了.
解决这个问题有两种思路:
(1)继承UIButton,定义一个CustomButton类,在这个子类里重写sendAction...方法.
(2) 使用运行时函数,替换掉UIButton的sendAction...方法.

方式(1)要改的地方太多了,几乎每个Button都要搜一下,替换一下,还有xib里的...不合适,因此采用方式(2).

2.实现

讲方法替换的文章网上太多,就不再复制粘贴。

直接贴代码,和注释

UIButton+InsensitiveTouch.h

#import 
@interface UIButton (InsensitiveTouch)
//开启UIButton防连点模式
+ (void)enableInsensitiveTouch;
//关闭UIButton防连点模式
+ (void)disableInsensitiveTouch;
//设置防连续点击最小时间差(s),不设置则默认值是0.5s
+ (void)setInsensitiveMinTimeInterval:(NSTimeInterval)interval;
@end

UIButton+InsensitiveTouch.m

#import "UIButton+InsensitiveTouch.h"
#import 

//最小时间差
static NSTimeInterval insensitiveMinTimeInterval = 0.5;
//原生sendAction:to:forEvent:实现
static void (*originalImplementation)(id, SEL, SEL, id, UIEvent *) = NULL;
//替换的sendAction:to:forEvent:实现
static void replacedImplementation(id object, SEL selector, SEL action, id target, UIEvent *event);

@implementation UIButton (InsensitiveTouch)

+ (void)enableInsensitiveTouch {
//获取当前"@selector(sendAction:to:forEvent:)"对应的Method
    Method methodNow = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
//得到当前sendAction:to:forEvent:实现地址
    IMP implementationNow = method_getImplementation(methodNow);
//这个实现地址已经是replacedImplementation,说明已经替换过了
    if (implementationNow == (IMP)replacedImplementation) {
        return;
    }
//保存原生的sendAction:to:forEvent:实现地址
    originalImplementation = (void (*)(id, SEL, SEL, id, UIEvent *))implementationNow;
    const char *type = method_getTypeEncoding(methodNow);
//将实现替换为replacedImplementation
    class_replaceMethod(self, @selector(sendAction:to:forEvent:), (IMP)replacedImplementation, type);
}

+ (void)disableInsensitiveTouch {
    IMP implementationNow = class_getMethodImplementation(self, @selector(sendAction:to:forEvent:));
    if (originalImplementation && implementationNow == (IMP)replacedImplementation) {
        class_replaceMethod(self, @selector(sendAction:to:forEvent:), (IMP)originalImplementation, NULL);
    }
}

+ (void)setInsensitiveMinTimeInterval:(NSTimeInterval)interval {
    insensitiveMinTimeInterval = interval;
}

- (NSTimeInterval)lastTouchTimestamp {
    return [objc_getAssociatedObject(self, @selector(lastTouchTimestamp)) doubleValue];
}

- (void)setLastTouchTimestamp:(NSTimeInterval)timestamp {
    objc_setAssociatedObject(self, @selector(lastTouchTimestamp), @(timestamp), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

//替换的sendAction:to:forEvent:实现
static void replacedImplementation(id object, SEL selector, SEL action, id target, UIEvent *event) {
//是按钮,并且是UIEventTypeTouches事件,才进行时间戳判断
//但是要排除这两种按钮 “CUShutterButton”和 "CAMShutterButton",这两个分别是8系统,10系统上相机拍照按钮的类名.这是两个特殊封装过的按钮,如果把它们的事件也用时间戳给过滤掉了,你就会发现app里弹出相机后,要长按才能拍照。
    if ([object isKindOfClass:UIButton.self] && ![NSStringFromClass([object class]) isEqualToString:@"CUShutterButton"] && ![NSStringFromClass([object class]) isEqualToString:@"CAMShutterButton"] && event.type == UIEventTypeTouches) {
//进行时间戳判断
        UIButton *button = (UIButton *)object;
        if (ABS(event.timestamp - button.lastTouchTimestamp) < insensitiveMinTimeInterval) {
//时间过短,就此返回,此次事件Send也中止
            return;
        }
        button.lastTouchTimestamp = event.timestamp;
    }
//时间戳上没问题,不属于快速点击
    if (originalImplementation) {
//调用系统原生实现,继续完成事件的Send
        originalImplementation(object, selector, action, target, event);
    }
}

3.使用

在Appdelegate launchWith... 里调用 [UIButton enableInsensitiveTouch]即可。网上大部分实现喜欢在+(void)load方法里完成替换,因此使用库的时候什么都不用调用,但我还是觉得让使用者知道自己做了什么比较好。

你可能感兴趣的:(UIButton防止连续点击)