SwiftUI - How to use onCommand with NSMenuItem on

2020-06-04 13:39发布

问题:

I am trying to find the best solution to connect a NSMenuItem with SwiftUI onCommand on macOS.

Currently I an doing the following:

  1. In AppDelegate I create a dummy function to be able to list the function First Responder received actions list.
@IBAction func changeColor(_ sender: Any) {
    print("Dummy Function")
}
  1. Create and connect a NSMenuItem with the First Responder function in the Main.storyboard.

  2. Add the required code to my SwiftUI view

struct TestView: View {

    let changeColor = #selector(AppDelegate.changeColor(_:))

    var body: some View {
        VStack {
            TextField("Text", text: .constant(""))
            Text("Hello World!")
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .onCommand(changeColor) {
            print("Change Color Action")
        }
    }
}

If the TextField was First Responder before I will see that "Change Color Action" is printed. The View will not become First Responder if the TextField was not First Responder before. Maybe this is currently a bug in Catalina Beta (19A558d) since I don't get focusable to work as well.

回答1:

I've been able to get this to work by first making the view focusable:

struct TestView: View {

    let changeColor = #selector(AppDelegate.changeColor(_:))

    var body: some View {
        VStack {
            TextField("Text", text: .constant(""))
            Text("Hello World!")
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .focusable() // Add this 
        .onCommand(changeColor) {
            print("Change Color Action")
        }
    }
}

then you also have to check the "Use keyboard navigation to move focus between controls" checkbox in Keyboard System Preferences



回答2:

I ended up creating a PassthroughSubject in my AppDelegate, passing it through to my SwiftUI view, and subscribing to it in the model object owned by my SwiftUI view. When my AppDelegate selector for the menu command is called, I send the event using the PassthroughSubject, which my model receives and handles appropriately.

My answer is definitely not ideal, and I'd love to know if you figure out a better way to do this.