iOS-UIButton防止重复点击(三种办法)

1,351 阅读10分钟
原文链接: www.jianshu.com
目录
  • 使用场景
  • 方法一 设置enableduserInteractionEnabled属性
  • 方法二 借助cancelPreviousPerformRequestsWithTarget:selector:object实现
  • 方法三 通过runtime交换方法实现
  • 注意事项
一 使用场景

在实际应用场景中,有几个业务场景需要控制UIButton响应事件的时间间隔。

  • 1 当点击按钮来执行网络请求时,若请求耗时稍长,用户往往会多次点击。这样,就执行了多次请求,造成资源浪费。
  • 2 在移动终端设备性能较差时,连续点击按钮会执行多次事件(比如push出来多个viewController)。
  • 3 防止暴力点击。
二 方法一

通过UIButtonenabled属性和userInteractionEnabled属性控制按钮是否可点击。此方案在逻辑上比较清晰、易懂,但具体代码书写分散,常常涉及多个地方。

  • 创建按钮
- (void)drawBtn {
    UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 200, 100)];
    [btn setTitle:@"按钮点击" forState:UIControlStateNormal];
    [btn setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
    // 按钮不可点击时,文字颜色置灰
    [btn setTitleColor:[UIColor grayColor] forState:UIControlStateDisabled];
    [btn setTitleColor:[UIColor blueColor] forState:UIControlStateHighlighted];
    btn.center = self.view.center;
    [btn addTarget:self action:@selector(tapBtn:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:btn];
}

按钮不可点击时,标题颜色置灰,方便对比

  • 点击事件
- (void)tapBtn:(UIButton *)btn {
    NSLog(@"按钮点击...");
    btn.enabled = NO;
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        btn.enabled = YES;
    });
}

运行结果

1.gif
2019-06-12 23:21:09.039455+0800 AvoidBtnRepeatClick[8102:362250] 按钮点击...
2019-06-12 23:21:11.658751+0800 AvoidBtnRepeatClick[8102:362250] 按钮点击...
2019-06-12 23:21:14.057510+0800 AvoidBtnRepeatClick[8102:362250] 按钮点击...
2019-06-12 23:21:16.254230+0800 AvoidBtnRepeatClick[8102:362250] 按钮点击...
2019-06-12 23:21:18.788004+0800 AvoidBtnRepeatClick[8102:362250] 按钮点击...
2019-06-12 23:21:21.155584+0800 AvoidBtnRepeatClick[8102:362250] 按钮点击...
2019-06-12 23:21:23.389769+0800 AvoidBtnRepeatClick[8102:362250] 按钮点击...

每隔2秒执行一次方法

方法二

通过 NSObject 的两个方法

// 此方法会在连续点击按钮时取消之前的点击事件,从而只执行最后一次点击事件
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;
// 多长时间后做某件事情
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;

按钮创建还是上面代码

  • 按钮点击事件如下
/** 方法一 */
- (void)tapBtn:(UIButton *)btn {
    NSLog(@"按钮点击了...");
    // 此方法会在连续点击按钮时取消之前的点击事件,从而只执行最后一次点击事件
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(buttonClickedAction:) object:btn];
    // 多长时间后做某件事情
    [self performSelector:@selector(buttonClickedAction:) withObject:btn afterDelay:2.0];
}

- (void)buttonClickedAction:(UIButton *)btn {
    NSLog(@"真正开始执行业务 - 比如网络请求...");
}
  • 运行结果
1.gif
2019-06-13 09:15:58.935540+0800 AvoidBtnRepeatClick[62321:2927724] 按钮点击了...
2019-06-13 09:15:59.284096+0800 AvoidBtnRepeatClick[62321:2927724] 按钮点击了...
2019-06-13 09:15:59.760772+0800 AvoidBtnRepeatClick[62321:2927724] 按钮点击了...
2019-06-13 09:16:00.238923+0800 AvoidBtnRepeatClick[62321:2927724] 按钮点击了...
2019-06-13 09:16:00.689305+0800 AvoidBtnRepeatClick[62321:2927724] 按钮点击了...
2019-06-13 09:16:02.689633+0800 AvoidBtnRepeatClick[62321:2927724] 真正开始执行业务 - 比如网络请求...
2019-06-13 09:16:03.479984+0800 AvoidBtnRepeatClick[62321:2927724] 按钮点击了...
2019-06-13 09:16:03.884124+0800 AvoidBtnRepeatClick[62321:2927724] 按钮点击了...
2019-06-13 09:16:04.334930+0800 AvoidBtnRepeatClick[62321:2927724] 按钮点击了...
2019-06-13 09:16:04.776324+0800 AvoidBtnRepeatClick[62321:2927724] 按钮点击了...
2019-06-13 09:16:05.179153+0800 AvoidBtnRepeatClick[62321:2927724] 按钮点击了...
2019-06-13 09:16:07.179512+0800 AvoidBtnRepeatClick[62321:2927724] 真正开始执行业务 - 比如网络请求...
2019-06-13 09:16:08.062850+0800 AvoidBtnRepeatClick[62321:2927724] 按钮点击了...
2019-06-13 09:16:10.064171+0800 AvoidBtnRepeatClick[62321:2927724] 真正开始执行业务 - 比如网络请求...
2019-06-13 09:16:10.947205+0800 AvoidBtnRepeatClick[62321:2927724] 按钮点击了...
2019-06-13 09:16:12.948065+0800 AvoidBtnRepeatClick[62321:2927724] 真正开始执行业务 - 比如网络请求...
2019-06-13 09:16:13.528897+0800 AvoidBtnRepeatClick[62321:2927724] 按钮点击了...
2019-06-13 09:16:13.776711+0800 AvoidBtnRepeatClick[62321:2927724] 按钮点击了...
2019-06-13 09:16:15.777735+0800 AvoidBtnRepeatClick[62321:2927724] 真正开始执行业务 - 比如网络请求...

通过打印结果可知,如果连续点击多次,只会响应最后一次点击事件,并且是在设定的时间间隔后执行,这边设置的时间间隔是 2S。

总结:会出现延时现象,并且需要对大量的UIButton做处理,工作量大,不方便。

方法三

通过Runtime交换UIButton的响应事件方法,从而控制响应事件的时间间隔。

实现步骤如下:

  • 1 创建一个UIButton的分类,使用runtime增加public属性cs_eventIntervalprivate属性cs_eventInvalid
  • 2 在+load方法中使用runtimeUIButton-sendAction:to:forEvent:方法与自定义的cs_sendAction:to:forEvent:方法进行交换
  • 3 使用cs_eventInterval作为控制cs_eventInvalid的计时因子,用cs_eventInvalid控制UIButtonevent事件是否有效。

*代码实现如下

@interface UIButton (Extension)

/** 时间间隔 */
@property(nonatomic, assign)NSTimeInterval cs_eventInterval;

@end
#import "UIButton+Extension.h"
#import <objc/runtime.h>

static char *const kEventIntervalKey = "kEventIntervalKey"; // 时间间隔
static char *const kEventInvalidKey = "kEventInvalidKey";   // 是否失效

@interface UIButton()

/** 是否失效 - 即不可以点击 */
@property(nonatomic, assign)BOOL cs_eventInvalid;

@end

@implementation UIButton (Extension)

+ (void)load {
    // 交换方法
    Method clickMethod = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
    Method cs_clickMethod = class_getInstanceMethod(self, @selector(cs_sendAction:to:forEvent:));
    method_exchangeImplementations(clickMethod, cs_clickMethod);
}

#pragma mark - click

- (void)cs_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    if (!self.cs_eventInvalid) {
        self.cs_eventInvalid = YES;
        [self cs_sendAction:action to:target forEvent:event];
        [self performSelector:@selector(setCs_eventInvalid:) withObject:@(NO) afterDelay:self.cs_eventInterval];
    }
}

#pragma mark - set | get

- (NSTimeInterval)cs_eventInterval {
    return [objc_getAssociatedObject(self, kEventIntervalKey) doubleValue];
}

- (void)setCs_eventInterval:(NSTimeInterval)cs_eventInterval {
    objc_setAssociatedObject(self, kEventIntervalKey, @(cs_eventInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (BOOL)cs_eventInvalid {
    return [objc_getAssociatedObject(self, kEventInvalidKey) boolValue];
}

- (void)setCs_eventInvalid:(BOOL)cs_eventInvalid {
    objc_setAssociatedObject(self, kEventInvalidKey, @(cs_eventInvalid), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
  • 测试代码如下
/** 方法三 */
- (void)drawExpecialBtn{
    UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 200, 100)];
    [btn setTitle:@"按钮点击" forState:UIControlStateNormal];
    [btn setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
    // 按钮不可点击时,文字颜色置灰
    [btn setTitleColor:[UIColor grayColor] forState:UIControlStateDisabled];
    [btn setTitleColor:[UIColor blueColor] forState:UIControlStateHighlighted];
    btn.center = self.view.center;
    [btn addTarget:self action:@selector(tapBtn:) forControlEvents:UIControlEventTouchUpInside];
    btn.cs_eventInterval = 2.0;
    [self.view addSubview:btn];
}

- (void)tapBtn:(UIButton *)btn {
    NSLog(@"按钮点击...");
}
  • 运行结果如下
1.gif
2019-06-13 19:18:48.314110+0800 AvoidBtnRepeatClick[89795:3312038] 按钮点击...
2019-06-13 19:18:50.346907+0800 AvoidBtnRepeatClick[89795:3312038] 按钮点击...
2019-06-13 19:18:52.512887+0800 AvoidBtnRepeatClick[89795:3312038] 按钮点击...
2019-06-13 19:18:54.515119+0800 AvoidBtnRepeatClick[89795:3312038] 按钮点击...
2019-06-13 19:18:56.577693+0800 AvoidBtnRepeatClick[89795:3312038] 按钮点击...
2019-06-13 19:18:58.679121+0800 AvoidBtnRepeatClick[89795:3312038] 按钮点击...
2019-06-13 19:19:00.681003+0800 AvoidBtnRepeatClick[89795:3312038] 按钮点击...
2019-06-13 19:19:02.752387+0800 AvoidBtnRepeatClick[89795:3312038] 按钮点击...
2019-06-13 19:19:04.879559+0800 AvoidBtnRepeatClick[89795:3312038] 按钮点击...
四 注意事项

在方法三中交互UIButtonsendAction:to:forEvent:方法,实际上交互的是UIControlsendAction:to:forEvent:方法,所以在使用·UIControl·或其·子类(比如UISlider)·的·sendAction:to:forEvent:·方法时会引起参数缺失的崩溃。

  • 测试代码如下
/** 注意事项 */
- (void)slideTest {
    UISlider *slide = [[UISlider alloc] initWithFrame:CGRectMake(0, 0, 200, 59)];
    [slide addTarget:self action:@selector(tapSlide:) forControlEvents:UIControlEventTouchUpInside];
    slide.center = self.view.center;
    [self.view addSubview:slide];
}

- (void)tapSlide:(UISlider *)slider {
    NSLog(@"UISlider点击...");
}

运行结果

image.png
(void *) $0 = 0x0000600002620000
2019-06-13 19:48:22.753320+0800 AvoidBtnRepeatClick[90086:3328087]  INFO: Reveal Server started (Protocol Version 32).
2019-06-13 19:48:26.329630+0800 AvoidBtnRepeatClick[90086:3328087] -[UISlider cs_eventInvalid]: unrecognized selector sent to instance 0x7ffd79c0f4d0
2019-06-13 19:48:26.340542+0800 AvoidBtnRepeatClick[90086:3328087] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[UISlider cs_eventInvalid]: unrecognized selector sent to instance 0x7ffd79c0f4d0'
*** First throw call stack:
(
    0   CoreFoundation                      0x0000000110be26fb __exceptionPreprocess + 331
    1   libobjc.A.dylib                     0x0000000110186ac5 objc_exception_throw + 48
    2   CoreFoundation                      0x0000000110c00ab4 -[NSObject(NSObject) doesNotRecognizeSelector:] + 132
    3   UIKitCore                           0x000000011397cc3d -[UIResponder doesNotRecognizeSelector:] + 287
    4   CoreFoundation                      0x0000000110be7443 ___forwarding___ + 1443
    5   CoreFoundation                      0x0000000110be9238 _CF_forwarding_prep_0 + 120
    6   AvoidBtnRepeatClick                 0x000000010f8af1cb -[UIButton(Extension) cs_sendAction:to:forEvent:] + 91
    7   UIKitCore                           0x00000001133a7f36 -[UIControl _sendActionsForEvents:withEvent:] + 450
    8   UIKitCore                           0x00000001133a6eec -[UIControl touchesEnded:withEvent:] + 583
    9   UIKitCore                           0x000000011398aeee -[UIWindow _sendTouchesForEvent:] + 2547
    10  UIKitCore                           0x000000011398c5d2 -[UIWindow sendEvent:] + 4079
    11  UIKitCore                           0x000000011396ad16 -[UIApplication sendEvent:] + 356
    12  UIKitCore                           0x0000000113a3b293 __dispatchPreprocessedEventFromEventQueue + 3232
    13  UIKitCore                           0x0000000113a3dbb9 __handleEventQueueInternal + 5911
    14  CoreFoundation                      0x0000000110b49be1 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
    15  CoreFoundation                      0x0000000110b49463 __CFRunLoopDoSources0 + 243
    16  CoreFoundation                      0x0000000110b43b1f __CFRunLoopRun + 1231
    17  CoreFoundation                      0x0000000110b43302 CFRunLoopRunSpecific + 626
    18  GraphicsServices                    0x00000001190d22fe GSEventRunModal + 65
    19  UIKitCore                           0x0000000113950ba2 UIApplicationMain + 140
    20  AvoidBtnRepeatClick                 0x000000010f8af500 main + 112
    21  libdyld.dylib                       0x00000001124c1541 start + 1
    22  ???                                 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
(lldb)

注意:因为在UIButton+Extension.m中的+load方法中交换了UIControlsendAction:to:forEvent:方法,所以在使用UIControl或其子类(比如UISlider)sendAction:to:forEvent:方法时会引起参数缺失的崩溃。可以将UIButton+Extension改成UIControl+Extension以避免此问题。


本文参考

iOS UIButton之防止重复点击(控制事件响应时间间隔)
iOS---防止UIButton重复点击的三种实现方式


项目链接地址 AvoidBtnRepeatClick