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

Kris SlazinskiKris Slazinski
5 min read

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

0
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