A SwiftUI keyboard shortcut remains active as long as the control it’s attached to is still in the view hierarchy, even when not onscreen.
Adding Keyboard Shortcuts
SwiftUI added The .keyboardShortcut
modifier in iOS 14. You apply the modifier to a control. The keyboard shortcut performs the primary action of the control. The shortcut defaults to using the command modifier but you can change that:
.keyboardShortcut("D", modifiers: [.control, .option])
Here’s an example of navigation from a root to detail view with keyboard shortcuts applied to the buttons:
struct ContentView: View {
@State private var isPresented: Bool = false
var body: some View {
NavigationStack {
VStack(spacing: 16) {
Button("Detail") {
isPresented = true
}
.keyboardShortcut("D")
Button("New") { ... }
.keyboardShortcut("N")
}
.navigationTitle("Title")
.navigationBarTitleDisplayMode(.inline)
.navigationDestination(isPresented: $isPresented) {
Button("Edit") { ... }
.keyboardShortcut("E")
.navigationTitle("Detail")
}
}
}
}
A command-N on the root view performs the new button action. A command-D performs the detail button action triggering a navigation to the detail view. On the detail view, command-E performs the edit button action.
Keyboard Shortcut Scope
What’s surprising to me is that the command-N and command-D keyboard shortcuts are still active on the detail view. You can see this when holding down the command key to view the available keyboard shortcuts. Here’s the available shortcuts from the root view:
If I navigate to the detail view, the new and detail button are no longer onscreen but their shortcuts still show up and trigger their actions:
What’s Happening?
From the documentation:
The target of a keyboard shortcut is resolved in a leading-to-trailing, depth-first traversal of one or more view hierarchies. On macOS, the system looks in the key window first, then the main window, and then the command groups; on other platforms, the system looks in the active scene, and then the command groups.
So the system searches for the first control associated with the shortcut in the window/scene view hierarchy. It doesn’t matter if the control is offscreen. I can see situations where you might want a shortcut to trigger when the control is offscreen but it’s not what I want here.
Workaround
The best workaround I’ve been able to come up with is to disable controls when I don’t want the shortcut to be active. In this example, I can use the isPresented state to disable the new and detail commands when I’m presenting the detail view:
Button("Detail") {
isPresented = true
}
.keyboardShortcut("D")
.disabled(isPresented)
Button("New") { ... }
.keyboardShortcut("N")
.disabled(isPresented)
See also this comment from Curt Clifton