如何在 swiftUI (macOS) 中检测按键按下和释放
How to detect key press and release in swiftUI (macOS)
除了标题,没什么好说的。我希望能够在按下和释放键时(在 macOS 上)在 swiftUI 视图中执行操作。在 swiftUI 中有什么好的方法可以做到这一点,如果没有,有什么解决方法吗?
不幸的是,键盘事件处理是其中一个令人痛苦的领域,SwiftUI 最初是为 iOS 设计的,而 macOS 是事后才想到的。
如果您尝试检测的键是鼠标点击的修饰键,例如 cmd
、option
或 shift
,您可以使用 .modifiers
和 onTapGesture
以区别于未修改的 onTapGesture
。在那种情况下,我的经验是您希望使用 .modifiers
的 .onTapGesture
调用 先于 未修改的调用。
处理任意视图的一般按键事件需要在 SwiftUI 之外。
如果你只需要它用于一个视图,一种可能性是使用 AppKit
实现该视图,这样你就可以通过普通的 Cocoa firstResponder
机制接收键盘事件,并且然后将该视图包装在 SwiftUI 的 NSViewRepresentable
中。在这种情况下,您的包装 NSView
将在 NSViewRespresentable
中更新一些 @State
属性。许多使用 SwiftUI for macOS 的开发者都是这样做的。虽然这对于少数视图来说很好,但如果事实证明您必须在 AppKit 中实现大量视图才能使它们在 SwiftUI 中可用,那么无论如何您都在某种程度上破坏了使用 SwiftUI 的意义。在这种情况下,只需将其设为普通 Cocoa 应用即可。
但还有另一种方式...
您可以使用另一个使用 CGEventSource
的线程结合 SwiftUI @EnvironmentObject
或 @StateObject
主动轮询键盘状态以将键盘状态更改传达给 SwiftUI View
对它们感兴趣。
假设您想检测向上箭头何时被按下。为了检测密钥,我在 CGKeyCode
.
上使用了扩展名
import CoreGraphics
extension CGKeyCode
{
// Define whatever key codes you want to detect here
static let kVK_UpArrow: CGKeyCode = 0x7E
var isPressed: Bool {
CGEventSource.keyState(.combinedSessionState, key: self)
}
}
当然,您必须使用正确的键码。我有一个包含所有旧键码的 gist。如果您愿意,可以将它们重命名为更 Swifty。列出的名称可追溯到经典 MacOS,并在 Inside Macintosh.
中定义
定义该扩展后,您可以随时测试是否按下了某个键:
if CGKeyCode.kVK_UpArrow.isPressed {
// Do something in response to the key press.
}
请注意,这些是而非 按键弹起或按键按下事件。它只是一个布尔值,用于检测执行检查时是否按下了键。要表现得更像事件,您需要通过跟踪关键状态更改来自己完成该部分。
有多种方法可以做到这一点,以下代码并不意味着这是“最佳”方法。这只是 a 方式。在任何情况下,当您在应用程序启动时,无论您在哪里进行全局初始化,都会执行(或从中调用)类似以下代码的内容。
// These will handle sending the "event" and will be fleshed
// out further down
func dispatchKeyDown(_ key: CGKeyCode) {...}
func dispatchKeyUp(_ key: CGKeyCode) {...}
fileprivate var keyStates: [CGKeyCode: Bool] =
[
.kVK_UpArrow: false,
// populate with other key codes you're interested in
]
fileprivate let sleepSem = DispatchSemaphore(value: 0)
fileprivate let someConcurrentQueue = DispatchQueue(label: "polling", attributes: .concurrent)
someConcurrentQueue.async
{
while true
{
for (code, wasPressed) in keyStates
{
if code.isPressed
{
if !wasPressed
{
dispatchKeyDown(code)
keyStates[code] = true
}
}
else if wasPressed
{
dispatchKeyUp(code)
keyStates[code] = false
}
}
// Sleep long enough to avoid wasting CPU cycles, but
// not so long that you miss key presses. You may
// need to experiment with the .milliseconds value.
let_ = sleepSem.wait(timeout: .now() + .milliseconds(50))
}
}
这个想法只是让一些代码定期轮询关键状态,将它们与以前的状态进行比较,在它们发生变化时调度适当的“事件”,并更新以前的状态。上面的代码通过 运行 并发任务中的无限循环来实现。它需要创建一个带有 .concurrent
属性的 DispatchQueue
。你不能在 DispatchQueue.main
上使用它,因为那个队列是 串行的 不是并发的,所以无限循环会阻塞主线程,并且程序会变得无响应。如果您已经有一个并发 DispatchQueue
用于其他原因,您可以只使用那个,而不是创建一个仅用于轮询。
但是,任何实现定期轮询基本目标的代码都可以,所以如果您还没有并发 [=34=] 并且不希望创建一个只是为了轮询键盘状态,这将是一个合理的反对意见,这里有一个替代版本,它使用 DispatchQueue.main
和一种称为“异步链接”的技术来避免 blocking/sleeping:
fileprivate var keyStates: [CGKeyCode: Bool] =
[
.kVK_UpArrow: false,
// populate with other key codes you're interested in
]
fileprivate func pollKeyStates()
{
for (code, wasPressed) in keyStates
{
if code.isPressed
{
if !wasPressed
{
dispatchKeyDown(code)
keyStates[code] = true
}
}
else if wasPressed
{
dispatchKeyUp(code)
keyStates[code] = false
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50))
{
// The infinite loop from previous code is replaced by
// infinite chaining.
pollKeyStates()
}
}
// Start up key state polling
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) {
pollKeyStates()
}
有了用于检测何时按下按键的代码,您现在需要一种方法将其与您的 SwiftUI View
进行通信。同样,给猫剥皮的方法不止一种。这是一个过于简单的方法,只要按下向上箭头,它就会更新 View
,但您可能想要实现一些更复杂的东西……可能是允许视图指定它们是什么键的东西有兴趣回应。
class UpArrowDetector: ObservableObject
{
@Published var isPressed: Bool = false
}
let upArrowDetector = UpArrowDetector()
func dispatchKeyDown(_ key: CGKeyCode)
{
if key == .kVK_UpArrow {
upArrowDetector.isPressed = true
}
}
func dispatchKeyUp(_ key: CGKeyCode) {
if key == .kVK_UpArrow {
upArrowDetector.isPressed = false
}
}
// Now we hook it into SwiftUI
struct UpArrowDetectorView: View
{
@StateObject var detector: UpArrowDetector
var body: some View
{
Text(
detector.isPressed
? "Up-Arrow is pressed"
: "Up-Arrow is NOT pressed"
)
}
}
// Use the .environmentObject() method of `View` to inject the
// `upArrowDetector`
struct ContentView: View
{
var body: some View
{
UpArrowDetectorView()
.environmentObject(upArrowDetector)
}
}
我已经在此 gist 中放置了一个完整的、可编译的、有效的示例,该示例基于您在评论中链接到的代码。它是从上面的代码稍微重构的,但是所有的部分都在那里,包括启动轮询代码。
我希望这能为您指明一个有用的方向。
除了标题,没什么好说的。我希望能够在按下和释放键时(在 macOS 上)在 swiftUI 视图中执行操作。在 swiftUI 中有什么好的方法可以做到这一点,如果没有,有什么解决方法吗?
不幸的是,键盘事件处理是其中一个令人痛苦的领域,SwiftUI 最初是为 iOS 设计的,而 macOS 是事后才想到的。
如果您尝试检测的键是鼠标点击的修饰键,例如 cmd
、option
或 shift
,您可以使用 .modifiers
和 onTapGesture
以区别于未修改的 onTapGesture
。在那种情况下,我的经验是您希望使用 .modifiers
的 .onTapGesture
调用 先于 未修改的调用。
处理任意视图的一般按键事件需要在 SwiftUI 之外。
如果你只需要它用于一个视图,一种可能性是使用 AppKit
实现该视图,这样你就可以通过普通的 Cocoa firstResponder
机制接收键盘事件,并且然后将该视图包装在 SwiftUI 的 NSViewRepresentable
中。在这种情况下,您的包装 NSView
将在 NSViewRespresentable
中更新一些 @State
属性。许多使用 SwiftUI for macOS 的开发者都是这样做的。虽然这对于少数视图来说很好,但如果事实证明您必须在 AppKit 中实现大量视图才能使它们在 SwiftUI 中可用,那么无论如何您都在某种程度上破坏了使用 SwiftUI 的意义。在这种情况下,只需将其设为普通 Cocoa 应用即可。
但还有另一种方式...
您可以使用另一个使用 CGEventSource
的线程结合 SwiftUI @EnvironmentObject
或 @StateObject
主动轮询键盘状态以将键盘状态更改传达给 SwiftUI View
对它们感兴趣。
假设您想检测向上箭头何时被按下。为了检测密钥,我在 CGKeyCode
.
import CoreGraphics
extension CGKeyCode
{
// Define whatever key codes you want to detect here
static let kVK_UpArrow: CGKeyCode = 0x7E
var isPressed: Bool {
CGEventSource.keyState(.combinedSessionState, key: self)
}
}
当然,您必须使用正确的键码。我有一个包含所有旧键码的 gist。如果您愿意,可以将它们重命名为更 Swifty。列出的名称可追溯到经典 MacOS,并在 Inside Macintosh.
中定义定义该扩展后,您可以随时测试是否按下了某个键:
if CGKeyCode.kVK_UpArrow.isPressed {
// Do something in response to the key press.
}
请注意,这些是而非 按键弹起或按键按下事件。它只是一个布尔值,用于检测执行检查时是否按下了键。要表现得更像事件,您需要通过跟踪关键状态更改来自己完成该部分。
有多种方法可以做到这一点,以下代码并不意味着这是“最佳”方法。这只是 a 方式。在任何情况下,当您在应用程序启动时,无论您在哪里进行全局初始化,都会执行(或从中调用)类似以下代码的内容。
// These will handle sending the "event" and will be fleshed
// out further down
func dispatchKeyDown(_ key: CGKeyCode) {...}
func dispatchKeyUp(_ key: CGKeyCode) {...}
fileprivate var keyStates: [CGKeyCode: Bool] =
[
.kVK_UpArrow: false,
// populate with other key codes you're interested in
]
fileprivate let sleepSem = DispatchSemaphore(value: 0)
fileprivate let someConcurrentQueue = DispatchQueue(label: "polling", attributes: .concurrent)
someConcurrentQueue.async
{
while true
{
for (code, wasPressed) in keyStates
{
if code.isPressed
{
if !wasPressed
{
dispatchKeyDown(code)
keyStates[code] = true
}
}
else if wasPressed
{
dispatchKeyUp(code)
keyStates[code] = false
}
}
// Sleep long enough to avoid wasting CPU cycles, but
// not so long that you miss key presses. You may
// need to experiment with the .milliseconds value.
let_ = sleepSem.wait(timeout: .now() + .milliseconds(50))
}
}
这个想法只是让一些代码定期轮询关键状态,将它们与以前的状态进行比较,在它们发生变化时调度适当的“事件”,并更新以前的状态。上面的代码通过 运行 并发任务中的无限循环来实现。它需要创建一个带有 .concurrent
属性的 DispatchQueue
。你不能在 DispatchQueue.main
上使用它,因为那个队列是 串行的 不是并发的,所以无限循环会阻塞主线程,并且程序会变得无响应。如果您已经有一个并发 DispatchQueue
用于其他原因,您可以只使用那个,而不是创建一个仅用于轮询。
但是,任何实现定期轮询基本目标的代码都可以,所以如果您还没有并发 [=34=] 并且不希望创建一个只是为了轮询键盘状态,这将是一个合理的反对意见,这里有一个替代版本,它使用 DispatchQueue.main
和一种称为“异步链接”的技术来避免 blocking/sleeping:
fileprivate var keyStates: [CGKeyCode: Bool] =
[
.kVK_UpArrow: false,
// populate with other key codes you're interested in
]
fileprivate func pollKeyStates()
{
for (code, wasPressed) in keyStates
{
if code.isPressed
{
if !wasPressed
{
dispatchKeyDown(code)
keyStates[code] = true
}
}
else if wasPressed
{
dispatchKeyUp(code)
keyStates[code] = false
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50))
{
// The infinite loop from previous code is replaced by
// infinite chaining.
pollKeyStates()
}
}
// Start up key state polling
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) {
pollKeyStates()
}
有了用于检测何时按下按键的代码,您现在需要一种方法将其与您的 SwiftUI View
进行通信。同样,给猫剥皮的方法不止一种。这是一个过于简单的方法,只要按下向上箭头,它就会更新 View
,但您可能想要实现一些更复杂的东西……可能是允许视图指定它们是什么键的东西有兴趣回应。
class UpArrowDetector: ObservableObject
{
@Published var isPressed: Bool = false
}
let upArrowDetector = UpArrowDetector()
func dispatchKeyDown(_ key: CGKeyCode)
{
if key == .kVK_UpArrow {
upArrowDetector.isPressed = true
}
}
func dispatchKeyUp(_ key: CGKeyCode) {
if key == .kVK_UpArrow {
upArrowDetector.isPressed = false
}
}
// Now we hook it into SwiftUI
struct UpArrowDetectorView: View
{
@StateObject var detector: UpArrowDetector
var body: some View
{
Text(
detector.isPressed
? "Up-Arrow is pressed"
: "Up-Arrow is NOT pressed"
)
}
}
// Use the .environmentObject() method of `View` to inject the
// `upArrowDetector`
struct ContentView: View
{
var body: some View
{
UpArrowDetectorView()
.environmentObject(upArrowDetector)
}
}
我已经在此 gist 中放置了一个完整的、可编译的、有效的示例,该示例基于您在评论中链接到的代码。它是从上面的代码稍微重构的,但是所有的部分都在那里,包括启动轮询代码。
我希望这能为您指明一个有用的方向。