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

Jesper Kristiansen
  • 1,759
  • 3
  • 17
  • 29

3 Answers3

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)
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
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! ❤️

tyirvine
  • 1,861
  • 1
  • 19
  • 29
  • how do you use override func pressesBegan in a swift ui view? – Benjamin B. Jul 26 '22 at 01:06
  • 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... – Benjamin B. Jul 26 '22 at 01:11
  • @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. – tyirvine Jul 26 '22 at 05:43
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) {
    }
}
micah
  • 838
  • 7
  • 21