目录
- 使用场景
- 方法一 设置
enabled
或userInteractionEnabled
属性- 方法二 借助
cancelPreviousPerformRequestsWithTarget:selector:object
实现- 方法三 通过
runtime
交换方法实现- 注意事项
一 使用场景
在实际应用场景中,有几个业务场景需要控制UIButton响应事件的时间间隔。
- 1 当点击按钮来执行网络请求时,若请求耗时稍长,用户往往会多次点击。这样,就执行了多次请求,造成资源浪费。
- 2 在移动终端设备性能较差时,连续点击按钮会执行多次事件(比如push出来多个viewController)。
- 3 防止暴力点击。
二 方法一
通过UIButton
的enabled
属性和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;
});
}
运行结果
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(@"真正开始执行业务 - 比如网络请求...");
}
- 运行结果
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_eventInterval
和private
属性cs_eventInvalid
。 - 2 在
+load
方法中使用runtime
将UIButton
的-sendAction:to:forEvent:
方法与自定义的cs_sendAction:to:forEvent:
方法进行交换 - 3 使用
cs_eventInterval
作为控制cs_eventInvalid
的计时因子,用cs_eventInvalid
控制UIButton
的event
事件是否有效。
*代码实现如下
@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(@"按钮点击...");
}
- 运行结果如下
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] 按钮点击...
四 注意事项
在方法三中交互UIButton
的sendAction:to:forEvent:
方法,实际上交互的是UIControl
的sendAction: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点击...");
}
运行结果
(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
方法中交换了UIControl
的sendAction:to:forEvent:
方法,所以在使用UIControl
或其子类(比如UISlider)
的sendAction:to:forEvent:
方法时会引起参数缺失的崩溃。可以将UIButton+Extension
改成UIControl+Extension
以避免此问题。
本文参考
iOS UIButton之防止重复点击(控制事件响应时间间隔)
iOS---防止UIButton重复点击的三种实现方式
项目链接地址 AvoidBtnRepeatClick