Home How to Leverage SwiftUI's TabView to Create Custom TabBars
Post
Cancel

How to Leverage SwiftUI's TabView to Create Custom TabBars

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:

Custom Bottom Tab Demo























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.

This post is licensed under CC BY 4.0 by the author.