Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to detect keyboard events in SwiftUI on macOS?

Tags:

How can I detect keyboard events in a SwiftUI view on macOS?

I want to be able to use key strokes to control items on a particular screen but it's not clear how I detect keyboard events, which is usually done by overriding the keyDown(_ event: NSEvent) in NSView.

like image 502
Duncan Groenewald Avatar asked Apr 11 '20 07:04

Duncan Groenewald


People also ask

Which event is specific to the keyboard?

There are three types of keyboard events: keydown , keypress , and keyup .

Which keyboard event is used for script runs when key is pressed?

The keydown event is fired when a key is pressed. Unlike the deprecated keypress event, the keydown event is fired for all keys, regardless of whether they produce a character value. The keydown and keyup events provide a code indicating which key is pressed, while keypress indicates which character was entered.

When a key from keyboard is released which event gets generated?

keyup – fires when you release a key on the keyboard. keypress – fires when you press a character keyboard like a , b , or c , not the left arrow key, home, or end keyboard, … The keypress also fires repeatedly while you hold down the key on the keyboard.

What is the keyboard event that will trigger when a key is released while the control has focus?

In this case, if you release a key while focus is held by either Button object, it raises the KeyUp event. The event is then bubbled up to the parent Canvas. The following example shows how to implement the KeyUp event handler for the corresponding XAML content in the preceding example.


1 Answers

New in SwiftUI bundled with Xcode 12 is the commands modified, which allows us to declare key input with keyboardShortcut view modifier. You then need some way of forwarding the key inputs to your child views. Below is a solution using a Subject, but since it is not a reference type it cannot be passed using environmentObject - which is really what we wanna do, so I've made a small wrapper, conforming to ObservableObject and for conveninece Subject itself (forwarding via the subject).

Using some additional convenience sugar methods, I can just write like this:

.commands {     CommandMenu("Input") {         keyInput(.leftArrow)         keyInput(.rightArrow)         keyInput(.upArrow)         keyInput(.downArrow)         keyInput(.space)     } } 

And forward key inputs to all subviews like this:

.environmentObject(keyInputSubject) 

And then a child view, here GameView can listen to the events with onReceive, like so:

struct GameView: View {          @EnvironmentObject private var keyInputSubjectWrapper: KeyInputSubjectWrapper     @StateObject var game: Game              var body: some View {         HStack {             board             info         }.onReceive(keyInputSubjectWrapper) {             game.keyInput($0)         }     } } 

The keyInput method used to declare the keys inside CommandMenu builder is just this:

private extension ItsRainingPolygonsApp {     func keyInput(_ key: KeyEquivalent, modifiers: EventModifiers = .none) -> some View {         keyboardShortcut(key, sender: keyInputSubject, modifiers: modifiers)     } } 

tetris_game

Full Code

extension KeyEquivalent: Equatable {     public static func == (lhs: Self, rhs: Self) -> Bool {         lhs.character == rhs.character     } }  public typealias KeyInputSubject = PassthroughSubject<KeyEquivalent, Never>  public final class KeyInputSubjectWrapper: ObservableObject, Subject {     public func send(_ value: Output) {         objectWillChange.send(value)     }          public func send(completion: Subscribers.Completion<Failure>) {         objectWillChange.send(completion: completion)     }          public func send(subscription: Subscription) {         objectWillChange.send(subscription: subscription)     }           public typealias ObjectWillChangePublisher = KeyInputSubject     public let objectWillChange: ObjectWillChangePublisher     public init(subject: ObjectWillChangePublisher = .init()) {         objectWillChange = subject     } }  // MARK: Publisher Conformance public extension KeyInputSubjectWrapper {     typealias Output = KeyInputSubject.Output     typealias Failure = KeyInputSubject.Failure          func receive<S>(subscriber: S) where S : Subscriber, S.Failure == Failure, S.Input == Output {         objectWillChange.receive(subscriber: subscriber)     } }       @main struct ItsRainingPolygonsApp: App {          private let keyInputSubject = KeyInputSubjectWrapper()          var body: some Scene {         WindowGroup {                          #if os(macOS)             ContentView()                 .frame(idealWidth: .infinity, idealHeight: .infinity)                 .onReceive(keyInputSubject) {                     print("Key pressed: \($0)")                 }                 .environmentObject(keyInputSubject)             #else             ContentView()             #endif         }         .commands {             CommandMenu("Input") {                 keyInput(.leftArrow)                 keyInput(.rightArrow)                 keyInput(.upArrow)                 keyInput(.downArrow)                 keyInput(.space)             }         }     } }  private extension ItsRainingPolygonsApp {     func keyInput(_ key: KeyEquivalent, modifiers: EventModifiers = .none) -> some View {         keyboardShortcut(key, sender: keyInputSubject, modifiers: modifiers)     } }  public func keyboardShortcut<Sender, Label>(     _ key: KeyEquivalent,     sender: Sender,     modifiers: EventModifiers = .none,     @ViewBuilder label: () -> Label ) -> some View where Label: View, Sender: Subject, Sender.Output == KeyEquivalent {     Button(action: { sender.send(key) }, label: label)         .keyboardShortcut(key, modifiers: modifiers) }   public func keyboardShortcut<Sender>(     _ key: KeyEquivalent,     sender: Sender,     modifiers: EventModifiers = .none ) -> some View where Sender: Subject, Sender.Output == KeyEquivalent {          guard let nameFromKey = key.name else {         return AnyView(EmptyView())     }     return AnyView(keyboardShortcut(key, sender: sender, modifiers: modifiers) {         Text("\(nameFromKey)")     }) }   extension KeyEquivalent {     var lowerCaseName: String? {         switch self {         case .space: return "space"         case .clear: return "clear"         case .delete: return "delete"         case .deleteForward: return "delete forward"         case .downArrow: return "down arrow"         case .end: return "end"         case .escape: return "escape"         case .home: return "home"         case .leftArrow: return "left arrow"         case .pageDown: return "page down"         case .pageUp: return "page up"         case .return: return "return"         case .rightArrow: return "right arrow"         case .space: return "space"         case .tab: return "tab"         case .upArrow: return "up arrow"         default: return nil         }     }          var name: String? {         lowerCaseName?.capitalizingFirstLetter()     } }  public extension EventModifiers {     static let none = Self() }  extension String {     func capitalizingFirstLetter() -> String {       return prefix(1).uppercased() + self.lowercased().dropFirst()     }      mutating func capitalizeFirstLetter() {       self = self.capitalizingFirstLetter()     } }  extension KeyEquivalent: CustomStringConvertible {     public var description: String {         name ?? "\(character)"     } }  
like image 182
Sajjon Avatar answered Sep 22 '22 05:09

Sajjon