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

多窗口SwiftUI应用中@FocusedObject响应键盘事件时为nil的问题

问题:SwiftUI多窗口应用中@FocusedObject在NSViewRepresentable事件回调中为nil

我开发了一款多窗口应用,使用focusedSceneObject将交互定向到当前窗口。视图中使用@FocusedObject时,视图显示状态正常,但通过NSViewRepresentable实现的键盘事件监听视图响应事件时,@FocusedObject变量为nil。尝试过移除.focusable()或调整其位置,但无效果。

相关代码

NSViewRepresentable视图代码

import SwiftUI

struct KeyAwareView: NSViewRepresentable {
    let onEvent: (NSEvent) -> Void

    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) {}
}

private class KeyView: NSView {
    var onEvent: (NSEvent) -> Void = { _ in }
    
    override var acceptsFirstResponder: Bool { true }
    override func keyDown(with event: NSEvent) {
        onEvent(event)
    }
    override func keyUp(with event: NSEvent) {
        onEvent(event)
    }
    override func flagsChanged(with event: NSEvent) {
        onEvent(event)
    }
}

应用相关代码

extension FocusedValues {
    struct DocumentFocusedValues: FocusedValueKey {
        typealias Value = MyDocument
    }

    var document: MyDocument? {
        get { self[DocumentFocusedValues.self] }
        set { self[DocumentFocusedValues.self] = newValue }
    }
}

@main
struct MyApp: App {
    var body: some Scene {
        DocumentGroup {
            MyDocument()
        } editor: { file in
            DocumentView(document: file.document)
                .focusedSceneObject(file.document as MyDocument)
        }
    }
}

视图简化代码

struct DocumentView: View {
    @FocusedObject var focusedDocument: MyDocument?
    
    var body: some View {
        ZStack {
            NavigationSplitView {
                // 列表视图
            }
            detail: {
                // 详情视图
            }
            .focusable()

            KeyAwareView { event in
                switch event.type {
                case .keyDown:
                    let notification = Notification(name: .keyDown, object: focusedDocument, userInfo: [KeyKeyCode: Int(event.keyCode)])
                    NotificationCenter.default.post(name: .keyboardEvent, object: focusedDocument, userInfo: [KeyNotification: notification])
                    
                case .keyUp:
                    // 按键抬起事件处理代码
                    
                case .flagsChanged:
                    // 修饰键变更事件处理代码
                    
                default:
                    break
                }
            }
        }
    }
}

解决方案

问题根源

你在KeyAwareView中通过view.window?.makeFirstResponder(view)强制将NSView设置为第一响应者,这会抢占SwiftUI的focus管理链。@FocusedObject依赖于SwiftUI自身的focus状态,当NSView成为第一响应者后,SwiftUI无法维护当前focused scene object的引用,导致focusedDocument变为nil。

修复方案

方案一:直接使用视图已持有的Document引用

由于DocumentView初始化时已经接收了document参数(从App的DocumentGroup代码可知),完全可以直接使用这个引用,无需依赖@FocusedObject:

修改DocumentView代码:

struct DocumentView: View {
    let document: MyDocument // 新增该参数,与初始化时传入的实例绑定
    @FocusedObject var focusedDocument: MyDocument? // 可保留用于其他SwiftUI场景
    
    var body: some View {
        ZStack {
            NavigationSplitView {
                // 列表视图
            }
            detail: {
                // 详情视图
            }
            .focusable()

            KeyAwareView { event in
                switch event.type {
                case .keyDown:
                    // 直接使用document实例,替代focusedDocument
                    let notification = Notification(name: .keyDown, object: document, userInfo: [KeyKeyCode: Int(event.keyCode)])
                    NotificationCenter.default.post(name: .keyboardEvent, object: document, userInfo: [KeyNotification: notification])
                    
                case .keyUp:
                    // 按键抬起事件处理代码
                    
                case .flagsChanged:
                    // 修饰键变更事件处理代码
                    
                default:
                    break
                }
            }
        }
    }
}

这种方式最简单直接,因为你已经拥有了当前窗口对应的Document实例引用,不需要通过focus状态间接获取。

方案二:修复SwiftUI的focus状态关联

如果你坚持要使用@FocusedObject,需要移除强制设置第一响应者的代码,让SwiftUI管理focus:

  1. 修改KeyAwareView的makeNSView方法,移除强制设置first responder的代码:
func makeNSView(context: Context) -> NSView {
    let view = KeyView()
    view.onEvent = onEvent
    // 移除以下行:
    // DispatchQueue.main.async { view.window?.makeFirstResponder(view) }
    return view
}
  1. 给KeyAwareView添加.focusable()修饰符,确保它能纳入SwiftUI的focus管理:
KeyAwareView { event in
    // 事件处理逻辑
}
.focusable()

这样SwiftUI会维护focus状态,@FocusedObject就能保持有效值,同时KeyAwareView作为focusable视图,依然可以接收到键盘事件。


内容的提问来源于stack exchange,提问作者Mussau

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 15:19:53