Search code examples
swiftswiftuitabbar

SwiftUi Change Tint of Unselected Item in Tab Bar


I was trying to change the tint color of an unselected item in SwiftUI.

Currently, I got it so it has a background and changes the tint color (making the tab bar visible in .tabItem gets rid of the .red tint and makes it gray)

import SwiftUI
import UIKit

struct Test: View{
    
    init(){
        UITabBar.appearance().backgroundColor = UIColor.systemBackground
        UITabBar.appearance().unselectedItemTintColor = .red
    }
    
    var body: some View{
        
        TabView{
            Text("1")
                .tabItem{
                    Image(systemName: "heart.fill")
                    Text("Heart")
                }
              
                
            Text("2")
                .tabItem{
                    Image(systemName: "gearshape.fill")
                    Text("Settings")
                }
        }
    }
}

#Preview{
    Test()
}

Image 1

Now I'm trying to add the divider line that separates the tab bar with the rest of the stuff on the page. Doing .toolbarBackground(.visible, for: .tabBar) brings back the divider, but then the unselected item tint doesn't work anymore.

TabView{
            Text("1")
                .tabItem{
                    Image(systemName: "heart.fill")
                    Text("Heart")
                }
                .toolbarBackground(.visible, for: .tabBar)

enter image description here

I also tried doing this method but it seems like it doesn't work anymore.


Solution

  • I would just abandon the native tab bar and use a custom one. Then you can style it any way you like.

    • It works well to use an enum to define the tab types.
    • The custom tab bar then has one button per enum type.
    • A custom LabelStyle can be used to style the labels of the buttons.

    Something like:

    enum TabType: CaseIterable {
        case heart
        case settings
    
        var titleKey: String {
            "\(self)".capitalized
        }
    
        var symbolName: String {
            switch self {
            case .heart: "heart"
            case .settings: "gearshape"
            }
        }
    }
    
    struct TabLabelStyle: LabelStyle {
        let isSelected: Bool
    
        func makeBody(configuration: Configuration) -> some View {
            VStack(spacing: 4) {
                configuration.icon
                    .dynamicTypeSize(.xxxLarge)
                configuration.title
                    .font(.caption)
            }
            .symbolVariant(isSelected ? .fill : .none)
            .foregroundStyle(isSelected ? Color.accentColor : .red)
            .frame(maxWidth: .infinity)
        }
    }
    
    struct CustomTabBar: View {
        @Binding var selection: TabType
    
        var body: some View {
            HStack {
                ForEach(TabType.allCases, id: \.self) { type in
                    Button {
                        withAnimation(.spring) {
                            selection = type
                        }
                    } label: {
                        Label(type.titleKey, systemImage: type.symbolName)
                            .labelStyle(TabLabelStyle(isSelected: selection == type))
                    }
                }
            }
            .padding()
            .background(.bar)
            .overlay(alignment: .top) { Divider() }
        }
    }
    

    The custom tab bar can be attached to the main content using .safeAreaInset(edge: .bottom).

    • For the main content itself, you can still use a TabView if you like. This might have some advantages in terms of preserving state (such as, preserving scroll position). When doing it this way, the native tab bar must be hidden for every child view:
    struct ContentView: View {
        @State private var selection = TabType.heart
    
        var body: some View {
            TabView(selection: $selection) {
                Text("1")
                    .tag(TabType.heart)
                    .toolbarVisibility(.hidden, for: .tabBar)
                    // pre iOS 18: .toolbar(.hidden, for: .tabBar)
                Text("2")
                    .tag(TabType.settings)
                    .toolbarVisibility(.hidden, for: .tabBar)
            }
            .safeAreaInset(edge: .bottom) {
                CustomTabBar(selection: $selection)
            }
        }
    }
    
    • Alternatively, you can perform your own switching, using a container such as a ZStack. This allows you to determine the transition when switching:
    ZStack {
        switch selection {
        case .heart:
            Text("1")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .transition(.move(edge: .leading))
        case .settings:
            Text("2")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .transition(.move(edge: .trailing))
        }
    }
    .safeAreaInset(edge: .bottom) {
        CustomTabBar(selection: $selection)
    }
    

    Animation

    If you want to use a sliding transition for three or more tabs, you might find the answer to SwiftUI bi-directional move transition moving the wrong way in certain cases useful.