SwiftUI iOS - how to capture hardware key events
Asked Answered
A

3

8

I’m new to iOS development. Following a tutorial I have created a simple calculator using SwiftUI.

I have a keyboard attached to my iPad, and I would like to be able to enter values using the keyboard.

How can I capture and handle hardware keyboard events in a SwiftUI app (with no text field) ?  I have tried to use the keyCommands on the SceneDelegate (UIResponder) as shown here, but that doesn’t work for me. As soon as I press any key on my iPad, I get “Connection to deamon was invalidated” in the XCode trace view.

 class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    override var canBecomeFirstResponder: Bool {
        return true;
    }
    override var keyCommands: [UIKeyCommand]? {
        return [
            UIKeyCommand(input: "a", modifierFlags: [], action: #selector(test)),
            UIKeyCommand(input: UIKeyCommand.inputLeftArrow, modifierFlags: [], action: #selector(test))
        ]
    }

    @objc func test(_ sender: UIKeyCommand) {
        print("test was pressed")
    }

Thanks

Aland answered 18/1, 2020 at 4:55 Comment(0)
S
8

It needs to override hosting view controller instead and all works. Tested with Xcode 11.2 / iOS 13.2

Here is example code

class KeyTestController<Content>: UIHostingController<Content> where Content: View {

    override func becomeFirstResponder() -> Bool {
        true
    }
    
    override var keyCommands: [UIKeyCommand]? {
        return [
            UIKeyCommand(input: "1", modifierFlags: [], action: #selector(test)),
            UIKeyCommand(input: "0", modifierFlags: [], action: #selector(test)),
            UIKeyCommand(input: UIKeyCommand.inputLeftArrow, modifierFlags: [], action: #selector(test))
        ]
    }

    @objc func test(_ sender: UIKeyCommand) {
        print(">>> test was pressed")
    }

}

and somewhere in SceneDelegate below

window.rootViewController = KeyTestController(rootView: contentView)
Salisbury answered 18/1, 2020 at 6:40 Comment(0)
V
5

If you're using the SwiftUI lifecycle @Asperi also shows how to access the rootViewController in this post - Hosting Controller When Using iOS 14 @main.

In summary, HostingWindowFinder tracks down the mutable version of the rootViewController and provides access to it.

struct ContentView: View {

    var body: some View {
      Text("Demo Root Controller access")
        .withHostingWindow { window in
            window?.rootViewController = KeyController(rootView: ContentView())
        }
    }
}

extension View {
    func withHostingWindow(_ callback: @escaping (UIWindow?) -> Void) -> some View {
        self.background(HostingWindowFinder(callback: callback))
    }
}

struct HostingWindowFinder: UIViewRepresentable {
    var callback: (UIWindow?) -> ()

    func makeUIView(context: Context) -> UIView {
        let view = UIView()
        DispatchQueue.main.async { [weak view] in
            self.callback(view?.window)
        }
        return view
    }

    func updateUIView(_ uiView: UIView, context: Context) {
    }
}

Also

If you want to just monitor raw keyboard input use pressesBegan(...) - WWDC 2019 Talk

class KeyController<Content>: UIHostingController<Content> where Content: View {

    override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
        for press in presses {
            guard let key = press.key else { continue }
            print(key)
        }
    }
}

Big thanks to @Asperi! ❤️

Vindication answered 6/11, 2021 at 2:57 Comment(4)
how do you use override func pressesBegan in a swift ui view?Forage
also this is crashing when I implement it in my app that has a GlobalEnvironment: Thread 1: Fatal error: No ObservableObject of type GlobalEnvironment found. A View.environmentObject(_:) for GlobalEnvironment may be missing as an ancestor of this view. I'm putting the .withHostingWindow view modifier on my @main root view...Forage
@benjamin-b I'm unsure how to help you with your error as it should work if set up correctly. Maybe try placing the modifier on a view within the root view as it sounds like you're trying to attach it outside the root view. As far as pressesBegan() goes you need to place it in your UIHostingController class, which wraps a SwiftUI view and is assigned as the .rootViewController above.Vindication
I've got this code working more or less. But I wonder how this works from a performance perspective. My "ContentView" is very comples as is intialized with many parameters. When you set the new rootViewController with ContentView() as rootView, doesn't this create a new (2nd) instance of ContentView()?Codeine
W
0

I wanted to handle the hardware keys inside in a single view rather than the root view in SwiftUI, so I just made a UIView to handle pressesBegan and pressesEnd and wrapped it in a struct for SwiftUI.

class KeyEventView: UIView {
    override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
        for press in presses {
            guard let key = press.key else { continue }
            print(key)
        }
        // If you still want to call the super functionality
        // super.pressesBegan(presses, with: event)
    }
    
    override func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
        for press in presses {
            guard let key = press.key else { continue }
            print(key)
        }
        // If you still want to call the super functionality
        // super.pressesEnded(presses, with: event)
    }
}

// View to use in SwiftUI
struct KeyBoardView: UIViewRepresentable{
    func makeUIView(context: Context) -> KeyEventView {
        KeyEventView()
    }

    func updateUIView(_ uiView: KeyEventView, context: Context) {
    }
}
Workingman answered 4/4, 2023 at 15:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.