iOS 使用runtime 解决按钮重复点击的问题

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实现自动解归档

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值