Last active
January 2, 2024 13:26
-
-
Save steipete/03d412f3752611f8f4554372a29cc29d to your computer and use it in GitHub Desktop.
Add Keyboard Shortcuts to SwiftUI on iOS 13 when using `UIHostingController`. Requires using KeyboardEnabledHostingController as hosting class) See https://steipete.com/posts/fixing-keyboardshortcut-in-swiftui/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// KeyCommand.swift | |
// Adds Keyboard Shortcuts to SwiftUI on iOS 13 | |
// See https://steipete.com/posts/fixing-keyboardshortcut-in-swiftui/ | |
// License: MIT | |
// | |
// Usage: (wrap view in `KeyboardEnabledHostingController`) | |
// Button(action: { | |
// print("Button Tapped!!") | |
// }) { | |
// Text("Button") | |
// } | |
// .keyCommand("e", modifiers: [.control]) | |
import SwiftUI | |
import Combine | |
/// Subclass for `UIHostingController` that enables using the `onKeyCommand` extension. | |
@available(iOS 13.0, *) | |
class KeyboardEnabledHostingController<Content>: UIHostingController<KeyboardEnabledHostingController.Wrapper> where Content: View { | |
private let registrator = KeyCommandRegistrator() | |
init(rootView: Content) { | |
super.init(rootView: Wrapper(content: rootView, registrator: registrator)) | |
} | |
struct Wrapper: View { | |
let content: Content | |
fileprivate let registrator: KeyCommandRegistrator | |
var body: some View { | |
content.environmentObject(registrator) | |
} | |
} | |
@objc required dynamic init?(coder aDecoder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
override var keyCommands: [UIKeyCommand]? { | |
registrator.keyCommands + (super.keyCommands ?? []) | |
} | |
// This method must be inside a responder, else it's hidden | |
@objc private func performKeyCommand(_ keyCommand: UIKeyCommand) { | |
guard let input = keyCommand.input else { return } | |
let keyPair = KeyCommandPair(input: input, modifiers: keyCommand.modifierFlags) | |
registrator.keyPublisher.send(keyPair) | |
} | |
override var canBecomeFirstResponder: Bool { true } | |
} | |
@available(iOS 13.0, *) | |
private struct KeyCommandStyle: PrimitiveButtonStyle { | |
var commandPair: KeyCommandPair | |
// Purely additive style: https://developer.apple.com/documentation/swiftui/button/init(_:) | |
func makeBody(configuration: Configuration) -> some View { | |
Button(configuration) | |
.keyCommand(keyPair: commandPair, action: configuration.trigger) | |
} | |
} | |
@available(iOS 13.0, *) | |
extension View { | |
/// Register a key command for the current button, invoking the button action when triggered. | |
func keyCommand(_ key: String, modifiers: UIKeyModifierFlags = .command, title: String = "") -> some View { | |
buttonStyle(KeyCommandStyle(commandPair: KeyCommandPair(input: key, modifiers: modifiers))) | |
} | |
/// Register a key command for the current view | |
func onKeyCommand(_ key: String, modifiers: UIKeyModifierFlags = .command, title: String = "", action: @escaping () -> Void) -> some View { | |
keyCommand(keyPair: KeyCommandPair(input: key, modifiers: modifiers, title: title), action: action) | |
} | |
fileprivate func keyCommand(keyPair: KeyCommandPair, action: @escaping () -> Void) -> some View { | |
self.modifier(KeyCommandModifier(commandPair: keyPair, action: action)) | |
} | |
} | |
@available(iOS 13.0, *) | |
private struct KeyCommandModifier: ViewModifier { | |
@EnvironmentObject var registrator: KeyCommandRegistrator | |
fileprivate var commandPair: KeyCommandPair | |
var action: () -> Void | |
func body(content: Content) -> some View { | |
content | |
.padding(0) // without a modification, onReceive is not called | |
.onReceive(registrator.keyPublisher.filter { $0 == self.commandPair }) { _ in action() } | |
.onAppear { | |
registrator.register(commandPair) | |
} | |
} | |
} | |
@available(iOS 13.0, *) | |
private class KeyCommandRegistrator: ObservableObject { | |
var keyCommands: [UIKeyCommand] = [] | |
let keyPublisher = PassthroughSubject<KeyCommandPair, Never>() | |
func register(_ commandPair: KeyCommandPair) { | |
let command = UIKeyCommand(title: commandPair.title ?? "", | |
action: NSSelectorFromString("performKeyCommand:"), | |
input: commandPair.input, | |
modifierFlags: commandPair.modifiers) | |
keyCommands += [command] | |
} | |
} | |
@available(iOS 13.0, *) | |
private struct KeyCommandPair: Equatable { | |
var input: String | |
var modifiers: UIKeyModifierFlags | |
var title: String? | |
static func == (lhs: KeyCommandPair, rhs: KeyCommandPair) -> Bool { | |
return lhs.input == rhs.input && lhs.modifiers == rhs.modifiers | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment