Organize Top-Level Screens with SwiftUI Tabs

Organize Top-Level Screens with SwiftUI Tabs

Explore, Saved and Settings are peer destinations in a travel app. They should be selectable without pushing one beneath another. Use a typed tab selection and give each destination its own clear navigation context.

You will learn: Distinguish tabs from navigation depth; bind selection to an enum; use matching Tab values; switch selection from an in-app action.

Prerequisites: the earlier SwiftUI Foundations lessons and Swift values/closures. These projects use the installed iOS SDK and local in-memory fixtures. They do not modify personal data or contact a server. The screenshot shows the initial screen before the interaction sequence.

Explore places, River park
Actual iPhone 18 Pro simulator capture, iOS 27.0; local apple-vision verification.

Use tabs for peer destinations

TabView describes a set of top-level destinations. Explore is not the parent of Settings, so a push link from Explore is the wrong relationship for the primary tab switch. Each tab supplies a text label and symbol that remain meaningful together.

The sample uses modern Tab declarations supported by its installed SDK and deployment target. If supporting an older OS that predates this API, use an appropriate availability strategy or the older tabItem/tag form; do not paste a newer declaration into an unsupported target and assume a styling fix will resolve compilation.

Make selection a typed value

TravelTab enumerates explore, saved and settings. The parent owns selection, and each Tab value uses the same enum type. This prevents mismatched string tags such as “Saved” versus “saved.”

enum TravelTab: Hashable { case explore, saved, settings }
@State private var selection: TravelTab = .explore
// Inside TabView(selection: $selection):
Tab("Saved", systemImage: "bookmark", value: .saved) {
    Text("Saved places")
}

This shortened excerpt demonstrates selection identity; the complete project places the real saved list inside its own stack. Display labels may change for localization without changing the enum identity used by code.

Switch by changing selection, not duplicating a screen

Show saved places assigns selection = .saved. It does not push a second Saved view inside Explore. That distinction prevents two visually similar screens with different back behavior from representing the same top-level destination.

Button("Show saved places") {
    selection = .saved
}

The result should agree with selecting the Saved tab itself. Use a programmatic switch when it helps a meaningful flow; surprising automatic tab changes can disorient the person and should not replace clear navigation choices.

Give independent flows appropriate ownership

Each tab here contains a NavigationStack. That provides its own title and a place for future pushes within that tab. A single outer stack around the entire TabView would represent a different hierarchy and can create confusing cross-tab depth.

This sample verifies selection changes and the corresponding root content. It does not establish restoration of independent deep paths or persistence after process termination. Add owned paths per tab and explicit restoration only when the product requires them. Tab bars can also adapt across platform and size classes; the screenshot is one named simulator configuration.

Build the complete example

Download the TravelTabs Xcode project. Open TravelTabs.xcodeproj, select an installed compatible iPhone simulator, and run. The app entry point already presents ContentView. The complete ContentView.swift below is the exact built source; earlier snippets are explanatory excerpts.

import SwiftUI

struct ContentView: View {
@State private var selection: TravelTab = .explore
 var body: some View {
TabView(selection: $selection) {
 Tab("Explore", systemImage: "map", value: .explore) {
  NavigationStack {
   VStack(spacing: 24) {
    Text("River park").font(.title)
    Button("Show saved places") { selection = .saved }.accessibilityIdentifier("showSaved")
   }.navigationTitle("Explore places")
  }
 }
 Tab("Saved", systemImage: "bookmark", value: .saved) {
  NavigationStack { List { Text("Old library"); Text("City garden") }.navigationTitle("Saved places") }
 }
 Tab("Settings", systemImage: "gear", value: .settings) {
  NavigationStack { Text("Distances: kilometres").navigationTitle("Travel settings") }
 }
}
 }
}

enum TravelTab: Hashable { case explore, saved, settings }

#Preview { ContentView() }

Accessibility identifiers in the file provide stable names for local automation; they are not visible menu labels. #Preview is an editor convenience. The following screenshots come from the launched app after actual automated control interactions.

Switch tabs programmatically

Press Show saved places in Explore. The selected tab changes and the screen displays Saved places with Old library and City garden. No back-to-Explore push control is introduced.

Saved places, Old library, City garden
Actual iPhone 18 Pro simulator capture, iOS 27.0; local native-accessibility verification.

Select another top-level destination

Select Settings from the tab bar. Travel settings and Distances: kilometres replace the saved list. The automation checks the new content, not just a changed icon color.

Travel settings, Distances: kilometres
Actual iPhone 18 Pro simulator capture, iOS 27.0; local native-accessibility verification.

Check a failure at the right boundary

If your output differs, compare the current input values and the relevant action before changing project settings. Save the complete source, confirm the selected scheme and destination, and inspect the first build diagnostic if compilation fails. A successful build establishes valid source; the action-result checks establish the particular UI transitions shown here.

Practice variations below are proposed changes, not additional captured states. Keep input validation, state ownership and presentation decisions explicit as you extend the project.

Try a new case and explain the result

Add a Recent tab with its own enum case and a short list. Explain how to select it from a button.

Worked answer

Add recent to TravelTab, declare a Tab with value .recent, and assign selection = .recent in the action. Keep labels and values distinct. If Recent needs its own push flow, give it an owned navigation context rather than reusing another tab’s path accidentally.

Check your understanding

Choose the explanation that follows from the worked example.

What should Show saved places change?
Choose one answer. You can try again.

Answer and explanation

The selected tab value.

Correct. Trace the owned data and the operation used by the example.

What to take forward

You organized peer destinations and switched tabs through typed selection. Next, add local search without mutating the source collection.

Apple documentation.

Reaching the recap and completing the quizzes are separate milestones.

    Next lesson: Search and Filter a SwiftUI List →