項目中常會遇到在按鈕的點擊事件中去執行一些耗時操作。如果處理不當經常會出現連續多次點擊push多次的情況,造成不好的用戶體驗。
一種情況是用戶快速連續點擊,這種情況無法避免。另一種情況是點擊一次后響應時間太長,導致用戶一直停留在點擊界面,也會去再此點擊按鈕確認是否能執行下一個界面。雖然我們可以在用戶點擊一次后去顯示一個HUB窗口隔絕用戶操作,但我們並不清楚服務器去響應這個操作究竟需要多長時間,如果HUB指示器顯示時間太長會顯得響應特別慢,如果太短,用戶很可能在指示器消失后再去點擊Button,這時也會出現重復push多次。
通常有三種方式解決此問題。
一、先說一種最不推薦使用的方法。
如果你的Navigation是自定義的,可以重寫- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated方法,在此方法中做處理,代碼如下:
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
if (![[super topViewController] isKindOfClass:[viewController class]]) { // 如果和上一個控制器一樣,隔絕此操作
[super pushViewController:viewController animated:animated];
}
}
此中方法可以防止多次重復push,但如果你想push的下一個控制器恰好和上一個控制器類型(Class)一樣,就不會push成功。所以並不推薦使用此方法。
二、第二種方式,點擊一次后將button的enabled變為NO。
具體思路是:如果在button的點擊事件中要做耗時操作,可能是網絡請求和請求成功后的數據處理比較耗時。如果只是單純的在請求成功和失敗的回調中寫一遍btn.enabled = YES就會發現如果連續點擊還是會出現push多次的情況。原因可能是push操作需要時間去執行,我們在這段時間連續快速點擊還是會導致push多次,感興趣的可以去試一下。我的思路是在請求失敗后單獨將buuton的enabled設為YES,請求成功后不對button做任何操作。最后在- viewWillAppear方法中將button的enabled設為YES,以防在pop回本控制器的時候button不可點擊。下面上代碼:

其中的WXDHTTPTool是封裝了一層AFNetworking的網絡請求類。
在- viewWillAppear方法中將button的enabled設為YES:

這樣做也可以實現防止重復push的問題,但並不能做到一勞永逸。每個控制器都需要這么去做,雖然代碼並不復雜,但方式並不優雅。
三、最優雅的方式,使用Runtime監聽點擊事件,忽略重復點擊,設置一個eventTimeInterval屬性,使其規定時間內只響應一次點擊事件。廢話不多說,上代碼。
1、為UIButton創建一個分類,這里我起名為WXD。
2、.h文件:添加一個屬性eventTimeInterval,用來設置button點擊間隔時間。
#import <UIKit/UIKit.h>
@interface UIButton (WXD)
/**
* 為按鈕添加點擊間隔 eventTimeInterval秒
*/
@property (nonatomic, assign) NSTimeInterval eventTimeInterval;
@end
3、.m文件:需要import<objc/runtime.h>庫。
#import "UIButton+WXD.h"
#import <objc/runtime.h>
#define defaultInterval 1 //默認時間間隔
@interface UIButton ()
/**
* bool YES 忽略點擊事件 NO 允許點擊事件
*/
@property (nonatomic, assign) BOOL isIgnoreEvent;
@end
@implementation UIButton (WXD)
static const char *UIControl_eventTimeInterval = "UIControl_eventTimeInterval";
static const char *UIControl_enventIsIgnoreEvent = "UIControl_enventIsIgnoreEvent";
// runtime 動態綁定 屬性
- (void)setIsIgnoreEvent:(BOOL)isIgnoreEvent
{
objc_setAssociatedObject(self, UIControl_enventIsIgnoreEvent, @(isIgnoreEvent), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)isIgnoreEvent{
return [objc_getAssociatedObject(self, UIControl_enventIsIgnoreEvent) boolValue];
}
- (NSTimeInterval)eventTimeInterval
{
return [objc_getAssociatedObject(self, UIControl_eventTimeInterval) doubleValue];
}
- (void)setEventTimeInterval:(NSTimeInterval)eventTimeInterval
{
objc_setAssociatedObject(self, UIControl_eventTimeInterval, @(eventTimeInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
+ (void)load
{
// Method Swizzling
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL selA = @selector(sendAction:to:forEvent:);
SEL selB = @selector(_wxd_sendAction:to:forEvent:);
Method methodA = class_getInstanceMethod(self,selA);
Method methodB = class_getInstanceMethod(self, selB);
BOOL isAdd = class_addMethod(self, selA, method_getImplementation(methodB), method_getTypeEncoding(methodB));
if (isAdd) {
class_replaceMethod(self, selB, method_getImplementation(methodA), method_getTypeEncoding(methodA));
}else{
//添加失敗了 說明本類中有methodB的實現,此時只需要將methodA和methodB的IMP互換一下即可。
method_exchangeImplementations(methodA, methodB);
}
});
}
- (void)_wxd_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
self.eventTimeInterval = self.eventTimeInterval == 0 ? defaultInterval : self.eventTimeInterval;
if (self.isIgnoreEvent){
return;
}else if (self.eventTimeInterval > 0){
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.eventTimeInterval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self setIsIgnoreEvent:NO];
});
}
self.isIgnoreEvent = YES;
// 這里看上去會陷入遞歸調用死循環,但在運行期此方法是和sendAction:to:forEvent:互換的,相當於執行sendAction:to:forEvent:方法,所以並不會陷入死循環。
[self _wxd_sendAction:action to:target forEvent:event];
}
最后就可以去在想要設置點擊間隔的控制器引入分類的頭文件,可以手動設置button.eventTimeInterval = 點擊間隔,也可以不設任何值去使用默認的時間間隔。還可以在pch文件中引入分類頭文件,讓項目中所有button都添加此分類。
寫在最后:如果有意見或者更優雅的解決方式,歡迎溝通交流。
