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")
}
}
}
}
}
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.
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()
}
}
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.
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))
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)
}
}
}
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.
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
}
}
}
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:
- Get an overview of navigation and presenting views in SwiftUI in the blog post
- Better Navigation in SwiftUI with Navigation Stack
- Exploring Navigation in SwiftUI: A Deep Dive into NavigationView
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
Loved the breakdown of customizing the tab bar in SwiftUI! Really helped me understand how to achieve the desired design for my app.
Thanks for this helpful tutorial! I was struggling to customize the tab bar in SwiftUI, but your steps made it easy to achieve the look I wanted. The screenshots and code examples were super helpful too!
I’m so glad you posted this tutorial! I’ve been struggling to customize the tab bar in my SwiftUI app and your steps are clear and easy to follow. The screenshot examples are particularly helpful. Thank you for sharing your knowledge!