用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. web攻击和防御措施

    1.SQL注入:参照下面的链接 http://www.cnblogs.com/chenhaoyu/p/8758888.html 2.跨网站脚本攻击(Cross Site Scripting, XSS) ...

  2. Apache 安装与配置(WIN10)

    本地坏境:windows 10 Pro 1709 Apache版本:httpd-2.4.32-Win64-VC15 Apache下载地址:https://www.apachelounge.com/do ...

  3. [WC 2013]糖果公园

    Description 题库链接 给你一棵 \(n\) 个节点,有 \(m\) 种颜色的树.每个节点上有一个颜色.定义一条树上路径的价值为 \[\sum_c V_c(\sum_{i=1}^{tim_c ...

  4. [SCOI2008]城堡

    题目描述 在一个国家里,有n个城市(编号为0 到n-1).这些城市之间有n条双向道 路相连(编号为0 到n-1),其中编号为i的道路连接了城市i和城市ri(一条道 路可以连接一个城市和它自身),长度为 ...

  5. codesforces 671D Roads in Yusland

    Mayor of Yusland just won the lottery and decided to spent money on something good for town. For exa ...

  6. NOIP2014-7-7模拟赛

    1.无线通讯网(wireless.pas/cpp/c) [题目描述] 国防部计划用无线网络连接若干个边防哨所.2种不同的通讯技术用来搭建无线网络:每个边防哨所都要配备无线电收发器:有一些哨所还可以增配 ...

  7. bzoj4596[Shoi2016]黑暗前的幻想乡 Matrix定理+容斥原理

    4596: [Shoi2016]黑暗前的幻想乡 Time Limit: 20 Sec  Memory Limit: 256 MBSubmit: 464  Solved: 264[Submit][Sta ...

  8. Android Activity的任务栈和四大启动模式

    在安卓系统中默认每次启动一个Activity时,系统会创建一个实例,并按照先进后出的原则放入任务栈中,当我们按back键时,就会有一个activity从任务栈顶移除,重复下去,直到任务栈为空,系统就会 ...

  9. Python中模块之xml的讲解

    xml模块的功能介绍 这里主要讲解xml模块下的etree.ElementTree类. 1. 创建 具体代码如下 import xml.etree.ElementTree as XM namelist ...

  10. Java锁机制了解一下

    前言 回顾前面: 多线程三分钟就可以入个门了! Thread源码剖析 多线程基础必要知识点!看了学习多线程事半功倍 只有光头才能变强! 本文章主要讲的是Java多线程加锁机制,有两种: Synchro ...