George
George

Reputation: 30451

Respond to key press events in SwiftUI

I want to respond to key presses, such as the esc key on macOS/OSX, and when using an external keyboard on iPad. How can I do this?

I have thought of using @available/#available with SwiftUI's onExitCommand, which looked very promising, but unfortunately that is only for macOS/OSX. How can I respond to key presses in SwiftUI for more than just macOS/OSX?

Upvotes: 9

Views: 11278

Answers (4)

Andrew
Andrew

Reputation: 11427

.keyboardShortcut(_:modifiers:)

ATTENTION! "modifiers" input parameter by default is not empty! So if you use this function/modifier with some key you will get unexpected result!


OR for other cases:

.onAppear() {
    NSEvent.addLocalMonitorForEvents(matching: .keyDown) { (aEvent) -> NSEvent? in
        if aEvent.keyCode == 53 { // if esc pressed
            appDelegate.hideMainWnd()
            return nil // do not do "beep" sound
        }
            
        return aEvent
    }
}

Upvotes: 5

dacasta
dacasta

Reputation: 99

An easier solution is the .onExitCommand modifier. As @Chuck H commented, this does not work if the text field is inside a ScrollView or like in my case, a Section. But I discovered that just by embedding the TextField and its .onExitCommand inside a HStack or VStack, the .onExitCommand just works.

                    HStack { // This HStack is for .onExitCommand to work
                        TextField("Nombre del proyecto", text: $texto)
                        .onExitCommand(perform: {
                            print("Cancelando edición....")
                        })
                        .textFieldStyle(PlainTextFieldStyle())
                        .lineLimit(1)
                    }

Upvotes: 5

George
George

Reputation: 30451

Update: SwiftUI 2 now has .keyboardShortcut(_:modifiers:).


OLD ANSWER:

With thanks to @Asperi to pointing me in the right direction, I have now managed to get this working.

The solution was to use UIKeyCommand. Here is what I did, but you can adapt it differently depending on your situation.

I have an @EnvironmentObject called AppState, which helps me set the delegate, so they keyboard input can be different depending on the view currently showing.

protocol KeyInput {
    
    func onKeyPress(_ key: String)
}


class KeyInputController<Content: View>: UIHostingController<Content> {
    
    private let state: AppState
    
    init(rootView: Content, state: AppState) {
        self.state = state
        super.init(rootView: rootView)
    }
    @objc required dynamic init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    
    override func becomeFirstResponder() -> Bool {
        true
    }
    
    override var keyCommands: [UIKeyCommand]? {
        switch state.current {
        case .usingApp:
            return [
                UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(keyPressed(_:)))
            ]
            
        default:
            return nil
        }
    }
    
    @objc private func keyPressed(_ sender: UIKeyCommand) {
        guard let key = sender.input else { return }
        state.delegate?.onKeyPress(key)
    }
}

AppState (@EnvironmentObject):

class AppState: ObservableObject {

    var delegate: KeyInput?
    /* ... */
}

And the scene delegate looks something like:

let stateObject = AppState()
let contentView = ContentView()
    .environmentObject(stateObject)

// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
    let window = UIWindow(windowScene: windowScene)
    window.rootViewController = KeyInputController(rootView: contentView, state: stateObject)

    /* ... */
}

This makes it really easy to now add functionality depending on the keys pressed.

Conform to KeyInput, e.g.:

struct ContentView: View, KeyInput {

    /* ... */

    var body: some View {
        Text("Hello world!")
            .onAppear {
                self.state.delegate = self
            }
    }
    
    func onKeyPress(_ key: String) {
        print(key)
        guard key == UIKeyCommand.inputEscape else { return }
        // esc key was pressed
        /* ... */
    }
}

Upvotes: 3

Peter Schorn
Peter Schorn

Reputation: 997

On macOS and tvOS there is an onExitCommand(perform:) modifier for views. From Apple's documentation:

Sets up an action that triggers in response to receiving the exit command while the view has focus.

The user generates an exit command by pressing the Menu button on tvOS, or the escape key on macOS.

For example:

struct ContentView: View {
    
    var body: some View {
        VStack {
            
            TextField("Top", text: .constant(""))
                .onExitCommand(perform: {
                    print("Exit from top text field")
                })
            
            TextField("Bottom", text: .constant(""))
                .onExitCommand(perform: {
                    print("Exit from bottom text field")
                })
        }
        .padding()
    }
    
}

Upvotes: 8

Related Questions