AppKit 中的响应者链

引言

大家好,我是一牛。记得博主刚入行的时候,那时候 iOS 面试题经常会考响应者和响应者链,不过背一背八股文倒是也不难,但是工作中却很少用到。今天,我就以一个桌面开发者的角度,谈一谈 AppKit 中的响应者和响应者链,探讨一下它们到底有什么用。

响应者

首先我们先介绍下响应者。响应者的定义:

  1. 可以接受事件的对象。
  2. 可以通过响应者链接受事件。
  3. AppKit 的响应者需继承NSResponder, 例如NSWIndow NSView NSWindowController

响应者定义了接收事件消息和动作消息的编程接口。

第一响应者

第一响应者通常是用户使用鼠标或者键盘激活的用户界面。NSWindow 的第一响应者是它自己,我们可以通过它的接口makeFirstResponder(_:)设置当前窗口的第一响应者。第一响应者就是响应者链的第一个响应者对象,接收事件消息和对象消息。

当窗口接受到鼠标按下事件时,它会询问当前的视图对象是否可以成为第一响应者,如果视图对象接受成为第一响应者,那么该视图对象成为第一响应者。NSView默认不接受成为第一响应者,我们可以重写方法acceptsFirstResponder来改变默认行为。点击NSTextField时,第一响应者会变成文本输入框,接收按键输入。当然我们也可以通过键盘来改变第一响应者。

响应者链

响应者链是由一系列相互关联的响应者对象组成的链路,事件或动作消息会沿着该链路传递并处理。当一个响应者无法处理某个特定事件消息或者动作消息,它会将该消息转发给它的继任者nextResponder,如果该响应者仍然不能处理该消息时,它会沿着链路继续向下传递,直到响应者的nextResponder为空。我们可以通过接口nextResponder改变默认的链路或者遍历该链路。

swift 复制代码
// 变量响应者链
var current = view.window?.firstResponder
while current != nil {
    current = current?.nextResponder
}

事件消息中的响应者链

事件消息包括鼠标事件,触摸板事件,以及键盘事件等。

  • 键盘事件的默认响应者链从窗口的第一响应者开始。
  • 鼠标事件的默认响应者链则始于用户事件发生的视图。

若事件未被处理,会沿视图层级向上传递至代表窗口本身的NSWindow对象。第一响应者通常是窗口内"被选中"的视图对象,其下一响应者为它的父视图,依此类推直至NSWindow对象。若窗口由NSWindowController管理,则控制器会成为链路的最后一个响应者。

若最终没有对象处理事件,链末端的响应者会调用noResponderFor:方法。响应者对象可重写此方法以执行自定义逻辑。当我们按下键盘的字母键时,如果响应者链中任何对象都没有实现keyDown,最终系统会调用 noResponderFor:,发出蜂鸣声。

动作消息中的响应者链

为了说明方便,我们使用Xcode创建一个Storyboard的普通应用(非文档应用),这是一个NSWindowController的应用。

在Storyboard上我们给视图控制器的视图增加一个自定义子视图(CustomView )。我们在这个子视图的类中重写了acceptsFirstResponder, 使得它能够成为第一响应者。当我们运行应用并展现窗口时,这个视图会成为当前窗口的第一响应者,并不需要我们手动点击这个视图来切换第一响应者。当然我们也可以编码决定谁成为第一响应者。此时,响应者链路如下:

CustomView -> 控制器的视图 -> 控制器 -> 窗口 -> 窗口控制器

然后我们在控制器视图中添加一个按钮, 只给按钮设置动作消息,不设置目标。

objective-c 复制代码
NSButton *actionButton = [NSButton buttonWithTitle:@"Click Me!" target:nil action:@selector(handleActionMessage:)];
[self.view addSubview:actionButton];

然后我们在CustomView类中添加动作方法

objective-c 复制代码
-(void)handleActionMessage:(NSButton *)sender {
    NSLog(@"CustomView received the action message!");
}

此时如果我们点击按钮,CustomView会收到这个动作消息。这是因为此时CustomView是第一响应者,它会首先处理动作消息。

我们在CustomViewhandleActionMessage:删除, 在ViewController中添加方法

objective-c 复制代码
-(void)handleActionMessage:(NSButton *)sender {
    NSLog(@"ViewController received the action message!");
}

点击按钮,ViewController会收到这个动作消息, 这是因为CustomView没有处理该消息,它会把消息传递给它的下一个响应者。

如何我们将按钮的目标设置为ViewController, 消息处理不会走响应者链,ViewController处理该消息。

objective-c 复制代码
//此时按钮的目标是ViewController
actionButton.target = self;

结语

响应者链作为AppKit的核心机制,其设计体现了"责任链模式"的经典思想。消息会从第一个响应者沿着响应者链传递,直到有响应者处理,或者调用noResponderFor:方法。对于动作消息和事件消息,响应者链路也是有区别的,这一点需要我们仔细甄别。

谢谢大家!

相关推荐
season_zhu2 小时前
iOS开发:关于日志框架
ios·架构·swift
Hello.Reader2 小时前
Git 安装全攻略Linux、macOS、Windows 与源码编译
linux·git·macos
Hope Fancy2 小时前
macOS 连接 Docker 运行 postgres,使用navicat添加并关联数据库
macos·docker·postgresql
John Song2 小时前
macOS 上使用 Homebrew 安装redis-cli
数据库·redis·macos
yanjiee3 小时前
编译一个Mac M系列可以用的yuview
macos
数据知道3 小时前
Mac电脑上本地安装 redis并配置开启自启完整流程
数据库·redis·macos
大熊猫侯佩7 小时前
SwiftUI 中如何花样玩转 SF Symbols 符号动画和过渡特效
swiftui·swift·apple
大熊猫侯佩8 小时前
SwiftData 共享数据库在 App 中的改变无法被 Widgets 感知的原因和解决
swiftui·swift·apple
大熊猫侯佩8 小时前
使用令牌(Token)进一步优化 SwiftData 2.0 中历史记录追踪(History Trace)的使用
数据库·swift·apple
大熊猫侯佩8 小时前
SwiftUI 在 iOS 18 中的 ForEach 点击手势逻辑发生改变的解决
swiftui·swift·apple