软件设计

Swift 责任链模式与 UIResponder 链

使用 Swift 开发 iOS 应用时,很多人都曾疑惑过:明明点击了按钮,为什么会是这个视图控制器收到事件?

5 分钟阅读
Swift 责任链模式与 UIResponder 链 封面图

使用 Swift 开发 iOS 应用时,很多人都曾疑惑过:明明点击了按钮,为什么会是这个视图控制器收到事件?

深入追踪触摸事件在屏幕中如何流动,就会发现底层使用了责任链模式。

先说结论,UIResponder 链是 Apple 在框架层实现责任链模式(Chain of Responsibility)的典型案例,而责任链模式是 GoF 设计模式之一。responder 会将责任传递给下一个 responder,直到找到能够处理事件的对象。

本文会通过拆解代码,整理责任链模式的概念、UIResponder 链的实际工作方式,以及了解这些内容对实际开发的帮助。

先看核心摘要

  1. 责任链模式是一种设计模式,多个对象像链条一样传递请求,直到找到能够处理请求的对象。
  2. iOS 的 UIResponder 链就是在框架层实现了这一模式。
  3. 触摸事件会依次从 UIView 传递到上层视图、视图控制器、窗口和应用程序。
  4. next 属性指向链中的下一个环节;如果没有对象处理事件,事件就会直接被丢弃。

什么是责任链模式?

责任链模式是一种行为型模式,用于解耦请求发送方与处理方。

简单来说,它就像公司里的审批流程。助理经理能处理就到此为止,不能处理就交给经理,经理也无法处理时再交给总监。

发起请求的人不需要知道最终由谁审批,只要把请求交给链上的第一个环节,后续就会自动传递。

这个模式的核心是,每个处理对象只需要知道两件事:自己能否处理请求,以及不能处理时下一个对象是谁。

用 Swift 做一个非常简单的版本,大致结构如下。

class Handler {
    var next: Handler?  // 链中的下一个环节
    func handle(_ request: Int) {
        // 无法处理时将责任传递给下一个对象
        next?.handle(request)
    }
}

通过一个 next 属性连接链条,无法处理时交给 next,这种结构原样内置在 UIResponder 中。


UIResponder 链是如何工作的?

在 iOS 中,所有能够接收触摸、动作、远程控制等事件的对象都会继承 UIResponder。UIView、UIViewController、UIWindow 和 UIApplication 都是 UIResponder 的子类。

因此,这些对象各自都有一个属性,其准确名称就是 next。它定义在 UIResponder 中。

// UIResponder中定义的属性
var next: UIResponder? { get }

用户触摸屏幕时,系统首先找到最内层、实际被触摸的视图。这称为命中测试,同时会确定 first responder 候选对象。

如果该视图没有处理事件,系统就沿着 next 向上查找。顺序大致如下。

沿着 next 向上转交的责任链
沿着 next 向上转交的责任链
  • UIView(被触摸的视图)
  • 上层的各个超视图
  • 管理该视图的 UIViewController
  • UIWindow
  • UIApplication
  • UIApplicationDelegate

即使遍历完整条链也没有对象处理事件,事件也只会被静默丢弃。应用不会崩溃或报错,只是忽略这个事件。

有趣的是,视图的 next 并不总是超视图。

如果该视图是视图控制器的根视图,next 就是视图控制器,而不是超视图。


了解这一点对实际开发有什么好处?

说实话,刚开始即使不了解这种内部结构,应用也能正常运行。但理解之后,调试方式会明显不同。

分享一个我遇到的案例:自定义视图中的点击手势没有生效。

按顺序追踪事件在哪个 responder 处停止后,我很快发现了原因:上层视图的 isUserInteractionEnabled 为 false,责任链在那里断开了。

自定义事件传播也很有用。你可以定义沿着 UIResponder 链向上传递的消息,让深层视图产生的操作无需 delegate,就能传递到上层视图控制器。

收起键盘时常用的 endEditing(true) 其实也利用了这条链。它会找到 first responder,并调用 resignFirstResponder。

总之,理解 UIResponder 链后,遇到事件无响应时,就能有逻辑地追踪中断位置,也能避免滥用 delegate,设计出向上传递事件的方案。

实际在代码中拆解 next 属性的那一天
实际在代码中拆解 next 属性的那一天

常见问题

Q. first responder 和 responder 链是不同的概念吗? A. first responder 是有资格优先接收事件的一个对象,而 responder 链是从该 first responder 向上延伸的整条链。

Q. next 总是指向超视图吗? A. 不是。普通视图指向超视图,但视图控制器的根视图会将视图控制器作为 next。

我追踪了这一次点击会沿着 responder 传递到哪里
我追踪了这一次点击会沿着 responder 传递到哪里

责任链模式这个名字可能听起来很难,但归根结底只有一个简单原则:无法处理时就交给下一个对象。希望本文能帮助你理解 UIResponder 链这个 iOS 隐藏的骨架。下次遇到事件无响应的 Bug 时,可以沿着这条链逐个环节追踪。

延伸阅读