如何在 macOS 上的 SwiftUI 视图中检测键盘事件?
我希望能够使用击键来控制特定屏幕上的项目,但不清楚如何检测键盘事件,这通常是通过覆盖keyDown(_ event: NSEvent)in来完成的NSView。
Saj*_*jon 17
与 Xcode 12 捆绑在一起的 SwiftUI 中的新功能是经过commands修改的,它允许我们使用keyboardShortcut视图修饰符声明键输入。然后,您需要某种方式将关键输入转发到您的子视图。下面是一个使用 a 的解决方案Subject,但由于它不是引用类型,因此不能使用environmentObject它传递- 这正是我们想要做的,所以我做了一个小包装器,符合ObservableObject并为方便Subject本身(通过 转发subject)。
使用一些额外的便利糖方法,我可以这样写:
.commands {
CommandMenu("Input") {
keyInput(.leftArrow)
keyInput(.rightArrow)
keyInput(.upArrow)
keyInput(.downArrow)
keyInput(.space)
}
}
Run Code Online (Sandbox Code Playgroud)
并将关键输入转发给所有子视图,如下所示:
.environmentObject(keyInputSubject)
Run Code Online (Sandbox Code Playgroud)
然后是一个子视图,这里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)
}
}
}
Run Code Online (Sandbox Code Playgroud)
keyInput用于在CommandMenubuilder 中声明键的方法是这样的:
private extension ItsRainingPolygonsApp {
func keyInput(_ key: KeyEquivalent, modifiers: EventModifiers = .none) -> some View {
keyboardShortcut(key, sender: keyInputSubject, modifiers: modifiers)
}
}
Run Code Online (Sandbox Code Playgroud)
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)"
}
}
Run Code Online (Sandbox Code Playgroud)
Asp*_*eri 15
到目前为止,还没有内置的原生 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())
}
}
Run Code Online (Sandbox Code Playgroud)
输出:
Bra*_*rst 10
还有另一种解决方案,非常简单,但仅适用于特定类型的密钥 - 您必须进行试验。只需Buttons使用.keyboardShortcut修改器创建,但在视觉上隐藏它们。
Group {
Button(action: { goAway() }) {}
.keyboardShortcut(.escape, modifiers: [])
Button(action: { goLeft() }) {}
.keyboardShortcut(.upArrow, modifiers: [])
Button(action: { goDown() }) {}
.keyboardShortcut(.downArrow, modifiers: [])
}.opacity(0)
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
7134 次 |
| 最近记录: |