按钮点击控制处理-Runtime

我们设计页面点击的时候肯定会出现多种事件点击的情况,如网络请求验证点击,回调点击,通知点击等等。。。。这样也就可能会造成一种延迟(停顿)的反应,有些页面可以通过加上遮罩防重点击,但有些页面显然不合适,这中情况下控制按钮的点击频率就显得很有必要了。

举个例子模拟网络回调Push到下个页面,如果我们连续点击会出现什么情况?
//模拟网络请求延迟(0.5s)
__weak typeof(self)weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
   [weakSelf performSegueWithIdentifier:@"pushToTextVc" sender:self];
});

连续点击按钮

GIF演示
栈图

从_UIModernBarButton - text 我看出栈中加入两层(push两次) HomeVc -> textVc - > textVc

解决办法:

  • 按钮响应后让屏幕不可点击(HUD)
  • 控制button的action做响应时间限制 (推荐)

这里我更推荐第二种,以上我只是举出一种很常见的情况,类似这样需要限定重复点击事件的在项目中还是很普遍得,所以第二种方法可以全局处理应用更多场景,而蒙板的使用会有一定限制。

解决:

如果说知道利用Runtime去做限制,那首先我们应该考虑怎么样去控制按钮的响应?那得简单介绍一下Target-Action机制。

  • Target-Action机制 : 直译就是“目标-行为”机制,我们给按钮添加点击事件:
[self.touchClickButton addTarget:self action:@selector(buttonClick:) forControlEvents:UIControlEventTouchUpInside];

按钮的点击事件发生时,会将消息发送到target(此处即为self对象),并由target对象的tapButton:方法来处理相应的事件。但是当sendAction:to:forEvent:addTarget:action:forControlEvents最后处理事件的sendAction。我们从这个方面入手利用Runtime在控制sendAction

设计:

从三个方面入手:

  • 创建一个UIButton和分类方法如 - UIButton+DCHook

  • 利用Runtime去交换sendAction:to:forEvent:方法如换成 - dc_sendAction:to:forEvent:

  • 添加一个外部属性可以自己控制按钮间隔的响应时间如 - NSTimeInterval actionInterval

实现核心代码:

  • UIButton+DCHook.h
@interface UIButton (DCHook)

#pragma mark - 响应时间的控制
@property (assign, nonatomic) NSTimeInterval actionInterval;

@end
  • UIButton+DCHook.m
#import <objc/runtime.h> 

@interface UIButton ()

@property (nonatomic, assign) BOOL ignoreEvent; // 是否忽视点击的事件

@end

#define defaultInterval 1.0  //默认时间间隔

static const char *UIControl_enventActionInterval = "UIControl_enventActionInterval";
static const char *UIControl_enventIsIgnoreEvent = "UIButton_EnventIsIgnoreEvent";

@implementation UIButton (DCHook)

#pragma mark - runtime 动态绑定 属性
- (void)setIsIgnoreEvent:(BOOL)isIgnoreEvent
{
    objc_setAssociatedObject(self, UIControl_enventIsIgnoreEvent, @(isIgnoreEvent), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)isIgnoreEvent{
    
    return [objc_getAssociatedObject(self, UIControl_enventIsIgnoreEvent) boolValue];
}

- (void)setActionInterval:(NSTimeInterval)actionInterval
{
    objc_setAssociatedObject(self, UIControl_enventActionInterval, @(actionInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSTimeInterval)actionInterval
{
    return [objc_getAssociatedObject(self, UIControl_enventActionInterval) doubleValue];
}

#pragma mark - Load
+ (void)load
{
    SEL selA = @selector(sendAction:to:forEvent:);
    SEL selB = @selector(dc_sendAction:to:forEvent:);
    Method methodA = class_getInstanceMethod([UIButton class],selA);
    Method methodB = class_getInstanceMethod([self class], selB);
    
    objc_setAssociatedObject([UIButton class], UIControl_enventIsIgnoreEvent, @(NO), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    BOOL isAdd = class_addMethod([UIButton class], selA, method_getImplementation(methodB), method_getTypeEncoding(methodB));
    
    if (isAdd) {
        class_replaceMethod([UIButton class], selB, method_getImplementation(methodA), method_getTypeEncoding(methodA));
    }else{
        //添加失败了 说明本类中有methodB的实现,此时只需要将methodA和methodB的IMP互换一下即可。
        method_exchangeImplementations(methodA, methodB);
    }
}

#pragma mark - 交换方法
- (void)dc_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
    NSTimeInterval interval = [objc_getAssociatedObject(self, UIControl_enventActionInterval) doubleValue];
    if ([objc_getAssociatedObject([UIButton class], UIControl_enventIsIgnoreEvent) boolValue]){ //忽略直接返回
        return;
    }else if (defaultInterval > 0){ // 点击事件
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(((interval == 0) ? defaultInterval : interval)  * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            objc_setAssociatedObject([UIButton class], UIControl_enventIsIgnoreEvent, @(NO), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        });
    }
    
    objc_setAssociatedObject([UIButton class], UIControl_enventIsIgnoreEvent, @(YES), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    // 这里看上去会陷入递归调用死循环,但在运行期此dc_sendAction方法是和sendAction:to:forEvent:互换的,相当于执行sendAction:to:forEvent:方法,所以并不会陷入死循环。
    [self dc_sendAction:action to:target forEvent:event];
}

@end

设置默认时间间隔为1s,可手动通过actionInterval更改如下

self.touchClickButton.actionInterval = 3; //测试更改为3s
打印响应图

这样就算你再怎么连续点击也不过造成重复响应的问题了

这里有在自定义中一个细节
#define defaultInterval 1.0  //默认时间间隔

NSTimeInterval interval = [objc_getAssociatedObject(self, UIControl_enventActionInterval) doubleValue];
(interval == 0) ? defaultInterval : interval;

我通过三目判断未设置属性时默认为1s间隔,那如果出现就需要重复点击的页面我们只需要把actionInterval设置为CGFLOAT_MIN即可;

// CGFLOAT_MIN : Float类型的最小非负值非低于正常值
self.touchClickButton.actionInterval = CGFLOAT_MIN;

如果说在不需要间隔的情况下每次都要设置属性觉得麻烦,也可以创建一个DCNOIntervalButton父类,以后继承就好了。

demo地址:DailyDemo

推荐阅读更多精彩内容