需求
公司的内网测试环境因为网络做过了限制,比较卡,所以测试连续点击button
或者cell
时可能会多次push
控制器.如何在代码改动范围最小的范围内来解决这个问题呢?
方法:防止按钮重复暴力点击
程序中大量按钮没有做连续响应的校验,连续点击出现了很多不必要的问题,例如发表帖子操作,用户手快点击多次,就会导致同一帖子发布多次。
#import <UIKit/UIKit.h>
//默认时间间隔
#define defaultInterval 1
@interface UIButton (Swizzling)
//点击间隔
@property (nonatomic, assign) NSTimeInterval timeInterval;
//用于设置单个按钮不需要被hook
@property (nonatomic, assign) BOOL isIgnore;
@end
#import "UIButton+Swizzling.h"
#import "NSObject+Swizzling.h"
@implementation UIButton (Swizzling)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self methodSwizzlingWithOriginalSelector:@selector(sendAction:to:forEvent:) bySwizzledSelector:@selector(sure_SendAction:to:forEvent:)];
});
}
- (NSTimeInterval)timeInterval{
return [objc_getAssociatedObject(self, _cmd) doubleValue];
}
- (void)setTimeInterval:(NSTimeInterval)timeInterval{
objc_setAssociatedObject(self, @selector(timeInterval), @(timeInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//当按钮点击事件sendAction 时将会执行sure_SendAction
- (void)sure_SendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{
if (self.isIgnore) {
//不需要被hook
[self sure_SendAction:action to:target forEvent:event];
return;
}
if ([NSStringFromClass(self.class) isEqualToString:@"UIButton"]) {
self.timeInterval =self.timeInterval == 0 ?defaultInterval:self.timeInterval;
if (self.isIgnoreEvent){
return;
}else if (self.timeInterval > 0){
[self performSelector:@selector(resetState) withObject:nil afterDelay:self.timeInterval];
}
}
//此处 methodA和methodB方法IMP互换了,实际上执行 sendAction;所以不会死循环
self.isIgnoreEvent = YES;
[self sure_SendAction:action to:target forEvent:event];
}
//runtime 动态绑定 属性
- (void)setIsIgnoreEvent:(BOOL)isIgnoreEvent{
// 注意BOOL类型 需要用OBJC_ASSOCIATION_RETAIN_NONATOMIC 不要用错,否则set方法会赋值出错
objc_setAssociatedObject(self, @selector(isIgnoreEvent), @(isIgnoreEvent), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)isIgnoreEvent{
//_cmd == @select(isIgnore); 和set方法里一致
return [objc_getAssociatedObject(self, _cmd) boolValue];
}
- (void)setIsIgnore:(BOOL)isIgnore{
// 注意BOOL类型 需要用OBJC_ASSOCIATION_RETAIN_NONATOMIC 不要用错,否则set方法会赋值出错
objc_setAssociatedObject(self, @selector(isIgnore), @(isIgnore), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)isIgnore{
//_cmd == @select(isIgnore); 和set方法里一致
return [objc_getAssociatedObject(self, _cmd) boolValue];
}
- (void)resetState{
[self setIsIgnoreEvent:NO];
}
@end
方法一(不推荐)
使用分类+运行时
来替换Button
的点击方法,可以设置一个时间间隔
,点击过后开启一个计时器,并关闭按钮的enable
属性,计时完成后再打开enable
.至于cell
暂时没有什么好点子.
优点:
- 改动比较小
缺点:
- 首先他要启动不少定时器
- 如果点击完成后,快速返回则不能再次点击!必须等计时器执行完毕
方法二(能解决问题,但不优雅)
一般我们的网络请求框架都会封装
两到三层AFN
,通过大量的block进行嵌套来完成一系列的请求
工作.所以我们可以设置一个全局id
变量,用来记录当前点击的button
和cell
,在最底层的网络请求开始时将这个按钮/cell的enable
关闭,成功后再次打开.
优点:
- 能解决问题
缺点:
- 记录cell点击,改动也不小
- 并发的问题
- 项目架构可能也有不适用的地方
方法三(推荐)
我们可以控制UINavigationController
中的push
方法,代码很简单,只需要判断当前的控制器和推入的控制器是否是相同的
一个class
就好了.但有一个缺点,若本来就想push
一个相同的控制器就很尴尬了.代码如下:
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
//cell因为网络请求延迟而多次push同一页面
if (![[super topViewController] isKindOfClass:[viewController class]]) { // 如果和上一个控制器一样,隔绝此操作
[super pushViewController:viewController animated:animated];
}
}
方法四(强烈推荐)
链接,这位前辈的方式很巧妙,也解决了我上面的缺点
.
override func performSegueWithIdentifier(identifier: String, sender: AnyObject?) {
if let navigationController = navigationController {
guard navigationController.topViewController == self else {
return
}
}
super.performSegueWithIdentifier(identifier, sender: sender)
}