What You Will Build

A custom tab bar with four tabs, each with a unique tint color. Icons bounce with .symbolEffect(.bounce) on selection and switch to filled variants. The bar uses .ultraThinMaterial for a frosted glass look.

Step 1: Tab Enum

private enum CustomTab: CaseIterable {
    case home, search, favorites, profile

    var title: String {
        switch self {
        case .home: "Home"
        case .search: "Search"
        case .favorites: "Favorites"
        case .profile: "Profile"
        }
    }

    var icon: String {
        switch self {
        case .home: "house"
        case .search: "magnifyingglass"
        case .favorites: "heart"
        case .profile: "person"
        }
    }

    var selectedIcon: String {
        switch self {
        case .home: "house.fill"
        case .search: "magnifyingglass"
        case .favorites: "heart.fill"
        case .profile: "person.fill"
        }
    }

    var tintColor: Color {
        switch self {
        case .home: .blue
        case .search: .orange
        case .favorites: .red
        case .profile: .purple
        }
    }
}

Step 2: Build the Custom Bar

ZStack(alignment: .bottom) {
    TabView(selection: $selectedTab) {
        ForEach(CustomTab.allCases, id: \.self) { tab in
            Text(tab.title).tag(tab)
        }
    }

    HStack {
        ForEach(CustomTab.allCases, id: \.self) { tab in
            Spacer()
            VStack(spacing: 4) {
                Image(systemName: selectedTab == tab
                    ? tab.selectedIcon : tab.icon)
                    .font(.title3)
                    .symbolEffect(.bounce, value: selectedTab == tab)
                Text(tab.title).font(.caption2)
            }
            .foregroundStyle(selectedTab == tab ? tab.tintColor : .gray)
            .onTapGesture {
                withAnimation(.spring) { selectedTab = tab }
            }
            Spacer()
        }
    }
    .padding(.vertical, 10)
    .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 20))
    .padding(.horizontal)
}

Tips

  • .symbolEffect requires iOS 17. Use scaleEffect with spring for iOS 16.
  • Hide default bar: .toolbar(.hidden, for: .tabBar) on iOS 16+.
  • Safe area: Add bottom padding for the home indicator.

iOS Version: iOS 15+ base, iOS 17+ for symbolEffect

Get New Tutorials by Email

No spam. Just clear, practical breakdowns you can apply right away.

Enjoy this tutorial?

Get new practical tech tutorials in your inbox.