How to add keyboard shortcuts to switch tabs in your iPadOS or macOS app


Recently, I added keyboard shortcuts to my time-tracking app Moons allowing users to switch tabs by pressing ⌘1, ⌘2, ⌘3 and ⌘4. I also added corresponding commands to the app’s menu. Here’s a comparison of the View menu in my app BEFORE and AFTER these changes:
Tabs
I’d like to share the code so anyone can add such shortcuts to their apps. It’s fairly simple. I needed to use two modifiers for this: commands and keyboardShortcut. I’ll come back to these, but let’s start from the beginning. To switch tabs, we need a TabView
first. TabView
in my ContentView looks like this:
TabView(selection: $appState.selectedTab) {
Tab("Timer", systemImage: "timer.circle.fill", value: .timer) {
TimerView()
}
Tab("Projects", image: "moons.project.done", value: .projects) {
ProjectsView()
}
Tab("Time Entries", systemImage: "list.bullet.circle.fill", value: .timeEntries) {
TimeEntriesView()
}
Tab("More", systemImage: "ellipsis.circle.fill", value: .more) {
MoreView()
}
}
As you can see, I’m using $appState.selectedTab
as the selection. This appState
is an EnvironmentObject
of a custom type AppState
- an ObservableObject I defined in another file. It looks like this:
final class AppState: ObservableObject {
enum Tab: Hashable {
case timer, projects, timeEntries, more
}
@Published var selectedTab: Tab? = .timer
// ...
// Some other properties I need for other shortcuts.
}
In the AppState
class, I have an enum called Tab
and a Published var selectedTab
of type Tab
.
Basically, every time user switches a tab in the app (by tapping or clicking a tab), the value of selectedTab
changes: .timer for TimerView(), .projects for ProjectsView() and so on. You can see this value in the Tab
part of the code I showed you earlier:
Tab("Timer", systemImage: "timer.circle.fill", value: .timer) { // <- here
TimerView()
}
Commands
To enable tab switching not only by tapping or clicking on the tabs, but also through the app’s menu, I needed to add the commands
modifier to the WindowGroup in the Scene of the App struct.
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(appState)
}
.commands {
CommandGroup(before: .sidebar) {
Divider()
Button("Timer") {
appState.selectedTab = .timer
}
Button("Projects") {
appState.selectedTab = .projects
}
Button("Time Entries") {
appState.selectedTab = .timeEntries
}
Button("More") {
appState.selectedTab = .more
}
Divider()
}
}
}
What is happening here… In the commands
modifier, I’ve added a CommandGroup
, which is a group of controls that appear in the app’s menu. Notice the (before: .sidebar)
part - it tells SwiftUI to place my custom commands before the Show/Hide Sidebar and Enter/Exit Full Screen.
This group contains four buttons, with a divider above and below them. Each button updates the value of appState.selectedTab
.
Here’s a comparison of my app BEFORE and AFTER adding this code:
As you may have noticed, the new commands now appear in the View menu (before the Hide Sidebar and Enter Full Screen, as specified). However, there are no keyboard shortcuts yet.
Keyboard Shortcuts
To assign keyboard shortcuts to commands, we need to add the keyboardShortcut
modifier to each button in the CommandGroup
. Like this:
.commands {
CommandGroup(before: .sidebar) {
Divider()
Button("Timer") {
appState.selectedTab = .timer
}
.keyboardShortcut("1", modifiers: [.command])
Button("Projects") {
appState.selectedTab = .projects
}
.keyboardShortcut("2", modifiers: [.command])
Button("Time Entries") {
appState.selectedTab = .timeEntries
}
.keyboardShortcut("3", modifiers: [.command])
Button("More") {
appState.selectedTab = .more
}
.keyboardShortcut("4", modifiers: [.command])
Divider()
}
}
Since the ⌘Command key is the default modifier for keyboard shortcuts, we can also write it like this:
.keyboardShortcut("1")
// Instead of this:
// .keyboardShortcut("1", modifiers: [.command])
With the keyboardShortcut
modifiers, my commands now have shortcuts assigned to them:
Alternative code (Optional)
Your code may be simpler than mine. I’ve created my ObservableObject AppState
for other reasons. For example, I’ve added keyboard shortcuts to create a New Project or New Task with ⌘N and ⌘T, and I have some conditional logic as well.
However, if your app is simpler and all you need are keyboard shortcuts to switch tabs, you can just use a State variable along with a Tab
enum:
enum Tab: Hashable {
case timer, projects, timeEntries, more
}
Since we want to switch tabs using commands attached to the WindowGroup
, we need to have a selectedTab
variable in the App
struct:
@State private var selectedTab: Tab? = .timer
The commands
in the App
would use this State variable and look like this:
.commands {
CommandGroup(before: .sidebar) {
Divider()
Button("Timer") {
selectedTab = .timer
}
.keyboardShortcut("1")
Button("Projects") {
selectedTab = .projects
}
.keyboardShortcut("2")
Button("Time Entries") {
selectedTab = .timeEntries
}
.keyboardShortcut("3")
Button("More") {
selectedTab = .more
}
.keyboardShortcut("4")
Divider()
}
}
In ContentView
, we need to have a binding to the selectedTab
variable:
@Binding var selectedTab: Tab?
And the Tabview
would look like this:
TabView(selection: $selectedTab) {
Tab("Timer", systemImage: "timer.circle.fill", value: .timer) {
TimerView()
}
// Other tabs
}
So basically, we would use $selectedTab
as the selection
, not $appState.selectedTab
.
And you would need to pass selectedTab
value to ContentView
in your WindowGroup
, like this:
WindowGroup {
ContentView(selectedTab: $selectedTab)
}
And that’s it! Now you know how to add keyboard shortcuts and commands to switch tabs in your iPadOS or macOS apps 🙂
Thank you for reading!
If you want to support my work, please like, comment or share the article.
And most importantly...
📱Check out my apps on the App Store:
https://apps.apple.com/developer/next-planet/id1495155532
☕ If you like what I do, consider supporting me on Ko-fi! Every little bit means the world!
https://ko-fi.com/kslazinski
Subscribe to my newsletter
Read articles from Kris Slazinski directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Kris Slazinski
Kris Slazinski
👨🏻💻 UX Designer, self-employed at NEXT PLANET - small design studio. 👨🏻💻 Indie iOS developer 📱 Creator of: Moons, Numi, Skoro, Wins and Emo. 🎸Guitarist at ZERO and Quadroom 🎨 MA in art 🌱 Vegan