Introduction
When it comes to creating a seamless and user-friendly tab-based interface in our iOS app, SwiftUI’s TabView
component is at the forefront of the list of powerful tools available. The TabView, allows us to implement tab-based navigation. By default, The TabView renders the bottom TabBar for us with the help of it’s tabItem
modifier, but with some customization as you will see in this tutorial, we can create a custom bottom TabBar
, which is a popular design pattern in many modern apps. Let’s dive into it.
Getting Started
To create our custom bottom TabBar, we start by defining an enum representation of our tab items like this:
1
2
3
4
5
6
7
enum Tab: String, Hashable, CaseIterable {
case home = "house"
case explore = "globe.europe.africa"
case bookmark = "star"
case notification = "bell"
case profile = "person"
}
Then we define a TabView
inside of a ZStack with multiple tabs, each tab represented by a separate view, and each tab using their corresponding enum case as a tag. Like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
struct MainView: View {
init() {
UITabBar.appearance().isHidden = true
}
@State private var selectedTab: Tab = Tab.bookmark
var body: some View {
ZStack(alignment: .bottom) {
TabView(selection: $selectedTab) {
Text("HOME")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red.opacity(0.5))
.tag(Tab.home)
Text("EXPLORE")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.yellow.opacity(0.5))
.tag(Tab.explore)
Text("BOOKMARK")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.green.opacity(0.5))
.tag(Tab.bookmark)
Text("NOTIFICATION")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.green.opacity(0.5))
.tag(Tab.notification)
Text("PROFILE")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.green.opacity(0.5))
.tag(Tab.profile)
}
CustomBottomTabBarView(currentTab: $selectedTab)
.padding(.bottom)
}
}
}
Pay attention to the selection state, this is where the magic really happens. By default, TabView handles the selection of tabs internally, and the selected tab is highlighted with a different color when we are using the tabItem modifier on a tabView’s child. However, to create a custom bottom TabBar, we need to customize the appearance of the tabs and handle the selection manually using selection binding.
Selection binding is a crucial concept in SwiftUI’s TabView. It’s a two-way binding that allows us to keep track of the currently selected tab and update it as needed. We can define a @State or @Binding property to serve as the selection binding for our TabView. And that’s exactly what we are doing with the currentTab
state variable.
The CustomBottomTabBarView()
component can really be anything you want it to be, so long as it takes a binding that can update the selected tab’s state it will work. Here is what mine looks like:
CustomTabBar Component
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
private let buttonDimen: CGFloat = 55
struct CustomBottomTabBarView: View {
@Binding var currentTab: Tab
var body: some View {
HStack {
TabBarButton(imageName: Tab.home.rawValue)
.frame(width: buttonDimen, height: buttonDimen)
.onTapGesture {
currentTab = .home
}
Spacer()
TabBarButton(imageName: Tab.explore.rawValue)
.frame(width: buttonDimen, height: buttonDimen)
.onTapGesture {
currentTab = .explore
}
Spacer()
TabBarButton(imageName: Tab.bookmark.rawValue)
.frame(width: buttonDimen, height: buttonDimen)
.onTapGesture {
currentTab = .bookmark
}
Spacer()
TabBarButton(imageName: Tab.notification.rawValue)
.frame(width: buttonDimen, height: buttonDimen)
.onTapGesture {
currentTab = .notification
}
Spacer()
TabBarButton(imageName: Tab.profile.rawValue)
.frame(width: buttonDimen, height: buttonDimen)
.onTapGesture {
currentTab = .profile
}
}
.frame(width: (buttonDimen * CGFloat(Tab.allCases.count)) + 60)
.tint(Color.black)
.padding(.vertical, 2.5)
.background(Color.white)
.clipShape(Capsule(style: .continuous))
.overlay {
SelectedTabCircleView(currentTab: $currentTab)
}
.shadow(color: Color.gray.opacity(0.5), radius: 5, x: 0, y: 10)
.animation(.interactiveSpring(response: 0.5, dampingFraction: 0.65, blendDuration: 0.65), value: currentTab)
}
}
private struct TabBarButton: View {
let imageName: String
var body: some View {
Image(systemName: imageName)
.renderingMode(.template)
.tint(.black)
.fontWeight(.bold)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
CustomBottomTabBarView(currentTab: .constant(.explore))
}
}
struct SelectedTabCircleView: View {
@Binding var currentTab: Tab
private var horizontalOffset: CGFloat {
switch currentTab {
case .home:
return -138
case .explore:
return -72
case .bookmark:
return 0
case .notification:
return 72
case .profile:
return 138
}
}
var body: some View {
ZStack {
Circle()
.fill(Color.blue)
.frame(width: buttonDimen , height: buttonDimen)
TabBarButton(imageName: "\(currentTab.rawValue).fill")
.foregroundColor(.white)
}
.offset(x: horizontalOffset)
}
}
Check out the source code here for full code sample.
Conclusion
As you can see creating a custom bottom tabbar is just a question of embedding your custom tabBar component on top of a tabView inside a ZStack Container. And then use the TabView’s selection binding to manually toggle between selected tab and unselected tabs. Voila! you got yourself a powerful customizable tabBar.