iOS 解决按钮防重复点击的问题

日常使用中经常会出现按钮重复点击导致的数据重复提交问题,从而导致数据出错,常用的解决办法有
1、在发起请求的时候来一个全屏的loading这样在loading期间按钮就无法被点击,这种方式有个弊端就是loading弹窗起来需要几百毫秒时间左右,在这段时间期间用户如果手速过快,仍然可以触发多次点击事件;
2、发起提交时把按钮enabled或者userInteractionEnabled设置为false,然后提交完成或者返回错误时再把数据恢复,这种方法需要时刻记住按钮的使能状态,如果接口调用业务比较复杂,可能会导致某个场景未设置使能状态,从而导致按钮无法点击的情况;
3、按钮点击后存一下点击时间,按钮再次点击时把当前时间和上次时间比较,做一个时间差的换算,这样也确实可以解决问题,但是呢,每次都要去存时间和取时间在算时间,多少有点麻烦了,当然可以把这个功能写成一个分类来实现,该方法确实能解决问题,也是目前很多人都在使用的方案;
4、使用cancelPreviousPerformRequestsWithTarget:和performSelector来实现,通过该方法可以实现在指定时间范围内,如果重复点击,只会执行一次,本例中将使用该方案实现,当然该方案也有一定的弊端,就是永远只有最后一次按钮点击会被执行,这样就可能导致从按钮点击到触发事件可能会有时间延迟,本着学习的态度,探索一下performSelector的底层实现,本例采用该方案。

以下是实现步骤:
1、创建UIButton分类,并给分类添加属性interval,通过objc_setAssociatedObject和objc_getAssociatedObject来存储成员变量,该值用来做多次点击时间间隔,默认为0,interval为0的情况下走默认按钮点击事件,不处理多次点击事件

-(void)setInterval:(NSTimeInterval)interval{
    objc_setAssociatedObject(self, &kIntervalkey, @(interval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

-(NSTimeInterval)interval{
    NSNumber *number = objc_getAssociatedObject(self, &kIntervalkey);
    return number.doubleValue;
}

相关参数介绍:
key:要保证全局唯一,key与关联的对象是一一对应关系。必须全局唯一。
value:要关联的对象。
policy:关联策略。有五种关联策略。
OBJC_ASSOCIATION_ASSIGN 等价于 @property(assign)。
OBJC_ASSOCIATION_RETAIN_NONATOMIC等价于 @property(strong, nonatomic)。
OBJC_ASSOCIATION_COPY_NONATOMIC等价于@property(copy, nonatomic)。
OBJC_ASSOCIATION_RETAIN等价于@property(strong,atomic)。
OBJC_ASSOCIATION_COPY等价于@property(copy, atomic)。

2、重写+load方法,通过runtime交换UIButton的sendAction:to:forEvent:事件,UIButton的addTarget事件最终都会走这个方法

+(void)load{
    Class class = [self class];

    SEL originalSelector = @selector(sendAction:to:forEvent:);
    SEL swizzledSelector = @selector(swizzled_sendAction:to:forEvent:);
    
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
    BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    if (success) {
        class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

注意 这里一定要先class_addMethod再执行交换操作,否则可能会导致[super class]调用交换方法时导致找不到方法crash
详细比较class_addMethod/class_replaceMethod/method_exchangeImplementations以及对应的区分和示例,可以查看该作者分享的内容,讲解的特别好,并且有详细的例子分析

https://www.jianshu.com/p/ccb75e5277b7

3、实现替换后的方法swizzled_sendAction:to:forEvent:并将参数传递给sendAction:to:forEvent:

-(void)swizzled_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{
    /// 不处理重复点击
    NSTimeInterval interval = [self interval];
    if(interval == 0){
        [self swizzled_sendAction:action to:target forEvent:event];
        return;
    }
    SEL aSelector  = @selector(swizzled_sendAction:to:forEvent:);
    NSDictionary *argument = @{
        @"action":NSStringFromSelector(action),
        @"aSelector":NSStringFromSelector(aSelector),
        @"target":target,
        @"event":event
    };
    /// 取消之前处理的时间
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(handleEvent:) object:argument];
    [self performSelector:@selector(handleEvent:) withObject:argument afterDelay:interval];
}

这里用的是cancelPreviousPerformRequestsWithTarget和performSelector来实现按钮重复点击只执行一次,由于处理按键事件需要把参数传递给sendAction:to:forEvent: 我这里用的一个NSInvocation来实现系统方法多个参数的传递,这种写法是performSelector底层传递方法调用和参数传递的实现

/// 方法1:
-(void)handleEvent:(NSDictionary *)anArgument{
    /// 解析参数
    SEL action = NSSelectorFromString(anArgument[@"action"]);
    id target = anArgument[@"target"];
    UIEvent *event = anArgument[@"event"];
    
    SEL aSelector = NSSelectorFromString(anArgument[@"aSelector"]);
    /// 创建NSInvocation对象并设置selector 和 target
    NSMethodSignature *signature = [self methodSignatureForSelector:aSelector];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature: signature];
    [invocation setSelector:aSelector];
    [invocation setTarget:self];
    
    /// 设置参数 从index=2开始
    [invocation setArgument:&action atIndex:2];
    [invocation setArgument:&target atIndex:3];
    [invocation setArgument:&event atIndex:4];
    
    /// 执行方法
    [invocation invoke];
}

如果方法1用到NSInvocation传参不好理解,可以使用方法2,更加简单

/// 方法2:
-(void)handleEvent:(NSDictionary *)anArgument{
    /// 解析参数
    SEL action = NSSelectorFromString(anArgument[@"action"]);
    id target = anArgument[@"target"];
    UIEvent *event = anArgument[@"event"];
    /// 执行该方法并传递参数
    [self swizzled_sendAction:action to:target forEvent:event];
}

4、使用

/// 需要处理重复点击 可以设置间隔时间
    UIButton *button = [UIButton new];
    button.backgroundColor = UIColor.redColor;
    /// 这里为了测试把间隔时间设置的比较大
    button.interval = 4;
    [button addTarget:self action:@selector(itemClick:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];

/// 不需要处理重复点击 不设置间隔时间即可
    UIButton *button2 = [UIButton new];
    button2.backgroundColor = UIColor.redColor;
//    button2.interval = 4;
    [button2 addTarget:self action:@selector(itemClick:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button2];

-(void)itemClick:(UIButton *)button{
    NSLog(@"按钮被点击");
}

5、源码:

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface UIButton (ForbidRepeat)

/// 设置两次点击时间间隔
@property (nonatomic,assign) double interval;

@end

NS_ASSUME_NONNULL_END

#import "UIButton+ForbidRepeat.h"
#import <objc/runtime.h>

static NSString *const  kIntervalkey = @"kIntervalkey";

@implementation UIButton (ForbidRepeat)


-(void)setInterval:(NSTimeInterval)interval{
    objc_setAssociatedObject(self, &kIntervalkey, @(interval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

-(NSTimeInterval)interval{
    NSNumber *number = objc_getAssociatedObject(self, &kIntervalkey);
    return number.doubleValue;
}

/// 设置
+(void)load{
    
    Class class = [self class];

    SEL originalSelector = @selector(sendAction:to:forEvent:);
    SEL swizzledSelector = @selector(swizzled_sendAction:to:forEvent:);
    
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
    BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    if (success) {
        class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

-(void)swizzled_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{
    /// 不处理重复点击
    NSTimeInterval interval = [self interval];
    if(interval == 0){
        [self swizzled_sendAction:action to:target forEvent:event];
        return;
    }
    SEL aSelector  = @selector(swizzled_sendAction:to:forEvent:);
    NSDictionary *argument = @{
        @"action":NSStringFromSelector(action),
        @"aSelector":NSStringFromSelector(aSelector),
        @"target":target,
        @"event":event
    };
    /// 取消之前处理的时间
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(handleEvent:) object:argument];
    [self performSelector:@selector(handleEvent:) withObject:argument afterDelay:interval];
}

-(void)handleEvent:(NSDictionary *)anArgument{
    /// 解析参数
    SEL action = NSSelectorFromString(anArgument[@"action"]);
    id target = anArgument[@"target"];
    UIEvent *event = anArgument[@"event"];

    SEL aSelector = NSSelectorFromString(anArgument[@"aSelector"]);
    /// 创建NSInvocation对象并设置selector 和 target
    NSMethodSignature *signature = [self methodSignatureForSelector:aSelector];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature: signature];
    [invocation setSelector:aSelector];
    [invocation setTarget:self];

    /// 设置参数 从index=2开始
    [invocation setArgument:&action atIndex:2];
    [invocation setArgument:&target atIndex:3];
    [invocation setArgument:&event atIndex:4];

    /// 执行方法
    [invocation invoke];
    
    // 或者直接调用该方法实现方法调用和参数回调
    // [self swizzled_sendAction:action to:target forEvent:event];
}
@end

参考内容:

https://www.jianshu.com/p/ccb75e5277b7

posted @   qqcc1388  阅读(774)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
历史上的今天:
2018-03-29 iOS 渐变色实现,渐变色圆环,圆环进度条
2017-03-29 sublime比较好用的插件
2017-03-29 sublime text3 支持终端打开文件
2017-03-29 swift MBProgressHUD加载gif或者apng的动图
点击右上角即可分享
微信分享提示