TabView in SwiftUI

Using Tabbed Navigation

Open the starter project for this lesson. You’ll recognize it as the project from lesson two changed back to a two-column view. In this lesson, you’ll adapt the list of flights to take advantage of tabs to view different versions of the flight list quickly.

First, you’ll extract the portion of the view that creates the list into a separate view. This change will make it easier to use across the tabs. Under the FlightStatusBoard group, create a new SwiftUI view named FlightList.swift and replace the view with:

struct FlightList: View {
  var flights: [FlightInformation]
  @State private var path: [FlightInformation] = []

  var body: some View {
    NavigationStack(path: $path) {
      List(flights, id: \.id) { flight in
        NavigationLink(flight.statusBoardName, value: flight)
      }
      .navigationDestination(
        for: FlightInformation.self,
        destination: { flight in
          FlightDetails(flight: flight)
        }
      )
    }
  }
}

This code extracts the list of flights from the current FlightStatusBoard into this new view. Next, change the preview to pass the sample data:

FlightList(
  flights: FlightData.generateTestFlights(date: Date())
)

Now open FlightStatusBoard.swift. Change the body of FlightStatusBoard to:

// 1
TabView {
  // 2
  FlightList(
    flights: shownFlights.filter { $0.direction == .arrival }
  )
  // 3
  .tabItem {
    // 4
    Image("descending-airplane")
      .resizable()
    Text("Arrivals")
  }

  FlightList(flights: shownFlights)
  .tabItem {
    Image(systemName: "airplane")
      .resizable()
    Text("All")
  }
  
  FlightList(
    flights: shownFlights.filter { $0.direction == .departure }
  )
  .tabItem {
    Image("ascending-airplane")
    Text("Departures")
  }
}
.navigationTitle("Today's Flight Status")
.navigationBarItems(
  trailing: Toggle("Hide Past", isOn: $hidePast)
)

Don’t panic. That seems like a lot of code, but the code for each of the tabs works identically. Here’s how it implements three tabs:

  1. First, you use the TabView control to declare that you’re creating a tab. The first tab will show only arriving flights, the second will show all flights, and the third will only display departing flights.
  2. You provide a view for each tab to the enclosure of TabView showing the extracted FlightList view.
  3. Then, you apply the tabItem(_:) modifier to each tab to set an image, text, or combination of the two. You always want to specify at least one of these labels so the user can clearly understand the tab’s contents.
  4. Each tab displays an image and a text label. You can only use Text, an Image, or an Image with Text as the tab label. If you use anything else, the tab will appear empty. You don’t need to create a VStack even when using multiple items.

Note: You may wonder why the view uses a custom image for the descending and ascending aircraft instead of modifying the SF Symbol font used for the central tab. SwiftUI ignores most modifiers to Image within the tab toolbar and won’t process them, including a rotation.

Run the app. Tap the Flight Status option, and you’ll see that your view now has three tabs that let you view all flights or only flights departing or arriving at the airport. Note that the toggle in the navigation still works, and the two navigation structures don’t conflict. You can select any flight as before and see more details about it.

Flight status with tabs
Flight status with tabs

Programmatically Tracking Tabs

When the user leaves and exits the new tabbed view, the app will always start back on the Arrivals tab. Remembering the last viewed tab would be a good interface change. To implement that, in FlightStatusBoard.swift, below the hidePast state variable, add the following line:

@AppStorage("FlightStatusCurrentTab") var selectedTab = 1

The @AppStorage property provides a quick way to persist values inside UserDefaults. It updates UserDefaults when the property changes and updates the UI if the value in UserDefaults changes. You also specify a default for SwiftUI to use if the value doesn’t exist in UserDefaults. It’s a handy way to persistently store a simple value such as the integer between uses of your app.

You now need to add a unique identifier to each tab. After the tabItem closure for the Arrivals tab, add the following modifier:

.tag(0)

You use the tag(_:) modifier to give each tab a unique identifier, in this case, an integer. You’ll often find yourself using an enumerable, but that complicates storing the value, and for only a few tabs, an integer also works. For the tab showing all flights, add the following code after the tabItem closure:

.tag(1)

Finally, after the tabItem closure for the Departures tab, add the following code:

.tag(2)

All these tag(_:) modifiers apply to the tabItem and must be unique for each tabItem. Next, you need to tell SwiftUI where to track the identifier for the currently selected tab. Change the definition of the TabView to:

TabView(selection: $selectedTab) {

The selection parameter to TabView tells SwiftUI to use the passed binding to store the current tab’s identifier. The binding works in both directions. Changing selectedTab will make the tab with the corresponding identifier active. If the user taps a tab, then selectedTab updates to contain the identifier of the new tab. Notice that since the default value for selectedTab is one, the first time the user shows the Flight List, SwiftUI shows the All tab with the identifier of one.

Using AppStorage to store the property persists the value to UserDefaults so the app will remember the change for future access. When the view loads, it sets the value of selectedTab to the previous value, restoring the tab your user last selected. When the user changes the tab, selectedTab updates and stores the newly activated tab identifier in UserDefaults.

Run the app and tap the Flight Status button. Choose the All or Departures tab and then return to the initial view. Now, tap the Flight Status button to see the same tab selected as before.

Storing and recalling the last tab viewed
Storing and recalling the last tab viewed

See forum comments
Download course materials from Github
Previous: Introduction Next: Demo