'SwiftUI iOS - how to capture hardware key events

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



Solution 1:[1]

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)

backup

Solution 2:[2]

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! ??

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 tyirvine