How to create a custom Tab Bar in SwiftUI

Share on facebook
Share on twitter
Share on pinterest

Welcome! In SwiftUI it’s super easy to add a simple tab bar to our app. However, we are limited to the default tab bar design that Apple provides us with. But the in-house tab bar can get boring and sometimes doesn’t offer the functionality we need for our app. In this tutorial, we will learn how to create our own fully customizable tab bar. We will not use a UITabBarController for this but implement the navigation logic only with SwiftUI and from scratch.

Our finished tab bar will look like this:


With the knowledge from this article you should be able to create almost any tab bar with SwiftUI!

Let’s get started! After opening Xcode 11 and creating a new SwiftUI Single View app, we can start preparing our custom tab bar. For the purpose of this tutorial, we can use the default ContentView.swift file.

Preparing our ContentView 👨‍💻

By using our tab bar, we will later be able to jump between two different views, a “Home” view and a “Settings” view. By default, the former should be displayed. This view should simply consist of a text reading “Home”. For this, we can adapt the default “Hello Word” Text accordingly.

struct ContentView: View {
    var body: some View {
       Text("Home")
    }
}

The tab bar should be displayed at the bottom of the screen. Therefore, we have to wrap our text view into a VStack. To make sure that the text is always centered, we add two spacers.

VStack {
            Spacer()
            Text("Home")
            Spacer()
        }

We are almost finished with the preparations. Finally, we just want to have the capability to know how high and wide the screen of the particular device the app runs on is. We need to know this in order to dynamically adjust the size of the tab bar. For this purpose, we wrap our VStack into a GeometryReader.

GeometryReader { geometry in
            VStack {
                Spacer()
                Text("Home")
                Spacer()
            }
        }

The GeometryReader reads out its parent view’s size dimensions. In our case, that’s the overall superview, the one that covers the entire screen.

We’re done with our preparations. Now it’s time to actually design our custom tab bar!

Designing our Tab Bar 🎨🖌

Our tab bar should contain three different icons that are arranged horizontally. Therefore, we insert a HStack into our VStack.

VStack {
    Spacer()
    Text("Home")
    Spacer()
    HStack {
                    
    }
}

The first icon is the “house” icon from the SF Symbols cataloge. The view around the icon should be a third as wide as the whole screen. We also apply some padding.

HStack {
    Image(systemName: "house")
        .resizable()
        .aspectRatio(contentMode: .fit)
        .padding(20)
        .frame(width: geometry.size.width/3, height: 75)
      }

Next, we can tell our HStack that it should always be a tenth of the height of the screen. We also create a white background with a smooth shadow effect.

HStack {
        //...
        }
            .frame(width: geometry.size.width, height: geometry.size.height/10)
            .background(Color.white.shadow(radius: 2))

Our tab bar slowly takes shape. However, it is (depending on the device the app runs on) slightly away from the bottom edge. This is because, by default, a SwiftUI view’s content stays inside the so-called safe area. This prevents our tab bar from reaching the lower edge of our screen. But we can explicitly tell our SwiftUI view to exceed the safe area’s lower boundary by adding the following modifier to the overall VStack:

GeometryReader { geometry in
            VStack {
                //...
                }
                    //...
            }.edgesIgnoringSafeArea(.bottom)
        }

Now our tab bar seamlessly attaches to the bottom of the device

Next, we can add the gear icon to our tab bar:

HStack {
        Image(systemName: "house")
             //...
        Image(systemName: "gear")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .padding(20)
            .frame(width: geometry.size.width/3, height: 75)
        }

Between the home and the gear icon, we want to place a slightly shifted plus button. We create this by adding a ZStack between the two icons. The first view inside this ZStack should simply be a white circle:

ZStack {
      Circle()
          .foregroundColor(Color.white)
          .frame(width: 75, height: 75)
        }

On top of this circle, we stack our actual plus icon:

ZStack {
        Circle()
            .foregroundColor(Color.white)
            .frame(width: 75, height: 75)
        Image(systemName: "plus.circle.fill")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: 75, height: 75)
            .foregroundColor(.blue)
      }

To make sure that the whole plus button is placed slightly above the tab view, we apply the .offset modifier to our ZStack. Our ZStack should be shifted upwards by the half the height of the tab bar. Since our tab view is one tenth as high as the screen, we write:

ZStack {
          //...
        }
          .offset(y: -geometry.size.height/10/2)

Awesome, we are already done with designing our own, custom tab bar. It’s simple as that! Your app preview should now look like this:

Implementing the navigation logic ⛓

Now it’s time to write the code for the navigation logic. How to navigate between views independently in SwiftUI is shown in detail in this tutorial. Therefore, we’ll keep it brief in the following.

To be able to switch back and forth between different views, we need a kind of a “manager” that tells our ContentView which view it should display (either the “Home” or the “Settings” view). For this purpose, we create a new File-New-File and select Swift file. We call this file ViewRouter.

Next, we import the SwiftUI and the Combine framework and create a class named ViewRouter that adapts the ObservableObject protocol.

import Foundation
import SwiftUI
import Combine

class ViewRouter: ObservableObject {
    
   
    
}

Within our ViewRouter, we need a variable which we can use to keep all observers up to date, which view should currently be displayed. Thus, we declare a variable currentPage. By default, we want to display the “home” view.

class ViewRouter: ObservableObject {
    
    @Published var currentView = "home"
    
}

With the @Published property wrapper, we notify all observing views to update themselves whenever the currentView variable changes.

That’s it! We can now use a ViewRouter instance within our ContentView and observe it by initializing it as an @ObservedObject.

struct ContentView: View {
    
    @ObservedObject var viewRouter = ViewRouter()
    
    var body: some View {
        //...
    }
}

As said, make sure you read this tutorial, if you don’t really understand the navigation logic we are using!

Depending on the viewRouter’s currentView variable we want to show either our “Home” text view or another simple Text view reading “Settings”. Therefore, we replace our current Text within the two spacers with the following statement.

if self.viewRouter.currentView == "home" {
                    Text("Home")
                } else if self.viewRouter.currentView == "settings" {
                    Text("Settings")
                }

We can now navigate to the settings view by adding a tap gesture to the gear icon that assigns the viewRouter’s currentView variable to “settings”.

 Image(systemName: "gear")
        //...
        .onTapGesture {
            self.viewRouter.currentView = "settings"
         }

Due to the @Published property wrapper’s functionality, this causes our observing ContentView to rebuild itself with eventually showing us the “Settings” Text view!

Let’s add the corresponding tap gesture to our house icon:

Image(systemName: "house")
        //...
        .onTapGesture {
              self.viewRouter.currentView = "home"
        }

Awesome, we’re now able to jump between the different views by tapping the tab bar’s home and gear icon!


To indicate the user which view is currently being shown, we can conditionally highlight the corresponding tab bar icon. For example, when our home view is currently being shown, we want the home icon to be black, otherwise we want it to be gray. We can achieve this by adding the following modifier to it:

Image(systemName: "house")
        //...
        .foregroundColor(self.viewRouter.currentView == "home" ? .black : .gray)
        .onTapGesture {
            self.viewRouter.currentView = "home"
        }

Let’s do the same for our gear icon!

Image(systemName: "gear")
    //...
    .foregroundColor(self.viewRouter.currentView == "settings" ? .black : .gray)
    .onTapGesture {
        self.viewRouter.currentView = "settings"
    }

Awesome, our tab bar now indicates which view is currently being shown!

Creating the pop up menu ➕

Last but not least, we want to display a cool pop-up menu when the user taps the plus sign icon.

For keeping track of whether the menu should be displayed, we add an according @State property to our ContentView, right below our @ObservedObject variable.

@State var showPopUp = false

When this State is true, the menu should be shown on top of our tab bar (we will shift its position in a moment). Therefore, we wrap our HStack into a ZStack.

ZStack {
    HStack {
      //...
      }
    }

For the menu, you can add the following struct right below your ContentViews_Previews struct:

struct PlusMenu: View {
    var body: some View {
        HStack(spacing: 50) {
            ZStack {
                Circle()
                    .foregroundColor(Color.blue)
                    .frame(width: 70, height: 70)
                Image(systemName: "camera")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .padding(20)
                    .frame(width: 70, height: 70)
                    .foregroundColor(.white)
            }
            ZStack {
                Circle()
                    .foregroundColor(Color.blue)
                    .frame(width: 70, height: 70)
                Image(systemName: "photo")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .padding(20)
                    .frame(width: 70, height: 70)
                    .foregroundColor(.white)
            }
        }
    }
}

If you want to learn, how floating menus can be created in SwiftUI, take a look at this tutorial!

Depending on the showPopUp State, we can now initialise the PlusMenu on top of our tab bar.

ZStack {
    if self.showPopUp {
        PlusMenu()
    }
    HStack {
        //...
    }
}

We shift the PlusMenu above our tab bar by using the .offset modifier again.

PlusMenu()
    .offset(y: -geometry.size.height/6)

We want to toggle the State by tapping on the plus icon. For this purpose, we use a tap gesture again.

ZStack {
    //...
  }
      .offset(y: -geometry.size.height/10/2)
      .onTapGesture {
          self.showPopUp.toggle()
      }

When we now tap on the plus icon of our tab bar, the floating menu gets displayed! We can animate this by wrapping the toggle statement inside a withAnimation statement.

withAnimation {
    self.showPopUp.toggle()
}

To replace the default fade animation with a catchier one, we can add the .transition modifier to our PlusMenu’s view with choosing the .scale option:

var body: some View {
        HStack(spacing: 50) {
            //...
        }
            .transition(.scale)
    }

Tap the plus icon again, to see how it looks!

Hint: Maybe your live preview doesn’t display the animation properly. In this case, run your app in the regular simulator.

Additionally, we want to rotate our plus icon when tapping on it. To do this, add the following modifier to the Image view inside the according ZStack.

Image(systemName: "plus.circle.fill")
     //...
     .rotationEffect(Angle(degrees: self.showPopUp ? 90 : 0))

Depending on whether the plus menu is being shown, this modifier rotates the plus icon by 90 degrees.

Our whole plus menu animation should now look like this:

Conclusion 🎊

Awesome, we’re now finished with creating our own, custom tab bar in SwiftUI! We’ve learned how to design a tab bar’s UI, how to implement a proper navigation logic and how to animate a cool pop-up menu. With this knowledge, you should be able to create your own, custom tab bar!

We’ve uploaded the whole source code of this app to GitHub.

I hope you enjoyed this tutorial! If you want to learn more about SwiftUI, check out our other tutorials! Also make sure you follow us on Instagram and subscribe to our newsletter to not miss any updates, tutorials and tips about SwiftUI and more!

12 replies on “How to create a custom Tab Bar in SwiftUI”

I would like to know how to change the background color to the “More” screen that is created when you have more .tabItem than will fit on the bottom TabBar.

Thank you for this tutorial! and your solution of using the ViewRouter!!! It saved me soooooo much headache!!! I was having issues with the navigation back to the list view after a user would click the nav link to view a “Detailed View”. This solved it quick!!!

Leave a Reply

Your email address will not be published. Required fields are marked *

small_c_popup.png

Covid-19 Forces you into quarantine?

Start Mastering swiftUI Today save your 33% discount

small_c_popup.png

Are you ready for a new era of iOS development?

Start learning swiftUI today - download our free e-book