I have a mult-window app, so I'm using focusedSceneObject
to direct interactions to the current window. I then have a @FocusedObject
in the view, which seems to be correct at the time the view appears. However, the view needs to see keyboard events, so I have a view that leverages NSViewRepresentable
to provide a way to access the events in SwiftUI. The problem is that the @FocusedObject
variable is nil when the view responds to the event.
Here is the NSViewRepresentable
view:
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)
}
}
The application has this sort of code (some details removed for clarity):
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)
}
}
}
A simplified version of the view:
struct DocumentView: View {
@FocusedObject var focusedDocument: MyDocument?
var body: some View {
ZStack {
NavigationSplitView {
// The list view
}
detail: {
// The detail view
}
.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:
// Code for key up event
case .flagsChanged:
// Code for modifiers changed event
default:
break
}
}
}
}
}
When I get to the keyDown
case, focusedDocument
is nil. What am I doing wrong? (BTW, I have experimented with removing the .focusable()
statement, or attaching it to a different part of the hierarchy, but it makes no difference.)
The problem is that the block that gets called has access to the context, but the @FocusedObject
is not set there, so that the work has to be done within the view struct.
A straightforward way of doing this is to create a binding in the KeyAwareView
of an NSEvent?
variable, then to modify the view to add a coordinator that passes the event to the bound variable. Then the work is done in an onChangeOf
modifier of the document view.
Here's the new version of KeyAwareView
:
protocol KeyEventHandler {
func handleEvent(event: NSEvent)
}
struct KeyAwareView: NSViewRepresentable {
class Coordinator: NSObject, KeyEventHandler {
var parent: KeyAwareView
init(_ parent: KeyAwareView) {
self.parent = parent
}
func handleEvent(event: NSEvent) {
parent.event = event
}
}
@Binding var event: NSEvent?
func makeNSView(context: Context) -> NSView {
let view = KeyView()
view.handler = context.coordinator
DispatchQueue.main.async {
view.window?.makeFirstResponder(view)
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
}
private class KeyView: NSView {
var handler: KeyEventHandler?
override var acceptsFirstResponder: Bool { true }
override func keyDown(with event: NSEvent) {
handler?.handleEvent(event: event)
}
override func keyUp(with event: NSEvent) {
handler?.handleEvent(event: event)
}
override func flagsChanged(with event: NSEvent) {
handler?.handleEvent(event: event)
}
}