iOS 你還在為UIButton 頻繁點(diǎn)擊而煩惱嗎院领?

在項(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章姓、UISliderUIDatePicker识埋、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.使用 Runtimeobjc_setAssociatedObjectobjc_getAssociatedObject 重寫分類中成員變量的 settergetter方法

2.使用RuntimeMethod-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)鏈,就不詳說了淫痰,要不然就跑題了毅臊,可以自行 Google

  • 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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市础倍,隨后出現(xiàn)的幾起案子烛占,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件忆家,死亡現(xiàn)場離奇詭異犹菇,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)芽卿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門揭芍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人卸例,你說我怎么就攤上這事称杨。” “怎么了筷转?”我有些...
    開封第一講書人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵姑原,是天一觀的道長。 經(jīng)常有香客問我呜舒,道長锭汛,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任袭蝗,我火速辦了婚禮唤殴,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘呻袭。我一直安慰自己眨八,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開白布左电。 她就那樣靜靜地躺著廉侧,像睡著了一般。 火紅的嫁衣襯著肌膚如雪篓足。 梳的紋絲不亂的頭發(fā)上段誊,一...
    開封第一講書人閱讀 49,046評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音栈拖,去河邊找鬼连舍。 笑死,一個(gè)胖子當(dāng)著我的面吹牛涩哟,可吹牛的內(nèi)容都是我干的索赏。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼贴彼,長吁一口氣:“原來是場噩夢啊……” “哼潜腻!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起器仗,我...
    開封第一講書人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤融涣,失蹤者是張志新(化名)和其女友劉穎童番,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體威鹿,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡剃斧,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了忽你。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片幼东。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖檀夹,靈堂內(nèi)的尸體忽然破棺而出筋粗,到底是詐尸還是另有隱情,我是刑警寧澤炸渡,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站蚌堵,受9級(jí)特大地震影響买决,放射性物質(zhì)發(fā)生泄漏吼畏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一泻蚊、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧性雄,春花似錦没卸、人聲如沸秒旋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至细卧,卻和暖如春尉桩,著一層夾襖步出監(jiān)牢的瞬間贪庙,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來泰國打工插勤, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人农尖。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像盛卡,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子滑沧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容