在項(xiàng)目中,為了避免按鈕被頻繁點(diǎn)擊够吩,我們一般會(huì)操作 UIButton 的可點(diǎn)擊狀態(tài):enabled
比然,但是如果需要處理的多了,會(huì)增加我們開發(fā)的工作量周循,也會(huì)增加邏輯不夠清晰下的遺漏處理導(dǎo)致按鈕無法點(diǎn)擊的重大問題强法,所以我們需要一個(gè)可以全局處理 UIButton 時(shí)間間隔點(diǎn)擊事件的方法万俗,同時(shí)可以根據(jù)具體的需求,調(diào)整時(shí)間間隔的時(shí)間饮怯。
一闰歪、為了解決這個(gè)需求,我們需要考慮以下幾點(diǎn):
-
UIButton
使用的點(diǎn)擊方法蓖墅,是UIButton
獨(dú)有的库倘,還是繼承于父類? - 如果繼承于父類论矾,處理父類的點(diǎn)擊方法教翩,是否對(duì)父類的其他子類有影響?
-
UIButton
有多種Event
拇囊,處理的時(shí)候是否會(huì)同時(shí)有多種Event
有影響迂曲? - 怎么實(shí)現(xiàn)點(diǎn)擊的時(shí)間間隔?
- 為了可擴(kuò)展性寥袭,要可以單獨(dú)設(shè)置某個(gè)
Button
的時(shí)間間隔路捧,以及是否使用增加的時(shí)間間隔方法
二、解決辦法
- 針對(duì)以上面的思考传黄,我們一一進(jìn)行解決
1.通過查看
- (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents
; 方法杰扫,我們可知:UIButton 使用到的方法,是來自其父類 `UIControl
2.
UIControl
的子類有:UIButton
膘掰、UITextField
章姓、UISlider
、UIDatePicker
识埋、UISegmentedControl
凡伊,也就是說,除了UIButton
,這些類也是可以使用Event
方法窒舟,所以在處理的時(shí)候系忙,要過濾當(dāng)前處理的類
3.為了兼容多個(gè)
Event
的場景,要增加一個(gè)屬性惠豺,用來記錄當(dāng)前觸發(fā)的方法名
4.增加時(shí)間間隔的屬性银还,用于控制響應(yīng)事件的響應(yīng)間隔
5.暴露屬性,讓
Button
通過修改默認(rèn)時(shí)間間隔和是否使用當(dāng)前類洁墙,實(shí)現(xiàn)單獨(dú)設(shè)置的需求
三蛹疯、解決技術(shù)
解決這個(gè)需求主要用到Runtime
的 2 個(gè)地方:
1.使用
Runtime
的objc_setAssociatedObject
和objc_getAssociatedObject
重寫分類中成員變量的setter
和getter
方法
2.使用
Runtime
的Method-Swizzing
交換原方法和自定義方法
注意:
里面涉及到幾個(gè)坑:
1.在交換方法的時(shí)候,要使用單例热监,讓方法只交換一次捺弦,避免交換多次,沒有達(dá)到方法實(shí)際交互的效果
2.要判斷當(dāng)前響應(yīng)的類是否是
UIButton
:[self isKindOfClass:[UIButton class]]
,避免UIControl
的其他子類受到影響
四羹呵、代碼實(shí)現(xiàn)解析
Runtime 交換方法圖解
比如說在現(xiàn)有類中有兩個(gè)方法骂际,方法 1 和 方法 2,當(dāng)經(jīng)過
Method - Swizzing
操作后冈欢,實(shí)際上就是修改方法選擇器 對(duì)應(yīng)實(shí)際的方法實(shí)現(xiàn)歉铝,比如經(jīng)過Method - Swizzing
操作后,相當(dāng)于方法 1 和方法 2 對(duì)應(yīng)的實(shí)現(xiàn)方法發(fā)生交換
分類中屬性效果的實(shí)現(xiàn)
在分類定義實(shí)現(xiàn)的時(shí)候凑耻,不能直接添加屬性太示,但是可以通過
Runtime
手動(dòng)添加setter/getter
方法,達(dá)到分類可以添加屬性的效果香浩。
isKindOfClass & isSubclassOfClass & isMemberOfClass 的區(qū)別
isKindOfClass
:判斷對(duì)象是否為某類或者其派生類的實(shí)例(對(duì)象方法)isSubclassOfClass
:判斷對(duì)象是否為某類或者其派生類的實(shí)例(類方法)isMemberOfClass
:判斷對(duì)象是否為某個(gè)特定類的實(shí)例(對(duì)象方法)
使用到的 Runtime 中的方法
- 獲得給定類的指定實(shí)例方法
注意:
如果給定的類或者父類沒有對(duì)應(yīng)的方法类缤,會(huì)返回nil
/**
cls:獲得哪個(gè)類中的方法
SEL name:獲得方法的對(duì)象
*/
class_getInstanceMethod(Class _Nullable __unsafe_unretained cls , SEL _Nonnull name)
- 重寫
getter
方法
/**
object:關(guān)聯(lián)的源對(duì)象
key:關(guān)聯(lián)的 key
*/
objc_getAssociatedObject(<#id _Nonnull object#>, <#const void * _Nonnull key#>);
- 重寫
setter
方法
/**
object:關(guān)聯(lián)的源對(duì)象
key:關(guān)聯(lián)的 key
value:關(guān)聯(lián)對(duì)象的值,可以通過將此值置成 nil 來清除關(guān)聯(lián)
policy:關(guān)聯(lián)的策略
*/
objc_setAssociatedObject(<#id _Nonnull object#>, <#const void * _Nonnull key#>, <#id _Nullable value#>, <#objc_AssociationPolicy policy#>)
五邻吭、具體代碼
注意:
這里我是使用自定義的方法餐弱,沒有像網(wǎng)上很多人使用系統(tǒng)的
+load
方法,這兩個(gè)區(qū)別是:系統(tǒng)的+load
方法會(huì)自動(dòng)調(diào)用囱晴,自定義方法需要自己調(diào)用膏蚓;我認(rèn)為自定義方法可以控制是否把功能加入項(xiàng)目,更靈活畸写,這里根據(jù)個(gè)人愛好決定是否在+load
方法中實(shí)現(xiàn)
有同學(xué)說為什么交換的是
sendAction: to: forEvent:
方法驮瞧,而不是addTarget: action: forControlEvents:
,探究這個(gè)原因枯芬,我們要區(qū)分一下這兩個(gè)方法的作用:
sendAction: to: forEvent:
當(dāng)用戶點(diǎn)擊了按鈕论笔,
UIControl
會(huì)調(diào)用sendAction:to:forEvent:
方法來將行為消息發(fā)送到UIApplication
對(duì)象 ,再由UIApplication
對(duì)象調(diào)用sendAction:to:fromSender:forEvent:
將消息分發(fā)到指定的target
上千所,從而達(dá)到監(jiān)聽某個(gè)特定的對(duì)象object
, 對(duì)于特定的事件event
做了什么特定的處理selector
狂魔。這里涉及到的具體響應(yīng)鏈,就不詳說了淫痰,要不然就跑題了毅臊,可以自行
addTarget: action: forControlEvents:
這個(gè)方法只是把
action/target
的映射加載到UIControl
上面,并不會(huì)馬上執(zhí)行selector
綜上所述可知:實(shí)際控制響應(yīng)間隔的時(shí)機(jī)需要在sendAction: to: forEvent:
方法中黑界,而不是在addTarget: action: forControlEvents:
方法里
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UIControl (KKClickInterval)
/// 點(diǎn)擊事件響應(yīng)的時(shí)間間隔,不設(shè)置或者大于 0 時(shí)為默認(rèn)時(shí)間間隔
@property (nonatomic, assign) NSTimeInterval clickInterval;
/// 是否忽略響應(yīng)的時(shí)間間隔
@property (nonatomic, assign) BOOL ignoreClickInterval;
+ (void)kk_exchangeClickMethod;
@end
NS_ASSUME_NONNULL_END
#import "UIControl+KKClickInterval.h"
#import <objc/runtime.h>
static double kDefaultInterval = 0.5;
@interface UIControl ()
/// 是否可以點(diǎn)擊
@property (nonatomic, assign) BOOL isIgnoreClick;
/// 上次按鈕響應(yīng)的方法名
@property (nonatomic, strong) NSString *oldSELName;
@end
@implementation UIControl (KKClickInterval)
+ (void)kk_exchangeClickMethod {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 獲得方法選擇器
SEL originalSel = @selector(sendAction:to:forEvent:);
SEL newSel = @selector(kk_sendClickIntervalAction:to:forEvent:);
//獲得方法
Method originalMethod = class_getInstanceMethod(self , originalSel);
Method newMethod = class_getInstanceMethod(self , newSel);
// 如果發(fā)現(xiàn)方法已經(jīng)存在皂林,返回NO朗鸠;也可以用來做檢查用,這里是為了避免源方法沒有存在的情況;如果方法沒有存在,我們則先嘗試添加被替換的方法的實(shí)現(xiàn)
BOOL isAddNewMethod = class_addMethod(self, originalSel, method_getImplementation(newMethod), "v@:");
if (isAddNewMethod) {
class_replaceMethod(self, newSel, method_getImplementation(originalMethod), "v@:");
} else {
method_exchangeImplementations(originalMethod, newMethod);
}
});
}
- (void)kk_sendClickIntervalAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
if ([self isKindOfClass:[UIButton class]] && !self.ignoreClickInterval) {
if (self.clickInterval <= 0) {
self.clickInterval = kDefaultInterval;
};
NSString *currentSELName = NSStringFromSelector(action);
if (self.isIgnoreClick && [self.oldSELName isEqualToString:currentSELName]) {
return;
}
if (self.clickInterval > 0) {
self.isIgnoreClick = YES;
self.oldSELName = currentSELName;
[self performSelector:@selector(kk_ignoreClickState:)
withObject:@(NO)
afterDelay:self.clickInterval];
}
}
[self kk_sendClickIntervalAction:action to:target forEvent:event];
}
- (void)kk_ignoreClickState:(NSNumber *)ignoreClickState {
self.isIgnoreClick = ignoreClickState.boolValue;
self.oldSELName = @"";
}
- (NSTimeInterval)clickInterval {
return [objc_getAssociatedObject(self, _cmd) doubleValue];
}
- (void)setClickInterval:(NSTimeInterval)clickInterval {
objc_setAssociatedObject(self, @selector(clickInterval), @(clickInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)isIgnoreClick {
return [objc_getAssociatedObject(self, _cmd) boolValue];
}
- (void)setIsIgnoreClick:(BOOL)isIgnoreClick {
objc_setAssociatedObject(self, @selector(isIgnoreClick), @(isIgnoreClick), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)ignoreClickInterval {
return [objc_getAssociatedObject(self, _cmd) boolValue];
}
- (void)setIgnoreClickInterval:(BOOL)ignoreClickInterval {
objc_setAssociatedObject(self, @selector(ignoreClickInterval), @(ignoreClickInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)oldSELName {
return objc_getAssociatedObject(self, _cmd);
}
- (void)setOldSELName:(NSString *)oldSELName {
objc_setAssociatedObject(self, @selector(oldSELName), oldSELName, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
源文作者:Gavin_Kang
鏈接:https://juejin.cn/post/6899057632716750855