用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. 前端之旅HTML与CSS篇之block与inline的区别

    display:block;和display:inline;的区别block元素特点: 1)处于常规流中时,如果width没有设置,会自动填充满父容器 2)可以应用margin/padding 3)在 ...

  2. [LeetCode] Longest Uncommon Subsequence II 最长非共同子序列之二

    Given a list of strings, you need to find the longest uncommon subsequence among them. The longest u ...

  3. 我们为什么要用springcloud?

    1 2 单体架构 在网站开发的前期,项目面临的流量相对较少,单一应用可以实现我们所需要的功能,从而减少开发.部署和维护的难度.这种用于简单的增删改查的数据访问框架(ORM)十分的重要.  垂直应用架构 ...

  4. Java爬虫原理分析

    当我们需要从网络上获取资源的时候,我们一般的做法就是通过浏览器打开某个网站,然后将我们需要的东西下载或者保存下来. 但是,当我们需要大量下载的时候,这个时候通过人工一个个的去点击下载,就显得太没有效率 ...

  5. [SDOI 2015]序列统计

    Description 题库链接 给出集合 \(S\) ,元素都是小于 \(M\) 的非负整数.问能够生成出多少个长度为 \(N\) 的数列 \(A\) ,数列中的每个数都属于集合 \(S\) ,并且 ...

  6. [SDOI2016]游戏

    Description Alice 和 Bob 在玩一个游戏. 游戏在一棵有 n 个点的树上进行.最初,每个点上都只有一个数字,那个数字是 123456789123456789. 有时,Alice 会 ...

  7. [POJ1741]树上的点对 树分治

    Description 给一棵有n个节点的树,每条边都有一个长度(小于1001的正整数). 定义dist(u,v)=节点u到节点v的最短路距离. 给出一个整数k,我们称顶点对(u,v)是合法的当且仅当 ...

  8. C++变量和基本类型——2.3.1引用

    引用:为对象起了另外一个名字,通常将申明符写成&d的形式来定义引用类型,其中d是申明的变量名: int ival =1024; int &refVal=ival 一般在初始化变量时,初 ...

  9. [TensorFlow 团队] TensorFlow 数据集和估算器介绍

    发布人:TensorFlow 团队 原文链接:http://developers.googleblog.cn/2017/09/tensorflow.html TensorFlow 1.3 引入了两个重 ...

  10. 百度移动深度学习 Mobile-deep-learning(MDL)

    Free and open source mobile deep learning framework, deploying by Baidu. This research aims at simpl ...