diff --git a/Example/KeyboardShortcutsExample/MainScreen.swift b/Example/KeyboardShortcutsExample/MainScreen.swift index 4cd8382e..5ee7fe9e 100644 --- a/Example/KeyboardShortcutsExample/MainScreen.swift +++ b/Example/KeyboardShortcutsExample/MainScreen.swift @@ -100,24 +100,17 @@ private struct DoubleShortcut: View { .frame(maxWidth: 300) .padding() .padding() + .onKeyboardShortcut(.testShortcut1) { + isPressed1 = $0 == .keyDown + } + .onKeyboardShortcut(.testShortcut2, type: .keyDown) { + isPressed2 = true + } .task { - KeyboardShortcuts.onKeyDown(for: .testShortcut1) { - isPressed1 = true - } - - KeyboardShortcuts.onKeyDown(for: .testShortcut2) { - isPressed2 = true - } - KeyboardShortcuts.onKeyUp(for: .testShortcut2) { isPressed2 = false } } - .task { - for await _ in KeyboardShortcuts.on(.keyUp, for: .testShortcut1) { - isPressed1 = false - } - } } } diff --git a/Sources/KeyboardShortcuts/KeyboardShortcuts.swift b/Sources/KeyboardShortcuts/KeyboardShortcuts.swift index e9c418b5..b4b00fdb 100644 --- a/Sources/KeyboardShortcuts/KeyboardShortcuts.swift +++ b/Sources/KeyboardShortcuts/KeyboardShortcuts.swift @@ -396,7 +396,7 @@ extension KeyboardShortcuts { var body: some View { Text(isUnicornMode ? "🦄" : "🐴") .task { - for await _ in KeyboardShortcuts.on(.keyUp, for: .toggleUnicornMode) { + for await event in KeyboardShortcuts.events(for: .toggleUnicornMode) where event == .keyUp { isUnicornMode.toggle() } } @@ -407,6 +407,69 @@ extension KeyboardShortcuts { - Note: This method is not affected by `.removeAllHandlers()`. */ @available(macOS 10.15, *) + public static func events(for name: Name) -> AsyncStream { + AsyncStream { continuation in + let id = UUID() + + DispatchQueue.main.async { + streamKeyDownHandlers[name, default: [:]][id] = { + continuation.yield(.keyDown) + } + + streamKeyUpHandlers[name, default: [:]][id] = { + continuation.yield(.keyUp) + } + + registerShortcutIfNeeded(for: name) + } + + continuation.onTermination = { _ in + DispatchQueue.main.async { + streamKeyDownHandlers[name]?[id] = nil + streamKeyUpHandlers[name]?[id] = nil + + unregisterShortcutIfNeeded(for: name) + } + } + } + } + + /** + Listen to keyboard shortcut events with the given name and type. + + You can register multiple listeners. + + You can safely call this even if the user has not yet set a keyboard shortcut. It will just be inactive until they do. + + Ending the async sequence will stop the listener. For example, in the below example, the listener will stop when the view disappears. + + ```swift + import SwiftUI + import KeyboardShortcuts + + struct ContentView: View { + @State private var isUnicornMode = false + + var body: some View { + Text(isUnicornMode ? "🦄" : "🐴") + .task { + for await event in KeyboardShortcuts.events(for: .toggleUnicornMode) where event == .keyUp { + isUnicornMode.toggle() + } + } + } + } + ``` + + - Note: This method is not affected by `.removeAllHandlers()`. + */ + @available(macOS 10.15, *) + public static func events(_ type: EventType, for name: Name) -> AsyncFilterSequence> { + events(for: name).filter { $0 == type } + } + + @available(macOS 10.15, *) + @available(*, deprecated, renamed: "events(_:for:)") public static func on(_ type: EventType, for name: Name) -> AsyncStream { AsyncStream { continuation in let id = UUID() diff --git a/Sources/KeyboardShortcuts/ViewModifiers.swift b/Sources/KeyboardShortcuts/ViewModifiers.swift new file mode 100644 index 00000000..bc24ce3d --- /dev/null +++ b/Sources/KeyboardShortcuts/ViewModifiers.swift @@ -0,0 +1,38 @@ +import SwiftUI + +@available(macOS 12, *) +extension View { + /** + Register a listener for keyboard shortcut events with the given name. + + You can safely call this even if the user has not yet set a keyboard shortcut. It will just be inactive until they do. + + The listener will stop automatically when the view disappears. + + - Note: This method is not affected by `.removeAllHandlers()`. + */ + public func onKeyboardShortcut(_ shortcut: KeyboardShortcuts.Name, perform: @escaping (KeyboardShortcuts.EventType) -> Void) -> some View { + task { + for await eventType in KeyboardShortcuts.events(for: shortcut) { + perform(eventType) + } + } + } + + /** + Register a listener for keyboard shortcut events with the given name and type. + + You can safely call this even if the user has not yet set a keyboard shortcut. It will just be inactive until they do. + + The listener will stop automatically when the view disappears. + + - Note: This method is not affected by `.removeAllHandlers()`. + */ + public func onKeyboardShortcut(_ shortcut: KeyboardShortcuts.Name, type: KeyboardShortcuts.EventType, perform: @escaping () -> Void) -> some View { + task { + for await _ in KeyboardShortcuts.events(type, for: shortcut) { + perform() + } + } + } +}