iOS 高效开发之 - 全局避免 UIButton 频繁点击

作者:Gavin_Kang
链接:https://juejin.cn/post/6899057632716750855


在项目中,为了避免按钮被频繁点击,我们一般会操作 UIButton 的可点击状态:enabled,但是如果需要处理的多了,会增加我们开发的工作量,也会增加逻辑不够清晰下的遗漏处理导致按钮无法点击的重大问题,所以我们需要一个可以全局处理 UIButton 时间间隔点击事件的方法,同时可以根据具体的需求,调整时间间隔的时间。

1、需求思考

  • 为了解决这个需求,我们需要考虑以下几点:
  1. UIButton 使用的点击方法,是 UIButton 独有的,还是继承于父类?
  2. 如果继承于父类,处理父类的点击方法,是否对父类的其他子类有影响?
  3. UIButton 有多种 Event,处理的时候是否会同时有多种 Event 有影响?
  4. 怎么实现点击的时间间隔?
  5. 为了可扩展性,要可以单独设置某个 Button 的时间间隔,以及是否使用增加的时间间隔方法

2、解决办法

  • 针对以上面的思考,我们一一进行解决
  1. 通过查看 - (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents; 方法,我们可知:UIButton 使用到的方法,是来自其父类 UIControl
  1. UIControl 的子类有:UIButton、UITextField、UISlider、UIDatePicker、UISegmentedControl,也就是说,除了 UIButton ,这些类也是可以使用 Event 方法,所以在处理的时候,要过滤当前处理的类
  2. 为了兼容多个 Event 的场景,要增加一个属性,用来记录当前触发的方法名
  3. 增加时间间隔的属性,用于控制响应事件的响应间隔
  4. 暴露属性,让 Button 通过修改默认时间间隔和是否使用当前类,实现单独设置的需求

3、解决技术

  • 解决这个需求主要用到 Runtime 的 2 个地方:
  1. 使用 Runtimeobjc_setAssociatedObjectobjc_getAssociatedObject 重写分类中成员变量的 settergetter 方法
  2. 使用 RuntimeMethod-Swizzing 交换原方法和自定义方法
  • 注意:
  • 里面涉及到 3 个坑:
  1. 在交换方法的时候,要使用单例,让方法只交换一次,避免交换多次,没有达到方法实际交互的效果。
  2. 要判断当前响应的类是否是 UIButton[self isKindOfClass:[UIButton class]],避免 UIControl 的其他子类受到影响

4、代码实现解析

Runtime 交换方法图解

Runtime 交换方法

比如说在现有类中有两个方法,方法 1 和 方法 2,当经过 Method - Swizzing 操作后,实际上就是修改方法选择器 对应实际的方法实现,比如经过 Method - Swizzing 操作后,相当于方法 1 和方法 2 对应的实现方法发生交换。

分类中属性效果的实现

在分类定义实现的时候,不能直接添加属性,但是可以通过 Runtime 手动添加 setter/getter 方法,达到分类可以添加属性的效果。

isKindOfClass & isSubclassOfClass & isMemberOfClass 的区别

  • isKindOfClass:判断对象是否为某类或者其派生类的实例(对象方法)
  • isSubclassOfClass:判断对象是否为某类或者其派生类的实例(类方法)
  • isMemberOfClass:判断对象是否为某个特定类的实例(对象方法)

使用到的 Runtime 中的方法

  • 获得给定类的指定实例方法;

注意:如果给定的类或者父类没有对应的方法,会返回 nil

/** 
 cls:获得哪个类中的方法
 SEL name:获得方法的对象
*/

class_getInstanceMethod(Class  _Nullable __unsafe_unretained cls , SEL  _Nonnull name)
  • 重写 getter 方法
/** 
 object:关联的源对象
 key:关联的 key
*/

objc_getAssociatedObject(<#id  _Nonnull object#>, <#const void * _Nonnull key#>);
  • 重写 setter 方法
 /**
 object:关联的源对象
 key:关联的 key
 value:关联对象的值,可以通过将此值置成 nil 来清除关联
 policy:关联的策略
*/
objc_setAssociatedObject(<#id  _Nonnull object#>, <#const void * _Nonnull key#>, <#id  _Nullable value#>, <#objc_AssociationPolicy policy#>)

具体代码

注意:

这里我是使用自定义的方法,没有像网上很多人使用系统的 +load 方法,这两个区别是:系统的 +load 方法会自动调用,自定义方法需要自己调用;我认为自定义方法可以控制是否把功能加入项目,更灵活,这里根据个人爱好决定是否在 +load 方法中实现。

有同学说为什么交换的是 sendAction: to: forEvent: 方法,而不是 addTarget: action: forControlEvents:,探究这个原因,我们要区分一下这两个方法的作用:

  • sendAction: to: forEvent:

当用户点击了按钮,UIControl 会调用 sendAction:to:forEvent: 方法来将行为消息发送到 UIApplication 对象 ,再由 UIApplication对象调用 sendAction:to:fromSender:forEvent: 将消息分发到指定的 target 上,从而达到监听某个特定的对象 object, 对于特定的事件event做了什么特定的处理selector。这里涉及到的具体响应链,就不详说了,要不然就跑题了,可以自行 Google

  • addTarget: action: forControlEvents:

这个方法只是把action/target的映射加载到 UIControl 上面,并不会马上执行 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);

        //   如果发现方法已经存在,返回NO;也可以用来做检查用,这里是为了避免源方法没有存在的情况;如果方法没有存在,我们则先尝试添加被替换的方法的实现
        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开发,全栈发展,欢迎入驻,共同进步!(群内会免费提供一些群主收藏的免费学习书籍资料以及整理好的几百道面试题和答案文档!)

不管你是小白还是大牛欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!!!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容