iOS 用RunTime来提升按钮的体验

时间:2023-12-16 17:15:50

用RunTime来提升按钮的体验

载请标明出处:http://blog.csdn.net/sk719887916/article/details/52597388,作者:Ryan

iOS 用RunTime来提升按钮的体验

经常处理按钮问题都是手动开和关,相信很多开发的同学跟我们一样,但是作为一个技术上的懒癌患者,我还是找到了懒癌的福音,现在分享给大家一个直接在消息发送端截断的方法

iOS中的按钮事件机制 >> Target-Action机制

  • 用户点击时,产生一个按钮点击事件消息
  • 这个消息发送给注册的Target处理
  • Target接收到消息,然后查找自己的SEL对应的具体实现IMP正儿八经的去处理点击事件

实际上该点击消息包含三个东西:

  • Target处理者
  • SEL方法Id
  • 按钮事件当时触发时的状态

      typedef NS_OPTIONS(NSUInteger, UIControlState) {
      UIControlStateNormal       = 0,
      UIControlStateHighlighted  = 1 << 0,                  // used when UIControl    isHighlighted is set
      UIControlStateDisabled     = 1 << 1,
      UIControlStateSelected     = 1 << 2,                  // flag usable by app     (see below)
      UIControlStateFocused NS_ENUM_AVAILABLE_IOS(9_0) = 1 << 3, // Applicable    only when the screen supports focus
      UIControlStateApplication  = 0x00FF0000,              // additional flags   available for application use
      UIControlStateReserved     = 0xFF000000               // flags reserved for         internal framework use
      };
    

已经知道点击按钮时候,会产生一个包装了Target、SEL、按钮事件状态三个东西的消息发送给Target处理.

问题: 是谁来包装UIButton的点击事件消息,并且完成发送消息了?

这个是解决连续点击按钮的关键问题所在,必须搞清楚。因为如果搞清楚具体包装和发送按钮点击时间消息的地方和时机,那么可以拦截这个地方执行,然后加入是否在指定的间隔时间内决定是否让其继续执行发送消息的操作。

  • 首先从UIButton.h头文件中查找,是否有send message 、send Action …等等包含send的方法
    无法找到

  • UIButton继承自UIControl,而UIControl又负责很多的UI事件处理,那么可以继续从UIControl.h中查找
    找到两个send相关的函数:

      // send the action. the first method is called for the event and is a point     at which you can observe or override behavior. it is called repeately by the second.
      - (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event;
    
      - (void)sendActionsForControlEvents:(UIControlEvents)controlEvents;                        // send all actions associated with events
    

根据注释可以得知,第一个方法就是包装了Target,SEL,按钮状态的始作俑者。

最终突破口就是这里,我们可以在包装阶段做一些间隔时间处理发送。

做法大概有以下三种,有更好的方法,可以回复我。

  • 第一种、自定义我们的UIButton类,以后程序中都使用我们UIButton类(只适合新项目,不太适合老项目,用的地方太多了)
  • 第二种、使用UIButton Category封装防止按钮连续点击处理的逻辑(这种挺好,对原来的UIButton使用代码绿色无公害)
  • 第三种、直接在main.m中执行main()之前,就替换掉UIControl的sendAction:to:forEvent:具体实现(稍微有点复杂)

首先看下使用UIButton子类实现

    #import <UIKit/UIKit.h>
    @interface MyButton : UIButton
    /**
    按钮点击的间隔时间
    */
    @property (nonatomic, assign) NSTimeInterval time;
    @end
#import "MyButton.h"

// 默认的按钮点击时间
static const NSTimeInterval defaultDuration = 3.0f;

// 记录是否忽略按钮点击事件,默认第一次执行事件
static BOOL _isIgnoreEvent = NO;

// 设置执行按钮事件状态
static void resetState() {
    _isIgnoreEvent = NO;
}

@implementation MyButton

- (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {

    //1. 按钮点击间隔事件
    _time = _time == 0 ? defaultDuration : _time;

    //2. 是否忽略按钮点击事件
    if (_isIgnoreEvent) {
        //2.1 忽略按钮事件

        // 直接拦截掉super函数进行发送消息
        return;

    } else if(_time > 0) {
        //2.2 不忽略按钮事件

        // 后续在间隔时间内直接忽略按钮事件
        _isIgnoreEvent = YES;

        // 间隔事件后,执行按钮事件
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_time * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            resetState();
        });

        // 发送按钮点击消息
        [super sendAction:action to:target forEvent:event];
    }
}

@end

ViewController中测试

    @implementation ViewController

    - (void)btnDidClick:(id)sender {
    NSLog(@"我被点击了 >>> %@", NSStringFromSelector(_cmd));
    }

    - (void)viewDidLoad {
    [super viewDidLoad];

     MyButton *btn = [[MyButton alloc] init];
    [btn setTitle:@"点我啊" forState:UIControlStateNormal];
    [btn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
    btn.layer.borderWidth = 1;
    btn.frame = CGRectMake(50, 100, 100, 50);
    [self.view addSubview:btn];

    // 设置按钮的点击间隔时间
    btn.time = 2.f;

    [btn addTarget:self action:@selector(btnDidClick:) forControlEvents:UIControlEventTouchUpInside];
    }```
运行程序后狂点按钮后的log如下

2016-04-22 16:58:39.998 RuntimeDemo[40146:474695] 我被点击了 >>> btnDidClick:
2016-04-22 16:58:42.308 RuntimeDemo[40146:474695] 我被点击了 >>> btnDidClick:
2016-04-22 16:58:44.545 RuntimeDemo[40146:474695] 我被点击了 >>> btnDidClick:
2016-04-22 16:58:46.783 RuntimeDemo[40146:474695] 我被点击了 >>> btnDidClick:
2016-04-22 16:58:49.046 RuntimeDemo[40146:474695] 我被点击了 >>> btnDidClick:
2016-04-22 16:58:51.281 RuntimeDemo[40146:474695] 我被点击了 >>> btnDidClick:
2016-04-22 16:58:53.526 RuntimeDemo[40146:474695] 我被点击了 >>> btnDidClick:
2016-04-22 16:58:55.886 RuntimeDemo[40146:474695] 我被点击了 >>> btnDidClick:“`

可以看到点击间隔最小是2秒


使用UIButton Category封装防止按钮连续点击的具体实现

其实大体上逻辑和上面的实现差不多,只是因为在Category分类里面,无法完成,重写sendAction:to:forEvent:对应的实现,只能通过runtime进行时替换方法来实现
UIButton分类完成按钮防止连续点击的代码实现

    #import <UIKit/UIKit.h>

    @interface UIButton (Helper)

    /**
    *  按钮点击的间隔时间
    */
    @property (nonatomic, assign) NSTimeInterval clickDurationTime;
    @end
    #import "UIButton+Helper.h"
    #import <objc/runtime.h>

    // 默认的按钮点击时间
    static const NSTimeInterval defaultDuration = 3.0f;

    // 记录是否忽略按钮点击事件,默认第一次执行事件
    static BOOL _isIgnoreEvent = NO;

// 设置执行按钮事件状态
static void resetState() {
    _isIgnoreEvent = NO;
}

@implementation UIButton (Helper)

@dynamic clickDurationTime;

+ (void)load {
    SEL originSEL = @selector(sendAction:to:forEvent:);
    SEL mySEL = @selector(my_sendAction:to:forEvent:);

    Method originM = class_getInstanceMethod([self class], originSEL);
    const char *typeEncodinds = method_getTypeEncoding(originM);

    Method newM = class_getInstanceMethod([self class], mySEL);
    IMP newIMP = method_getImplementation(newM);

    if (class_addMethod([self class], mySEL, newIMP, typeEncodinds)) {
        class_replaceMethod([self class], originSEL, newIMP, typeEncodinds);
    } else {
        method_exchangeImplementations(originM, newM);
    }
}

- (void)my_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {

    // 保险起见,判断下Class类型
    if ([self isKindOfClass:[UIButton class]]) {

        //1. 按钮点击间隔事件
        self.clickDurationTime = self.clickDurationTime == 0 ? defaultDuration : self.clickDurationTime;

        //2. 是否忽略按钮点击事件
        if (_isIgnoreEvent) {
            //2.1 忽略按钮事件
            return;
        } else if(self.clickDurationTime > 0) {
            //2.2 不忽略按钮事件

            // 后续在间隔时间内直接忽略按钮事件
            _isIgnoreEvent = YES;

            // 间隔事件后,执行按钮事件
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.clickDurationTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                resetState();
            });

            // 发送按钮点击消息
            [self my_sendAction:action to:target forEvent:event];
        }

    } else {
        [self my_sendAction:action to:target forEvent:event];
    }
}

        #pragma mark - associate

        - (void)setClickDurationTime:(NSTimeInterval)clickDurationTime {
    objc_setAssociatedObject(self, @selector(clickDurationTime),    @(clickDurationTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }

    - (NSTimeInterval)clickDurationTime {
    return [objc_getAssociatedObject(self, @selector(clickDurationTime)) doubleValue];
    }

@end

我们的按钮类不需要做任何的事情,完全不知道被拦截附加完成了防止连续点击的逻辑.

基本上不需要做什么修改,可以导入UIButton分类,对该按钮设置点击间隔时间.

到这里就可以完成一次华丽的逆转,希望对你有用!