swiftmacosuikitswiftui

Passing data from simple NSView to SwiftUI View


I'm making a macOS app with SwiftUI. I have a struct conforming to NSViewRepresentable, whose purpose is to detect the key code of whichever key was pressed. I want to pass the event.keyCode back to SwiftUI and save it into an environment object so I can use the key code elsewhere in my app.
I know I am supposed to use SwiftUI coordinators, but all tutorials and Stack Overflow questions I can find use ready-made classes such as UIPageControl or UISearchBar that have pre-configured delegates. I'm not sure what to do when using a simple custom NSView. Can somebody explain how to pass the data from the NSViewRepresentable struct into my @EnvironmentObject when using a custom NSView?

struct KeyboardEvent: NSViewRepresentable {

    class KeyView: NSView {
        override var acceptsFirstResponder: Bool { true }
        override func keyDown(with event: NSEvent) {
            print("\(event.keyCode)")
        }
    }

    func makeNSView(context: Context) -> NSView {
        let view = KeyView()
        DispatchQueue.main.async {
            view.window?.makeFirstResponder(view)
        }
        return view
    }

    func updateNSView(_ nsView: NSView, context: Context) {
    }

}

struct ContentView: View {
    @EnvironmentObject var input: KeyboardInput     // save the keyCode here
    var body: some View {
        VStack {
            Text(input.keyCode)
            KeyboardEvent()
        }
    }
}

Right now it prints the key code normally to the Xcode console, so the detection works fine.


Solution

  • Here is a solution (with some replicated parts). Tested with Xcode 11.4 / macOS 10.15.4

    class KeyboardInput: ObservableObject {
        @Published var keyCode: UInt16 = 0
    }
    
    struct KeyboardEvent: NSViewRepresentable {
        @Binding var keyStorage: UInt16          // << here !!
        init(into storage: Binding<UInt16>) {
            _keyStorage = storage
        }
    
        class KeyView: NSView {
            var owner: KeyboardEvent?   // << view holder
    
            override var acceptsFirstResponder: Bool { true }
            override func keyDown(with event: NSEvent) {
                print("\(event.keyCode)")
                owner?.keyStorage = event.keyCode
            }
        }
    
        func makeNSView(context: Context) -> NSView {
            let view = KeyView()
            view.owner = self           // << inject
            DispatchQueue.main.async {
                view.window?.makeFirstResponder(view)
            }
            return view
        }
    
        func updateNSView(_ nsView: NSView, context: Context) {
        }
    
    }
    
    struct ContentView: View {
        @EnvironmentObject var input: KeyboardInput     // save the keyCode here
        var body: some View {
            VStack {
                Text("Code: \(input.keyCode)")
                KeyboardEvent(into: $input.keyCode) // << binding !!!
            }
        }
    }