iOS 高效開發(fā)之 - 全局避免 UIButton 頻繁點擊

原文地址
在項目中,為了避免按鈕被頻繁點擊,我們一般會操作 UIButton 的可點擊狀態(tài):enabled,但是如果需要處理的多了,會增加我們開發(fā)的工作量,也會增加邏輯不夠清晰下的遺漏處理導(dǎo)致按鈕無法點擊的重大問題,所以我們需要一個可以全局處理 UIButton 時間間隔點擊事件的方法,同時可以根據(jù)具體的需求,調(diào)整時間間隔的時間。

1、需求思考

  • 為了解決這個需求,我們需要考慮以下幾點:
  1. UIButton 使用的點擊方法,是 UIButton 獨有的,還是繼承于父類?
  2. 如果繼承于父類,處理父類的點擊方法,是否對父類的其他子類有影響?
  3. UIButton 有多種 Event,處理的時候是否會同時有多種 Event 有影響?
  4. 怎么實現(xiàn)點擊的時間間隔?
  5. 為了可擴展性,要可以單獨設(shè)置某個 Button 的時間間隔,以及是否使用增加的時間間隔方法

2、解決辦法

  • 針對以上面的思考,我們一一進行解決
  1. 通過查看 - (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents; 方法,我們可知:UIButton 使用到的方法,是來自其父類 UIControl
UICotrol
  1. UIControl 的子類有:UIButton、UITextField、UISlider、UIDatePicker、UISegmentedControl,也就是說,除了 UIButton ,這些類也是可以使用 Event 方法,所以在處理的時候,要過濾當(dāng)前處理的類
  2. 為了兼容多個 Event 的場景,要增加一個屬性,用來記錄當(dāng)前觸發(fā)的方法名
  3. 增加時間間隔的屬性,用于控制響應(yīng)事件的響應(yīng)間隔
  4. 暴露屬性,讓 Button 通過修改默認時間間隔和是否使用當(dāng)前類,實現(xiàn)單獨設(shè)置的需求

3、解決技術(shù)

  • 解決這個需求主要用到 Runtime 的 2 個地方:
  1. 使用 Runtimeobjc_setAssociatedObjectobjc_getAssociatedObject 重寫分類中成員變量的 settergetter 方法
  2. 使用 RuntimeMethod-Swizzing 交換原方法和自定義方法
  • 注意:
  • 里面涉及到 3 個坑:
  1. 在交換方法的時候,要使用單例,讓方法只交換一次,避免交換多次,沒有達到方法實際交互的效果。
  2. 要判斷當(dāng)前響應(yīng)的類是否是 UIButton[self isKindOfClass:[UIButton class]],避免 UIControl 的其他子類受到影響

4、代碼實現(xiàn)解析

Runtime 交換方法圖解

Runtime 交換方法

比如說在現(xiàn)有類中有兩個方法,方法 1 和 方法 2,當(dāng)經(jīng)過 Method - Swizzing 操作后,實際上就是修改方法選擇器 對應(yīng)實際的方法實現(xiàn),比如經(jīng)過 Method - Swizzing 操作后,相當(dāng)于方法 1 和方法 2 對應(yīng)的實現(xiàn)方法發(fā)生交換。

分類中屬性效果的實現(xiàn)

在分類定義實現(xiàn)的時候,不能直接添加屬性,但是可以通過 Runtime 手動添加 setter/getter 方法,達到分類可以添加屬性的效果。

isKindOfClass & isSubclassOfClass & isMemberOfClass 的區(qū)別

  • isKindOfClass:判斷對象是否為某類或者其派生類的實例(對象方法)
  • isSubclassOfClass:判斷對象是否為某類或者其派生類的實例(類方法)
  • isMemberOfClass:判斷對象是否為某個特定類的實例(對象方法)

使用到的 Runtime 中的方法

  • 獲得給定類的指定實例方法;

注意:如果給定的類或者父類沒有對應(yīng)的方法,會返回 nil 。

/** 
 cls:獲得哪個類中的方法
 SEL name:獲得方法的對象
*/

class_getInstanceMethod(Class  _Nullable __unsafe_unretained cls , SEL  _Nonnull name)
復(fù)制代碼
  • 重寫 getter 方法
/** 
 object:關(guān)聯(lián)的源對象
 key:關(guān)聯(lián)的 key
*/

objc_getAssociatedObject(<#id  _Nonnull object#>, <#const void * _Nonnull key#>);
復(fù)制代碼
  • 重寫 setter 方法
 /**
 object:關(guān)聯(lián)的源對象
 key:關(guān)聯(lián)的 key
 value:關(guān)聯(lián)對象的值,可以通過將此值置成 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#>)
復(fù)制代碼

具體代碼

注意:

這里我是使用自定義的方法,沒有像網(wǎng)上很多人使用系統(tǒng)的 +load 方法,這兩個區(qū)別是:系統(tǒng)的 +load 方法會自動調(diào)用,自定義方法需要自己調(diào)用;我認為自定義方法可以控制是否把功能加入項目,更靈活,這里根據(jù)個人愛好決定是否在 +load 方法中實現(xiàn)。

ps;推薦一個iOS技術(shù)交流群,歡迎你的加入563513413

有同學(xué)說為什么交換的是 sendAction: to: forEvent: 方法,而不是 addTarget: action: forControlEvents:,探究這個原因,我們要區(qū)分一下這兩個方法的作用:

  • sendAction: to: forEvent:

當(dāng)用戶點擊了按鈕,UIControl 會調(diào)用 sendAction:to:forEvent: 方法來將行為消息發(fā)送到 UIApplication 對象 ,再由 UIApplication對象調(diào)用 sendAction:to:fromSender:forEvent: 將消息分發(fā)到指定的 target 上,從而達到監(jiān)聽某個特定的對象 object, 對于特定的事件event做了什么特定的處理selector。這里涉及到的具體響應(yīng)鏈,就不詳說了,要不然就跑題了,可以自行 Google

  • addTarget: action: forControlEvents:

這個方法只是把action/target的映射加載到 UIControl 上面,并不會馬上執(zhí)行 selector

綜上所述可知:實際控制響應(yīng)間隔的時機需要在 sendAction: to: forEvent: 方法中,而不是在 addTarget: action: forControlEvents: 方法里。


#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface UIControl (KKClickInterval)
/// 點擊事件響應(yīng)的時間間隔,不設(shè)置或者大于 0 時為默認時間間隔
@property (nonatomic, assign) NSTimeInterval clickInterval;
/// 是否忽略響應(yīng)的時間間隔
@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;
/// 上次按鈕響應(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;也可以用來做檢查用,這里是為了避免源方法沒有存在的情況;如果方法沒有存在,我們則先嘗試添加被替換的方法的實現(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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容