1、分析UIButton
- 需要将按钮的点击事件方法和自定义方法在运行时动态交换。在自定义方法实现中,在间隔一段时间后设置按钮的点击事件中的操作有效。在间隔时间范围内的点击操作无效。从而达到解决按钮重复点击的效果。
- UIButton 这个类本身是没有 sendAction:to:forEvent: 方法的,但是它继承自 UIControl,UIControl 有这个方法。
- UIControl的子类包括:UIButton、UIDatePicker、UIPageControl、UISegmentedControl、UISlider、UIStepper、UISwitch。
- 所以可以将父类 UIControl 的这个系统方法 - sendAction:to:forEvent: 作为切入点。
2、使用runtime 解决按钮重复点击问题的错误方法
- 方法一、直接将系统方法 sendAction:to:forEvent: 和自定义方法 wyr_sendAction:交换IMP。
- 弊端:UIButton是没有sendAction:to:forEvent:方法的,直接交换IMP其实是和UIButton的父类,也就是UIControl交换了方法实现。那么当你使用UISwitch等UIControl的其他子类的时候,就会因为找不到fq_sendAction方法而崩溃。
- 方法二、创建UIControl分类,自定义wyr_sendAction:方法,和系统的 sendAction:to:forEvent: 交换IMP。
- 弊端: 这样做可以避免UIControl的子类调用崩溃,但是所有UIControl的子类都会走fq_sendAction方法,会导致事件不连续。
3、正确解决方法
直接上代码:
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
//创建 UIButton的分类
@interface UIButton (WYRTapAction)
//是否忽略点击
@property (nonatomic, assign) BOOL ignoreEvent;
@end
NS_ASSUME_NONNULL_END
#import "UIButton+WYRTapAction.h"
#import <objc/runtime.h> //引入 runtime 头文件
#define EVENTINTERVAL 5 // 间隔时间
@implementation UIButton (WYRTapAction)
// load方法是应用程序把这个类加载到内存的时候调用,而且只会调用一次,所以在这个方法中实现方法的交换最合适
+(void)load {
//1、获取系统方法 sendAction:to:forEvent: 的 Method 信息 赋值给 sendEvent
Method sendEvent = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
//2、获取自定义方法 wyr_sendAction:to:forEvent: 的 Method 信息 赋值给 my_sendEvent
Method my_sendEvent = class_getInstanceMethod(self, @selector(wyr_sendAction:to:forEvent:));
//3、使用 class_addMethod 给UIButton添加系统的 sendAction:to:forEvent:
BOOL addsuccess = class_addMethod(self, @selector(sendAction:to:forEvent:), method_getImplementation(sendEvent), method_getTypeEncoding(sendEvent));
if (addsuccess) {
//4、添加成功说明UIButton没有这个方法的实现,那么 sendEvent 里面的class信息其实是父类UIControl的,这个时候我们再 class_getInstanceMethod 获取一遍就是 UIButton的 Method信息
sendEvent = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
}
//5、交换两个方法的 IMP(方法的实现函数)
method_exchangeImplementations(sendEvent, my_sendEvent);
}
//自定义方法的实现
- (void)wyr_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
//如果此时点击不忽略
if (!self.ignoreEvent) {
//设置忽略点击
self.ignoreEvent = YES;
//调用自定义方法 (此时调用的UIButton的系统方法sendAction:to:forEvent:)
[self wyr_sendAction:action to:target forEvent:event];
//手动使用 performSelector 调用 setIgnoreEvent:方法,在间隔一段时间后设置 ignoreEvent 的值 为 NO:设置点击不忽略(本次点击的操作有效)
[self performSelector:@selector(setIgnoreEvent:) withObject:@(NO) afterDelay:EVENTINTERVAL];
} else {
NSLog(@"忽略本次操作");
}
}
//因为在分类中 @property 只有setter、getter的声明,没有setter、getter的实现 和 _变量。所以手动实现 setter 和 getter方法
-(void)setIgnoreEvent:(BOOL)ignoreEvent {
//使用 runtime 在 setter 方法中动态创建属性
/**
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
id _Nullable value, objc_AssociationPolicy policy)
1、object:保存到哪个对象中(给哪个对象的属性赋值)
2、key:属性对应的 key
3、value:设置属性值
4、policy:使用的策略,是一个枚举值,和copy,retain,assign是一样的,手机开发一般都选择NONATOMIC
*/
objc_setAssociatedObject(self, @selector(ignoreEvent), @(ignoreEvent), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(BOOL)ignoreEvent {
//动态获取属性
// 此时 objc_getAssociatedObject 获取的 ignoreEvent 的值为 NSNumber类型,因此需要 如下操作转换成 BOOL类型的数据
return [objc_getAssociatedObject(self, @selector(ignoreEvent)) boolValue];
}
使用时候的代码
#import "ViewController.h"
//引入分类头文件
#import "UIButton+WYRTapAction.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIButton *btn = [UIButton buttonWithType:UIButtonTypeContactAdd];
btn.center = CGPointMake([UIScreen mainScreen].bounds.size.width / 2, [UIScreen mainScreen].bounds.size.height / 2);
[btn addTarget:self action:@selector(btnClick:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];
}
//在按钮点击的时候设置这个按钮不可点击,等待 多少秒延时后,再设置按钮可以点击;或者在操作技术的时候设置可以点击
//-(void)btnClick:(UIButton *)sender {
// NSLog(@"点击按钮");
// sender.enabled = NO;
// for (int i = 0; i < 10000; i++) {
// // NSLog(@"i = %d",i);
// NSLog(@"按钮不可点击");
// }
// NSLog(@"循环执行完毕 - 按钮可点击了");
// sender.enabled = YES;
//}
//如果涉及到按钮不同状态不同样式的时候, 用enabled不见得够用.还得额外加个变量来记录状态.
//如果全局解决按钮重复点击的问题,还是需要使用runtime 来解决。
-(void)btnClick:(UIButton *)sender {
NSLog(@"点击按钮");
//sender.ignoreEvent = NO;
}
@end
相关使用 runtime 的应用场景:
iOS 运行时动态交换两个方法(Method-Swizzling)
iOS 通过runtime给分类添加动态属性
iOS 使用runtime动态添加方法
ios 使用runtime实现自动解归档