当前位置: 首页 > 工具软件 > Swift Then > 使用案例 >

SWIFTUI 键盘快捷键 键盘监听

房时铭
2023-12-01

方法❶

使用NSEvent.addLocalMonitorForEvents(matching: [.keyDown])

不细述

方法❷

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)
    }
}

正在上传…重新上传取消

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)"
    }
}

Share

Follow

edited Aug 24, 2022 at 12:21

正在上传…重新上传取消

andrewz

4,52944 gold badges4848 silver badges6666 bronze badges

answered Jul 1, 2020 at 12:03

正在上传…重新上传取消

Sajjon

8,32755 gold badges6060 silver badges9090 bronze badges

  • 1

    Code is not complete without the following two lines: ``` import SwiftUI import Combine ``` 

    – Tom Grushka

     Apr 9, 2021 at 17:06 
  • 1

    Thanks for this! I had trouble adding support for characters such as keyInput("a"). I was able to solve this by changing your code slightly. If you change default: return nil for lowerCaseName to return String(self.character).lowercased(), then you will be able to use characters. 

    – Matt54

     Mar 12, 2022 at 19:04
  • @Sajjon Adding to Command .tab and .escape don't seem to work. 

    – Joannes

     Mar 24, 2022 at 8:39
  • 2

    How about if a key is released? 

    – Shaun Budhram

     May 21, 2022 at 21:58
  • This is excellent code, but I want to alert the readers this is code that publishes the given menu click (eg: Input->left arrow) to any interested child views through the environment. 

    – Klajd Deda

     Dec 5, 2022 at 3:01

方法❸

There is no built-in native SwiftUI API for this, so far.

Here is just a demo of a possible approach. Tested with Xcode 11.4 / macOS 10.15.4

struct KeyEventHandling: NSViewRepresentable {
    class KeyView: NSView {
        override var acceptsFirstResponder: Bool { true }
        override func keyDown(with event: NSEvent) {
            print(">> key \(event.charactersIgnoringModifiers ?? "")")
        }
    }

    func makeNSView(context: Context) -> NSView {
        let view = KeyView()
        DispatchQueue.main.async { // wait till next event cycle
            view.window?.makeFirstResponder(view)
        }
        return view
    }

    func updateNSView(_ nsView: NSView, context: Context) {
    }
}

struct TestKeyboardEventHandling: View {
    var body: some View {
        Text("Hello, World!")
            .background(KeyEventHandling())
    }
}

Output:

正在上传…重新上传取消

Share

Follow

edited Aug 23, 2022 at 13:42

正在上传…重新上传取消

Martijn Pieters

1.0m286286 gold badges39893989 silver badges33033303 bronze badges

answered Apr 11, 2020 at 10:25

正在上传…重新上传取消

Asperi

212k1818 gold badges394394 silver badges618618 bronze badges

  • thanks - unfortunately this locks up because I need to create multiple shapes and attach to shapes. The keyboard event handling should only be activated if the shape has been selected (longPress). What would be the best way to achieve this? 

    – Duncan Groenewald

     Apr 11, 2020 at 22:34
  • Ignore this I just realised I need to do this on the main layout window and send commands to the selected objects because more than one object may be selected. However there is still an issue with focus handling - how does one detect when a window loses focus ? 

    – Duncan Groenewald

     Apr 11, 2020 at 22:55
  • 4

    The code works in Xcode 11.6 and macOS 10.15.6. However, there is an error sound for each key press detected. 

    – Dummy

     Aug 17, 2020 at 7:12 
  • The solution works but it just detects the alphas and arrow keys, those mods like fn, control, option, command keys does not work and whatever key pressed there was an error sound 

    – Neung Chung

     Aug 21, 2020 at 17:15
  • 5

    About the error sound: if you remove super.keyDown(with: event) (which tells the rest responder chain that the key stroke was not handled) then the sound should no longer be emitted. 

    – Milos

方法❹

There's another solution that is very simple but only works for particular types of keys - you'd have to experiment. Just create Buttons with .keyboardShortcut modifier, but hide them visually.

Group {
    Button(action: { goAway() }) {}
        .keyboardShortcut(.escape, modifiers: [])
    Button(action: { goLeft() }) {}
        .keyboardShortcut(.upArrow, modifiers: [])
    Button(action: { goDown() }) {}
        .keyboardShortcut(.downArrow, modifiers: [])
}.opacity(0)

 类似资料: