多窗口SwiftUI应用中@FocusedObject响应键盘事件时为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:
- 修改
KeyAwareView的makeNSView方法,移除强制设置first responder的代码:
func makeNSView(context: Context) -> NSView { let view = KeyView() view.onEvent = onEvent // 移除以下行: // DispatchQueue.main.async { view.window?.makeFirstResponder(view) } return view }
- 给
KeyAwareView添加.focusable()修饰符,确保它能纳入SwiftUI的focus管理:
KeyAwareView { event in // 事件处理逻辑 } .focusable()
这样SwiftUI会维护focus状态,@FocusedObject就能保持有效值,同时KeyAwareView作为focusable视图,依然可以接收到键盘事件。
内容的提问来源于stack exchange,提问作者Mussau

