iOS响应者链是一种事件处理机制,它描述了在iOS系统中,当一个事件(比如用户点击屏幕或者发送手势)发生时,它将从触发事件的源头开始,按照预定义的顺序传递给各个视图或控件进行处理,直到最终被处理或被丢弃。
响应者链中的各个对象被称为“响应者”(responder),响应者链的根节点是UIApplication对象,所有的事件都从UIApplication对象开始传递。当事件发生时,UIApplication对象首先将其传递给当前显示在屏幕上的UIWindow对象,然后递归地向下传递给其子视图,依次传递给UIViewController、UIView等响应者对象进行处理,直到事件被处理完毕或被丢弃。
在响应者链中,每个响应者对象都可以处理事件,也可以选择将事件传递给下一个响应者对象进行处理,或者直接丢弃事件。响应者链中的每个响应者对象都可以重写几个方法来处理事件,这些方法包括touchesBegan:withEvent:、touchesMoved:withEvent:、touchesEnded:withEvent:等等。
响应者链的机制在iOS开发中非常重要,它确保了事件可以在正确的对象中得到处理,保证了iOS应用的用户交互和响应体验。
首先,在iOS里能够处理事件的对象被称为响应者对象,但是不是任何对象都能够处理响应者事件。
在iOS中,响应者对象是指能够响应用户事件并进行相关处理的对象,它们通常是UIView或其子类,也可以是UIViewController或其子类。响应者对象都实现了UIResponder协议,并提供了一些方法来响应用户事件,比如touchesBegan:withEvent:、touchesMoved:withEvent:、touchesEnded:withEvent:等方法
这里的实现UIResponder协议指UIApplication、UIViewController、UIView 都继承于 UIResponder。
UIResponder是iOS中的一个基类,定义了一些接口,用于处理触摸事件和键盘事件等用户事件。所有能够接收并处理事件的对象都继承自UIResponder。
// 一根或者多根手指开始触摸view,系统会自动调用view的下面方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
// 一根或者多根手指在view上移动,系统会自动调用view的下面方法(随着手指的移动,会持续调用该方法)
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
// 一根或者多根手指离开view,系统会自动调用view的下面方法
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
// 触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,系统会自动调用view的下面方法[可选]
- (void)touchesCancelled:(nullable NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
通过重写UIResponder中定义的方法,开发者可以在自己的类中处理用户事件,并做出相应的响应。
注意⚠️:这些方法可以被任何继承自 UIResponder 的对象重写,在自己的类中处理设备运动事件,并进行相应的响应。需要注意的是,只有在设备支持相关的运动事件时,这些方法才会被调用。
- (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(4_0);
remoteControlReceivedWithEvent 是一个 iOS 中的 UIResponder 方法,用于接收远程控制事件。这些远程控制事件可以来自于耳机、锁屏界面和控制中心等,例如暂停音乐、切换歌曲等。
当接收到远程控制事件时,系统会将事件封装成一个 UIEvent 对象,然后将该对象传递给当前响应者对象的 remoteControlReceivedWithEvent 方法。在该方法中,开发者可以获取 UIEvent 对象,并根据事件类型进行相应的处理。
需要注意的是,**只有当应用程序处于播放状态时,才会接收到远程控制事件。**如果应用程序没有进行播放,系统不会向应用程序发送远程控制事件。此外,在使用远程控制功能时,开发者还需要在应用程序的 Info.plist 文件中声明相应的背景模式。
// 记录了触摸事件产生或变化时的时间,单位是秒 The relative time at which the acceleration event occurred(read-only)
@property(nonatomic,readonly) NSTimeInterval timestamp;
// 当前触摸事件所处的状态
@property(nonatomic,readonly) UITouchPhase phase;
// touch down within a certain point within a certain amount of timen 短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击
@property(nonatomic,readonly) NSUInteger tapCount;
@property(nonatomic,readonly) UITouchType type NS_AVAILABLE_IOS(9_0);
// 触摸产生时所处的窗口
@property(nullable,nonatomic,readonly, strong) UIWindow *window;
// 触摸产生时所处的视图
@property(nullable,nonatomic,readonly, strong) UIView *view;
// The gesture-recognizer objects currently attached to the view.
@property(nullable,nonatomic,readonly,copy) NSArray <UIGestureRecognizer *> *gestureRecognizers
/*返回值表示触摸在view上的位置
这里返回的位置是针对view的坐标系的(以view的左上角为原点(0, 0))
调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置*/
- (CGPoint)locationInView:(nullable UIView *)view;
// 该方法记录了前一个触摸点的位置
- (CGPoint)previousLocationInView:(nullable UIView *)view;
// Use these methods to gain additional precision that may be available from touches.
// Do not use precise locations for hit testing. A touch may hit test inside a view, yet have a precise location that lies just outside.
//获取指定视图上的精确触摸位置,该方法会考虑到多点触控时不同触点之间的偏移。
- (CGPoint)preciseLocationInView:(nullable UIView *)view API_AVAILABLE(ios(9.1));
// 获取指定视图上上一次触摸的精确位置。
- (CGPoint)precisePreviousLocationInView:(nullable UIView *)view API_AVAILABLE(ios(9.1));
UIEvent 是 iOS 中用于表示触摸事件的类,一个 UIEvent 对象包含了所有与触摸事件相关的信息,比如触摸的位置、时间、阶段,以及多点触控时不同触点之间的状态等等。
UIEvent 对象是由系统自动创建和管理的,通常情况下不需要手动创建。每个 UIEvent 对象都与一个或多个 UITouch 对象相关联,这些 UITouch 对象保存着触摸事件的具体信息,比如触摸的位置和时间。
每产生一个事件,就会产生一个 UIEvent 对象,UIEvent 称为事件对象,记录事件产生的时刻和类型。
//事件类型,枚举值包括触摸、运动、遥控等。
@property(nonatomic,readonly) UIEventType type NS_AVAILABLE_IOS(3_0);
// 事件子类型,对于触摸事件,其子类型包括touch down、touch move、touch up等。
@property(nonatomic,readonly) UIEventSubtype subtype NS_AVAILABLE_IOS(3_0);
事件发生的时间戳,单位为秒。
@property(nonatomic,readonly) NSTimeInterval timestamp;
刚才说到每个 UIEvent 对象都与一个或多个 UITouch 对象相关联,这些 UITouch 对象保存着触摸事件的具体信息,比如触摸的位置和时间,下面看看如何产生关系
这是总的方法
- (nullable NSSet <UITouch *> *)allTouches;
- (nullable NSSet <UITouch *> *)touchesForWindow:(UIWindow *)window;
- (nullable NSSet <UITouch *> *)touchesForView:(UIView *)view;
- (nullable NSSet <UITouch *> *)touchesForGestureRecognizer:(UIGestureRecognizer *)gesture NS_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 NS_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 NS_AVAILABLE_IOS(9_0);
@property(nonatomic, readonly, nullable) NSSet <UITouch *> *allTouches;
allTouches 是 UIEvent 类中的一个只读属性,用于获取与当前事件关联的所有 UITouch 对象的集合。
它返回的是一个包含 UITouch 对象的集合,可以通过该集合来获取当前事件中的所有手指相关的信息,如触摸位置、时间、阶段等。在多点触控的场景下,该集合中包含多个 UITouch 对象,每个对象对应一个手指。
// 返回发生在指定窗口上的UITouch对象集合。
- (nullable NSSet <UITouch *> *)touchesForWindow:(UIWindow *)window;
// // 返回发生在指定视图上的UITouch对象集合。
- (nullable NSSet <UITouch *> *)touchesForView:(UIView *)view;
该方法需要传入一个 UIGestureRecognizer 对象作为参数,用于指定要获取哪个手势识别器相关联的 UITouch 对象的集合。它返回的是一个包含 UITouch 对象的集合,用于表示与指定手势识别器相关联的所有手指信息。
该方法可以通过以下方式访问:
- (nullable NSSet <UITouch *> *)touchesForGestureRecognizer:(UIGestureRecognizer *)gesture API_AVAILABLE(ios(3.2));
- (nullable NSArray <UITouch *> *)coalescedTouchesForTouch:(UITouch *)touch API_AVAILABLE(ios(9.0));
在 iOS 中,由于触摸事件的处理通常需要花费一定的时间,因此在用户快速滑动或快速触摸时,可能会出现丢失触摸事件或处理不准确的情况。predictedTouchesForTouch(_) 方法可以在用户快速移动手指时提供额外的触摸事件预测,从而改善触摸事件的响应速度和精度。
具体而言,predictedTouchesForTouch(_) 方法可以返回一系列 UITouch 对象,这些对象描述了未来几个周期中可能发生的触摸事件**。这些事件是由系统根据当前触摸事件的历史记录和其他信息预测出来的,并不是实际发生的触摸事件。开发者可以利用这些预测事件来更准确地响应触摸事件,以提高应用程序的性能和响应速度。
- (nullable NSArray <UITouch *> *)predictedTouchesForTouch:(UITouch *)touch API_AVAILABLE(ios(9.0));```
寻找目标是通过UIView的以下两个方法:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;//这个方法返回目标view
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event; //这个方法判断触摸点是否在当前view范围内
hitTest 是 iOS 中用于响应者链中事件响应查找的方法,它会在视图层次结构中递归地查找能够响应触摸事件的视图对象,并返回最终响应者
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
其中,point 参数表示触摸点在当前视图的坐标系下的位置,event 参数表示触摸事件对象。返回值为最终响应触摸事件的视图对象,如果没有合适的响应视图,则返回 nil。
流程:
模拟hitTest方法
// hitTest : withEvent: 作用:找做合适的view;当事件传递给一个控件的时候调用
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"%s",__func__);
// [super hitTest:point withEvent:event];// 使用系统默认做法
// 1、判断自己能否接受事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) {
// 结束事件传递
return nil;
}
// 2、点是否在自己身上
if (![self pointInside:point withEvent:event]) {
return nil;
}
// 3、判断自己的子控件,去找有没有比自己更合适的view;从后往前遍历自己的子控件
for (int i = self.subviews.count-1; i >= 0; i--) {
// 获取子控件
UIView *childView = [self subviews][i];
// 坐标系转换
CGPoint childPoint = [self convertPoint:point toView:childView];
UIView *fitView = [childView hitTest:childPoint withEvent:event];
if (fitView) {
return fitView;
}
}
return self;
}
pointInside:withEvent: 方法是 UIView 的方法,用于判断指定的点是否在当前视图内。
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
一次完整的触摸过程,会经历 3 个状态。 上面说了完整的响应事件有如下四个事件
触摸开始:- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
触摸移动:- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
触摸结束:- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
触摸取消(可能会经历):- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
一个完整的触摸事件流程通常包括以下几个步骤:
UIView 不接收触摸事件的三种情况:
当一个 UIView 的 alpha 属性的值接近于 0.0 时,视图实际上变得透明,即不可见。当 alpha 设置为 0.0 时,视图完全透明,不会显示在屏幕上。由于不可见,用户无法与其进行交互或触发事件。
在 iOS 中,用户交互是通过触摸事件来处理的。当一个视图的 alpha 属性小于等于 0.01 时,它几乎是不可见的,甚至对于触摸事件也是如此。这是因为系统会忽略对透明度低于一定阈值的视图的触摸事件。这是为了提高性能和减少用户交互的复杂性。
如果你想让一个视图透明并且仍然能够响应触摸事件,你可以考虑设置一个较低的透明度值,但不要将其完全设置为 0.0。例如,你可以将 alpha 设置为 0.01 或更高的值,以确保视图在屏幕上可见,但仍然具有较低的透明度。这样,视图将保持可见性并能够响应触摸事件。
记住 先触发响应者事件,接着触发触摸事件,也就是先寻找命中者(子->父)!!!!
看别人的博客的时候就在想他们是不是一个过程
实际上,触摸事件的传递确实是从父控件到子控件,而响应者链的传递是从子控件到父控件,两者的传递方向是相反的
下面是触摸事件的传递过程:
而响应者链的传递过程则是从子控件到父控件,如下:
记住 先触发响应者事件,接着触发触摸事件,也就是先寻找命中者!!!!!刚开始一直没搞懂,现在明白了。
触摸事件传递过程是从父控件到子控件,即由UIApplication将事件发送到最顶层的控件,然后由这个控件向下逐级传递事件,直到找到最合适的处理者为止。在传递过程中,如果一个控件能够响应事件,那么就会处理事件并结束传递;如果不能响应事件,那么就会将事件传递给下一个响应者。
而响应者链的传递过程则是从子控件到父控件。当一个控件接收到事件后,它首先会将事件交给自己的响应者对象处理,然后再交给它的父控件的响应者对象处理,直到到达最顶层的响应者对象为止。在传递过程中,如果一个响应者对象能够响应事件,那么就会处理事件并结束传递;如果不能响应事件,那么就会将事件传递给它的父响应者对象。
因此,虽然两个传递过程都涉及到父子控件之间的传递,但它们的传递顺序和目的不同。触摸事件传递过程主要是为了找到最合适的控件来处理事件,而响应者链传递过程则是为了让控件的父子关系中的响应者对象能够逐级处理事件。
响应者链的事件传递过程:
触摸事件处理的详细过程:
如何判断上一个响应者:
事件传递的完整过程:
iOS的响应者链机制的步骤是先通过hitTest和PointSide方法找到合适的控件-Initial View如果这个响应者能够响应
则进行
Initial View -> View Controller(如果存在) -> superview -> · ·· -> rootView -> UIWindow -> UIApplication
如果一个View有一个视图控制器(View Controller),它的下一个响应者是这个视图控制器,紧接着才是它的父视图(Super View),如果一直到Root View都没有处理这个事件,事件会传递到UIWindow(iOS中有一个单例Window),此时Window如果也没有处理事件,便进入UIApplication,UIApplication是一个响应者链的终点,它的下一个响应者指向nil,以结束整个循环
多看些博客才搞懂了什么是真正的响应过程,有的博客讲了一整篇也没讲清触摸事件和响应者链的区别联系,触摸事件就是基于响应者链的事件。推荐看道哥的iOS开发-响应者链Responder Chain】