如何在 macOS 上检测 SwiftUI 中的键盘事件?
Posted
技术标签:
【中文标题】如何在 macOS 上检测 SwiftUI 中的键盘事件?【英文标题】:How to detect keyboard events in SwiftUI on macOS? 【发布时间】:2020-04-11 07:44:08 【问题描述】:如何在 macOS 上的 SwiftUI 视图中检测键盘事件?
我希望能够使用击键来控制特定屏幕上的项目,但不清楚我如何检测键盘事件,这通常通过覆盖 NSView
中的 keyDown(_ event: NSEvent)
来完成。
【问题讨论】:
【参考方案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)
(它告诉其余响应者链未处理击键),则不应再发出声音。以上是关于如何在 macOS 上检测 SwiftUI 中的键盘事件?的主要内容,如果未能解决你的问题,请参考以下文章
(SwiftUI, MacOS 11.0) 如何在 SecureTextFeild 中检测焦点?
如何检测何时点击 TextField? (MacOS 上的 SwiftUI)
如何使用 SwiftUI 在 macOS 上可靠地检索窗口的背景颜色?