On this page

On this page

Getting Started

A tab bar lets your users navigate between top-level sections of your app. Each tab in the tab bar represents a different, distinct section or feature of your app. Use a tab bar to provide navigation, and not provide actions within your app.

Creating a Tab

To create a tab bar with tabs, first create a TabView. Then, create a Tab within the TabView.

struct ContentView: View {
    var body: some View {
        TabView {
            Tab("Nearby", systemImage: "safari") {
                NearbyView()
            }
    
            Tab("Cuisines", systemImage: "fork.knife") {
                CuisinesView()
            }
    
            Tab("Favourites", systemImage: "heart") {
                FavouritesView()
            }
        }
    }
}

Creating a Search Tab

A TabRole is a value that defines the purpose of the tab. Currently, there is only one TabRole—the search role. Tab views will prefer to have the first tab with the search role implement search. If no tabs are specified as the search tab, the tab view will apply search to all tabs, resetting search state as the selected tab changes.

To create a search tab, simply populate the role argument with .search. The .search TabRole by default provides a magnifying glass SF Symbol in the tab bar.

Tab(role: .search) {
    SearchView()
}

Implementing this into our TabView will give us the following result:

Programmatically Switch Tabs

Within the initialiser for a tab view, there's a selection: argument that takes in a Binding SelectionValue. The selection: argument updated the Binding SelectionValue when you navigate between tabs, updating the value to the currently selected tab. This also means that we can modify the Binding SelectionValue to programmatically switch between tabs, and set the default tab that opens.

First, create an enum called TabSelection, with a case for each of your tabs as shown in the example below.

enum TabSelection {
    case nearby, cuisines, favourites, search
}

Next, create a State variable of type TabSelection, and assign it to the tab you want to present when the app first launches. In this case, it'll be the cuisine tab.

@State private var selectedTab: TabSelection = .cuisines

Afterwards, bind selectedTab State variable's value to the selection: argument.

TabView(selection: $selectedTab) {
    ...
}

Then, assign the value: argument with a case from TabSelection for each of your tabs. Take note that the value: argument precedes the role: argument for tabs that are initialised using TabRole.

Tab("Nearby", systemImage: "safari", value: .nearby) {
    NearbyView()
}
Tab(value: .search, role: .search) {
    SearchView()
}

Putting everything together, you'll get this:

TabView(selection: $selectedTab) {
    Tab("Nearby", systemImage: "safari", value: .nearby) {
        NearbyView()
    }
    
    Tab("Cuisines", systemImage: "fork.knife", value: .cuisines) {
        CuisineView()
    }
    
    Tab("Favourites", systemImage: "heart", value: .favourites) {
        FavouritesView()
    }

    Tab(value: .search, role: .search) {
        SearchView()
    }
}

Launching the app, it'll look like this:

If you wish to programmatically switch and navigate between tabs, you can assign the selection: argument's value to another case in your enum.

selectedTab = .favourites

Going Further

Exploring SidebarAdaptable Style

Starting in iOS 18 and iPadOS 18, tab views have a new style called SidebarAdaptable. The SidebarAdaptable style behaves differently depending on the platform. On iOS, it always displays as a bottom bar. While on iPadOS, it displays as a displays a top tab bar that can adapt into a sidebar.

To make your tab bar on iPadOS adapt into a sidebar, you can use the .tabViewStyle modifier and populate it with the .sidebarAdaptable style, as shown in the example below.

TabView(selection: $selectedTab) {
    ...
}
.tabViewStyle(.sidebarAdaptable)

Styling the Sidebar

Apple has provided three modifiers to configure and extra content to the sidebar.

The first modifier is .tabViewSidebarHeader. This places a view on top of the list of tabs in your sidebar. In the example below, it shows the text "Main Tabs" above the list of tabs.

.tabViewSidebarHeader {
    HStack {
        Text("Main Tabs")
            .font(.title2)
            .fontWeight(.bold)
        Spacer()
    }
}

The second modifier is .tabViewSidebarFooter. This places a view below the list of tabs in your sidebar. In the example below, it shows a copyright text below the list of tabs.

.tabViewSidebarFooter {
    Text("© Copyright 2025 Tristan Chay. All rights reserved.")
        .font(.caption)
        .foregroundStyle(.secondary)
        .multilineTextAlignment(.center)
}

The third modifier is .tabViewSidebarBottomBar. This places inside the bottom bar region of your sidebar. In the example below, it shows a user account in the bottom bar of the sidebar.

.tabViewSidebarBottomBar {
    Label("Tristan", systemImage: "person.circle")
}

Creating Tab Sections

TabSection helps to organise tabs into separate sections. Each section has custom tab content that you provide on a per-instance basis. You can also provide a header for each of the sections. Tab sections default to the last presented tab in the section. If the app has just launched, it defaults to the first tab in the section.

Do note that TabSection does not work if you have a value passed into the selection: argument for your TabView.

The code below separates out all the cuisines into tabs in a tab section.

TabSection("Cuisines") {
    Tab("Chinese", systemImage: "fork.knife") {
        Text("Chinese Cuisine")
    }

    Tab("Japanese", systemImage: "fork.knife") {
        Text("Japanese Cuisine")
    }

    Tab("Korean", systemImage: "fork.knife") {
        Text("Korean Cuisine")
    }
}

Styling Tab Sections

You can set the default visibility for a tab section using the .defaultVisibility modifier. As shown in the example below, this can be useful if you want to only show the tab section within the sidebar, and not in the tab bar.

TabSection("Cuisines") {
    ...
}
.defaultVisibility(.hidden, for: .tabBar)

You're also able to add views to the end of a section using the .sectionAction modifier. Typically, buttons are added to the end of a section to perform an action relevant to that section.

TabSection("Cuisines") {
    ...
}
.defaultVisibility(.hidden, for: .tabBar)
.sectionAction {
    Button("Invent New Cuisine") {
        inventCuisine()
    }
}

Enabling Tab Customisation

Tab bars can also be made user customisable, allowing users to add, remove, and reorganise the tabs to their liking. To enable tab customisation, first create an AppStorage variable of type TabViewCustomization. An AppStorage property wrapper is being used instead of a State property wrapper to persist the customisations made by the user, even when the app has been quit.

@AppStorage("customisation") private var customisation: TabViewCustomization

Then, use the .tabViewCustomization modifier and bind your variable to it.

.tabViewCustomization($customisation)

Lastly, add a customisation ID using the .customizationID modifier to every single tab, including the ones within tab sections. Each customisation ID should be unique.

Tab("Nearby", systemImage: "safari") {
    NearbyView()
}
.customizationID("sg.swiftin.nearby")

After that, you will be able to customise your tab bar and sidebar simply by toggling or dragging the tabs around.

Exploring Page Style

The Page tab view style displays a paged scrolling TabView, allowing you to swipe through multiple views/pages, with page indicators at the bottom to let you know where you are in the pages.

To present a paged scrolling TabView, you can use the .tabViewStyle modifier and populate it with the .page style, as shown in the example below.

TabView {
    FirstItem()
    SecondItem()
    ThirdItem()
    ...
}
.tabViewStyle(.page)

Styling Page Index

There are two ways you're able to style your page index for page tab style views. First, is to set the index display mode using the indexDisplayMode: argument in the .page tab view style.

Setting the argument to .always will always show the page index, while setting it to .never will never show the page index. By default, it's set to .automatic.

.tabViewStyle(.page(indexDisplayMode: .always))

Next, you can also set the background display mode for your page index by using the .indexViewStyle modifier, and setting the backgroundDisplayMode: argument for the .page tab view style.

Setting the argument to .always will always show the page index background, setting it to .interactive will only show the page index background when you're swiping between pages, and setting it to .never will never show the page index background. By default, it's set to .automatic.

.tabViewStyle(.page(indexDisplayMode: .always))
.indexViewStyle(.page(backgroundDisplayMode: .always))

In These Collections

This article can also be found in these collections.

© 2025 Tinkertanker Pte Ltd / Swift Accelerator. All rights reserved.

© 2025 Tinkertanker Pte Ltd / Swift Accelerator. All rights reserved.

© 2025 Tinkertanker Pte Ltd / Swift Accelerator. All rights reserved.