【问题标题】:How to detect keyboard events in SwiftUI on macOS?如何在 macOS 上检测 SwiftUI 中的键盘事件?
【发布时间】:2020-04-11 07:44:08
【问题描述】:

如何在 macOS 上的 SwiftUI 视图中检测键盘事件?

我希望能够使用击键来控制特定屏幕上的项目,但不清楚我如何检测键盘事件,这通常通过覆盖 NSView 中的 keyDown(_ event: NSEvent) 来完成。

【问题讨论】:

    标签: swiftui keydown


    【解决方案1】:

    与 Xcode 12 捆绑的 SwiftUI 中的新功能是修改了 commands,它允许我们使用 keyboardShortcut view modifier 声明键输入。然后,您需要某种方式将关键输入转发到您的子视图。下面是使用Subject 的解决方案,但由于它不是引用类型,因此无法使用environmentObject 传递——这正是我们想要做的,所以我做了一个小包装器,符合ObservableObject 和为方便Subject 本身(通过subject 转发)。

    使用一些额外的便利糖方法,我可以这样写:

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

    并将关键输入转发到所有子视图,如下所示:

    .environmentObject(keyInputSubject)
    

    然后是子视图,这里GameView可以用onReceive监听事件,像这样:

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

    用于在CommandMenu builder 中声明键的keyInput 方法是这样的:

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

    完整代码

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

    【讨论】:

    • 没有以下两行代码是不完整的:``` import SwiftUI import Combine ```
    • 谢谢!我在添加对 keyInput("a") 等字符的支持时遇到了麻烦。我可以通过稍微更改您的代码来解决这个问题。如果更改默认值:return nil for lowerCaseName 以返回 String(self.character).lowercased(),那么您将能够使用字符。
    【解决方案2】:

    到目前为止,还没有内置的原生 SwiftUI API。

    这里只是一个可能的方法的演示。使用 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())
        }
    }
    

    输出:

    【讨论】:

    • 谢谢 - 不幸的是,这被锁定了,因为我需要创建多个形状并附加到形状上。只有选择了形状(长性)时,才应激活键盘事件处理。实现这一目标的最佳方法是什么?
    • 忽略这一点 我刚刚意识到我需要在主布局窗口上执行此操作并向所选对象发送命令,因为可能会选择多个对象。然而,焦点处理仍然存在问题——如何检测窗口何时失去焦点?
    • 该代码适用于 Xcode 11.6 和 macOS 10.15.6。但是,每次检测到按键都会发出错误声音。
    • 该解决方案有效,但它只检测 alpha 和箭头键,fn、控制、选项、命令键等模块不起作用,并且按下的任何键都会发出错误声音
    • 关于错误声音:如果您删除super.keyDown(with: event)(它告诉其余响应者链未处理击键),则不应再发出声音。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-04-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-18
    相关资源
    最近更新 更多