IOS中触摸事件学习
Posted GY-93
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了IOS中触摸事件学习相关的知识,希望对你有一定的参考价值。
ios中触摸事件学习
本文学习的内容大致包括:
- 触摸事件由触屏生成后是如何传递到当前应用的
- 应用接收触摸事件后如何寻找最佳响应者?实现原理?
- 触摸事件如何沿着响应链流动?
- 响应链、手势识别器、UIControl之间对于触摸事件的响应有着什么样的关系?
1. 事件的声明周期
当指尖触摸屏幕的那一刻,一个触摸事件就在系统中生成了。经过IPC进程通信,事件最终被传递到合适的应用。在经历过一些过程之后,最终被释放,流程如下:
2. 系统相应阶段
- 手指触摸屏幕,屏幕感应到触碰后, 会将事件交给IOKit处理。
- IOKit将触摸事件封装成一个IOHIDEvent对象,并通过mach port传递给SpringBoad进程。
mach port 进程端口,各进程之间通过它进行通信。
SpringBoad.app 是一个系统进程,可以理解为桌面系统,可以统一管理和分发系统接收到的触摸事件。(学过逆向的应该知道)
- SpringBoard进程因接收到触摸事件,触发了主线程runloop的source1事件源的回调。
此时SpringBoard会根据当前桌面的状态,判断应该由谁处理此次触摸事件。因为事件发生时,你可能正在桌面上翻页,也可能正在刷微博。若是前者(即前台无APP运行),则触发SpringBoard本身主线程runloop的source0事件源的回调,将事件交由桌面系统去消耗;若是后者(即有app正在前台运行),则将触摸事件通过IPC传递给前台APP进程,接下来的事情便是APP内部对于触摸事件的响应了。
3. APP响应阶段
- APP进程的mach port接受到SpringBoard进程传递来的触摸事件,主线程的runloop被唤醒,触发了source1回调。
- source1回调又触发了一个source0回调,将接收到的IOHIDEvent对象封装成UIEvent对象,此时APP将正式开始对于触摸事件的响应
- source0回调将触摸事件添加到UIApplication的事件队列,当触摸事件出队后UIApplication为触摸事件寻找最佳响应者。
- 寻找到最佳响应者之后,接下来的事情便是事件在响应链中传递和响应。
- 触摸事件历经坎坷后要么被某个响应对象捕获后释放,要么致死也没能找到能够响应的对象,最终释放。至此,这个触摸事件的使命就算终结了。runloop若没有其他事件需要处理,也将重归于眠,等待新的事件到来后唤醒。
触摸事件从触屏产生后,由IOKit将触摸事件传递给SpringBoard进程,再由SpringBoard分发给当前前台APP处理。
4. 触摸、事件、响应者
4.1 UITouch(触摸)
- 一个手指一次触摸屏幕,就对应生成一个UITouch对象。多个手指同时触摸,生成多个UITouch对象。
- 多个手指先后触摸,系统会根据触摸的位置判断是否更新同一个UITouch对象。若两个手指一前一后触摸同一个位置(即双击),那么第一次触摸时生成一个UITouch对象,第二次触摸更新这个UITouch对象(UITouch对象的 tap count 属性值从1变成2);若两个手指一前一后触摸的位置不同,将会生成两个UITouch对象,两者之间没有联系
- 每个UITouch对象记录了触摸的一些信息,包括触摸时间、位置、阶段、所处的视图、窗口等信息
//触摸各个阶段的状态
//例如:当手指移动时,会更新UITouchPhase状态到UITouchPhaseMoved,手指离开屏幕后会更新到UITouchPhaseEnded
typedef NS_ENUM(NSInteger, UITouchPhase) {
UITouchPhaseBegan, // whenever a finger touches the surface.
UITouchPhaseMoved, // whenever a finger moves on the surface.
UITouchPhaseStationary, // whenever a finger is touching the surface but hasn't moved since the previous event.
UITouchPhaseEnded, // whenever a finger leaves the surface.
UITouchPhaseCancelled, // whenever a touch doesn't end but we need to stop tracking (e.g. putting device to face)
UITouchPhaseRegionEntered API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos), // whenever a touch is entering the region of a user interface
UITouchPhaseRegionMoved API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos), // when a touch is inside the region of a user interface, but hasn’t yet made contact or left the region
UITouchPhaseRegionExited API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos), // when a touch is exiting the region of a user interface
};
- 当手指离开屏幕后,确定一段时间UITouch不会再更新之后将被释放
4.2 UIEvent(事件真身)
- 触摸的目的是生成触摸事件供响应者响应,一个触摸事件对应一个UIEvent对象,其中的type标识了事件的类型
typedef NS_ENUM(NSInteger, UIEventType) {
UIEventTypeTouches,
UIEventTypeMotion,
UIEventTypeRemoteControl,
UIEventTypePresses API_AVAILABLE(ios(9.0)),
UIEventTypeScroll API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos) = 10,
UIEventTypeHover API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos) = 11,
UIEventTypeTransform API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos) = 14,
};
// UIEvent类
UIKIT_EXTERN API_AVAILABLE(ios(2.0)) @interface UIEvent : NSObject
@property(nonatomic,readonly) UIEventType type API_AVAILABLE(ios(3.0));
@property(nonatomic,readonly) UIEventSubtype subtype API_AVAILABLE(ios(3.0));
@property(nonatomic,readonly) NSTimeInterval timestamp;
@property (nonatomic, readonly) UIKeyModifierFlags modifierFlags API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos);
@property (nonatomic, readonly) UIEventButtonMask buttonMask API_AVAILABLE(ios(13.4)) API_UNAVAILABLE(tvos, watchos);
@property(nonatomic, readonly, nullable) NSSet <UITouch *> *allTouches;
- (nullable NSSet <UITouch *> *)touchesForWindow:(UIWindow *)window;
- (nullable NSSet <UITouch *> *)touchesForView:(UIView *)view;
- (nullable NSSet <UITouch *> *)touchesForGestureRecognizer:(UIGestureRecognizer *)gesture API_AVAILABLE(ios(3.2));
// An array of auxiliary UITouch’s for the touch events that did not get delivered for a given main touch. This also includes an auxiliary version of the main touch itself.
- (nullable NSArray <UITouch *> *)coalescedTouchesForTouch:(UITouch *)touch API_AVAILABLE(ios(9.0));
// An array of auxiliary UITouch’s for touch events that are predicted to occur for a given main touch. These predictions may not exactly match the real behavior of the touch as it moves, so they should be interpreted as an estimate.
- (nullable NSArray <UITouch *> *)predictedTouchesForTouch:(UITouch *)touch API_AVAILABLE(ios(9.0));
@end
//事件除了触摸事件,还有其他的事件
- UIEvent对象中包含了触发该事件的触摸对象集合,因为一个触摸事件可能由多个手指同时触摸产生的,触摸对象集合通过allTouches属性获取
4.3 UIResponder(响应者)
每一个响应者都是一个UIResponder对象,及所有派生自UIResponder的对象,本身都具备响应事件的能力,因此一下类的实例都是响应者都是实例:
- UIView
- UIViewController
- UIApplication
- AppDelegate
响应者之所以能够响应事件,是因为提供了4个触摸事件的方法
//手指触碰屏幕,触摸开始
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//手指在屏幕上移动
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//手指离开屏幕,触摸结束
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//触摸结束前,某个系统事件中断了触摸,例如电话呼入
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
这个几个方法在响应者对象接收到事件的调用,用于做出对事件的响应。关于响应者何时接收到事件以及事件如何沿着响应链传递
5. 寻找事件的最佳响应者(Hit-Testing)
每个事件的理想宿命是被能够响应它的对象响应后释放,然而响应者诸多,事件一次只有一个,谁都想把事件抢到自己碗里来,为避免纷争,就得有一个先后顺序,也就是得有一个响应者的优先级。因此这就存在一个寻找事件最佳响应者(又称第一响应者 first responder)的过程,目的是找到一个具备最高优先级响应权的响应对象(the most appropriate responder object),这个过程叫做Hit-Testing,那个命中的最佳响应者称为hit-tested view。
当APP通过mach port得到这个触摸事件时,APP中有那么多UIView或者UIViewController,到底应该给谁去响应呢?寻找最佳响应者就是找出这个优先级最高的响应对象。
5.1 事件自下而上的传递
应用接收到事件后先将其置入事件队列中以等待处理。出队后,application首先将事件传递给当前应用最后显示的窗口(UIWindow)询问其能否响应事件,若窗口能响应事件,则传递给子视图询问是否能响应,子视图若能响应则继续询问子视图。子视图询问的顺序是优先询问后添加的子视图,即子视图数组中靠后的视图。事件传递顺序如下
UIApplication ——> UIWindow ——> 子视图 ——> ... ——> 子视图
事实上吧UIwindow也看成是视图即可,这个过程就是一个递归询问子视图能否响应事件的过程,且后添加的子视图优先级高(对于window而言就是后显示的window优先级高)
事件的具体流程:
- UIApplication首先将事件传递给窗口对象(UIWindow),若存在多个窗口,则优先询问后显示的窗口
- 若窗口不能响应事件,则将事件传递其他窗口;若窗口能响应事件,则从后往前询问窗口的子视图
- 重复步骤2。即视图若不能响应,则将事件传递给上一个同级子视图;若能响应,则从后往前询问当前视图的子视图。
- 视图若没有能响应的子视图了,则自身就是最合适的响应者
视图层级如下(同一层级的视图越在下面,表示越后添加):
A
├── B
│ └── D
└── C
├── E
└── F
现在假设在E视图所处的屏幕位置触发一个触摸,应用接收到这个触摸事件事件后,先将事件传递给UIWindow,然后自下而上开始在子视图中寻找最佳响应者。事件传递的顺序如下所示:
- UIWindow将事件传递给其子视图A
- A判断自身能响应该事件,继续将事件传递给C(因为视图C比视图B后添加,因此优先传给C)
- C判断自身能响应事件,继续将事件传递给F(同理F比E后添加)
- F判断自身不能响应事件,C又将事件传递给E
- E判断自身能响应事件,同时E已经没有子视图,因此最终E就是最佳响应者
5.2 Hit-Testing的本质
上面讲了事件在响应者之间传递的规则,视图通过判断自身能否响应事件来决定是否继续向子视图传递。那么问题来了:视图如何判断能否响应事件?以及视图如何将事件传递给子视图?
那么我们首先需要知道,以下几种状态是无法响应事件:
不允许交互:userInteractionEnabled = NO
隐藏:hidden = YES 如果父视图隐藏,那么子视图也会隐藏,隐藏的视图无法接收事件
透明度:alpha < 0.01 如果设置一个视图的透明度<0.01,会直接影响子视图的透明度。alpha:0.0~0.01为透明。
hitTest:withEvent:
每个UIView对象都有一个 hitTest:withEvent:
方法,这个方法是Hit-Testing
过程中最核心的存在,其作用是询问事件在当前视图中的响应者,同时又是作为事件传递的桥梁。
hitTest:withEvent:
返回一个UIView对象,作为当前视图层次中的响应者。
其方法默认实现:
- 若当前视图无法响应事件,则返回nil
- 若当前视图可以响应事件,但无子视图可以响应事件,则返回自身作为当前视图层次中的事件响应者
- 若当前视图可以响应事件,同时有子视图可以响应,则返回子视图层次中的事件响应者
一开始UIApplication
将事件通过调用UIWindow
对象的hitTest:withEvent:
传递给UIWindown对象,UIWindow的 hitTest:withEvent:
在执行时若判断本身能响应事件,则调用子视图的 hitTest:withEvent:
将事件传递给子视图并询问子视图上的最佳响应者。最终UIWindow返回一个视图层次中的响应者视图给UIApplication,这个视图就是hit-testing的最佳响应者。
系统对于视图能否响应事件的判断逻辑除了之前提到的3种限制状态,默认能响应的条件就是触摸点在当前视图的坐标系范围内。因此,hitTest:withEvent:
的默认实现就可以推测了,大致如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
//3种状态无法响应事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
//触摸点若不在当前视图上则无法响应事件
if ([self pointInside:point withEvent:event] == NO) return nil;
//从后往前遍历子视图数组
int count = (int)self.subviews.count;
for (int i = count - 1; i >= 0; i--)
{
// 获取子视图
UIView *childView = self.subviews[i];
// 坐标系的转换,把触摸点在当前视图上坐标转换为在子视图上的坐标
CGPoint childP = [self convertPoint:point toView:childView];
//询问子视图层级中的最佳响应视图
UIView *fitView = [childView hitTest:childP withEvent:event];
if (fitView)
{
//如果子视图中有更合适的就返回
return fitView;
}
}
//没有在子视图中找到更合适的响应视图,那么自身就是最合适的
return self;
}
值得注意的是 pointInside:withEvent:
这个方法,用于判断触摸点是否在自身坐标范围内。默认实现是若在坐标范围内则返回YES,否则返回NO。
现在我们在上述示例的视图层次中的每个视图类中添加下面3个方法来验证一下之前的分析(注意 :hitTest:withEvent:
和 pointInside:withEvent:
方法都要调用父类的实现,否则不会按照默认的逻辑来执行Hit-Testing)
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"%s",__func__);
return [super hitTest:point withEvent:event];
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"%s",__func__);
return [super pointInside:point withEvent:event];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%s",__func__);
}
那么单击E视图,调用过程如下:
可以看到最终是视图E对事件进项了响应, 同时事件的传递过程也和上面分析的一致,事实上单击从[AView hitTest:withEvent:]
到[EView pointInside:withEvent:]
的过程会执行两边,两次传的是同一个Touch
,区别在Touch
的状态不同,第一次是begin
状态,第二阶段是end
阶段。也就是说对于事件的传递起源与触摸状态的变化
5.3 Hit-Testing过程中的事件拦截(自定义事件流向)
实际开发过程中可能会遇到一些特殊的交互需求,需要定制视图对于事件的响应,例如下面
G和H视图都是跟控制器视图的子视图,I视图是添加在H视图上,当我们触摸I视图在G视图中的那部分时,我们看打印结果:
通过打印结果我们可以发现,事件根本没有传递到I视图
这里,这是为什么了?
原来触摸事件最早传递到H视图,然后调用H视图的[HView hitTest:withEvent:]
,在这个方法中会调用[HView pointInside:withEvent:]
,判断触摸点是否在视图范围内,这里由于触摸点在G视图的那部分,所以不在H视图,因此该方法返回NO
,这样H视图的事件传递就结束了,于是事件就传递到了G视图内,由于G视图可以响应触摸事件,而且G视图内没有子视图,所以G视图就是事件的最佳响应者。
那么这显示不是我们想要的结果,我们希望当触摸I视图
时,不管触摸I视图
的哪里,I视图
都能成为最佳响应者响应事件
要解决这个问题也很简单, 我们可以重写H视图中的pointInside:withEvent:
方法
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
NSLog(@"%s", __func__);
CGPoint tmpPoint = [self convertPoint:point toView:_iView];
if([_iView pointInside:tmpPoint withEvent:event]){
return YES;
}
return [super pointInside:point withEvent:event];
}
我们判断触摸点位置是否在视图I范围内,如果在视图I的范围内,则直接返回YES,这样一来I视图就可以响应触摸事件了
5.4 事件的响应以及在响应链中的传递
经过Hit-Testing
后,UIApplication已经知道了事件的最佳响应者是谁了,那么接下来要做的事情就是:
- 将事件传递给最佳响应者
- 事件沿着响应链传递
5.4.1 事件响应的前奏
因为最佳响应者具有最高的事件响应优先级,因此UIApplication会先将事件传递给其它响应者。首先UIApplication将时间通过sendEvent:
传递给事件所属的Window,window同样通过sendEvent:
再将事件传递给hit-tested view,即最佳响应者
UIApplication ——> UIWindow ——> hit-tested view
以事件自下而上的传递
结点中为例,我们在E视图中的touchesBegan:withEvent:
断点,然后查看调用栈就能看清除这一过程
那么问题来了,这个过程中,假如应用中存在多个window对象,UIApplication是怎么知道要把事件传给哪个window的?window又是怎么知道哪个视图才是最佳响应者的呢?
其实简单思考一下,这两个过程都是传递事件的过程,涉及的方法都是 sendEvent:
,而该方法的参数(UIEvent
对象)是唯一贯穿整个经过的线索,那么就可以大胆猜测必然是该触摸事件对象上绑定了这些信息。事实上之前在介绍UITouch
的时候就说过touch对象
保存了触摸所属的window及view,而event对象又绑定了touch对象,如此一来,是不是就说得通了。要是不信的话,那就自定义一个Window
类,重写 sendEvent:
方法,捕捉该方法调用时参数event
的状态,答案就显而易见了。
至于这两个属性是什么时候绑定到touch对象上的,必然是在hit-testing的过程中呗,仔细想想hit-testing干的不就是这个事儿吗~
5.4.2 事件的响应
前面介绍UIResponsder的时候说过,每个响应者必定都是UIResponder对象,通过4个响应触摸事件的方法来响应事件。每个UIResponder对象默认都已经实现了这4个方法,但是默认不对事件做任何处理,单纯只是将事件沿着响应链传递。若要截获事件进行自定义的响应操作,就要重写相关的方法。例如,通过重写 touchesMoved: withEvent:
方法实现简单的视图拖动。
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
每个响应触摸事件的方法会接收两个参数,分别对应触摸对象集合和事件对象。通过监听触摸对象中保存触摸点位置的变动,可以时时修改视图的位置。视图(UIView)作为响应者对象,本身已经实现了 touchesMoved: withEvent:
方法,因此要创建一个自定义视图(继承自UIView),重写该方法。
//MovedView
//重写touchesMoved方法(触摸滑动过程中持续调用)
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//获取触摸对象
UITouch *touch = [touches anyObject];
//获取前一个触摸点位置
CGPoint prePoint = [touch previousLocationInView:self];
//获取当前触摸点位置
CGPoint curPoint = [touch locationInView:self];
//计算偏移量
CGFloat offsetX = curPoint.x - prePoint.x;
CGFloat offsetY = curPoint.y - prePoint.y;
//相对之前的位置偏移视图
self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);
}
每个响应者都有权决定是否执行事件的响应,只要重写触摸事件方法即可
5.4.3事件的传递(响应链)
前面一直说的最佳响应者,之所以被称为最佳,是其具备响应事件的最高优先权。最佳响应者首先接收到事件,然后便拥有对事件的绝对控制权,即它可以选择独吞这个事件,也可以把这个事件像下传递给其它响应者,这个由响应者构成的视图链就称之为响应链。
需要注意的是,上一节中也说到了事件的传递,与此处所说的事件的传递有本质区别。上一节所说的事件传递的目的是为了寻找事件的最佳响应者,是自下而上的传递;而这里的事件传递目的是响应者做出对事件的响应,这个过程是自上而下的。前者为“寻找”,后者为“响应”。
5.4.4 响应者对于事件的操作方式
响应者对于事件的拦截以及传递都是通过touchesBegan:withEvent:
方法控制的,该方法默认实现是将事件沿着默认的响应链往下传递。
响应者对于接收事件有一下3种操作:
不拦截,默认操作
:事件会沿着默认的响应链往下传递拦截,不在往下分发事件
:重写touchesBegan:withEvent:
进行事件处理,不调用父类的touchesBegan:withEvent:
方法拦截,继续往下分发事件
:重写touchesBegan:withEvent:
进行事件处理,同时调用父类的touchesBegan:withEvent:
方法往下传递事件
5.4.5 响应链中的事件规则
每一个响应者(UIResponder对象)都有一个 nextResponder
方法,用于获取响应链中当前对象的下一个响应者,因此一旦事件的最佳响应者确定了,这个事件的响应链就确定了
对于响应者,默认的 nextResponder
实现如下:
UIView
:如果视图是控制器的根视图,则其nextResponder
为控制器对象;否则,其nextResponder为父视图。UIViewController
:若控制器的视图是window的根视图,则其nextResponder为窗口对象;若控制器是从别的控制器present出来的,则其nextResponder为presenting view controllerUIWindow
:nextResponder为UIApplication对象。UIApplication
:若当前应用的app delegate是一个UIResponder对象,且不是UIView、UIViewController或app本身,则UIApplication的nextResponder为app delegate。
上图是官网对于响应链的展示,若触摸发生在UITextField上,则事件的传递顺序是:
UITextField ——> UIView ——> UIView ——> UIViewController ——> UIWindow ——> UIApplication ——> UIApplicationDelegation
图中虚线箭头是指若该UIView是作为UIViewController根视图存在的,则其nextResponder为UIViewController对象;若是直接add在UIWindow上的,则其nextResponder为UIWindow对象
可以使用一下方式打印一个响应链上每一个响应者对象, 在最佳响应者的touchBegin:withEvent:
方法中调用即可(别忘了调用父类的方法)
我们以上面事件拦截中的案例
来看看:
上面结果打印出了完整的响应链,另外如果有需要,完全可以重写响应者的nextResponder
方法来自定义响应链
6 UIResponder、UIGestureRecognizer、UIControl
IOS中,除了UIResponder能够响应事件,手势识别器、UIControl同样具备对事件的处理能力。
6.1 UIGestureRecognizer(手势识别器)
我们首先来看一个场景,给下图中的黄色View(AxsView)添加一个点击手势:
查看运行结果:
从日志上看出YellowView最后Cancel了对触摸事件的响应,而正常应当是触摸结束后,AxsView的touchesEnded:withEvent:
的方法被调用才对。另外,期间还执行了手势识别器绑定的action 。我从官方文档找到了这样的解释:
A window delivers touch events to a gesture recognizer before it delivers them to the hit-tested view attached to the gesture recognizer. Generally, if a gesture recognizer analyzes the stream of touches in a multi-touch sequence and doesn’t recognize its gesture, the view receives the full complement of touches. If a gesture recognizer recognizes its gesture, the remaining touches for the view are cancelled.The usual sequence of actions in gesture recognition follows a path determined by default values of the cancelsTouchesInView, delaysTouchesBegan, delaysTouchesEnded properties.
大致理解是,Window
在将事件传递给hit-tested view
之前,会先将事件传递给相关的手势识别器并由手势识别器优先识别。若手势识别器成功识别了事件,就会取消hit-tested view
对事件的响应;若手势识别器没能识别事件,hit-tested view才完全接手事件的响应权。
一句话概括:手势识别器比UIResponder具有更高的事件响应优先级!!!!
按照这个解释,Window在将事件传递给hit-tested view即AxsView之前,先传递给了控制器根视图上的手势识别器。手势识别器成功识别了该事件,通知Application取消AxsView对事件的响应。
然而看日志,却是AxsView的 touchesBegan:withEvent:
先调用了,既然手势识别器先响应,不应该上面的action先执行吗,这又怎么解释?事实上这个认知是错误的。手势识别器的action的调用时机(即此处的 actionTapView)并不是手势识别器接收到事件的时机,而是手势识别器成功识别事件后的时机,即手势识别器的状态变为UIGestureRecognizerStateRecognized
。因此从该日志中并不能看出事件是优先传递给手势识别器的,那该怎么证明Window先将事件传递给了手势识别器?
要解决这个问题,只要知道手势识别器是如何接收事件的,然后在接收事件的方法中打印日志对比调用时间先后即可。说起来你可能不信,手势识别器对于事件的响应也是通过这4个熟悉的方法来实现的。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded<以上是关于IOS中触摸事件学习的主要内容,如果未能解决你的问题,请参考以下文章