BLACK FRIDAY: Save 50% on all my Swift books and bundles! >>

Creating tabs with TabView and tabItem()

Paul Hudson    @twostraws   

Navigation stacks are great for letting us create hierarchical stacks of views that let users drill down into data, but they don’t work so well for showing unrelated data. For that we need to use SwiftUI’s TabView, which creates a button strip across the bottom of the screen, where tapping each button shows a different view.

Placing tabs inside a TabView is as simple as listing them out one by one, like this:

TabView {
    Text("Tab 1")
    Text("Tab 2")
}

However, in practice you will always want to customize the way the tabs are shown – in the code above the tab bar will be an empty gray space. Although you can tap on the left and right parts of that gray space to activate the two tabs, it’s a pretty terrible user experience.

Instead, it’s a better idea to attach the tabItem() modifier to each view that’s inside a TabView. This lets you customize the way the view is shown in the tab bar, providing an image and some text to show next to it like this:

TabView {
    Text("Tab 1")
        .tabItem {
            Label("One", systemImage: "star")
        }

    Text("Tab 2")
        .tabItem {
            Label("Two", systemImage: "circle")
        }
}

As well as letting the user switch views by tapping on their tab item, SwiftUI also allows us to control the current view programmatically using state. This takes four steps:

  1. Create an @State property to track the tab that is currently showing.
  2. Modify that property to a new value whenever we want to jump to a different tab.
  3. Pass that as a binding into the TabView, so it will be tracked automatically.
  4. Tell SwiftUI which tab should be shown for each value of that property.

The first three of those are simple enough, so let’s get them out of the way. First, we need some state to track the current tab, so add this as a property to ContentView:

@State private var selectedTab = "One"

Second, we need to modify that somewhere, which will ask SwiftUI to switch tabs. In our little demo we could make our text be a button instead, like this:

Button("Show Tab 2") {
    selectedTab = "Two"
}
.tabItem {
    Label("One", systemImage: "star")
}

Third, we need to bind the selection of the TabView to $selectedTab. This is passed as a parameter when we create the TabView, so update your code to this:

TabView(selection: $selectedTab) {

Now for the interesting part: when we say selectedTab = "Two" how does SwiftUI know which tab that represents? You might think that the tabs could be treated as an array, in which case the second tab would be at index 1, but that causes all sorts of problems: what if we move that tab to a different position in the tab view?

At a deeper level, it also breaks one of the core SwiftUI concepts: that we should be able to compose views freely. If our button was able to say "go to the second tab in the array", it means it has intimate knowledge of how its parent, the TabView, is configured – it has to know the exact tab structure of its parent.

This is A Very Bad Idea, and so SwiftUI offers a better solution: we can attach a unique identifier to each view, and use that for the selected tab. These identifiers are called tags, and are attached using the tag() modifier like this:

Text("Tab 2")
    .tabItem {
        Image(systemName: "circle")
        Text("Two")
    }
    .tag("Two")

So, our entire view would be this:

struct ContentView: View {
    @State private var selectedTab = "One"

    var body: some View {
        TabView(selection: $selectedTab) {
            Button("Show Tab 2") {
                selectedTab = "Two"
            }
            .tabItem {
                Label("One", systemImage: "star")
            }
            .tag("One")

            Text("Tab 2")
                .tabItem {
                    Label("Two", systemImage: "circle")
                }
                .tag("Two")
        }
    }
}

And now that code works: you can switch between tabs by pressing on their tab items, or by activating our button in the first tab.

Of course, just using “One” and “Two” isn’t ideal – those values are fixed and so it solves the problem of views being moved around, but they aren’t easy to remember. Fortunately, you can use whatever values you like instead: give each view a string tag that is unique and reflects its purpose, then use that for your @State property. This is much easier to work with in the long term, and is recommended over integers.

Tip: It’s common to want to use NavigationStack and TabView at the same time, but you should be careful: TabView should be the parent view, with the tabs inside it having a NavigationStack as necessary, rather than the other way around.

Save 50% in my WWDC sale.

SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.3/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.