Search code examples
iosswiftui

How can I align the icons and text of my custom tab bar?


For my app I'm making a custom tab bar so I can have a bar above the active tab (like in the LinkedIn app for example). I couldn't figure out how to do it by customizing the default tab bar. I'm trying to make my tab bar look like the default tab bar when possible.

I'm having trouble aligning the icons and text of the buttons in my tab bar. Not all SF Symbols icons have the same height. This causes the text below the icons to not be aligned and the bar above the selected tab have a variable height depending on the active tab. What is the best way to align the icons and text? I want the text of each tab on the same horizontal line, the same goes for the icons.

In this image the text Card is positioned higher than the text of the other tabs. enter image description here

Compared with the previous image the bar indicating the selected tab is positioned lower when the Card tab is active.
enter image description here

This is the code of my tab bar.

struct TabBar: View {
    @Bindable private var navigator = NavigationManager.nav
    
    @Namespace var namespace
    
    var body: some View {
        HStack {
            tabButton(title: "Home", systemName: "house.fill", tabTag: 1)
            
            tabButton(title: "Search", systemName: "magnifyingglass", tabTag: 2)
            
            tabButton(title: "Card", systemName: "creditcard.fill", tabTag: 3)
            
            tabButton(title: "Account", systemName: "person.crop.circle.fill", tabTag: 4)
        }
        .overlay(Divider(), alignment: .top)
        .background(.white)
        .padding(.bottom, 30)
        .border(Color.red, width: 1)
    }
    
    func tabButton(title: String, systemName: String, tabTag: Int) -> some View {
        Button {
            navigator.selectedTab = tabTag
        } label: {
            VStack {
                if navigator.selectedTab == tabTag {
                    Color.black.frame(height: 3)
                        .matchedGeometryEffect(id: "underline", in: namespace, properties: .frame)
                } else {
                    Color.clear.frame(height: 2)
                }
                
                Image(systemName: systemName)
                    .font(.title)
                    .foregroundStyle(navigator.selectedTab == tabTag ? .primary : .secondary)
                    .foregroundStyle(Color.anthracite)
                
                Text(title)
                    .font(.caption)
                    .foregroundStyle(navigator.selectedTab == tabTag ? .primary : .secondary)
                    .foregroundStyle(Color.anthracite)
            }
            .animation(.spring(), value: navigator.selectedTab)
        }
        .buttonStyle(.plain)
    }
}

Which is used in the ContentView.

struct ContentView: View {
    @Bindable private var navigator = NavigationManager.nav

    init() {
        // Disable the standard tab bar.
        // Hiding the tab bar with `.toolbar(.hidden, for: .tabBar)` causes it to reappear when switching between tabs multiple times.
        UITabBar.appearance().isHidden = true
    }

    var body: some View {
        VStack {
            TabView(selection: navigator.tabHandler) {
                    Text("Home View")
                        .tag(1)

                    Text("Search View")
                        .tag(2)

                    Text("Card View")
                        .tag(3)

                    Text("Account View")
                        .tag(4)
            }
            .border(Color.blue, width: 1)

            TabBar()
        }
        .edgesIgnoringSafeArea(.bottom)
    }
}

I tried putting a VStack around the icon and text and put a Spacer() between them. When I set the frame of the outer most VStack in the tab bar button label everything gets aligned better but that doesn't feel like a robust solution.

Bonus Question

In the images the content view has a blue border and the tab bar has a red border. For some reason there is a slight gap between both. Why is that and how can I get rid of it?


Solution

  • To align the text labels at the bottom, but keep the selector bar at the same height at the top, I would suggest 2 changes:

    1. Set maxHeight: .infinity on the images:
    Image(systemName: systemName)
        .font(.title)
        .foregroundStyle(selectedTab == tabTag ? .primary : .secondary)
        .foregroundStyle(Color.anthracite)
        .frame(maxHeight: .infinity) // 👈 here
    
    1. Set .fixedSize(horizontal: false, vertical: true) on the HStack in TabBar.body:
    HStack {
        // tabButtons
    }
    .fixedSize(horizontal: false, vertical: true) // 👈 here
    // + other modifiers, as before
    

    What this does is to fix the height of the HStack to the maximum height of any of the single buttons. By adding maxHeight on the images in the buttons, they will fill the space between the bar and the text label.

    Then, to remove the gap between the content and the custom tab bar, add (spacing: 0) to the VStack in ContentView.body.

    While you're at it, you could also replace .edgesIgnoringSafeArea(.bottom) (which is deprecated) with .ignoresSafeArea(edges: .bottom):

    VStack(spacing: 0) { // 👈 here
        // ...
    }
    .ignoresSafeArea(edges: .bottom) // 👈 and here
    

    Screenshot

    Btw, when a tab is selected, you show a bar of height 3 above the image. When a tab is not selected, you are using a clear space of height 2 as placeholder. Are you intentionally using a placeholder with less height? This causes a very small movement of the label when the selection changes.