导语 说说 iOS 中关于事件是如何传递与响应的。
因素
iOS
关键词
时间
点击/长按/摇一摇/音乐暂停/播放时
地点
响应者 UIButton/UIView
…
事件派发 & 响应者链条
人物
用户/运行循环
NSRunLoop
& UIApplication
事件
UITapGestureRecognizer/UITouchUpInside
…
UIEvent
如何
执行的具体操作
外部业务实现
环境 & 工具
macOS Sierra 10.12.3 Xcode 8.2.1chisel
例子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 #import "ViewController.h" @interface ViewController ()@end @implementation ViewController - (void )viewDidLoad { [super viewDidLoad]; UIView *v = [[UIView alloc] initWithFrame:CGRectMake (0 , 100 , 100 , 100 )]; v.backgroundColor = [UIColor redColor]; UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector (tapAction:)]; [v addGestureRecognizer:tap]; [self .view addSubview:v]; UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake (0 , 200 , 100 , 30 )]; btn.backgroundColor = [UIColor grayColor]; [btn setTitle:@"测试" forState:UIControlStateNormal ]; [btn addTarget:self action:@selector (btnAction:) forControlEvents:UIControlEventTouchUpInside ]; [self .view addSubview:btn]; NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector (timerAction:) userInfo:nil repeats:true ]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes ]; CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector (displayLinkAction:)]; [link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode ]; link.preferredFramesPerSecond = 6 ; } - (void )btnAction:(UIButton *)btn { NSLog (@"点击事件" ); } - (void )tapAction:(UITapGestureRecognizer *)tap { NSLog (@"触摸事件" ); } - (void )timerAction:(NSTimer *)timer { NSLog (@"timer事件" ); } - (void )displayLinkAction:(CADisplayLink *)link { NSLog (@"displayLink事件" ); } @end
调用堆栈
类型
截图
点击
手势
Timer
CADisplayLink
小结 各种类型的事件都是由 RunLoop
接收。触屏事件由 UIApplication
通过队列的方式 进行派发。
相关的类: UIEvent
类 UIEvent
是事件派发中的基本单元
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 NS_CLASS_AVAILABLE_IOS (2 _0) @interface UIEvent : NSObject @property (nonatomic ,readonly ) UIEventType type NS_AVAILABLE_IOS (3 _0);@property (nonatomic ,readonly ) UIEventSubtype subtype NS_AVAILABLE_IOS (3 _0);@property (nonatomic ,readonly ) NSTimeInterval timestamp;#if UIKIT_DEFINE_AS_PROPERTIES @property (nonatomic , readonly , nullable ) NSSet <UITouch *> *allTouches;#else - (nullable NSSet <UITouch *> *)allTouches; #endif - (nullable NSSet <UITouch *> *)touchesForWindow:(UIWindow *)window; - (nullable NSSet <UITouch *> *)touchesForView:(UIView *)view; - (nullable NSSet <UITouch *> *)touchesForGestureRecognizer:(UIGestureRecognizer *)gesture NS_AVAILABLE_IOS (3 _2); - (nullable NSArray <UITouch *> *)coalescedTouchesForTouch:(UITouch *)touch NS_AVAILABLE_IOS (9 _0); - (nullable NSArray <UITouch *> *)predictedTouchesForTouch:(UITouch *)touch NS_AVAILABLE_IOS (9 _0); @end NS_ASSUME_NONNULL_END
触屏事件 事件类型
事件驱动应用程序的主要职责是处理用户事件,即由鼠标,键盘,跟踪器和平板电脑等设备生成的事件。
到 iOS 应用中相应的成了触屏、加速计、远程控制等事件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 NS_CLASS_AVAILABLE_IOS (2 _0) @interface UIResponder : NSObject <UIResponderStandardEditActions >#if UIKIT_DEFINE_AS_PROPERTIES @property (nonatomic , readonly , nullable ) UIResponder *nextResponder;#else - (nullable UIResponder *)nextResponder; #endif ... - (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; - (void )touchesEstimatedPropertiesUpdated:(NSSet <UITouch *> *)touches NS_AVAILABLE_IOS (9 _1); ... - (void )motionBegan:(UIEventSubtype )motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS (3 _0); - (void )motionEnded:(UIEventSubtype )motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS (3 _0); - (void )motionCancelled:(UIEventSubtype )motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS (3 _0); - (void )remoteControlReceivedWithEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS (4 _0); @end
响应者
应用程序使用响应者对象接收和处理事件。响应者对象是 UIResponder
类的任何实例,常见的子类包括 UIView
,UIViewController
和 UIApplication
。 UIKit
自动管理大多数响应者相关的行为,包括事件如何从一个响应者传递到下一个。但是,您可以修改默认行为来更改事件在应用程序中的传送方式。
响应者链条
在当前场景下的 btn 的响应者链条如下
1 2 3 4 5 6 7 (lldb) presponder btn <UIButton : 0x115e0fea0 ; frame = (0 200 ; 100 30 ); opaque = NO ; layer = <CALayer : 0x1702214a0 >> | <UIView : 0x115d07820 ; frame = (0 0 ; 375 667 ); autoresize = W+H; layer = <CALayer : 0x17403df00 >> | | <ViewController: 0x115e0d720 > | | | <UIWindow : 0x115e0de20 ; frame = (0 0 ; 375 667 ); autoresize = W+H; gestureRecognizers = <NSArray : 0x170245d90 >; layer = <UIWindowLayer : 0x170220e00 >> | | | | <UIApplication : 0x115e007f0 > | | | | | <AppDelegate: 0x17403b6a0 >
通过 UIApplication.h 文件中的注释与下面 lldb 调试的结果
action A selector identifying an action method. See the discussion for information on the permitted selector forms. target The object to receive the action message. If target is nil, the app sends the message to the first responder, from whence it progresses up the responder chain until it is handled. sender The object that is sending the action message. The default sender is the UIControl object that invokes this method. event A UIEvent object that encapsulates information about the event originating the action message.
比较容易推断出
描述
值
sendAction
btnAction:
to
ViewController
from
UIButton
forEvent
UITounesEvent
UIView
不能接收触屏事件的三种情况:
不接受用户交互:userInteractionEnabled = NO;
隐藏:hidden = YES;
透明:alpha = 0.0~0.01
事件分发
发生触摸事件后,系统会将该事件加入到一个由 UIApplication
管理的队列事件中(从上面的调用堆栈中可知)。
UIApplication
会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常会先发送事件给应用程序的主窗口(keyWindow
)
主窗口 (keyWindow
) 对象首先会使用 hitTest:withEvent:
方法寻找此次 Touch
操作初始点所在的视图(View
),即需要将触摸事件传递给最合适的视图,这个过程称之为 hit-test view
应用如何找到最合适的控件来处理事件?有以下准则
首先判断主窗口(keyWindow
)自己是否能接受触摸事件
触摸点是否在自己身上
从后往前遍历子控件,重复前面的两个步骤(首先查找数组中最后一个元素)
如果没有符合条件的子控件,那么就认为自己最合适处理 注意:之所以会采取从后往前遍历子控件的方式寻找最合适的view只是为了做一些循环优化。因为相比较之下,后添加的view在上面,降低循环次数。
事件分发 && 响应者链条都是为了找出合适的响应者
小结 hit-test view
确定最合适的视图,若该视图如果不能响应该事件时,可通过响应者链条找到合适的响应者。
在当前场景下, UIButton
作为第一响应者能响应当前的点击事件,触发 UIResponser
协议的 touchesEnded:withEvent
代理方法,最后作为 sender
参数传递给 ViewController
的 btnAction:
方法。
手势 UIView
中有
1 2 3 4 5 6 7 8 @interface UIView (UIViewGestureRecognizers )@property (nullable , nonatomic ,copy ) NSArray <__kindof UIGestureRecognizer *> *gestureRecognizers NS_AVAILABLE_IOS (3 _2);- (void )addGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer NS_AVAILABLE_IOS (3 _2); - (void )removeGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer NS_AVAILABLE_IOS (3 _2); ...
The gesture-recognizer objects currently attached to the view.
gestureRecognizers
保存着与当前的视图对象相关联的 UIGestureRecognizer
类型对象
为视图添加手势
为视图添加 addTarget:Action:
,将 响应者链条 部分稍作处理可得
小结 无论是用 addTarget:Action:
还是将手势添加到视图对象上,最终都是创建了 UIEvent
这个事件单元对象。
手势相比触碰事件的好处是可以直接使用已经定义好的手势,开发者不用自己计算手指移动轨迹。缺点就是没办法自定义手势,只能用系统已经实现的手势。如果想实现自己发明的某种手势还得去用触摸。
总结 当事件发生时,RunLoop
接收到外部或内部注册的事件,将其传递给 UIApplication
对象,UIApplication
封装成 UIEvent
类型的对象并将其分发给应用程序的主窗口(keyWindow
),主窗口对象使用 hit-test view
的流程找到响应当前事件最合适的视图,若该视图无法响应事件,则通过响应者链条向上传递,直到找到合适的响应者,该响应者需要实现了 touchesBegan:withEvent:
等代理方法,若没找到就将事件抛弃,最终会将响应者传递给外部的调用者。
参考
iOS 点击事件传递及响应
Understanding Responders and the Responder Chain
Cocoa Event Handling Guide
iOS手势(UIGestureRecognizer)和触摸(touche event)的区别和联系