iOS 使用响应者和响应者链处理事件

| Swift

 

内容概览

  • 综述
  • 确定一个事件的第一响应者(First Responder)
  • 确定哪个响应者包含一个触控事件
  • 改变响应者链
  • 在视图(UIView)中处理触控事件

 
 

综述

 

iOS 应用使用响应者对象接收和处理事件。一个响应者对象是 UIResponder 类的实例,常见的子类包括: UIView, UIViewController, UIApplication。响应者接收原始的事件数据,并且必须对其进行 处理 或者将其 转发给另一个响应者对象。当你的应用接收到一个事件时,UIKit 自动将这个事件传递给最适合处理这个事件的响应者对象 —— 第一响应者(first responder)。

未被处理的事件会在响应链中的响应者之间传递,这是应用中的响应者对象的动态配置。

图片alt

上图展示了一个应用中的响应者,还显示了事件如何按照响应者链从一个响应者传递到下一个响应者。

如果 text field 不处理事件,则 UIKit 会将事件发送到 text field 的 super view 对象,然后是 window 的 root view。
从 root view 开始,响应链在将事件传递到 window 之前,先传递到 root view 所属的 view controller。
如果 window 无法处理事件,则 UIKit 会将事件传递给 UIApplication 对象,如果该 UIApplication 对象是 UIResponder 实例并且还不是响应者链的一部分,则可能传递给应用程序委托。

 
 

确定一个事件的第一响应者(First Responder)

 

UIKit 根据事件的类型将对象指定为事件的第一响应者。

事件类型 第一响应者
触控事件 发生触控事件的视图
按下设备的物理按键触发的事件 有焦点的对象
摇晃动作事件 由 UIKit 或者开发者指定
远程控制事件 由 UIKit 或者开发者指定
编辑菜单消息 由 UIKit 或者开发者指定

与加速度计、陀螺仪和磁力计有关的运动事件不遵循响应程序链。
相反,Core Motion 将这些事件直接传递到指定的对象。
有关更多信息,请参见 Core Motion Framework

 

定义事件类型的代码:

    public enum EventType : Int {
        // 点击屏幕相关的事件
        case touches
        // 设备移动相关的事件,如:用户摇晃设备
        case motion
        // 来自控制设备多媒体的外部配件,如:耳机
        case remoteControl
        // 按下设备的物理按键
        @available(iOS 9.0, *)
        case presses
    }

控件(UIControl)使用动作(action)消息直接与其关联的目标对象进行通信。

    // 为特定事件添加 target/action。你可以多次调用,也可以为特定事件指定多个 target/action
    // 当 target 为nil时,事件沿着响应者链传递。action 可能会包括发送者和事件的顺序
    // action 不能为NULL。target 不会被强引用
    func addTarget(_ target: Any?, action: Selector, for controlEvents: UIControl.Event)

当用户与控件交互时,控件会将操作消息发送到其目标对象(target)。
动作消息不是事件,但它们仍可以利用响应者链。当控件的目标对象为 nil 时,UIKit 从目标对象开始并遍历响应者链,直到找到实现适当操作方法的对象为止。
例如,UIKit 编辑菜单使用此行为来搜索响应者对象,这些对象实现了诸如 cut(_:), copy(_:), or paste(_:) 之类的方法。

手势识别器(Gesture recognizer) 会在视图处理事件之前接收触摸(touch)和按下(press)事件。
如果视图的手势识别器无法识别一系列触摸,则 UIKit 会将触摸发送给视图。
如果视图无法处理触摸,则 UIKit 会将其向上传递到响应者链。
有关使用手势识别器处理事件的更多信息,请参见处理 Handling UIKit Gestures

 
 

确定哪个响应者包含一个触控事件

 

UIKit 使用基于视图的点击测试来确定触摸事件发生的位置。
具体来说,UIKit 将触摸位置与视图层次结构中视图对象的边界(bounds)进行比较。
UIViewfunc hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? 方法遍历视图层次结构,查找包含指定触摸的最深的子视图,该子视图会成为触摸事件的第一响应者。

 

如果触摸位置在视图范围(bounds)之外,则 func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? 方法将忽略该视图及其所有子视图。因此,如果视图的 clipsToBounds 属性为 false,则即使该视图正好包含触摸,也不会返回该视图范围(bounds)之外的子视图。
有关点击测试行为的更多信息,请参见UIView中关于 hitTest(_:with:) 方法的讨论。

 

func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? 方法的文档:

图片alt

  • 讨论解析:

此方法通过调用每个子视图的 point(inside:with:) 方法来遍历视图层次结构,以确定哪个子视图应接收触摸事件。

// 检测该点是否在接收者的 bounds 范围内
func point(inside point: CGPoint, with event: UIEvent?) -> Bool

如果 point(inside:with:) 返回 true,则将遍历子视图的视图层次结构,直到找到最前面的包含指定点的视图。如果视图不包含该点,则将忽略其视图层次结构的分支。
你很少需要自己调用此方法,但是你可以重写此方法以在子视图中隐藏触摸事件。

此方法将忽略 隐藏的禁用用户交互的alpha值小于0.01 的视图对象。
确定点击时,此方法不会考虑视图的内容。即使指定点位于该视图内容的透明部分中,该视图仍然可以被返回。

位于接收者范围之外的点,即使实际上位于接收者子视图内,也不会被点击测试匹配。如果当前视图的 clipsToBounds 属性设置为 false,并且受影响的子视图超出了视图的范围,则可能发生这种情况。

 
 

发生触摸时,UIKit 会创建一个 UITouch 对象并将其与视图关联。
随着触摸位置或其他参数的更改,UIKit 会使用新信息更新相同的UITouch对象。但唯一不变的属性是视图。 (即使触摸位置移到原始视图之外,UITouch 对象的 view 属性中的值也不会更改。)
触摸结束时,UIKit 会释放该 UITouch 对象。

 
 

改变响应者链

 

var next: UIResponder?

您可以通过重写响应者对象的 next 属性来更改响应者链。
如果重写,下一个响应者将会是你返回的对象。

许多 UIKit 类已经重写此属性并返回特定的对象,比如:

  • UIView 对象。如果 view 是 view controller 的 root view,则下一个响应者是 view controller;否则,下一个响应者是 view 的 super view。

  • UIViewController 对象

    • 如果 view controller 的 view 是 window 的 root view,则下一个响应者是 window 对象。

    • 如果 view controller B 是由 view controller A present的,则下一个响应者是 view controller A。

  • UIWindow 对象。window 的下一个响应者是 UIApplication 对象。

  • UIApplication 对象。下一个响应者是 app delegate,但仅当 app delegate 是 UIResponder 的实例且不是 view,view controller 或 UIApplication 对象本身时,才是下一个响应者。

 
 

在视图(UIView)中处理触控事件

 

如果你不打算在 UIView 中使用 UIGestureRecognizer 来处理触控事件,你可以使用 UIView 本身来处理。
因为 UIView 继承于 UIResponder,所以它可以处理多点触控事件和其他类型的事件。

UIKit 确定一个事件发生在某个 UIView 上时,它会调用该 view 的 touchesBegan:withEvent:, touchesMoved:withEvent:, 或者 touchesEnded:withEvent: 等方法。

你在 UIView 中重写的这些方法将处理点击事件处理过程中不同的阶段。

点击事件的不同阶段,如下图所示:
图片alt

当手指(或者苹果笔)点击屏幕时,UIKit 会创建一个 UITouch 对象,然后将其触控位置设置为合适的点,将其 phase 属性设置为 UITouchPhaseBegan
当同一个手指在屏幕上移动时,UIKit 会更新该 UITouch 对象的触控位置并将 phase 属性设置为 UITouchPhaseMoved
当手指抬起并离开屏幕,UIKit 会将 phase 属性设置为 UITouchPhaseEnded,然后点击事件结束。

类似地,系统可能会在任何时候取消正在进行的点击事件。比如,电话呼入可以打断应用的执行过程。
这时,UIKit 会通过调用 touchesCancelled:withEvent: 方法通知你的 view。
你可以通过该方法来清理 view 中的数据结构。

UIKit 会为每个点击屏幕的新手指创建 UITouch 对象。这些 UITouch 对象会和当前的 UIEvent 对象一起被传递。UIKit 可以区分来自手指和苹果笔的触控,所以你可以对它们做不同的处理。

默认情况下,一个 view 只接收第一个 UITouch 和相应的事件,即使不只一个手指点击了屏幕。
如果需要接收多个触控,你需要将 multipleTouchEnabled 设置为 true

 
 


参考内容:
Using Responders and the Responder Chain to Handle Events
Handling Touches in Your View

 
 

觉得不错?点个赞呗~

本文链接:iOS 使用响应者和响应者链处理事件

转载声明:本站文章如无特别说明,皆为原创。转载请注明:Ficow Shen's Blog

评论区(期待你的留言)