SwiftUI Tabview Tutorial: How to Customize the Tab Bar

SwiftUI TabView is a main element in many iOS apps. It makes navigation easy to follow for the user thanks to the tab bar items at the bottom.

In previous blog posts, I’ve dissected the art of SwiftUI presentations and navigation, from presenting views in SwiftUI using sheets, modals, popovers, and alerts to navigating better in SwiftUI with NavigationView. If you’re looking for a comprehensive overview of those topics, I encourage you to read The A-Z of SwiftUI Presentations and Navigation and Better Navigation in SwiftUI with Navigation Stack.

As I guide you through this subject, you’ll comprehend TabView’s functionality. You’ll learn how to implement and customize it. While delving into the core components, you’ll see the versatility of TabView. You’ll also understand its unique interplay with SwiftUI’s declarative syntax.

Basics of SwiftUI’s TabView

To kick off, let’s create a TabView in SwiftUI. It’s an easy process, requiring a TabView block with nested View elements. Each element represents a tab. Here’s a simple initialization:

struct ContentView: View {
    var body: some View {
        TabView {
            Text("Tab 1")
                .tabItem {
                    Label("Tab 1",
                          systemImage: "1.circle")
                }

            Text("Tab 2")
                    .tabItem {
                        Label("Tab 2",
                              systemImage: "2.circle")
                    }
            }
        }
    }
}
swiftui tabview with 2 tab items

This creates a TabView with two tabs, each holding a Text view. I added tab items with an icon and text.

How to Add Tabs to a TabView in SwiftUI

Adding more tabs is as simple as adding more view blocks. Each block within the TabView represents a new tab. The order of the blocks determines the order of the tabs.

struct ContentView: View {
    var body: some View {
        TabView {
            NavigationStack {
                HomeView()
                    .navigationTitle("Home")
            }
            .tabItem {
                Label("Tab 1", systemImage: "1.circle")
            }

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

            Text("Third View")
                .tabItem {
                    Label("Tab 3", systemImage: "3.circle")
                }
            Text("Forth View")
                .tabItem {
                    Label("Tab 4", systemImage: "4.circle")
                }
            Text("Fixed View")
                .tabItem {
                    Label("Tab 5", systemImage: "5.circle")
                }
            Text("Fixed View")
                .tabItem {
                    Label("Tab 6", systemImage: "6.circle")
                }

            SettingsView()
                .tabItem {
                    Label("Settings", systemImage: "gear")
                }
        }

    }
}

In this example, Tab 1 holds a NavigationStack with a custom view HomeView, Tab 2 to Tab 6 hold simple Text Views and the last tab is another custom view SettingsView. On the iPhone, you can show a maximum of 5 tabs because of the limited space. In the following image, you can see a ´more´ tab that holds all tabs after the first 4. On the iPad, there is much more space and you get all tabs in this example. It automatically adjusts for compact mode. 

Swiftui tabview with 7 tabs on the iPhone
SwiftUI tabview with 7 tabs on the iPhone.
Swiftui tabview with 7 tabs on the iPhad
SwiftUI tabview with 7 tabs on the iPad in fullscreen (left) and split screen mode (right).

How can I add icons to the tabs in a SwiftUI TabView?

You can set the icon and text that is displayed for each tab with the tab item modifier:

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

In the above example, I am using Label, but you can also use a Text or Image view. All other view types will not be used and simply be ignored. Here are more examples:

struct ContentView: View {

    var body: some View {
        TabView {
            Text("Tab 1")
                .tabItem {
                    Label("Tab 1",
                          systemImage: "1.circle")
                }

            Text("Tab 2")
                    .tabItem {
                        Image("test_icon")
                        Text("My own icon")
                    }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
swiftui tabview with custom icon

In the above example, I use a custom icon that I added to my project’s asset catalog. If you set the render mode to template, the icon is displayed accordingly to where it is used. Inside a tab item it is scaled to the right size and the correct foreground color is applied. 

setting the render mode of a swiftui image to render mode
Go to your assets catalog to set setting the render mode of a SwiftUI image to template.

Customizing TabView Appearance

SwiftUI’s TabView offers a lot of room for customization. You can alter the default appearance of tabs in a TabView, from basic attributes to more advanced styling. 

TabViewStyle to Customize TabView

Since iOS 14 and macOS 11 you can use the tabViewStyle modifier and use a page style instead. For example, you can create a horizontal scrolling image gallery with a page indicator.  I am using a ForEach inside the TabView to loop through all images.

struct ImageGalleryView: View {
    let images = ["mont-blanc", "sky", "mountain"]

    var body: some View {
        VStack {
            TabView {
                ForEach(images, id: \.self) { imageName in
                    Image(imageName)
                        .resizable()
                        .scaledToFill()
                }
            }
            .tabViewStyle(.page(indexDisplayMode: .always))
            .frame(height: 250)
        }
    }
}

A very similar result can be accomplished with ScrollView. You can see examples in this post How to use SwiftUI ScrollView. Note that ScrollView will allow you to smoothly scroll and stop in-between images. TabView with page styling will `snap` into the page. However for larger data sets with e.g. a lot of images, the performance of ScrollView should be better.

Using TabViewStyle to create a paged view for an onboarding screen

Typically, an iOS app displays an onboarding screen when the app first launches. For this, you would want to guide the user through a few screens with useful information. A page-styled interaction is often used. For SwiftUI you can customize a TabView with a tab view styling of the page. In the following code snippet, I added 3 onboarding pages:

struct OnboardingView: View {
    var body: some View {
        TabView {
            OnboardingPageView(imageName: "figure.mixed.cardio",
                               title: "Welcome",
                               description: "Welcome to MyApp! Get started by exploring our amazing features.")

            OnboardingPageView(imageName: "figure.archery",
                               title: "Discover",
                               description: "Discover new content and stay up-to-date with the latest news and updates.")

            OnboardingPageView(imageName: "figure.yoga",
                               title: "Connect",
                               description: "Connect with friends and share your experiences with the community.")
        }
        .tabViewStyle(.page(indexDisplayMode: .always))
        .indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
    }
}

In order to use a page style use the tabViewStyle modifier and tell it to always show the page indicator:

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

Per default, the dotes at the bottom have a foreground color of white in light mode and black in dark mode. In my example, I am using a white background and you would not see the dots. You can add a background around the dotes with the following snippet:

.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
swiftui onboarding screen with tabview and page styling

Is it Possible to Remove the Tab Bar at the Bottom of TabView in SwiftUI?

Yes, you can remove the tab bar. If I continue the example from above with the image gallery, I can set the indexDisplayMode to never which will hide the page indicator. Therefor no tabs or indicator is shown:

.tabViewStyle(.page(indexDisplayMode: .never))

Note that this solution is tacky and I don’t think Apple intended it like this. Also, the top bar is also removed when you use a NavigationStack inside the tab view.

How to change the Tab bar background color?

Since iOS 16 and macOS 13 you can now directly change the tab bar background color. Here is an example:

struct ContentView: View {
    var body: some View {
        TabView {
            Group {
                NavigationStack {
                    HomeView()
                        .navigationTitle("Home view")
                        .toolbarBackground(.yellow,
                                           for: .navigationBar)
                        .toolbarBackground(.visible,
                                           for: .navigationBar)
                }
                .tabItem {
                    Label("Tab 1", systemImage: "1.circle")
                }

                SettingsView()
                    .tabItem {
                        Label("Tab 2", systemImage: "2.circle")
                    }
            }
            .toolbarBackground(.indigo, for: .tabBar)
            .toolbarBackground(.visible, for: .tabBar)
            .toolbarColorScheme(.dark, for: .tabBar)
        }
    }
}
Swiftui tabview with custom background color for tab bar

I am using the toolbarBackground modifier to set the tab bar background color to indigo:

.toolbarBackground(.indigo, for: .tabBar)

If the content inside the tab view does not fill out the whole area, the tab bar background is not shown. For my example, the settings tab would not show an indigo background. In order to always show the background I use this:

.toolbarBackground(.visible, for: .tabBar)

Indigo is a dark color and the contrast would be bad to the grey and blue colors (defined by the accent color). I am setting to change the foreground color to a dark scheme which then uses white for the tab items:

.toolbarColorScheme(.dark, for: .tabBar)

In the above example I not only used the toolbarBackground modifier to change the tab bar but also the navigation bar. This works with the new updates for navigation with NavigationStack.

Tab bar background color for iOS 15 and earlier

You can reach down to the underlying UIKit component and change the tab bar appearance:

struct ContentView: View {
    var body: some View {
        TabView {
           ...
        }
        .onAppear() {
            UITabBar.appearance().barTintColor = UIColor(.indigo)
            UITabBar.appearance().backgroundColor = UIColor(.indigo)
        }
    }
}

Note that this solution will change the appearance of all tab bars in your application.

Is there a way to animate transitions between tabs in SwiftUI’s TabView?

Currently, until iOS 17, you can not change the transitions between tabs in SwiftUI.

Where to use TabView for MacOS

A tab bar navigation is typically used for iOS apps. On macOS this is not used in the same way. One place where you could use a TabView is inside a Settings window. 

You start by defining a settings window in the main app file:

@main
struct TabViewProjectApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }

        #if os(macOS)
        Settings {
            SettingsView()
        }
        #endif
    }
}

And then use a tab view inside the SettingsView:

struct TabStylingExampleView: View {
    var body: some View {
        TabView {
            Form {
               ...
            }
             .tabItem {
                 Label("Settings", systemImage: "gear")
              }

            Text("Account View")
                .tabItem {
                    Label("Account", systemImage: "person")
                }

            Text("Third View")
                .tabItem {
                    Label("Tab 3", systemImage: "3.circle")
                }
        }
        .padding()
        .frame(minWidth: 300, maxWidth: .infinity,
               minHeight: 300, maxHeight: .infinity)
    }
}

I am using Form instead of List to get a nicely styled layout. 

Swiftui tabview example on Macos to create a settings window
TabView on macOS inside Settings displays the tab items above the main content.

Navigation and Tab Control in TabView

Navigating through the waters of SwiftUI’s TabView often involves more than just creating and styling tabs. In this section, I’ll dive into integrating TabView with NavigationStack, programmatically changing the selected tab, adding navigation functionality to tabs, and handling tab selection events.

Can I use TabView with NavigationView/ NavigationStack in SwiftUI?

Integrating a TabView with a NavigationView or the new NavigationStack in SwiftUI is possible and quite useful. You can create complex navigation structures where each tab has its own navigation stack. Here’s a basic example:

TabView {
    NavigationStack {
        HomeView()
             .navigationTitle("Home")
    }
    .tabItem {
        Label("Tab 1", systemImage: "1.circle")
    }
    NavigationStack {
        SettingsView()
    }
    .tabItem {
        Label("Settings", systemImage: "gear")
    }
}

In this example, each tab has its own NavigationStack, allowing separate navigation within each tab.

How to Change the Selected Tab Programmatically in SwiftUI

Changing the selected tab programmatically is another useful technique in SwiftUI. You can achieve this by binding a state variable to the TabView. For the above example with the Onboarding flow, you might want to add a next button, that programmatically scrolls to the next page. 

struct OnboardingView: View {
    @State private var selection: Int = 0
    var body: some View {
        TabView(selection: $selection) {
             OnboardingPageView(...)
                   .tag(0)
            OnboardingPageView(...)
                   .tag(1)
            OnboardingPageView(...)
                   .tag(2)
        }
        .tabViewStyle(.page(indexDisplayMode: .always))
        .indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
    }
}

A added a tag to each page that identifies the tab selection with the corresponding page. In the beginning, the selection is 0 and the first page is shown.

Each page would then want to change the selection state when the next button is tapped. Additionally, I want to dismiss the Onboarding flow when the last screen is reached:

struct OnboardingPageView: View {
    let imageName: String
    let title: String
    let description: String

    let showDoneButton: Bool

    var nextAction: () -> Void

    var body: some View {
        VStack(spacing: 20) {
            Image(systemName: imageName)
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(height: 250)
                .foregroundColor(.mint)

            Text(title)
                .font(.title)
                .fontWeight(.bold)

            Text(description)
                .font(.body)
                .multilineTextAlignment(.center)
                .padding(.horizontal, 40)
                .foregroundColor(.gray)

            if showDoneButton {
                Button("Lets get started") {
                    nextAction()
                }
                .buttonStyle(.borderedProminent)
                .padding(.top)
            } else {
                Button("Next") {
                   nextAction()
                }
                .buttonStyle(.bordered)
                .padding()
            }
        }
        .padding()
    }
}

The last screen will show a ´Lets get started´ button and all others a ´Next´ button. Here is how you could then use this onboarding view inside a tab view:

struct OnboardingView: View {

    @Binding var showOnboarding: Bool
    @State private var selection: Int = 0

    var body: some View {
        TabView(selection: $selection) {
            OnboardingPageView(imageName: "figure.mixed.cardio",
                               title: "Welcome",
                               description: "Welcome to MyApp! Get started by exploring our amazing features.",
                               showDoneButton: false,
                               nextAction: goNext)
               .tag(0)

            OnboardingPageView(imageName: "figure.archery",
                               title: "Discover",
                               description: "Discover new content and stay up-to-date with the latest news and updates.",
                               showDoneButton: false,
                               nextAction: goNext)
               .tag(1)

            OnboardingPageView(imageName: "figure.yoga",
                               title: "Connect",
                               description: "Connect with friends and share your experiences with the community.",
                               showDoneButton: true,
                               nextAction: {
                showOnboarding = false

            })
              .tag(2)

        }
        .tabViewStyle(.page(indexDisplayMode: .always))
        .indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
    }
    
    func goNext() {
        withAnimation {
            selection += 1
        }
    }
}
swiftui tabview with programmatic navigation

Notice that I added a binding to a showOnboarding property. I am using this to dismiss the onboarding flow from the main content view:

struct ContentView: View {
    @AppStorage("showOnboarding") var showOnboarding: Bool = true

    var body: some View {
        TabView {
            NavigationStack {
                HomeView()
                    .navigationTitle("Home")
            }
            .tabItem {
                Label("Tab 1", systemImage: "1.circle")
            }

            SettingsView()
                .tabItem {
                    Label("Settings", systemImage: "gear")
                }
        }
        #if os(iOS)
        .fullScreenCover(isPresented: $showOnboarding, content: {
            OnboardingView(showOnboarding: $showOnboarding)
        })
        #else
        .sheet(isPresented: $showOnboarding) {
            OnboardingView(showOnboarding: $showOnboarding)
        }
        #endif
    }
}

How to Handle Events or Actions When a Tab is Selected in SwiftUI

Handling events or actions when a tab is selected can be achieved using the onChange modifier on the TabView. This modifier can be used to perform an action when the selected tab changes.

@State private var selectedTab = 0

TabView(selection: $selectedTab) {
    // Tab items...
}
.onChange(of: selectedTab) { newValue in
    // Handle the selection change.
}

This example will execute a block of code whenever the selected tab changes.

Conclusion

In this blog post, you’ve navigated the depths of SwiftUI’s TabView. You’ve uncovered how to construct, customize its appearance, and integrate it with iOS 16’s new Navigation Stack. Remember, effective tab management and stylish visuals enhance user interaction, contributing to an engaging app experience. I encourage you to experiment with these techniques, apply them in your projects, and see the difference they make.

Further Reading and Resources

For more detailed insights and examples on the topics covered in this blog post, consider checking out the following resources:

1 thought on “SwiftUI Tabview Tutorial: How to Customize the Tab Bar”

  1. Really like your Snippets, Shopping app, and the TabView article above.

    Making a Folder Network for notes in differing subjects > Snippets

    Shopping App, eCommerce app and also Company Products apps

    TabView for adding to the Shopping App…. as a variation of it…

    Thankyou, James Avakian

    Reply

Leave a Comment

Subscribe to My Newsletter

Want the latest iOS development trends and insights delivered to your inbox? Subscribe to our newsletter now!

Newsletter Form

New Course Announcement!!!

50% OFF Launch Sale

MacOS Development with SwiftUI