用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. [HNOI 2016]大数

    Description 题库链接 给你一个长度为 \(n\) ,可含前导零的大数,以及一个质数 \(p\) . \(m\) 次询问,每次询问你一个大数的子区间 \([l,r]\) ,求出子区间中有多少 ...

  2. SRM340 VegetableGarden

    Description 你的蔬菜园形成了一个矩形网格.你决定检查一些小块土地.从左上角开始,你将走过菜园,回到起点.现在你想要检查一下菜园内的田地,于是你决定从左上角出发,在菜园里走一圈回到原处.最后 ...

  3. 洛谷P2221 [HAOI2012]高速公路

    线段树 #include<cstdio> #include<cstdlib> #include<algorithm> #include<cstring> ...

  4. USACO 2017 January Platinum

    因为之前忘做了,赶紧补上. T1.Promotion Counting 题目大意:给定一个以1为根的N个节点的树(N<=100,000),每个节点有一个权值,对于每个节点求出权值比它大的子孙的个 ...

  5. Codeforces Round #402 (Div. 1)

    A题卡壳了,往离线倒着加那方面想了会儿,后来才发现方向错了,二十多分钟才过掉,过了B后做D,想法好像有点问题,最后只过两题,掉分了,差一点回紫. AC:AB Rank:173 Rating:2227- ...

  6. ●BZOJ 3551 [ONTAK2010]Peaks(在线)

    题链: http://www.lydsy.com/JudgeOnline/problem.php?id=3551 题解: 最小生成树 Kruskal,主席树,在线 这个做法挺巧妙的...以Kruska ...

  7. 对中断的理解handle_level_irq【原创】

    如下为本人原创,在解决问题的过程中的一点心得,如果有描述不准确的地方还请各位指出,非常感谢 Linux内核版本:linux-4.9.18 曾有一次调试触摸屏的时候遇到如下的问题 /startup/mo ...

  8. cmake 没有那个目录

    问题:bash: /usr/bin/cmake: 没有那个文件或目录 因为直接使用cmake系统回到默认的/usr/bin中去寻找,但是src中安装的cmake是在/usr/local/bin中,所以 ...

  9. python webdriver环境搭建

    一.准备安装包 1.下载python 2.下载setuptools 3.下载pip 二.windows环境安装 1.安装python,建议选择python2.7.5版本. 2.安装setuptools ...

  10. 详解linux进程间通信-管道 popen函数 dup2函数

    前言:进程之间交换信息的唯一方法是经由f o r k或e x e c传送打开文件,或通过文件系统.本章将说明进程之间相互通信的其他技术-I P C(InterProcess Communication ...