如何在macOS平台SwiftUI中持续接收键盘按键事件?
问题根源
你遇到的问题核心原因是切换布局时NavigationView的列表/编辑器视图会抢占第一响应者身份,而KeyAwareView只有在初始化时会申请成为第一响应者,后续视图状态变化触发的重绘如果没有修改KeyAwareView的入参,updateNSView根本不会触发,你加的代码自然不会执行。
以下是两种可行的解决方案:
方案1:不依赖第一响应者,直接监听窗口本地事件(推荐)
不用靠成为第一响应者来接收键盘事件,直接给当前窗口添加本地事件监视器,不管当前第一响应者是谁都能拿到键盘事件,代码修改如下:
struct KeyAwareView: NSViewRepresentable { let onEvent: (NSEvent) -> Void func makeNSView(context: Context) -> NSView { let view = KeyView() view.onEvent = onEvent return view } func updateNSView(_ nsView: NSView, context: Context) {} } private class KeyView: NSView { var onEvent: (NSEvent) -> Void = { _ in } private var eventMonitor: Any? override var acceptsFirstResponder: Bool { true } override func viewDidMoveToWindow() { super.viewDidMoveToWindow() // 移除旧的监视器避免重复添加 if let oldMonitor = eventMonitor { NSEvent.removeMonitor(oldMonitor) } guard let window = window else { return } // 添加本地事件监视器,只接收当前窗口的键盘相关事件 eventMonitor = NSEvent.addLocalMonitorForEvents(matching: [.keyDown, .keyUp, .flagsChanged]) { [weak self] event in guard let self = self, event.window == self.window else { return event } self.onEvent(event) // 如果你需要事件继续传递给其他响应者就return event,不需要就return nil return event } } deinit { if let eventMonitor = eventMonitor { NSEvent.removeMonitor(eventMonitor) } } // 原来的响应方法可以保留作为 fallback override func keyDown(with event: NSEvent) { onEvent(event) super.keyDown(with: event) } override func keyUp(with event: NSEvent) { onEvent(event) super.keyUp(with: event) } override func flagsChanged(with event: NSEvent) { onEvent(event) super.flagsChanged(with: event) } }
这个方案的优势是完全不依赖第一响应者身份,只要视图在当前窗口层级里就能稳定接收事件,不会被其他视图抢占。
方案2:监听视图变化主动夺回第一响应者
如果你不想用事件监视器,可以在每次selectedLayout变化之后,手动触发KeyAwareView重新申请第一响应者:
- 首先给
KeyAwareView添加一个需要刷新的触发参数,绑定selectedLayout即可:
struct KeyAwareView: NSViewRepresentable { let onEvent: (NSEvent) -> Void // 新增触发刷新的参数,只要这个值变化就会触发updateNSView let refreshTrigger: AnyHashable func makeNSView(context: Context) -> NSView { let view = KeyView() view.onEvent = onEvent DispatchQueue.main.async { view.window?.makeFirstResponder(view) } return view } func updateNSView(_ nsView: NSView, context: Context) { // 每次refreshTrigger变化就重新申请第一响应者 DispatchQueue.main.async { nsView.window?.makeFirstResponder(nsView) } } }
- 然后在调用的时候把
selectedLayout作为refreshTrigger传入:
KeyAwareView(onEvent: { event in switch event.type { case .keyDown: KeyboardEventResponder.shared.handleKeyDown(keyCode: Int(event.keyCode)) case .keyUp: KeyboardEventResponder.shared.handleKeyUp(keyCode: Int(event.keyCode)) case .flagsChanged: KeyboardEventResponder.shared.updateModifiers(newModifiers: event.modifierFlags) default: break } }, refreshTrigger: selectedLayout)
这样每次切换布局selectedLayout变化的时候,都会触发updateNSView重新申请第一响应者。
注意:如果你的LayoutEditor里有文本输入框等需要第一响应者的控件,方案2会导致输入框失去焦点,这种场景下优先使用方案1。
内容的提问来源于stack exchange,提问作者Mussau
相关产品推荐
相关产品推荐

