hihi,勇敢的小伙伴儿们大家好,北京的疫情终于告一段落,我每次出门我都担心自己被感染,非常焦虑,我不怕自己被感染,主要是怕自己感染身边的朋友、家人,所以这种焦虑是因为“爱”~ 说出来就感觉轻松了一些。所以有些心事不能总藏在心里,要学会表达。每次都有好多话想说,但又担心不同的人听了会有不同的感受,会对我造成误解,于是渐渐地学会沉默寡言了。每次都想告诉别人我有多么多么感谢别人对我的帮助,表达我有多么多么真诚,但有时候常常苦于证明自己,而忘记了在意对方是否舒适。真想做一个自信、高情商的人,与人相处能让人“如沐春风”,可是好难呀~ 我会困惑自己是否是真的不错的一个人,会矛盾自己是否做得不妥,然后小心翼翼的表达,事后又开始无限反思我是否说了什么错话,然后感叹“我真是太不会说话了吧”,可是无论如何,我都做不到人人都喜欢。希望你们喜欢我吧~ 感谢!
每次写文章我都喜欢啰嗦一段(官方吐槽),那么言归正传,今天我们要从UIButton这样已有基础控件来“由浅入深”的学习相关的知识:响应者链,还有扩大点击范围,还有防暴力点击的内容。同时也是分享一个学习思路给大家,虽然我有些愚笨,但是你们不同,一千个读者一千个哈姆雷特,希望对你有所帮助。全文Demo地址:Demo,请结合本文看哦~
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,就像是在挖地洞,随着洞越来越深,我们不断地发现未知的新世界。
UIControl是什么?UIControl的主要角色是定义一套接口和基础实现,为iOS的人机交互制定了一系列的标准,
为了当确定的事件发生的时候(比如点击了按钮)准备好动作消息(Action)并开始派发它们到自己的目标(Target,eg:UIViewController)。UIControl是控件的基类,不能直接的实例化,它只能通过继承的方式为子类提供公共的接口和动作结构。
下图简单的列举了几个继承于UIControl的控件方便大家理解。
有的同学可能对UIStepper比较陌生,我这也是从https://developer.apple.com/documentation/uikit/uicontrol#see-also里面看到的,具体的样子就是如下图所示,它是一个可以增加或减少值的控件。
UIControl怎么用?我们今天需要特别学习它的一个重要方法是准备并发送动作消息sendAction:to:forEvent:,具体移步https://www.jianshu.com/p/bab7a7ec4b72进行学习。
UIView是什么?An object that manages the content for a rectangular area on the screen.即UIView表示屏幕上的一块矩形区域。它在App中占有绝对重要的地位,因为iOS中几乎所有可视化控件都是UIView的子类,其中大名鼎鼎的UIWindow就是继承于UIView。负责渲染区域的内容,并且响应该区域内发生的触摸事件。
其中UIView不能响应事件的三种情况是:
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。
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。
什么是UIEvent,就是表示App里的一个交互事件,前面也讲到了,iOS中的事件有以下几类。
typedef NS_ENUM(NSInteger, UIEventType) {
UIEventTypeTouches,//触摸事件
UIEventTypeMotion,//运动事件
UIEventTypeRemoteControl,//远程控制事件
UIEventTypePresses API_AVAILABLE(ios(9.0)),//按压事件
};
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];
}
当我们触发了事件后,由IOKit.framework
生成一个 IOHIDEvent
事件,而IOKit
是苹果的硬件驱动框架,由它进行底层接口的抽象封装与系统进行交互传递硬件感应的事件,它专门处理用户交互设备,由IOHIDServices
和IOHIDDisplays
两部分组成,其中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
响应者链是什么?在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如图:
我们可以利用这一特性,找到某view所在的ViewController:
- (UIViewController *)parentController
{
UIResponder *responder = [self nextResponder];
while (responder) {
if ([responder isKindOfClass:[UIViewController class]]) {
return (UIViewController *)responder;
}
responder = [responder nextResponder];
}
return nil;
}
如果当前view是控制器的view,那么控制器就是上一个响应者,事件就传递给控制器;如果当前view不是控制器的view,那么父视图就是当前view的上一个响应者,事件就传递给它的父视图。
在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理。
如果window对象也不处理,则其将事件或消息传递给UIApplication对象。
如果UIApplication也不能处理该事件或消息,则将其丢弃。
总结:事件的传递和响应的区别是“事件的传递是从上到下(父控件到子控件),事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件。”
NSObject是什么?我们在https://developer.apple.com/search/?q=NSObject中找到的结果可以看出:它是Protocol也是Class。
这两个大家应该都不陌生,这部分内容非常重要,欢迎移步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;
铺垫也铺垫完了,接下来也该开始讲讲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~
ok,最后一个引申就是UIButton的防暴力点击。
有几个实际业务场景需要控制UIButton
响应事件的时间间隔。比如:
1、当通过点击按钮来执行网络请求时,若请求耗时稍长,用户往往会再点一次。这样,就执行了两次请求,造成了资源浪费。
2、在移动终端性能较差时(比如iPhone 6
升级到iOS 11
),连续点击按钮会执行多次事件(比如push出来多个viewController
)。
3、防止暴力点击。
那么如何控制UIButton响应事件的时间间隔呢?我想到的就是最简单的方法了,控制其enable或者UIInteractionEnabled,让其在需要等待的时间内不可点击。
所以方案一来啦~
方案一:通过UIButton
的enabled
属性和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
的类别,使用runtime
为UIButton
增加public
属性eventInterval
和private
属性eventUnavailable
。
2、在+load方法中使用runtime
将UIButton
的-sendAction:to:forEvent:
方法与自定义的-hook_sendAction:to:forEvent:
方法交换Implementation
。
3、使用eventInterval
作为控制eventUnavailable
的计时因子,用eventUnavailable
开控制UIButton
的event
事件是否有效。
具体代码实现如下:
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
感谢各位耐心阅读,如有错误烦请指出,感激不尽!