<简书 — 刘小壮> https://www.jianshu.com/p/b0884faae603app
很久没写博客了,先后算起来恰好有一年了。这期间博客也不是一直没变化,细心的同窗应该能发现,我一直在回复评论区和私信的问题,还更新了好几篇以前的博客。ide
去年是有意义的一年,从各个方面我也学到了很多的东西,也不局限于技术方面。不少人都写年终总结,我比较懒就不写了,心里作自我总结吧,哈哈。函数
回归正题,在项目中常常会遇到各类手势或者点击事件处理之类的,这些都属于响应事件处理。可是不少人对iOS中的响应事件处理并不清楚,常常会遇到手势冲突、事件不响应之类的问题,因此就去查博客。 可是如今不少博客写的并非很完整,或者说质量并不高,我这两天抽时间把我所学习和理解的iOS事件处理写出来,供各位参考。oop
**UIResponder
是iOS中用于处理用户事件的API,能够处理触摸事件、按压事件(3D touch)
、远程控制事件、硬件运动事件。**能够经过touchesBegan
、pressesBegan
、motionBegan
、remoteControlReceivedWithEvent
等方法,获取到对应的回调消息。UIResponder
不仅用来接收事件,还能够处理和传递对应的事件,若是当前响应者不能处理,则转发给其余合适的响应者处理。学习
应用程序经过响应者来接收和处理事件,响应者能够是继承自UIResponder
的任何子类,例如UIView
、UIViewController
、UIApplication
等。当事件来到时,系统会将事件传递给合适的响应者,而且将其成为第一响应者。测试
第一响应者未处理的事件,将会在响应者链中进行传递,传递规则由UIResponder
的nextResponder
决定,能够经过重写该属性来决定传递规则。当一个事件到来时,第一响应者没有接收消息,则顺着响应者链向后传递。ui
查找第一响应者时,有两个很是关键的API
,查找第一响应者就是经过不断调用子视图的这两个API
完成的。代理
调用方法,获取到被点击的视图,也就是第一响应者。code
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
hitTest:withEvent:
方法内部会经过调用这个方法,来判断点击区域是否在视图上,是则返回YES
,不是则返回NO
。对象
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
应用程序接收到事件后,将事件交给keyWindow
并转发给根视图,根视图按照视图层级逐级遍历子视图,而且遍历的过程当中不断判断视图范围,并最终找到第一响应者。
从keyWindow
开始,向前逐级遍历子视图,不断调用UIView
的hitTest:withEvent:
方法,经过该方法查找在点击区域中的视图后,并继续调用返回视图的子视图的hitTest:withEvent:
方法,以此类推。若是子视图不在点击区域或没有子视图,则当前视图就是第一响应者。
在hitTest:withEvent:
方法中,会从上到下遍历子视图,并调用subViews
的pointInside:withEvent:
方法,来找到点击区域内且最上面的子视图。若是找到子视图则调用其hitTest:withEvent:
方法,并继续执行这个流程,以此类推。若是子视图不在点击区域内,则忽略这个视图及其子视图,继续遍历其余视图。
能够经过重写对应的方法,控制这个遍历过程。经过重写pointInside:withEvent:
方法,来作本身的判断并返回YES
或NO
,返回点击区域是否在视图上。经过重写hitTest:withEvent:
方法,返回被点击的视图。
此方法在遍历视图时,忽略如下三种状况的视图,若是视图具备如下特征则忽略。可是视图的背景颜色是clearColor
,并不在忽略范围内。
hidden
等于YES。alpha
小于等于0.01。userInteractionEnabled
为NO。若是点击事件是发生在视图外,但在其子视图内部,子视图也不能接收事件并成为第一响应者。这是由于在其父视图进行hitTest:withEvent:
的过程当中,就会将其忽略掉。
UIApplication
接收到事件,将事件传递给keyWindow
。keyWindow
遍历subViews
的hitTest:withEvent:
方法,找到点击区域内合适的视图来处理事件。UIView
的子视图也会遍历其subViews
的hitTest:withEvent:
方法,以此类推。UIApplication
。nextResponder
方法,一直找响应者链中能处理该事件的对象。UIApplication
后仍然没有能处理该事件的对象,则该事件被废弃。模拟代码
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { if (self.alpha <= 0.01 || self.userInteractionEnabled == NO || self.hidden) { return nil; } BOOL inside = [self pointInside:point withEvent:event]; if (inside) { NSArray *subViews = self.subviews; // 对子视图从上向下找 for (NSInteger i = subViews.count - 1; i >= 0; i--) { UIView *subView = subViews[i]; CGPoint insidePoint = [self convertPoint:point toView:subView]; UIView *hitView = [subView hitTest:insidePoint withEvent:event]; if (hitView) { return hitView; } } return self; } return nil; }
如上图所示,响应者链以下:
UITextField
后其会成为第一响应者。textField
未处理事件,则会将事件传递给下一级响应者链,也就是其父视图。UIViewController
的View
。View
未处理事件,则会交给控制器处理。UIWindow
。UIApplication
。UIApplicationDelegate
,若是其未处理则丢弃事件。事件经过UITouch
进行传递,在事件到来时,第一响应者会分配对应的UITouch
,UITouch
会一直跟随着第一响应者,而且根据当前事件的变化UITouch
也会变化,当事件结束后则UITouch
被释放。
UIViewController
没有hitTest:withEvent:
方法,因此控制器不参与查找响应视图的过程。可是控制器在响应者链中,若是控制器的View
不处理事件,会交给控制器来处理。控制器不处理的话,再交给View
的下一级响应者处理。
hitTest:withEvent:
方法时,若是该视图是hidden
等于NO的那三种被忽略的状况,则改视图返回nil
。UIImageView
的userInteractionEnabled
默认为NO,若是想要UIImageView
响应交互事件,将属性设置为YES便可响应事件。有时候想让指定视图来响应事件,再也不向其子视图继续传递事件,能够经过重写hitTest:withEvent:
方法。在执行到方法后,直接将该视图返回,而再也不继续遍历子视图,这样响应者链的终端就是当前视图。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { return self; }
在开发过程当中,常常会遇到子视图显示范围超出父视图的状况,这时候能够重写该视图的pointInside:withEvent:
方法,将点击区域扩大到可以覆盖全部子视图。
假设有上面的视图结构,SuperView
的Subview
超出了其视图范围,若是点击Subview
在父视图外面的部分,则不能响应事件。因此经过重写pointInside:withEvent:
方法,将响应区域扩大为虚线区域,包含SuperView
的全部子视图,便可让子视图响应事件。
若是想让响应者链中,每一级UIResponder
均可以响应事件,能够在每级UIResponder
中都实现touches
并调用super
方法,便可实现响应者链事件逐级传递。
只不过这并不包含UIControl
子类以及UIGestureRecognizer
的子类,这两类会直接打断响应者链。
若是有事件到来时,视图有附加的手势识别器,则手势识别器优先处理事件。若是手势识别器没有处理事件,则将事件交给视图处理,视图若是未处理则顺着响应者链继续向后传递。
当响应者链和手势同时出现时,也就是既实现了touches
方法又添加了手势,会发现touches
方法有时会失效,这是由于手势的执行优先级是高于响应者链的。
事件到来后先会执行hitTest
和pointInside
操做,经过这两个方法找到第一响应者,这个在上面已经详细讲过了。当找到第一响应者并将其返回给UIApplication
后,UIApplication
会向第一响应者派发事件,而且遍历整个响应者链。若是响应者链中可以处理当前事件的手势,则将事件交给手势处理,并调用touches
的cancelled
方法将响应者链取消。
在UIApplication
向第一响应者派发事件,而且遍历响应者链查找手势时,会开始执行响应者链中的touches
系列方法。会先执行touchesBegan
和touchesMoved
方法,若是响应者链可以继续响应事件,则执行touchesEnded
方法表示事件完成,若是将事件交给手势处理则调用touchesCancelled
方法将响应者链打断。
根据苹果的官方文档,手势不参与响应者链传递事件,可是也经过hitTest
的方式查找响应的视图,手势和响应者链同样都须要经过hitTest
方法来肯定响应者链的。在UIApplication
向响应者链派发消息时,只要响应者链中存在可以处理事件的手势,则手势响应事件,若是手势不在响应者链中则不能处理事件。
Apple UIGestureRecognizer Documentation
根据上面的手势和响应者链的处理规则,咱们会发现UIButton
或者UISlider
等控件,并不符合这个处理规则。UIButton
能够在其父视图已经添加tapGestureRecognizer
的状况下,依然正常响应事件,而且tap
手势不响应。
以UIButton
为例,UIButton
也是经过hitTest
的方式查找第一响应者的。区别在于,若是UIButton
是第一响应者,则直接由UIApplication
派发事件,不经过Responder Chain
派发。若是其不能处理事件,则交给手势处理或响应者链传递。
不仅UIButton
是直接由UIApplication
派发事件的,全部继承自UIControl
的类,都是由UIApplication
直接派发事件的。
为了有依据的推断响应事件的实现和传递机制,咱们作如下测试。
假设RootView
、SuperView
、Button
都实现touches
方法,而且Button
添加buttonAction:
的action
,点击button
后的调用以下。
RootView -> hitTest:withEvent: RootView -> pointInside:withEvent: SuperView -> hitTest:withEvent: SuperView -> pointInside:withEvent: Button -> hitTest:withEvent: Button -> pointInside:withEvent: RootView -> hitTest:withEvent: RootView -> pointInside:withEvent: Button -> touchesBegan:withEvent: Button -> touchesEnded:withEvent: Button -> buttonAction:
仍是上面的视图结构,咱们给RootView
加上UITapGestureRecognizer
手势,而且经过tapAction:
方法接收回调,点击上面的SuperView
后,方法调用以下。
RootView -> hitTest:withEvent: RootView -> pointInside:withEvent: SuperView -> hitTest:withEvent: SuperView -> pointInside:withEvent: Button -> hitTest:withEvent: Button -> pointInside:withEvent: RootView -> hitTest:withEvent: RootView -> pointInside:withEvent: RootView -> gestureRecognizer:shouldReceivePress: RootView -> gestureRecognizer:shouldBeRequiredToFailByGestureRecognizer: SuperView -> touchesBegan:withEvent: RootView -> gestureRecognizerShouldBegin: RootView -> tapAction: SuperView -> touchesCancelled:
上面的视图中Subview1
、Subview2
、Subview3
是同级视图,都是SuperView
的子视图。咱们给Subview1
加上UITapGestureRecognizer
手势,而且经过subView1Action:
方法接收回调,点击上面的Subview3
后,方法调用以下。
SuperView -> hitTest:withEvent: SuperView -> pointInside:withEvent: Subview3 -> hitTest:withEvent: Subview3 -> pointInside:withEvent: SuperView -> hitTest:withEvent: SuperView -> pointInside:withEvent: Subview3 -> touchesBegan:withEvent: Subview3 -> touchesEnded:withEvent:
经过上面的例子来看,虽然Subview1
在Subview3
的下面,而且添加了手势,点击区域是在Subview1
和Subview3
两个视图上的。可是因为通过hitTest
和pointInside
以后,响应者链中并无Subview1
,因此Subview1
的手势并无被响应。
根据咱们上面的测试,推断iOS响应事件的优先级,以及总体的响应逻辑。
当事件到来时,会经过hitTest
和pointInside
两个方法,从Window
开始向上面的视图查找,找到第一响应者的视图。找到第一响应者后,系统会判断其是继承自UIControl
仍是UIResponder
,若是是继承自UIControl
,则直接经过UIApplication
直接向其派发消息,而且再也不向响应者链派发消息。
若是是继承自UIResponder
的类,则调用第一响应者的touchesBegin
,而且不会当即执行touchesEnded
,而是调用以后顺着响应者链向后查找。若是在查找过程当中,发现响应者链中有的视图添加了手势,则进入手势的代理方法中,若是代理方法返回能够响应这个事件,则将第一响应者的事件取消,并调用其touchesCanceled
方法,而后由手势来响应事件。
若是手势不能处理事件,则交给第一响应者来处理。若是第一响应者也不能响应事件,则顺着响应者链继续向后查找,直到找到可以处理事件的UIResponder
对象。若是找到UIApplication
尚未对象响应事件的话,则将此次事件丢弃。
在UIApplication
接收到响应事件以前,还有更复杂的系统级的处理,处理流程大体以下。
系统经过IOKit.framework
来处理硬件操做,其中屏幕处理也经过IOKit
完成(IOKit
多是注册监听了屏幕输出的端口) 当用户操做屏幕,IOKit
收到屏幕操做,会将此次操做封装为IOHIDEvent
对象。经过mach port
(IPC进程间通讯)将事件转发给SpringBoard
来处理。
SpringBoard
是iOS系统的桌面程序。SpringBoard
收到mach port
发过来的事件,唤醒main runloop
来处理。 main runloop
将事件交给source1
处理,source1
会调用__IOHIDEventSystemClientQueueCallback()
函数。
函数内部会判断,是否有程序在前台显示,若是有则经过mach port
将IOHIDEvent
事件转发给这个程序。 若是前台没有程序在显示,则代表SpringBoard
的桌面程序在前台显示,也就是用户在桌面进行了操做。 __IOHIDEventSystemClientQueueCallback()
函数会将事件交给source0
处理,source0
会调用__UIApplicationHandleEventQueue()
函数,函数内部会作具体的处理操做。
例如用户点击了某个应用程序的icon,会将这个程序启动。 应用程序接收到SpringBoard
传来的消息,会唤醒main runloop
并将这个消息交给source1
处理,source1
调用__IOHIDEventSystemClientQueueCallback()
函数,在函数内部会将事件交给source0
处理,并调用source0
的__UIApplicationHandleEventQueue()
函数。 在__UIApplicationHandleEventQueue()
函数中,会将传递过来的IOHIDEvent
转换为UIEvent
对象。
在函数内部,调用UIApplication
的sendEvent:
方法,将UIEvent
传递给第一响应者或UIControl
对象处理,在UIEvent
内部包含若干个UITouch
对象。
source1
是runloop
用来处理mach port
传来的系统事件的,source0
是用来处理用户事件的。 source1
收到系统事件后,都会调用source0
的函数,因此最终这些事件都是由source0
处理的。
在开发中,有时会有找到当前View
对应的控制器的需求,这时候就能够利用咱们上面所学,根据响应者链来找到最近的控制器。
在UIResponder
中提供了nextResponder
方法,经过这个方法能够找到当前响应环节的上一级响应对象。能够从当前UIView
开始不断调用nextResponder
,查找上一级响应者链的对象,就能够找到离本身最近的UIViewController
。
示例代码:
- (UIViewController *)parentController { UIResponder *responder = [self nextResponder]; while (responder) { if ([responder isKindOfClass:[UIViewController class]]) { return (UIViewController *)responder; } responder = [responder nextResponder]; } return nil; }