引言
大家好,我是一牛。记得博主刚入行的时候,那时候 iOS 面试题经常会考响应者和响应者链,不过背一背八股文倒是也不难,但是工作中却很少用到。今天,我就以一个桌面开发者的角度,谈一谈 AppKit 中的响应者和响应者链,探讨一下它们到底有什么用。
响应者
首先我们先介绍下响应者。响应者的定义:
- 可以接受事件的对象。
- 可以通过响应者链接受事件。
- 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
是第一响应者,它会首先处理动作消息。
我们在CustomView
中handleActionMessage:
删除, 在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:
方法。对于动作消息和事件消息,响应者链路也是有区别的,这一点需要我们仔细甄别。
谢谢大家!