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以及对应的区分和示例,可以查看该作者分享的内容,讲解的特别好,并且有详细的例子分析
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
参考内容:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!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的动图