作者:Gavin_Kang
鏈接:https://juejin.cn/post/6899057632716750855
在項目中,為了避免按鈕被頻繁點擊赞别,我們一般會操作 UIButton 的可點擊狀態(tài):
enabled
识啦,但是如果需要處理的多了,會增加我們開發(fā)的工作量相满,也會增加邏輯不夠清晰下的遺漏處理導致按鈕無法點擊的重大問題,所以我們需要一個可以全局處理 UIButton 時間間隔點擊事件的方法饿肺,同時可以根據(jù)具體的需求,調(diào)整時間間隔的時間卷中。
1矛双、需求思考
- 為了解決這個需求,我們需要考慮以下幾點:
-
UIButton
使用的點擊方法蟆豫,是UIButton
獨有的议忽,還是繼承于父類? - 如果繼承于父類十减,處理父類的點擊方法栈幸,是否對父類的其他子類有影響?
-
UIButton
有多種Event
帮辟,處理的時候是否會同時有多種Event
有影響速址? - 怎么實現(xiàn)點擊的時間間隔?
- 為了可擴展性由驹,要可以單獨設置某個
Button
的時間間隔芍锚,以及是否使用增加的時間間隔方法
2、解決辦法
- 針對以上面的思考蔓榄,我們一一進行解決
- 通過查看
- (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;
方法并炮,我們可知:UIButton 使用到的方法,是來自其父類UIControl
-
UIControl
的子類有:UIButton甥郑、UITextField逃魄、UISlider、UIDatePicker澜搅、UISegmentedControl
伍俘,也就是說,除了UIButton
,這些類也是可以使用Event
方法店展,所以在處理的時候养篓,要過濾當前處理的類 - 為了兼容多個
Event
的場景秃流,要增加一個屬性赂蕴,用來記錄當前觸發(fā)的方法名 - 增加時間間隔的屬性,用于控制響應事件的響應間隔
- 暴露屬性舶胀,讓
Button
通過修改默認時間間隔和是否使用當前類概说,實現(xiàn)單獨設置的需求
3、解決技術
- 解決這個需求主要用到
Runtime
的 2 個地方:
- 使用
Runtime
的objc_setAssociatedObject
和objc_getAssociatedObject
重寫分類中成員變量的setter
和getter
方法 - 使用
Runtime
的Method-Swizzing
交換原方法和自定義方法
- 注意:
- 里面涉及到 3 個坑:
- 在交換方法的時候嚣伐,要使用單例糖赔,讓方法只交換一次,避免交換多次轩端,沒有達到方法實際交互的效果放典。
- 要判斷當前響應的類是否是
UIButton
:[self isKindOfClass:[UIButton class]]
,避免UIControl
的其他子類受到影響
4、代碼實現(xiàn)解析
Runtime 交換方法圖解
比如說在現(xiàn)有類中有兩個方法奋构,方法 1 和 方法 2壳影,當經(jīng)過 Method - Swizzing
操作后,實際上就是修改方法選擇器 對應實際的方法實現(xiàn)弥臼,比如經(jīng)過 Method - Swizzing
操作后宴咧,相當于方法 1 和方法 2 對應的實現(xiàn)方法發(fā)生交換。
分類中屬性效果的實現(xiàn)
在分類定義實現(xiàn)的時候径缅,不能直接添加屬性掺栅,但是可以通過 Runtime
手動添加 setter/getter
方法,達到分類可以添加屬性的效果纳猪。
isKindOfClass & isSubclassOfClass & isMemberOfClass 的區(qū)別
-
isKindOfClass
:判斷對象是否為某類或者其派生類的實例(對象方法) -
isSubclassOfClass
:判斷對象是否為某類或者其派生類的實例(類方法) -
isMemberOfClass
:判斷對象是否為某個特定類的實例(對象方法)
使用到的 Runtime 中的方法
- 獲得給定類的指定實例方法氧卧;
注意:如果給定的類或者父類沒有對應的方法,會返回 nil
兆旬。
/**
cls:獲得哪個類中的方法
SEL name:獲得方法的對象
*/
class_getInstanceMethod(Class _Nullable __unsafe_unretained cls , SEL _Nonnull name)
- 重寫
getter
方法
/**
object:關聯(lián)的源對象
key:關聯(lián)的 key
*/
objc_getAssociatedObject(<#id _Nonnull object#>, <#const void * _Nonnull key#>);
- 重寫
setter
方法
/**
object:關聯(lián)的源對象
key:關聯(lián)的 key
value:關聯(lián)對象的值假抄,可以通過將此值置成 nil 來清除關聯(lián)
policy:關聯(lián)的策略
*/
objc_setAssociatedObject(<#id _Nonnull object#>, <#const void * _Nonnull key#>, <#id _Nullable value#>, <#objc_AssociationPolicy policy#>)
具體代碼
注意:
這里我是使用自定義的方法,沒有像網(wǎng)上很多人使用系統(tǒng)的 +load
方法丽猬,這兩個區(qū)別是:系統(tǒng)的 +load
方法會自動調(diào)用宿饱,自定義方法需要自己調(diào)用;我認為自定義方法可以控制是否把功能加入項目脚祟,更靈活谬以,這里根據(jù)個人愛好決定是否在 +load
方法中實現(xiàn)。
有同學說為什么交換的是 sendAction: to: forEvent:
方法由桌,而不是 addTarget: action: forControlEvents:
为黎,探究這個原因,我們要區(qū)分一下這兩個方法的作用:
-
sendAction: to: forEvent:
:
當用戶點擊了按鈕行您,UIControl
會調(diào)用 sendAction:to:forEvent:
方法來將行為消息發(fā)送到 UIApplication
對象 铭乾,再由 UIApplication
對象調(diào)用 sendAction:to:fromSender:forEvent:
將消息分發(fā)到指定的 target
上,從而達到監(jiān)聽某個特定的對象 object
, 對于特定的事件event
做了什么特定的處理selector
娃循。這里涉及到的具體響應鏈炕檩,就不詳說了,要不然就跑題了捌斧,可以自行 Google
笛质。
addTarget: action: forControlEvents:
這個方法只是把action/target
的映射加載到 UIControl
上面,并不會馬上執(zhí)行 selector
捞蚂。
綜上所述可知:實際控制響應間隔的時機需要在 sendAction: to: forEvent:
方法中妇押,而不是在 addTarget: action: forControlEvents:
方法里。
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UIControl (KKClickInterval)
/// 點擊事件響應的時間間隔姓迅,不設置或者大于 0 時為默認時間間隔
@property (nonatomic, assign) NSTimeInterval clickInterval;
/// 是否忽略響應的時間間隔
@property (nonatomic, assign) BOOL ignoreClickInterval;
+ (void)kk_exchangeClickMethod;
@end
NS_ASSUME_NONNULL_END
#import "UIControl+KKClickInterval.h"
#import <objc/runtime.h>
static double kDefaultInterval = 2.5;
@interface UIControl ()
/// 是否可以點擊
@property (nonatomic, assign) BOOL isIgnoreClick;
/// 上次按鈕響應的方法名
@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俊马;也可以用來做檢查用,這里是為了避免源方法沒有存在的情況;如果方法沒有存在,我們則先嘗試添加被替換的方法的實現(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
結交人脈
最后推薦個我的iOS交流群:789143298
'有一個共同的圈子很重要,結識人脈肩杈!里面都是iOS開發(fā)潭袱,全棧發(fā)展,歡迎入駐锋恬,共同進步M突弧(群內(nèi)會免費提供一些群主收藏的免費學習書籍資料以及整理好的幾百道面試題和答案文檔!)
- ——點擊加入:iOS開發(fā)交流群
以下資料在群文件可自行下載