如何在 macOS 上的 SwiftUI 中检测键盘事件? [英] How to detect keyboard events in SwiftUI on macOS?

查看:115
本文介绍了如何在 macOS 上的 SwiftUI 中检测键盘事件?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

解决方案

与 Xcode 12 捆绑的 SwiftUI 中的新功能是修改了 commands,它允许我们使用

完整代码

extension KeyEquivalent: Equatable {public static func == (lhs: Self, rhs: Self) ->布尔{lhs.character == rhs.character}}public typealias KeyInputSubject = PassthroughSubject公共最终类 KeyInputSubjectWrapper: ObservableObject, Subject {公共功能发送(_值:输出){objectWillChange.send(value)}public func send(completion: Subscribers.Completion) {objectWillChange.send(完成:完成)}公共功能发送(订阅:订阅){objectWillChange.send(订阅:订阅)}公共类型别名 ObjectWillChangePublisher = KeyInputSubject公共让 objectWillChange: ObjectWillChangePublisher公共初始化(主题:ObjectWillChangePublisher = .init()){objectWillChange = 主题}}//标记:发布者一致性公共扩展 KeyInputSubjectWrapper {typealias 输出 = KeyInputSubject.Outputtypealias 失败 = KeyInputSubject.Failurefunc receive(subscriber: S) where S : Subscriber, S.Failure == Failure, S.Input == Output {objectWillChange.receive(订阅者:订阅者)}}@主要的struct ItsRainingPolygonsApp: App {private let keyInputSubject = KeyInputSubjectWrapper()var主体:一些场景{窗口组{#if 操作系统(macOS)内容视图().frame(idealWidth: .infinity, IdealHeight: .infinity).onReceive(keyInputSubject) {打印(按下的键:($ 0)")}.environmentObject(keyInputSubject)#别的内容视图()#万一}.命令{命令菜单(输入"){键输入(.leftArrow)键输入(.rightArrow)键输入(.upArrow)键输入(.downArrow)键输入(.空格)}}}}私人扩展它的RainingPolygonsApp {func keyInput(_ key: KeyEquivalent, modifiers: EventModifiers = .none) ->一些视图{键盘快捷方式(键,发件人:keyInputSubject,修饰符:修饰符)}}public func keyboardShortcut(_ 键:KeyEquivalent,发件人:发件人,修饰符:EventModifiers = .none,@ViewBuilder 标签:() ->标签) ->some View where Label: View, Sender: Subject, Sender.Output == KeyEquivalent {按钮(动作:{sender.send(key)},标签:标签).keyboardShortcut(键,修饰符:修饰符)}public func keyboardShortcut(_ 键:KeyEquivalent,发件人:发件人,修饰符:EventModifiers = .none) ->some View where Sender: Subject, Sender.Output == KeyEquivalent {守卫 let nameFromKey = key.name else {返回 AnyView(EmptyView())}返回 AnyView(键盘快捷方式(键,发件人:发件人,修饰符:修饰符){文本((nameFromKey)")})}扩展键等效 {varlowerCaseName:字符串?{切换自我{case .space: 返回空格";case .clear: 返回清除";case .delete: 返回删除";case .deleteForward:返回删除转发";case .downArrow: 返回向下箭头";case .end:返回结束";case .escape: 返回 "escape";case .home: 返回家"case .leftArrow: 返回左箭头";case .pageDown:返回向下翻页";case .pageUp:返回向上翻页";case .return: 返回返回";case .rightArrow: 返回右箭头";case .space: 返回空格";case .tab: 返回tab"case .upArrow: 返回向上箭头";默认值:返回零}}变量名称:字符串?{lowerCaseName?.capitalizingFirstLetter()}}公共扩展 EventModifiers {静态 let none = Self()}扩展字符串{func capitalizingFirstLetter() ->细绳 {返回前缀(1).uppercased() + self.lowercased().dropFirst()}变异函数大写第一字母(){self = self.capitalizingFirstLetter()}}扩展 KeyEquivalent:CustomStringConvertible {公共变量描述:字符串{名称 ??"(字符)";}}

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.

解决方案

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

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

这篇关于如何在 macOS 上的 SwiftUI 中检测键盘事件?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆