iOS 从UIButton学习响应者链及相关技术

 

前言

hihi,勇敢的小伙伴儿们大家好,北京的疫情终于告一段落,我每次出门我都担心自己被感染,非常焦虑,我不怕自己被感染,主要是怕自己感染身边的朋友、家人,所以这种焦虑是因为“爱”~ 说出来就感觉轻松了一些。所以有些心事不能总藏在心里,要学会表达。每次都有好多话想说,但又担心不同的人听了会有不同的感受,会对我造成误解,于是渐渐地学会沉默寡言了。每次都想告诉别人我有多么多么感谢别人对我的帮助,表达我有多么多么真诚,但有时候常常苦于证明自己,而忘记了在意对方是否舒适。真想做一个自信、高情商的人,与人相处能让人“如沐春风”,可是好难呀~ 我会困惑自己是否是真的不错的一个人,会矛盾自己是否做得不妥,然后小心翼翼的表达,事后又开始无限反思我是否说了什么错话,然后感叹“我真是太不会说话了吧”,可是无论如何,我都做不到人人都喜欢。希望你们喜欢我吧~ 感谢!

每次写文章我都喜欢啰嗦一段(官方吐槽),那么言归正传,今天我们要从UIButton这样已有基础控件来“由浅入深”的学习相关的知识:响应者链,还有扩大点击范围,还有防暴力点击的内容。同时也是分享一个学习思路给大家,虽然我有些愚笨,但是你们不同,一千个读者一千个哈姆雷特,希望对你有所帮助。全文Demo地址:Demo,请结合本文看哦~

正文

1.了解UIButton

UIButton是什么?其实就是一个按钮控件,既能显示文字,又能显示图片,能通过点击来触发执行事件,就像台灯的开关,按钮执行了开灯和关灯的事件。是一种在iOS中非常常用的控件,在和用户的交互中扮演了非常重要的角色。

UIButton怎么用?欢迎移步https://developer.apple.com/documentation/uikit/uibutton进行学习。

那么问题来了,UIButton的继承关系你仔细关注过了吗?如果你没有,刚好我们一起来了解一下。

UIButton继承于UIControl,UIControl继承于UIView,UIView继承于UIResponder,UIResponder继承于NSObject。

UIKIT_EXTERN API_AVAILABLE(ios(2.0)) @interface UIButton : UIControl 

UIKIT_EXTERN API_AVAILABLE(ios(2.0)) @interface UIControl : UIView

UIKIT_EXTERN API_AVAILABLE(ios(2.0)) @interface UIView : UIResponder 

UIKIT_EXTERN API_AVAILABLE(ios(2.0)) @interface UIResponder : NSObject 

@interface NSObject  {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

你看,根据一个UIButton,我们知道了UIControl,知道了UIView,知道了UIResponder,知道了NSObject,就像是在挖地洞,随着洞越来越深,我们不断地发现未知的新世界。

2.了解UIControl

UIControl是什么?UIControl的主要角色是定义一套接口和基础实现,为iOS的人机交互制定了一系列的标准,
为了当确定的事件发生的时候(比如点击了按钮)准备好动作消息(Action)并开始派发它们到自己的目标(Target,eg:UIViewController)。UIControl是控件的基类,不能直接的实例化,它只能通过继承的方式为子类提供公共的接口和动作结构。

下图简单的列举了几个继承于UIControl的控件方便大家理解。

iOS 从UIButton学习响应者链及相关技术_第1张图片

有的同学可能对UIStepper比较陌生,我这也是从https://developer.apple.com/documentation/uikit/uicontrol#see-also里面看到的,具体的样子就是如下图所示,它是一个可以增加或减少值的控件。

UIControl怎么用?我们今天需要特别学习它的一个重要方法是准备并发送动作消息sendAction:to:forEvent:,具体移步https://www.jianshu.com/p/bab7a7ec4b72进行学习。

3.了解UIView

UIView是什么?An object that manages the content for a rectangular area on the screen.即UIView表示屏幕上的一块矩形区域。它在App中占有绝对重要的地位,因为iOS中几乎所有可视化控件都是UIView的子类,其中大名鼎鼎的UIWindow就是继承于UIView。负责渲染区域的内容,并且响应该区域内发生的触摸事件。

其中UIView不能响应事件的三种情况是:

  • 不允许交互:userInteractionEnabled = NO
  • 隐藏:如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件
  • 透明度:如果设置一个控件的透明度<=0.01,会直接影响子控件的透明度。alpha:0.0~0.01为透明。

UIView怎么用?1.管理矩形区域里的内容 2.处理矩形区域中的事件 3.子视图的管理 4.还能实现动画。当然,UIView的子类也具有这些功能。

UIView的基本用法我相信在座的各位小伙伴儿们应该都很熟练了,所以不赘述,这篇文章里主要学习UIView的几个方法:

//在两个view的坐标系中转换point或者rect
- (CGPoint)convertPoint:(CGPoint)point toView:(nullable UIView *)view;
- (CGPoint)convertPoint:(CGPoint)point fromView:(nullable UIView *)view;
- (CGRect)convertRect:(CGRect)rect toView:(nullable UIView *)view;
- (CGRect)convertRect:(CGRect)rect fromView:(nullable UIView *)view;

//view中的点击测试
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;   // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;   // default returns YES if point is in bounds

在后面的代码中你会看到它们的用法。

关于UIView我们用的比较多,了解的也比较多,比如它和CALayer的关系和区别之类的问题,可以搜索学习一下,我们今天主要学习一下UIView的父类UIResponder。

4.了解UIResponder

UIResponder是什么?Apple官方的备注是An abstract interface for responding to and handling events.翻译过来就是一个用于响应和处理事件的抽象接口。

所以不是任何对象都可以响应并处理事件的,只有继承了UIResponder的对象才能接收并处理事件,我们称为响应者对象。

不仅UIView是继承于UIResponder,我们熟知的UIViewController和UIApplication都是UIResponder的子类。另外SpriteKit中的SKNode也是继承自UIResponder类。

那么这些事件有都有什么?iOS中的事件主要有:触摸事件:点击、滑动等;运动事件:摇一摇;远程控制事件:通过耳机控制音量。

UIResponder怎么用?它可以管理响应者链,还可以管理输入视图,还可以响应触摸事件、响应移动事件、响应远程控制事件、验证命令、管理文本输入模式、支持User Activities。具体可参考https://www.jianshu.com/p/de0d30047e82这篇文章进行学习。

这里我们需要了解UIResponder的几个属性和方法列表。其中有几个方法在我以前的博客文章iOS 实现摇一摇中使用过,所以看着陌生的方法,其实我们早就用过了,只是当时还不了解这些,这种恍然大悟的感觉真好呀~

//响应者链
@property(nonatomic, readonly, nullable) UIResponder *nextResponder;

@property(nonatomic, readonly) BOOL canBecomeFirstResponder;    // default is NO

- (BOOL)becomeFirstResponder;

@property(nonatomic, readonly) BOOL canResignFirstResponder;    // default is YES

- (BOOL)resignFirstResponder;

@property(nonatomic, readonly) BOOL isFirstResponder;

//响应触摸事件 一个手指对应一个UITouch对象,所以touches中放的是手指对应的UITouch对象
- (void)touchesBegan:(NSSet *)touches withEvent:(nullable UIEvent *)event;

- (void)touchesMoved:(NSSet *)touches withEvent:(nullable UIEvent *)event;

- (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event;

- (void)touchesCancelled:(NSSet *)touches withEvent:(nullable UIEvent *)event;

//响应按压事件
- (void)pressesBegan:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0));

- (void)pressesChanged:(NSSet *)presses withEvent:(nullable 
UIPressesEvent *)event API_AVAILABLE(ios(9.0));

- (void)pressesEnded:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0));

- (void)pressesCancelled:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0));

//响应移动事件,e.g.摇一摇
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event API_AVAILABLE(ios(3.0));

- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event API_AVAILABLE(ios(3.0));

- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event API_AVAILABLE(ios(3.0));

//响应远程控制事件
- (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event API_AVAILABLE(ios(4.0));

//验证命令
- (BOOL)canPerformAction:(SEL)action withSender:(nullable id)sender API_AVAILABLE(ios(3.0));
- (nullable id)targetForAction:(SEL)action withSender:(nullable id)sender API_AVAILABLE(ios(7.0));

学习到这里,我们进入今天的重头戏啦——响应者链~~!!诶诶诶,别着急,我们先来了解一下传递的事件UIEvent。

4.1 了解UIEvent

什么是UIEvent,就是表示App里的一个交互事件,前面也讲到了,iOS中的事件有以下几类。

typedef NS_ENUM(NSInteger, UIEventType) {
    UIEventTypeTouches,//触摸事件
    UIEventTypeMotion,//运动事件
    UIEventTypeRemoteControl,//远程控制事件
    UIEventTypePresses API_AVAILABLE(ios(9.0)),//按压事件
};

4.1.1 事件的产生和传递

  • 系统把事件加入到一个由UIApplication管理的事件队列中,之所以加入队列而不是栈是因为队列先进先出,意味着先产生的事件,先处理。
  • UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)。
  • 主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步。
  • 然后,事件会按照UIApplication -> UIWindow -> SuperView -> SubView的顺序不断的检测寻找最合适的view(检测就是靠上面说的两个方法hitTest与pointInside)。
  • 找到合适的视图控件后,就算事件传递成功了。
  • 发生了某事件的通知到位后就要执行该事件让程序做的事情,也就是要进行事件响应,这一步就会调用视图控件的touches方法来作具体的事件处理(touches默认做法是把事件顺着响应者链条向上抛,结合后面“响应者链”部分食用更佳)。

touches默认做法:

#import "SubView.h"
@implementation SubView 
//只要点击控件,就会调用touchBegin,如果没有重写这个方法,自己处理不了触摸事件
// 上一个响应者可能是父控件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ 
// 默认会把事件传递给上一个响应者,上一个响应者是父控件,交给父控件处理
[super touchesBegan:touches withEvent:event]; 
// 注意不是调用父控件的touches方法,而是调用父类的touches方法
// super是父类 superview是父控件 
}
@end

根据这个原理,我们可以实现一个事件多个对象处理:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ 
// 1.自己先处理事件...
NSLog(@"do somthing...");
// 2.再调用系统的默认做法,再把事件交给上一个响应者处理
[super touchesBegan:touches withEvent:event]; 
}

4.1.2 事件和runloop

当我们触发了事件后,由IOKit.framework生成一个 IOHIDEvent事件,而IOKit是苹果的硬件驱动框架,由它进行底层接口的抽象封装与系统进行交互传递硬件感应的事件,它专门处理用户交互设备,由IOHIDServicesIOHIDDisplays两部分组成,其中IOHIDServices是专门处理用户交互的,它会将事件封装成IOHIDEvents对象,然后这些事件又由SpringBoard接收,它只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,接着用mach port转发给需要的App进程,Source1 接收IOHIDEvent,之后再回调__IOHIDEventSystemClientQueueCallback()内触发的Source0,Source0再触发的 __dispatchPreprocessEventFromEventQueue()__dispatchPreprocessEventFromEventQueue()IOHIDEvent处理包装成UIEvent进行处理分发,我们平时的UIGesture/处理屏幕旋转/发送给 UIWindow/UIButton 点击、touchesBegin/Move/End/Cancel这些事件,都是在这个回调中完成。

可以在touchesBegan或其他方法中写下下面的代码打印出当前线程栈:

    NSLog(@"%@",[NSThread callStackSymbols]);
2020-07-21 19:54:43.461735+0800 ResponderChainDemo[31394:4116079] (
	0   ResponderChainDemo                  0x00000001027a2b85 -[SubView touchesBegan:withEvent:] + 117
	1   UIKitCore                           0x00007fff480ce8de -[UIWindow _sendTouchesForEvent:] + 1867
	2   UIKitCore                           0x00007fff480d04c6 -[UIWindow sendEvent:] + 4596
	3   UIKitCore                           0x00007fff480ab53b -[UIApplication sendEvent:] + 356
	4   UIKitCore                           0x00007fff4812c71a __dispatchPreprocessedEventFromEventQueue + 6847
	5   UIKitCore                           0x00007fff4812f1e0 __handleEventQueueInternal + 5980
	6   CoreFoundation                      0x00007fff23bd4471 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
	7   CoreFoundation                      0x00007fff23bd439c __CFRunLoopDoSource0 + 76
	8   CoreFoundation                      0x00007fff23bd3b74 __CFRunLoopDoSources0 + 180
	9   CoreFoundation                      0x00007fff23bce87f __CFRunLoopRun + 1263
	10  CoreFoundation                      0x00007fff23bce066 CFRunLoopRunSpecific + 438
	11  GraphicsServices                    0x00007fff384c0bb0 GSEventRunModal + 65
	12  UIKitCore                           0x00007fff48092d4d UIApplicationMain + 1621
	13  ResponderChainDemo                  0x00000001027a2174 main + 116
	14  libdyld.dylib                       0x00007fff5227ec25 start + 1
	15  ???                                 0x0000000000000001 0x0 + 1

4.2 响应者链

响应者链是什么?在iOS程序中无论是最后面的UIWindow还是最前面的某个按钮,它们的摆放是有前后关系的,一个控件可以放到另一个控件上面或下面,那么用户点击某个控件时是触发上面的控件还是下面的控件呢,这种先后关系构成一个链条就叫“响应者链”。也可以说,响应者链是由多个响应者对象连接起来的链条。

通过上面的学习,我们可以知道通过nextResponder(Returns the next responder in the responder chain, or nil if there is no next responder.)可以得到当前响应者在响应者链中的下一个响应者,如果没有则返回nil。

我们在ViewController中的touchesBegan:withEvent:方法中写下如下代码:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UIResponder *res = self.view;
    while (res) {
        NSLog(@"****************TOUCHES***************\n%@",res);
        res = [res nextResponder];
    }
}

得到的输出为:

****************TOUCHES***************
>

>
>
; layer = >
; persistentIdentifier = 026533D6-8677-4E43-8267-A59AD5224D10; activationState = UISceneActivationStateForegroundActive; settingsCanvas = ; windows = (
    "; layer = >",
    ">"
)>

 

然后我们在ViewController的view上添加一个Button,并在其事件方法中写下上面的代码:

得到的输出为:

2020-07-21 16:51:48.506992+0800 ResponderChainDemo[30161:4002890] ****************Button***************
>

>
>
; layer = >
; persistentIdentifier = 026533D6-8677-4E43-8267-A59AD5224D10; activationState = UISceneActivationStateForegroundActive; settingsCanvas = ; windows = (
    "; layer = >",
    ">"
)>

从上面例子中我们可以看出响应者链基本可以概括为 UIButton->UIView(subView没有可忽略)->UIView(superView)->UIViewController->UIWindow(keyWindow)->UIApplication如图:

iOS 从UIButton学习响应者链及相关技术_第2张图片

我们可以利用这一特性,找到某view所在的ViewController:

- (UIViewController *)parentController
{
    UIResponder *responder = [self nextResponder];
    while (responder) {
        if ([responder isKindOfClass:[UIViewController class]]) {
            return (UIViewController *)responder;
        }
        responder = [responder nextResponder];
    }
    return nil;
}

4.2.1 响应者链的事件传递过程

  • 如果当前view是控制器的view,那么控制器就是上一个响应者,事件就传递给控制器;如果当前view不是控制器的view,那么父视图就是当前view的上一个响应者,事件就传递给它的父视图。

  • 在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理。

  • 如果window对象也不处理,则其将事件或消息传递给UIApplication对象。

  • 如果UIApplication也不能处理该事件或消息,则将其丢弃。

总结:事件的传递和响应的区别是“事件的传递是从上到下(父控件到子控件),事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件。”

5.了解NSObject

NSObject是什么?我们在https://developer.apple.com/search/?q=NSObject中找到的结果可以看出:它是Protocol也是Class。

iOS 从UIButton学习响应者链及相关技术_第3张图片

这两个大家应该都不陌生,这部分内容非常重要,欢迎移步https://www.jianshu.com/p/4eb6a0633ff8学习。我们今天主要学习NSObject的下面两个方法,主要是为后面的内容做一下铺垫。

//实例方法 用默认模式在当前线程延时执行方法
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;

//类方法 取消执行先前注册到performSelector:with object:afterDelay:的请求。
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;

6.扩大UIButton的点击范围

铺垫也铺垫完了,接下来也该开始讲讲UIButton的拓展知识,扩大其点击范围了~

在实际开发中经常会有一些设计上的Button特别小,人的手指是圆滑的不想鼠标可以精准的点击一个点甚至是一个像素(相比手指而言),所以手指点击需要较大的面积来容错,当然可能不止这一个原因,反正就是需要我们能够扩大一下UIButton的点击范围,那么我们如何实现呢?

我以前遇到过类似的情况,Button的frameUI小姐姐标注的极小,严重影响了点击效果,所以我就在保证button的文字或者图标的大小符合UI设计图之后扩大了UIButton的frame,严格意义上来说虽然可以实现想要的效果,但是不够严谨。

所以我们来想想更好的办法。不过像我这种普通人真的很难想出来接下来的这种方法,原因一个是对UIKit的一些方法不熟悉,另外一个可能还是没有精益求精的去挖掘更好的方法吧。小时候可以举一反三,长大后竟然缺少这种灵活性了呢。惭愧惭愧。

今天我们用的方法就是重写hitTest:WithEvent:和pointInside:pointInside:这两个方法。

主要的原理是判断手指点击屏幕的Point的位置是不是在某块区域里,如果是在某块区域,并且重写的Button的父视图方法,就要就找到想要扩展的Button,使其成为点击事件的响应者,执行Button的点击方法。如果是重写Button自己的方法,便直接返回其本身self。这样就有效扩大了Button的点击范围。

重写父视图的主要代码:

//点击这个view的任意位置都可以执行Button的点击事件,在某种程度上扩大了button的点击范围
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (self.userInteractionEnabled == NO || self.alpha <= 0.01 || self.hidden) {
        return nil;
    }
    
    if (![self pointInside:point withEvent:event]) {
        return nil;
    }

    //这里的self.bounds也可以进行自定义缩小,只要大小在view的bounds范围内就可以
    return CGRectContainsPoint(self.bounds, point) ? Button : nil;
}

重写Button的主要代码:

// recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    /**
     在系统的UIView中以下z四种事件不响应:
     1.隐藏(hidden=YES)的视图
     2.禁止用户操作(userInteractionEnabled=NO)的视图
     3.alpha<0.01的视图
     4.视图超出父视图的区域
     */
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
    
    //扩大Button响应范围,该处修改也可以通过修改pointInside:withEvent:来实现
    /**
     CGRectInset(CGRect rect, CGFloat dx, CGFloat dy)中的三个参数
     rect:待操作的CGRect;
     dx:为正数时,向右平移dx,宽度缩小2dx。为负数时,向左平移dx,宽度增大2dx;
     dy:为正数是,向下平移dy,高度缩小2dy。为负数是,向上平移dy,高度增大2dy。
     CGRectContainsPoint(CGRect rect, CGPoint point)判断手势点击的坐标point(x,y)是否落在rect(x,y,w,h)内.在区域内返回YES,不在返回NO.
     */
    CGRect enlageRect = CGRectInset(self.bounds, -50, -50);
    if (!CGRectContainsPoint(enlageRect, point)) {
        return nil;
    }
    
    //从后向前遍历子视图,之所以会采取从后往前遍历子控件的方式寻找最合适的view只是为了做一些循环优化。因为相比较之下,后添加的view在上面,降低循环次数。这里是为了证明上面的寻找最合适的view.
//    for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
//        //按照子视图坐标系转换点的坐标
//        CGPoint converedPoint = [self convertPoint:point toView:subview];
//        UIView *fitView = [subview hitTest:converedPoint withEvent:event];
//        if (fitView) {
//            return fitView;
//        }
//    }
   return self;

}

上面的代码中可以在pointInside:withEvent:中修改Rect效果也是一样的。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {

    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
 
    if (![self pointInside:point withEvent:event]) {
        return nil;
    }
    
    return self;

}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    CGRect bounds = self.bounds;
    bounds = CGRectInset(bounds, -50, -50);
    // CGRectContainsPoint  判断点是否在矩形内
    return CGRectContainsPoint(bounds, point);
}

以上就是扩大UIButton点击范围的方法,当然不限于UIButton~

7.UIButton防暴力点击

ok,最后一个引申就是UIButton的防暴力点击。

有几个实际业务场景需要控制UIButton响应事件的时间间隔。比如:
1、当通过点击按钮来执行网络请求时,若请求耗时稍长,用户往往会再点一次。这样,就执行了两次请求,造成了资源浪费。
2、在移动终端性能较差时(比如iPhone 6升级到iOS 11),连续点击按钮会执行多次事件(比如push出来多个viewController)。
3、防止暴力点击。

那么如何控制UIButton响应事件的时间间隔呢?我想到的就是最简单的方法了,控制其enable或者UIInteractionEnabled,让其在需要等待的时间内不可点击。

所以方案一来啦~

方案一:通过UIButtonenabled属性和userInteractionEnabled属性控制按钮是否可点击。此方案在逻辑上比较清晰、易懂,但具体代码书写分散,常常涉及多个方法。

- (void)buttonClicked:(UIButton *)sender {
    
    sender.enabled = NO;
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        sender.enabled = YES;
    });
}

举一反三嘛,事情总不会如此简单。结合前面我给大家铺垫的方法。方案二也应运而生。

方案二:通过NSObject的+cancelPreviousPerformRequestsWithTarget:selector:object:方法和-performSelector:withObject:afterDelay:方法控制按钮的响应事件的执行时间间隔。此方案会在连续点击按钮时取消之前的点击事件,从而只执行最后一次点击事件,会出现延迟现象。

- (void)buttonClicked:(UIButton *)sender {
    
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(buttonClickedAction:) object:sender];
    [self performSelector:@selector(buttonClickedAction:) withObject:sender afterDelay:2.0];
}

但这两个方法都是需要在每个Button的点击事件里写上几句话,不仅重复操作浪费时间还有点代码冗余,所以用什么方法能够让我们在Button点击事件上一劳永逸呢?方案三不胫而走。

方案3:通过Runtime控制UIButton响应事件的时间间隔。思路如下:
1、创建一个UIButton的类别,使用runtimeUIButton增加public属性eventIntervalprivate属性eventUnavailable
2、在+load方法中使用runtimeUIButton-sendAction:to:forEvent:方法与自定义的-hook_sendAction:to:forEvent:方法交换Implementation
3、使用eventInterval作为控制eventUnavailable的计时因子,用eventUnavailable开控制UIButtonevent事件是否有效。

具体代码实现如下:

UIButton+EventInterval.h

#import 

NS_ASSUME_NONNULL_BEGIN

@interface UIButton (EventInterval)

@property (nonatomic, assign) NSTimeInterval eventInterval;

@end

NS_ASSUME_NONNULL_END

UIButton+EventInterval.m

#import "UIButton+EventInterval.h"

#import 

static char * const eventIntervalKey = "eventIntervalKey";
static char * const eventUnavailableKey = "eventUnavailableKey";

@interface UIButton ()

@property (nonatomic, assign) BOOL eventUnavailable;

@end

@implementation UIButton (EventInterval)

+ (void)load {
    [super load];
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method oldButtonMethod = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
        Method newButtonMethod = class_getInstanceMethod(self, @selector(hook_sendAction:to:forEvent:));
        if (class_addMethod(self, @selector(sendAction:to:forEvent:), method_getImplementation(newButtonMethod), method_getTypeEncoding(newButtonMethod))) {
            class_replaceMethod(self, @selector(hook_sendAction:to:forEvent:), method_getImplementation(oldButtonMethod), method_getTypeEncoding(oldButtonMethod));
        } else {
            //将sendAction:to:forEvent:方法的实现换成hook_sendAction:to:forEvent:的实现
            method_exchangeImplementations(oldButtonMethod, newButtonMethod);
        }
    });
}

//当执行到点击事件的时候会执行本方法
- (void)hook_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    //   
    if([self isMemberOfClass:[UIButton class]]) {
         if (self.eventUnavailable == NO) {
               self.eventUnavailable = YES;
               [self hook_sendAction:action to:target forEvent:event];
               [self performSelector:@selector(setEventUnavailable:) withObject:0           afterDelay:self.eventInterval];
         }
    } else {
         [self hook_sendAction:action to:target forEvent:event];
     }
}

#pragma mark - Setter & Getter functions

- (NSTimeInterval)eventInterval {
    return [objc_getAssociatedObject(self, eventIntervalKey) doubleValue];
}

- (void)setEventInterval:(NSTimeInterval)eventInterval {
    objc_setAssociatedObject(self, eventIntervalKey, @(eventInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (BOOL)eventUnavailable {
    
    return [objc_getAssociatedObject(self, eventUnavailableKey) boolValue];
}

- (void)setEventUnavailable:(BOOL)eventUnavailable {
    
    objc_setAssociatedObject(self, eventUnavailableKey, @(eventUnavailable), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

我在外面设置的enlargeButton的eventInterval是2.0秒,所以我疯狂点击后,控制台输出的是两秒后才执行了第二次。如图:

大功告成!

历时好几天的这篇博客终于完成啦,撒花✿✿ヽ(°▽°)ノ✿,但这还只是开始,我们的征途是星辰大海~学海无涯!

防暴力点击这块儿内容鸣谢:https://www.jianshu.com/p/c2243ac4f620

 

感谢各位耐心阅读,如有错误烦请指出,感激不尽!

 

 

你可能感兴趣的:(iOS)