UIButton防止按钮和手势的暴力点击

首先理解下几个概念
1、IMP:它是指向一个方法具体实现的指针,每一个方法都有一个对应的IMP,当你发起一个消息之后,最终它会执行的那段代码,就是由IMP这个函数指针指向了这个方法实现的
2、SEL:方法名称的描述,只记录方法的编号不记录具体的方法,具体的方法是 IMP
3、Method:是一个类实例,里面的结构体有一个方法选标 SEL – 表示该方法的名称,一个types – 表示该方法参数的类型,一个 IMP - 指向该方法的具体实现的函数指针。

针对UIButton、UISegmentedControl、UISwitch这些继承自UIControl的控件可通过hook sendAction:to:forEvent:这个方法实现控件的重复点击
具体思路:
1、创建UIButton的类别,使用runtime添加public属性eventInterval作为计时因子
2、添加private属性eventUnavailable来控制button的点击事件是否有效
3、在+load方法中实现系统sendAction:to:forEvent:方法与自定义方法进行交换
代码实现:

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method origMethod = class_getInstanceMethod([self class], @selector(sendAction:to:forEvent:));
        SEL origsel = @selector(sendAction:to:forEvent:);
        Method swizMethod = class_getInstanceMethod([self class], @selector(tk_sendAction:to:forEvent:));
        SEL swizsel = @selector(tk_sendAction:to:forEvent:);
        BOOL addMehtod = class_addMethod([self class], origsel, method_getImplementation(swizMethod), method_getTypeEncoding(swizMethod));
        
        if (addMehtod) {
            class_replaceMethod([self class], swizsel, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
        } else {
            method_exchangeImplementations(origMethod, swizMethod);
        }
    });
}
 
- (void)tk_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    if (self.eventUnavailable == NO) {
        self.eventUnavailable = YES;
        [self tk_sendAction:action to:target forEvent:event];
        [self performSelector:@selector(setEventUnavailable:) withObject:@(NO) afterDelay:self.eventInterval];
    }
}

针对单击事件可对UITapGestureRecognizer的initWithTarget:action:或addTarget:action:进行hook
具体思路:
1、创建UITapGestureRecognizer的类别,使用runtime添加public属性eventInterval作为计时因子
2、添加private属性eventUnavailable来控制button的点击事件是否有效
3、在+load方法中实现系统initWithTarget:action:方法与自定义方法进行交换
4、将target和selector关联到创建的类别中并且将selector替换成类别中自定义的响应方法
代码实现:

- (instancetype)initTKWithTarget:(id)target action:(SEL)action {
    self = [self initTKWithTarget:self action:@selector(tap:)];
    objc_setAssociatedObject(self, gestureTargetKey, target, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    objc_setAssociatedObject(self, gestureSelKey, NSStringFromSelector(action), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    return self;
}
 
- (void)tap:(UIGestureRecognizer *)tapGesture {
    id target = objc_getAssociatedObject(self, gestureTargetKey);
    SEL action = NSSelectorFromString(objc_getAssociatedObject(self, gestureSelKey));
    
    if (self.eventUnavailable == NO) {
        self.eventUnavailable = YES;
        [target performSelector:action];
        [self performSelector:@selector(setEventUnavailable:) withObject:@(NO) afterDelay:self.eventInterval];
    }
}

针对单击事件的第二种实现方式实现带参数的init方法
具体思路:
1、在分类中实现initWithTarget:action:eventIntervl:的方法通过传进去计时因子控制点击事件是否可以响应,将手势的代理设置成新建的分类
2、添加属性eventUnavailable来控制点击事件是否有效
3、通过重写gestureRecognizer:shouldReceiveTouch:来控制点击事件是否响应
代码实现:

-(instancetype)initWithTarget:(id)target action:(SEL)action eventIntervl:(NSTimeInterval)eventIntervl {
    self = [super init];
    if (self) {
        self.eventInterval = eventIntervl;
        self.delegate = self;
        [self addTarget:target action:action];
    }
    return self;
}
 
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{
    if (self.eventUnavailable == NO) {
        self.eventUnavailable = YES;
        [self performSelector:@selector(setEventUnavailable:) withObject:@(NO) afterDelay:self.eventInterval];
        return YES;
    } else {
        return NO;
    }
}