macosswiftui

Focused object becomes nil with user interaction


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.)


Solution

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