响应者、响应者链和事件处理

概述

应用程序使用响应者对象来接收和处理事件,属于UIResponder类的实例对象都是响应者,常见的子类包括UIViewUIViewControllerUIApplication。响应者接收到原始事件后,必须处理该事件或者将此事件转发给另一个响应者。当应用程序接收到一个事件时,UIKit会自动将该事件指向最合适的响应者对象,此响应者称为第一响应者,第一响应者会将未处理的事件传递给处于激活状态的响应者链中的下一个响应者对象。应用程序中不存在单一的响应者链,UIkit定义了如何将事件从一个响应者传递到下一个响应者的默认规则,我们可以随时通过覆盖响应者对象中的nextResponder属性来更改这些规则。

下图显示了应用程序中的默认响应者链,其界面包含一个label,一个text field,一个button和两个background view。如果text field没有处理触摸事件,UIKit会将事件发送到text field的父视图对象,如果事件还是未被处理,UIKit会继续发送该事件到此视图的父视图,直到发送到window的根视图,然后响应者链从根视图转移到持有此根视图的视图控制器,再从视图控制器转移到window。如果window不处理这个事件,UIKit会将事件传递给UIApplication对象。如果应用程序的委托对象是UIResponder类的实例并且响应者链中还不包含该对象,那么UiKit可能将该事件传递给应用程序的委托对象。

图1-1

确定事件的第一响应者

对于每种类型的事件,UIKit都会指定一个第一响应者,并首先将事件发送给该对象,第一响应者根据事件的类型而有所不同:

  • 触摸事件(Touch events):第一响应者是触摸点所在的视图。
  • 按压事件(Press events):第一响应者是有焦点的响应者。
  • 摇晃运动事件(Shake-motion events):第一响应者是由我们自己(或者UIKit)指定为第一响应者的对象。
  • 远程控制事件(Remote-control events):第一响应者是由我们自己(或者UIKit)指定为第一响应者的对象。
  • 编辑菜单消息(Editing menu messages):第一响应者是由我们自己(或者UIKit)指定为第一响应者的对象。

注意:与加速计、陀螺仪和磁力计相关的运动事件不遵循响应者链,Core Motion会将这些事件直接传递给我们指定的对象。有关更多信息,可以参看Core Motion Framework

控件使用动作消息直接与其关联的目标对象进行通信。当用户与控件交互时,控件会调用其target对象的action方法——换句话说,控件会向目标对象发送一个动作消息。动作消息不属于事件,但是它也可以使用响应者链。当控件的target对象为nil时,UIKit会从target对象开始顺着响应链寻找,直到找到实现了对应action方法的对象。例如,编辑菜单就使用这种方式去搜索实现了方法名为cut:copy:paste:的响应者对象。

如果一个视图附加有手势识别器,手势识别器会先于视图接收到触摸和按压事件。如果所有视图的手势识别器都无法识别它们的手势,那么事件就会被传递给视图进行处理。如果视图没有处理它们,UIKit会继续沿着响应者链传递事件。有关使用手势识别器处理事件的更多信息,可以参看Handling UIKit Gestures

更改响应者链

可以通过覆写响应者对象的nextResponder属性来更改响应者链,许多UIKit类已经覆盖此属性并返回了特定的对象。

  • UIView对象:如果这个视图是视图控制器的根视图,那么下一个响应者就是这个视图控制器;否则,下一个响应者就是它的父视图。
  • UIViewController对象:如果视图控制器的视图是window的根视图,则下一个响应者就是window;如果视图控制器是被另一个视图控制器呈现的,则下一个响应者是这个呈现视图控制器。
  • UIWindow对象:window的下一个响应者是UIApplication对象。
  • UIApplication对象:当应用程序的委托对象是UIResponder类的实例,而不是视图、视图控制器或者应用程序对象本身时,其下一个响应者就是应用程序的委托对象。

触摸事件(Touch events)

如何确定触摸事件的第一响应者

触摸事件的第一响应者是整个视图层中包含触摸位置并且能够响应用户交互的最上层视图,UIKit基于视图的命中测试来确定该视图。具体来说,UIKit会调用视图层中每个视图对象的hitTest:withEvent:方法来将视图的边界与触摸位置进行比较。

hitTest:withEvent:方法的内部实现中,如果当前视图不能响应用户交互,或者被隐藏,或者alph小于0.01,则会忽略当前视图及其子视图。否则,会调用其pointInside:withEvent:方法来判断当前视图是否包含触摸点。如果不包含,则会忽略当前视图及其子视图;如果包含,则会倒叙遍历(最先访问最后添加的子视图)当前视图的子视图,并调用每个子视图的hitTest:withEvent:方法来查找当前子视图层中包含触摸点的最上层视图。

UIKit会将每个触摸事件永久指定给包含触摸位置的最上层视图,当触摸开始时,UIKit会为每个触摸事件创建一个UITouch对象,直到触摸结束之后才会释放UITouch对象。随着触摸位置或其他参数的改变,UIKit会使用新信息更新UITouch对象,唯一不变的属性是触摸事件所属的view。即使触摸位置移动到触摸事件所属的原始视图之外,触摸事件所属视图也不会改变。

处理触摸事件

响应者对象都是UIResponder类的实例,在处理特定类型的事件时,系统会调用响应者对象相应的方法去响应事件,响应者必须覆写实现相应的方法。为了处理触摸事件,响应者对象需要实现touchesBegan:withEvent:touchesMoved:withEvent:touchesEnded:withEvent:方法中的一个或者多个。UIKit确定触摸事件的第一响应者之后,如果这个响应者类覆写实现了touchesBegan:withEvent:touchesMoved:withEvent:touchesEnded:withEvent:方法中的一个或者多个,那么当触摸开始发生时,系统会调用响应者对象的touchesBegan:withEvent:方法去回应触摸事件。当触摸位置移动时,会调用响应者对象的touchesMoved:withEvent:方法去回应,当触摸结束时,会调用touchesEnded:withEvent:方法去回应。如果这几个方法一个都没有被实现,那么UIKit会沿着默认的响应者链去传递触摸事件。如果响应者链中有响应者实现了前述方法,那么该响应者对象就会去处理传递来的触摸事件。否则,该触摸事件就不会被处理。

系统还可以随时取消正在进行的触摸序列,当有来电打断应用程序时,UIKit会调用响应者的touchesCancelled:withEvent:方法去通知响应者当前触摸事件已经被系统取消了。

图4-1 触摸事件的阶段

touchesBegan:withEvent:touchesMoved:withEvent:touchesEnded:withEvent:touchesCancelled:withEvent:方法分别对应于触摸事件处理过程的不同阶段。当手指(或Apple Pencil)触摸屏幕时,UIKit会创建一个UITouch对象,将触摸点设置为相应的屏幕坐标点,并将其phase属性值设为UITouchPhaseBegan。当手指在屏幕上移动时,UIKit会更新触摸位置,并将UITouch对象的phase属性值改变为UITouchPhaseMoved。当用户从屏幕上移开手指时,UIKit会将phase属性值改变为UITouchPhaseEnded,触摸序列结束。当触摸事件被系统取消时,UIKit会将phase属性值改变为UITouchPhaseCancelled

重要:在默认配置下,当多个手指同时触摸视图时,视图也只会接收与事件关联的第一个UITouch对象。要接收额外的触摸事件,必须将视图的multipleTouchEnabled属性设为YES

摇晃-运动事件

当系统监听到摇晃事件时,会寻找摇晃事件的第一响应者,并将该摇晃事件传递给第一响应者去处理,而摇晃事件的第一响应者是被我们自己(或者UIKit)指定为第一响应者的对象。覆写响应者对象的canBecomeFirstResponder方法并返回YES,同时调用其becomeFirstResponder方法,该响应者对象就会被指定为第一响应者。要对摇晃事件进行处理,响应者对象还需要至少覆写实现motionBegan:withEvent:motionEnded:withEvent:方法中的一个。当摇晃事件开始发生时,系统会调用响应者对象的motionBegan:withEvent:方法去回应摇晃事件。当摇晃事件结束时,系统会调用响应者对象的motionEnded:withEvent:方法回应。如果第一响应者没有处理,那么UIKit会沿着响应者链传递该摇晃事件。

当我们不需要再对摇晃事件进行处理时,需要调用当前响应者对象的resignFirstResponder方法注销其第一响应者身份。

注意:本人在iOS 11(11以下系统未试)下通过代码实践发现,按照上述步骤指定第一响应对象后,响应者对象并未接收到摇晃事件。当系统监听到摇晃事件时,UIKit直接将该摇晃事件发送给当前视图控制器,如果当前视图控制器覆写实现了motionBegan:withEvent:motionEnded:withEvent:方法,那么当前视图控制器就会去处理该摇晃事件。如果没有,这个事件就不会被应用程序处理。

远程控制事件

远程控制事件主要是由耳机线控操作触发的,它和音频播放有关。远程控制事件有以下几种类型:

  • UIEventSubtypeRemoteControlPlay:播放事件,在暂停状态下,按耳机线控中间按钮一下触发。
  • UIEventSubtypeRemoteControlPause:暂停事件,在播放状态下,按耳机线控中间按钮一下触发。
  • UIEventSubtypeRemoteControlStop:停止事件
  • UIEventSubtypeRemoteControlTogglePlayPause:播放或暂停切换,在播放或暂停状态下,按耳机线控中间按钮一下触发。
  • UIEventSubtypeRemoteControlNextTrack:下一曲,按耳机线控中间按钮两下触发。
  • UIEventSubtypeRemoteControlPreviousTrack:上一曲,按耳机线控中间按钮三下触发。
  • UIEventSubtypeRemoteControlBeginSeekingBackward:快退开始,按耳机线控中间按钮三下不要松开触发。
  • UIEventSubtypeRemoteControlEndSeekingBackward:快退停止,按耳机线控中间按钮三下到了快退的位置松开触发。
  • UIEventSubtypeRemoteControlBeginSeekingForward:快进开始,按耳机线控中间按钮两下不要松开触发。
  • UIEventSubtypeRemoteControlEndSeekingForward:快进停止,按耳机线控中间按钮两下到了快进的位置松开触发。

接收远程控制事件首先需要在应用程序启动完成后调用应用程序中唯一的UIApplication对象的beginReceivingRemoteControlEvents方法启用远程控制事件接收。要对远程控制事件进行处理,需要响应者对象覆写实现remoteControlReceivedWithEvent:方法,如果这个响应者对象不是UIApplication类或者UIViewController类的实例,还需要指定该响应者对象为第一响应者。要停止接收远程控制事件,需要调用应用程序中唯一的UIApplication对象的endReceivingRemoteControlEvents方法。

Demo

示例代码下载:https://github.com/zhangshijian/EventHandlingDemo.git

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 用户以多种方式操纵他们的iOS设备,例如触摸屏幕或摇动设备。 iOS会解释用户何时以及如何操作硬件并将此信息传递到...
    坤坤同学阅读 4,057评论 7 19
  • -- iOS事件全面解析 概览 iPhone的成功很大一部分得益于它多点触摸的强大功能,乔布斯让人们认识到手机其实...
    翘楚iOS9阅读 3,043评论 0 13
  • 好奇触摸事件是如何从屏幕转移到APP内的?困惑于Cell怎么突然不能点击了?纠结于如何实现这个奇葩响应需求?亦或是...
    Lotheve阅读 58,448评论 51 604
  • 事件传递:响应者链 当你设计一个app的时候,你很可能需要你的app能够动态响应某些事件。比如,触摸可以发生在屏幕...
    hjfrun阅读 1,054评论 1 5
  • 照片上的姑娘叫程鸣,我习惯叫她没有名字,我认识她时微信上的网名。她的故事,一切还得从我认识她那天说起。 春节后的某...
    朱常在阅读 299评论 0 1