用RunTime来提升按钮的体验

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

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

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分类,对该按钮设置点击间隔时间.

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

iOS 用RunTime来提升按钮的体验的更多相关文章

  1. 加链接太麻烦?使用 linkit 模块提升用户编辑体验

    在制作网站内容时,适当地添加链接会非常用利于网站内容的SEO.加入链接的文章可以让访客了解到更多相关内容,从而提升文章的质量.被链接到的内容也能因此获得更多的访问和关注.只不过,在内容编辑时添加链接却 ...

  2. 如何将 iOS 工程打包速度提升十倍以上

    如何将 iOS 工程打包速度提升十倍以上   过慢的编译速度有非常明显的副作用.一方面,程序员在等待打包的过程中可能会分心,比如刷刷朋友圈,看条新闻等等.这种认知上下文的切换会带来很多隐形的时间浪费. ...

  3. 关于iOS的runtime

    runtime是一个很有意思的东西,如果你学iOS开发很经常就会用到或被问到runtime.那么runtime是什么呢,如何去了解它. runtime:中文名 运行时,系统在编译时留下的一些 类型,操 ...

  4. ios 修改导航条返回按钮

    ios 修改导航条返回按钮 方式一:使用系统的:可以更改系统的文字:以及通过设置导航条的颜色来达到预期的效果 UIBarButtonItem *backBtns = [[UIBarButtonItem ...

  5. IOS 改变导航栏返回按钮的标题

    IOS 改变导航栏返回按钮的标题   下午又找到了一个新的方法 这个方法不错 暂时没有发现异常的地方. 新写的App中需要使用UINavigationController对各个页面进行导航,但由于第一 ...

  6. ios之runtime学习

    今天学习了一下ios的runtime,看了其他博主的博客写的很不错,自己就不班门弄斧了,仅在此转载: 1.关于oc中类和元类:http://husbandman.diandian.com/post/2 ...

  7. IOS 中runtime 不可变数组__NSArray0 和__NSArrayI

    IOS 中runtime 不可变数组__NSArray0 和__NSArrayI 大家可能都遇到过项目中不可变数组避免数组越界的处理:runtime,然而有时候并不能解决所有的问题,因为类簇不一样 # ...

  8. IOS开发中UIBarButtonItem上按钮切换或隐藏实现案例

    IOS开发中UIBarButtonItem上按钮切换或隐藏案例实现案例是本文要介绍的内容,这个代码例子的背景是:导航条右侧有个 edit button,左侧是 back button 和 add bu ...

  9. iOS运用runtime全局修改UILabel的默认字体

    iOS运用runtime全局修改UILabel的默认字体 一.需求背景介绍 在项目比较成熟的基础上,遇到了这样一个需求,应用中需要引入新的字体,需要更换所有Label的默认字体,但是同时,对于一些特殊 ...

随机推荐

  1. ios11,弹出层内的input框光标错位 键盘弹出时,输入信息,光标一直乱跳

      之前开发了一个微信项目,维护期中苹果手机突然出现光标错位现象,经过排查,发现是最新的ios11系统的锅. 具体情况:弹出层使用position: fixed:弹出层内附带input/textare ...

  2. jQuery中的for循环var与let的区别

    今天在写jQuery请求接口中发现一个问题: 在用AJAX发送请求中又嵌套了一个AJAX请求,发现在内层请求的success中对第一次success中的循环变量 i 无法获取,具体代码如下: $.aj ...

  3. 做了两年多salesforce平台开发,转Java的经历

    2015年毕业,转眼已经三年多了.三年对于现在的我,真的很快,一开始对软件开发的执着一直没有变.我是一个很普通很普通长沙的一个专科毕业.刚进大学,对于软件开发真的是小白,仅仅只是存在对于游戏,和桌面软 ...

  4. [NOI 2014]起床困难综合症

    Description 21 世纪,许多人得了一种奇怪的病:起床困难综合症,其临床表现为:起床难,起床后精神不佳.作为一名青春阳光好少年,atm 一直坚持与起床困难综合症作斗争.通过研究相关文献,他找 ...

  5. 2015 多校联赛 ——HDU5349(水)

    Problem Description A simple problem Problem Description You have a multiple set,and now there are t ...

  6. AQS

    AQS介绍 AQS,即AbstractQueuedSynchronizer, 队列同步器,它是Java并发用来构建锁和其他同步组件的基础框架. AQS的核心思想是基于volatile int stat ...

  7. Linux下用程序实现统计cpu和内存的利用率

    Linux下没有直接可以调用系统函数知道CPU占用和内存占用.那么如何知道CPU和内存信息呢.只有通过proc伪文件系统来实现. proc伪文件就不介绍了,只说其中4个文件.一个是/proc/stat ...

  8. 第三次C语言作业

    (一)改错题 计算f(x)的值:输入实数x,计算并输出下列分段函数f(x)的值,输出时保留1位小数. 输入输出样例1: Enterr x: 10.0 f(10.0) = 0.1 输入输出样例2: En ...

  9. 重新设置Eclipse的workspace路径

    有3中方法可以更改workspace的路径设置: 1. 启动Eclipse/MyEclipse后, 打开"Window -> Preferences -> General -&g ...

  10. bootmgr is missing 开机无法进系统怎么办

    认识 bootmgr: 启动管理器.Bootmgr是Boot Manager的缩写,是在Windows Vista和Windows 7中使用的新的启动管理器,以代替Windows xp中的启动管理器- ...