`TabView` and Toolbars in SwiftUI: Building App-Wide Navigation


Open any Pixar app — a Toy Box manager, a Monsters Inc. scare tracker, a Ratatouille recipe book — and you’ll find tabs at the bottom. Tabs let users jump between major sections of your app with a single tap, and toolbars give them quick access to actions without leaving the current screen.

You’ll learn how to build a TabView with custom icons, add toolbar buttons with .toolbar, and combine tabs with navigation stacks. We won’t cover fully custom tab bar designs or advanced ToolbarRole configurations — those belong in intermediate territory.

What You’ll Learn

What Is a TabView?

A TabView is a container that organizes your app into multiple sections, each represented by a tab at the bottom of the screen. The user taps a tab to switch between sections.

Apple Docs: TabView — SwiftUI

Think of it like the departments at Monsters, Inc. — the Scare Floor, the Laugh Floor, and the Admin Office are all in the same building, but you pick which one to visit. Each tab is a department.

Creating Tabs

Each child inside a TabView becomes a separate tab. You give each tab a label using the .tabItem modifier.

struct MonsterHQView: View {
    var body: some View {
        TabView {
            Text("Scare Floor")
                .tabItem {
                    Label("Scares", systemImage: "flame")
                }
            Text("Laugh Floor")
                .tabItem {
                    Label("Laughs", systemImage: "face.smiling")
                }
            Text("Admin")
                .tabItem {
                    Label("Admin", systemImage: "gear")
                }
        }
    }
}

Each .tabItem takes a Label with a title and an SF Symbol icon. The tab bar appears at the bottom of the screen, and tapping a tab switches the visible content.

Tip: Keep your tab count between 2 and 5. More than 5 tabs clutter the bar and make your app harder to navigate. If you need more sections, consider a “More” tab or a different navigation pattern.

Controlling the Selected Tab

By default, the first tab is selected. To control which tab is active programmatically, add a @State property and a selection binding.

struct PixarHubView: View {
    @State private var selectedTab = 0

    var body: some View {
        TabView(selection: $selectedTab) {
            Text("Movies")
                .tabItem { Label("Movies", systemImage: "film") }
                .tag(0)
            Text("Characters")
                .tabItem { Label("Characters", systemImage: "person.2") }
                .tag(1)
            Text("Settings")
                .tabItem { Label("Settings", systemImage: "gear") }
                .tag(2)
        }
    }
}

Each tab needs a .tag that matches the type of your selectedTab property. Here we use Int tags, but you can use a String or a custom enum for more readable code.

You can switch tabs from code by changing selectedTab — for example, a button in the Movies tab could set selectedTab = 1 to jump to Characters.

Adding Toolbars

A toolbar adds buttons and controls to the navigation bar at the top of the screen, or the bottom bar area. You create toolbar items using the .toolbar modifier.

Apple Docs: toolbar(content:) — SwiftUI

struct ToyInventoryView: View {
    @State private var toys = ["Woody", "Buzz", "Rex"]

    var body: some View {
        NavigationStack {
            List(toys, id: \.self) { toy in
                Text(toy)
            }
            .navigationTitle("Toy Box")
            .toolbar {
                ToolbarItem(placement: .primaryAction) {
                    Button("Add Toy") { }
                }
            }
        }
    }
}

ToolbarItem wraps each button and accepts a placement parameter that controls where it appears. Common placements include:

  • .primaryAction — trailing edge (top-right on iOS)
  • .cancellationAction — leading edge (top-left)
  • .bottomBar — bottom toolbar area
  • .navigation — alongside the back button

Multiple Toolbar Items

You can add several items to different placements.

.toolbar {
    ToolbarItem(placement: .cancellationAction) {
        Button("Cancel") { }
    }
    ToolbarItem(placement: .primaryAction) {
        Button("Save") { }
    }
}

This creates a “Cancel” button on the left and a “Save” button on the right — a common pattern for modal forms.

Combining Tabs and Navigation

Most real apps put a NavigationStack inside each tab, not around the TabView. This gives each tab its own independent navigation history.

struct PixarAppView: View {
    var body: some View {
        TabView {
            NavigationStack {
                List {
                    NavigationLink("Toy Story", value: "Toy Story")
                    NavigationLink("Finding Nemo", value: "Finding Nemo")
                }
                .navigationTitle("Movies")
                .navigationDestination(for: String.self) { movie in
                    Text("Details for \(movie)")
                }
            }
            .tabItem { Label("Movies", systemImage: "film") }

            NavigationStack {
                Text("Favorites coming soon!")
                    .navigationTitle("Favorites")
            }
            .tabItem { Label("Favorites", systemImage: "star") }
        }
    }
}

When the user navigates into a movie detail and then switches tabs, the Movies tab remembers its navigation state. Switching back reveals the detail view right where they left it.

Common Mistakes

Wrapping TabView in NavigationStack

Placing NavigationStack outside TabView means all tabs share one navigation stack. Pushing a view in one tab affects the others.

// ❌ Don't do this — all tabs share one navigation stack
NavigationStack {
    TabView {
        MovieListView()
            .tabItem { Label("Movies", systemImage: "film") }
        SettingsView()
            .tabItem { Label("Settings", systemImage: "gear") }
    }
}
// ✅ Do this — each tab has its own navigation stack
TabView {
    NavigationStack { MovieListView() }
        .tabItem { Label("Movies", systemImage: "film") }
    NavigationStack { SettingsView() }
        .tabItem { Label("Settings", systemImage: "gear") }
}

Put the NavigationStack inside each tab so every section manages its own navigation independently.

Forgetting Tags When Using Selection

If you use a selection binding on TabView but forget to add .tag modifiers to each tab, SwiftUI can’t match the selection value and the binding won’t work.

// ❌ Don't do this — missing tags
TabView(selection: $selectedTab) {
    Text("Movies").tabItem { Label("Movies", systemImage: "film") }
    Text("Settings").tabItem { Label("Settings", systemImage: "gear") }
}
// ✅ Do this — each tab has a matching tag
TabView(selection: $selectedTab) {
    Text("Movies")
        .tabItem { Label("Movies", systemImage: "film") }
        .tag(0)
    Text("Settings")
        .tabItem { Label("Settings", systemImage: "gear") }
        .tag(1)
}

Every tab must have a .tag whose type matches the selection property.

What’s Next?

  • TabView organizes your app into switchable sections with a tab bar
  • .tabItem provides the label and icon for each tab
  • Use a selection binding and .tag to control tabs programmatically
  • .toolbar adds buttons to the navigation bar and bottom bar
  • Place NavigationStack inside each tab, not around the TabView

Ready to make your app visually rich? Head over to Images in SwiftUI to learn about SF Symbols, AsyncImage, and custom assets.