You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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重新申请第一响应者:

  1. 首先给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)
        }
    }
}
  1. 然后在调用的时候把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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.29 03:09:02