调试iOS用户交互事件响应流程一、响应链1.1 Next Responder1.1.1 调试nextResponder1.2 Target-Action和响应链1.2.1 注册UIControlEvents1.2.2 调试UIControlEvents的传递结论一:Action不会在同级视图层级中传递结论二:Target为空时Action仍可以被响应结论三:Target为空时Action沿响应链传递1.3 手势识别和响应链1.4 修改响应链二、Touch事件传递2.1 碰撞检测2.2 调试Touch事件传递步骤零:准备工作步骤一:下断点步骤二:简单分析 touch 事件在 Window 层的分发步骤三:分析 Touch 事件的产生步骤四:分析 touch 事件开始后的传递情况一:点击 Button 控件时情况二:点击 Label 视图步骤五:分析 touch 事件结束后的传递三、RunLoop与事件(TODO)四、总结
调试iOS用户交互事件响应流程
2020-03-19
通常 iOS 界面开发中处理各种用户交互事件。其中,UIControlEvent
以注册的 Target-Action 的方式绑定到控件;UIGestureRecognizer
通过addGestureRecognizer:
添加到UIView
的gestureRecognizers
属性中;UIResponder
提供了touchesBegin/Moved/Ended/Canceled/:withEvent:
、motionsXXX:withEvent:
、pressXX:withEvent:
系列接口,将用户设备的触摸、运动、按压事件通知到UIResponder
对象等等。以上都是常用开发者处理用户交互事件的方式,那么隐藏在这些接口之下,从驱动层封装交互事件对象到 UI 控件接收到用户事件的流程是怎样的呢?本文主要探讨的就是这个问题。
一、响应链
Apple Documentation 官方文档Using Responders and the Responder Chain to Handle Events介绍了利用UIResponder
的响应链来处理用户事件。UIResponder
实现了touchesXXX
、pressXXX
、motionXXX
分别用于响应用户的触摸、按压、运动(例如UIEventSubtypeMotionShake
)交互事件。UIResponder
包含nextResponder
属性。UIView
、UIWindow
、UIController
、UIApplication
都是UIResponder
的派生类,所以都能响应以上事件。
1.1 Next Responder
响应链结构如下图所示,基本上是通过UIResponder
的nextResponder
成员串联而成,基本上是按照 view 的层级,从前向后由子视图向父视图传递,且另外附加其他规则。总的响应链的规则如下:
View 的
nextResponder
是其父视图;当 View 为 Controller 的根视图时,
nextResponder
是 Controller;Controller 的
nextResponder
是 present Controller 的控制器;当 Controller 为根控制器时,
nextResponder
是 Window;Window 的
nextResponder
是 Application;Application 的
nextResponder
是 App Delegate(仅当 App Delegate 为UIResponder
类型);
UIResponder
响应touchesXXX
、pressXXX
、motionXXX
事件不需要指定userInteractionEnabled
为YES
。但是对于UIView
则需要指定userInteractionEnabled
,原因是UIView
重新实现了这些方法。响应UIGesture
则需要指定userInteractionEnabled
,addGestureRecognizer:
是UIView
类的接口。
注意:新版本中,分离了 Window 和 View 的响应链。当 Controller 为根控制器时,
nextResponder
实际上是nil
;Windows 的nextResponder
是 Window Scene;Window Scene 的nextResponder
是 Application。在后面的调试过程会有体现。
1.1.1 调试nextResponder
使用一个简单的 Demo 调试nextResponder
。界面如下图所示,包含三个 Label,从颜色可以判断其层次从后往前的顺序是:A >> B >> C。下面两个按钮另做他用,先忽略。
运行 Demo,查看各个元素的nextResponder
,确实如前面所述。
1.2 Target-Action和响应链
UIControl
控件与关联的 target 对象通信,直接通过向 target 对象发送 action 消息。虽然 Action 消息虽然不是事件,但是 Action 消息的传递是要经过响应链的。当接收到用户交互事件的控件的 target 为nil
时,会沿着控件的响应链向下搜索,直到找到实现该 action 方法的对象为止。UIKit 的编辑菜单就是通过这个机制实现的,UIKit 会沿着控件的响应链搜索实现了cut:
、copy:
、paste:
等方法的对象。
1.2.1 注册UIControlEvents
当UIControl
控件调用addTarget:action:forControlEvents:
方法注册事件时,会将构建UIControlTargetAction
对象并将其添加到UIControl
控件的(NSMutableArray*)_targetActions
私有成员中,addTarget:action:forControlEvents:
方法的 Apple Documentation 注释中有声明调用该方法时UIControl
并不会持有 target 对象,因此无需考虑循环引用的问题。UIControl Events 注册过程的简单调试过程如下:
附注:The control does not retain the object in the target parameter. It is your responsibility to maintain a strong reference to the target object while it is attached to a control.
1.2.2 调试UIControlEvents的传递
前面内容提到,控件的 action 是沿着响应链传递的,那么,当两个控件在界面上存在重合的区域,那么在重合区域触发用户事件时,action 消息会在哪个控件上产生呢?在 1.1.1 中的两个重合的按钮就是为了验证这个问题。
稍微改造一下 1.1.1 的 Demo 程序,将 Label A、B、C 指定为自定义的继承自UILabel
的类型TestEventsLabel
,将两个 Button 指定为继承自UIButton
的TestEventsButton
类型。然后在TestEventsLabel
、TestEventsButton
、ViewController
中,为touchesXXX:
系列方法、nextResponder
方法、hitTest:withEvent:
方法添加打印日志的代码,以TestEventsButton
的实现为例(当然也可以用 AOP 实现):
@implementation TestEventsButton
-(UIResponder *)nextResponder{
UIResponder* responder = [super nextResponder];
NSLog(@"Next Responder Button %@ - return responder: %@", [self titleForState:UIControlStateNormal], responder);
return responder;
}
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
UIView* view = [super hitTest:point withEvent:event];
NSLog(@"Hit Test Button %@ - return view: %@", [self titleForState:UIControlStateNormal], view);
return view;
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[super touchesBegan:touches withEvent:event];
NSLog(@"Button %@ - %s", [self titleForState:UIControlStateNormal], __func__);
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[super touchesEnded:touches withEvent:event];
NSLog(@"Button %@ - %s", [self titleForState:UIControlStateNormal], __