Creating Custom Tab Bars in SwiftUI: A Comprehensive Guide
Written on
Chapter 1: Introduction to Tab Bars in SwiftUI
In the realm of mobile applications, one of the most common features is the tab bar located at the bottom of the screen. This component allows users to effortlessly navigate between various key sections of an app. SwiftUI simplifies the process of creating a tab bar UI with the TabView component.
This tutorial will guide you through the steps of creating and customizing a tab bar, and it's part of my ongoing SwiftUI Tutorial series. Before diving in, please ensure that you have a new project open for practice. If you need a step-by-step guide on starting a new project, follow this link.
Section 1.1: Creating a Basic TabView
To initiate a basic TabView with a single tab, make the following adjustments to the ContentView:
struct ContentView: View {
var body: some View {
TabView {
Text("Home view")
.tabItem {
Image(systemName: "house.fill")}
} // TabView
} // body
} // ContentView
This code snippet will display a simple tab in the canvas, as shown in Figure 2. Here, all views are encapsulated within the TabView. The .tabItem modifier designates what type of button will be represented in the tab bar. In this case, we've utilized a system image for our tab button.
However, a single tab item renders the tab bar ineffective. Let's enhance it by adding multiple tab items, which we can achieve by modifying the ContentView as follows:
struct ContentView: View {
var body: some View {
TabView {
Text("Home view")
.tabItem {
Image(systemName: "house.fill")}
Text("Search view")
.tabItem {
Image(systemName: "magnifyingglass")}
Text("Photo view")
.tabItem {
Image(systemName: "photo.fill")}
Text("Message view")
.tabItem {
Image(systemName: "envelope.fill")}
Text("Profile view")
.tabItem {
Image(systemName: "person.crop.circle.fill")}
} // TabView
} // body
} // ContentView
After executing the application, you can switch between different tabs to see how the views adapt accordingly to their respective tab items. To improve user experience, we can add labels to each tab item. Adjust the ContentView code as follows:
struct ContentView: View {
var body: some View {
TabView {
Text("Home view")
.tabItem {
Image(systemName: "house.fill")
Text("Home")
}
Text("Search view")
.tabItem {
Image(systemName: "magnifyingglass")
Text("Search")
}
Text("Photo view")
.tabItem {
Image(systemName: "photo.fill")
Text("Photos")
}
Text("Message view")
.tabItem {
Image(systemName: "envelope.fill")
Text("Messages")
}
Text("Profile view")
.tabItem {
Image(systemName: "person.crop.circle.fill")
Text("Profile")
}
} // TabView
} // body
} // ContentView
This adjustment will present an enhanced layout with labels, as illustrated in Figure 4.
Section 1.2: Structuring Views for Each Tab
In a practical application, each tab typically comprises multiple views. To follow best practices, it's advisable to create a separate struct for each tab. Here's how to code a basic HomeView:
struct HomeView: View {
var body: some View {
List(1...50, id: .self) {
Text("Content ($0)")}
} // body
} // HomeView
This example provides a simple ForEach List view mimicking a typical home page for a content-driven app.
Next, let's create a separate view for the "Photos" tab:
struct PhotosView: View {
private var imageList = [
"hare.fill",
"tortoise.fill",
"pawprint.fill",
"ant.fill",
"ladybug.fill"
]
var body: some View {
ScrollView(showsIndicators: false) {
ForEach(imageList, id: .self) { index in
Image(systemName: "(index)")
.font(.system(size: 150))
.padding()
}
}
} // body
} // PhotosView
Now that we have our separate structs, let's modify the ContentView to use these new views for the Home and Photos tabs:
struct ContentView: View {
var body: some View {
TabView {
HomeView()
.tabItem {
Image(systemName: "house.fill")
Text("Home")
}
Text("Search view")
.tabItem {
Image(systemName: "magnifyingglass")
Text("Search")
}
PhotosView()
.tabItem {
Image(systemName: "photo.fill")
Text("Photos")
}
Text("Message view")
.tabItem {
Image(systemName: "envelope.fill")
Text("Messages")
}
Text("Profile view")
.tabItem {
Image(systemName: "person.crop.circle.fill")
Text("Profile")
}
} // TabView
} // body
} // ContentView
After running the application again, you can navigate between tabs, especially testing the Home and Photos views.
Section 1.3: Customizing the Tab Bar Appearance
By default, the active tab item appears blue. If you prefer a different color, you can customize it as follows:
TabView {
// Code
}
.accentColor(.black)
Currently, SwiftUI does not provide a direct modifier for changing the tab bar color. To customize it, you can access UIKit's UITabBar.appearance within the init() function of your ContentView:
init() {
UITabBar.appearance().barTintColor = UIColor.systemBlue
}
To change the color of unselected tabs to white:
UITabBar.appearance().unselectedItemTintColor = UIColor.white
To ensure the tab bar isn't transparent, set it to opaque:
UITabBar.appearance().isOpaque = false
Combining all customization code will give you a tailored UI, as demonstrated in Figure 6.
Section 1.4: Programmatically Switching Tabs
While the tab view automatically switches based on the selected tab item, you can also implement a button outside the tab bar to achieve similar functionality. For instance, create a button that directs the user from the profile view to the message view.
First, let's code the ProfileView:
struct ProfileView: View {
var selection: () -> Void
var body: some View {
VStack {
Image(systemName: "person.crop.circle")
.font(.system(size: 100))
.padding()
Button(action: selection) {
Image(systemName: "envelope.fill")
.font(.system(size: 50))}
.padding()
} // VStack
} // body
} // ProfileView
Next, in ContentView, introduce a state variable to track the selected tab:
@State private var selection = 0
Then, modify the TabView to bind the selection variable:
TabView(selection: $selection) {
HomeView()
.tabItem {
Image(systemName: "house.fill")
Text("Home")
}
.tag(0)
Text("Search view")
.tabItem {
Image(systemName: "magnifyingglass")
Text("Search")
}
.tag(1)
PhotosView()
.tabItem {
Image(systemName: "photo.fill")
Text("Photos")
}
.tag(2)
Text("Message view")
.tabItem {
Image(systemName: "envelope.fill")
Text("Messages")
}
.tag(3)
ProfileView(selection: {
selection = (selection + 4) % 5})
.tabItem {
Image(systemName: "person.crop.circle.fill")
Text("Profile")
}
.tag(4)
} // TabView
The action in the ProfileView will trigger the tab switch. The expression (selection + 4) % 5 allows you to cycle through the tab items accurately.
Run the application and press the button in the Profile view; it should seamlessly switch to the Message view.
For the complete source code of this project, you can find it on GitHub. Just click on this link.
May the code be with you,
- Arc