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

2024-03-22

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

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


与 Xcode 12 捆绑在一起的 SwiftUI 中的新功能是commands修饰符,它允许我们声明键输入keyboardShortcut视图修改器 https://developer.apple.com/documentation/swiftui/keyboardshortcut。然后,您需要某种方法将关键输入转发到您的子视图。下面是一个使用的解决方案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)
        }
    }
}

The keyInput用于声明内部键的方法CommandMenu构建器就是这样:

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何在 macOS 上的 SwiftUI 中检测键盘事件? 的相关文章

随机推荐

  • 如何将小部件构造函数作为参数传递给另一个要构建的小部件

    有没有一种方法可以将一个小部件作为参数传递给另一个小部件 我的代码有类似的东西有两个TextWidget类型的BoldWidget and ItalicWidget abstract class TextWidget extends Sta
  • 未解析的外部符号 boost::chrono::system_clock::now(void)

    谷歌一直不友善 我最近下载了 boost 1 50 并尝试使用它来构建我的项目 这是一个大型项目 使用了多个 boost 功能 线程 信号 指针类 spirit 等 一些具体细节 MSVC 9 0 2008 静态链接增强 我在链接每个生成的
  • 使用导航抽屉更改片段(?)布局或活动

    我正在尝试使用新的抽屉式导航 我遵循了指南 并设法让它与所选项目上的部分名称标题更改等一起使用 现在我陷入了困境 因为我不知道如何归档我下一步想做的事情 当我单击抽屉中的某个项目时 home 例如 我希望它能将我带到一个屏幕 其中包含我在特
  • Swift:通过 NSNotificationCenter 的键盘观察器不起作用

    我试图在我的 iOS 8 Swift 应用程序中实现一个简单的键盘观察器 但它确实不起作用 这是我当前使用的代码 override func viewDidAppear animated Bool NSNotificationCenter
  • 如何为Google的MLKIT使用图像格式YUV_420_888

    ImageReader 从相机预览中获取每一帧是具有格式的图像YUV 420 888 我想用它作为 MLKIT 的输入 在谷歌的文档中 我可以运行检测器 输入是 Bitmap 媒体图像 字节缓冲区 字节数组 A File 我尝试转换YUV
  • ExpressJS:承诺和错误处理中间件

    我定义了一些错误处理中间件和返回承诺的路由 但是当这个承诺出现错误时 我必须手动附加 catch err gt next err 在每一个承诺之后 虽然这不是问题 但对于 ExpressJs 来说 查看路由是否返回 Promise 如果返回
  • 快速使函数中的计时器无效

    我正在尝试创建一个带有主比赛时钟和开始 停止按钮的曲棍球比赛时钟应用程序 但我的 stopGameclock 函数遇到了问题 计时器不会失效 通过在这里搜索其他问题 我认为这与我有关 var gameclockTimer NSTimer 接
  • NSArray 和 NSMutableArray 的区别

    黑白有什么区别NSArray and NSMutableArray NSMutableArray 以及所有其他类Mutable名称中 可以修改 所以 如果你创建一个普通的NSArray 您以后无法更改其内容 无需重新创建它 但如果你创建一个
  • IdentityServer4 PKCE 错误:“转换后的代码验证程序与代码质询不匹配”

    我无法获得使用 Postman 工作的 IdentityServer4 PKCE 授权 使用在线工具我创建了必要的部分 选择一个随机字符串 1234567890 获取其 SHA 256 哈希值 c775e7b757ede630cd0aa11
  • Safari 中存在块作用域变量的 bug?

    我正在测试是否可以使用块作用域来替换 IIFE 以通过闭包创建 私有 变量 在 Safari 11 0 3 11604 5 6 1 1 中进行测试之前一切进展顺利 该版本支持块作用域 但存在块和闭包的错误 例如 let i 0 functi
  • ConcurrentDictionary 陷阱 - GetOrAdd 和 AddOrUpdate 的委托工厂是否同步?

    的文档ConcurrentDictionary没有明确说明 所以我想我们不能指望代表valueFactory and updateValueFactory让它们的执行同步 分别来自 GetOrAdd 和 AddOrUpdate 操作 所以
  • 在文件中每行的开头和结尾添加字符

    在每行的开头和结尾添加一些字符的最佳方法是什么 可以使用 Vim 或其他方式完成吗 在vim中 你可以这样做 s 1 s regex replace 是用于搜索和替换的 vim 命令 使其适用于整个文件 and 分别表示行的开始和结束 捕捉
  • 在摩卡测试之间重新导入模块

    在我的节点 打字稿快递应用程序中 我将配置设置存储在settings json作为对象加载和导出的文件config ts 每个使用配置设置的模块都会像这样导入模块 import Config from config config ts看起来
  • 如何使用 C# 验证文件是否是受密码保护的 ZIP 文件

    给定文件路径 如何验证该文件是否是受密码保护的 zip 文件 即 我将如何实现这个功能 bool IsPasswordProtectedZipFile string pathToFile 我不需要解压缩该文件 我只需要验证它是否是 ZIP
  • 可拖动的CALayer

    有什么方法可以让用户拖动 CALayer 吗 如果是这样 怎么办 在可可 Mac 中 图层本身无法接收鼠标事件 您必须在包含该层的视图或视图控制器中进行事件处理 If a mouseDragged 事件起源于某个层 请参见 CALayer
  • 在C++中使用const成员变量有什么优点

    我写代码像 template
  • python-re:如何匹配字母字符

    如何将字母字符与正则表达式匹配 我想要一个角色 w但不在 d 我希望它与 unicode 兼容 这就是为什么我不能使用 a zA Z 你的前两句话互相矛盾 在 w但不在 d 包括下划线 我从你的第三句话假设你不需要下划线 在信封背面使用维恩
  • iOS:用于登录屏幕的表格样式文本字段?

    我想制作一个像 Facebook 应用程序那样的登录屏幕 我想要复制的部分是两个文本字段 它们堆叠起来看起来像一个表格组 但我不明白他们是怎么做到的 谁知道其中的窍门 我无法发布图片 因为我是 stackoverflow 的新手 这是一种效
  • PHP 图片大小小于 1mb

    目前我正在使用以下内容来计算文件大小是否小于 1MB 但是由于以下代码来自 9lession 示例站点 它说要检查 1mb 的大小 但如果我乘以 1024 2 这就是他们在这里所做的不等于 1mb 而是 2048kb 说它上传的大小不是以
  • 如何在 macOS 上的 SwiftUI 中检测键盘事件?

    如何在 macOS 上的 SwiftUI 视图中检测键盘事件 我希望能够使用击键来控制特定屏幕上的项目 但不清楚如何检测键盘事件 这通常是通过覆盖keyDown event NSEvent in NSView 与 Xcode 12 捆绑在一