Playback speed
×
Share post
Share post at current time
0:00
/
0:00

💡 SwiftUI FYI #9 - Custom Tabbar

Beautiful Custom Tabbar using SwiftUI

In this post we create a beautiful custom Tabbar using SwiftUI. The implementation features the following:

  • Adaptable and dynamic spacing of the tabour items.

  • Use of iOS’s thin material background.

  • Using symbolVariant to differentiate the selected tab

  • A tabbar indicator that correctly positions itself with the selected tabor item, and adapts to different spacings. For example, this works well even in landscape mode.

  • Use of @ViewBuilder to create the correct spacings for the tabbar indicator.

🚨 Full code below and also on Github

🤝 Find more about me on saidmarouf.com. I’m also on XThreads, and LinkedIn

Thanks for reading Your Weekly SwiftUI FYI 💡! Subscribe for free to receive new posts and support my work.


import SwiftUI

struct CustomTabbarDemoView: View {
  
  var body: some View {
    
    ZStack {
      Image("background")
        .resizable()
        .scaledToFill()
        .edgesIgnoringSafeArea(.all)
      
      VStack {
        Spacer()
        
        CustomTabBar()
          .padding(.bottom)
        
        Spacer()
      }
    }
  }
}


struct CustomTabBar: View {
  
  enum TabItemKind: Int, Identifiable {
    var id: Int {
      self.rawValue
    }
    
    case home = 0
    case trends
    case post
    case search
    case profile
  }
  
  struct TabItem {
    let imageName: String
    let type: TabItemKind
  }
  
  private let items = [
    TabItem(imageName: "house", type: .home),
    TabItem(imageName: "chart.bar", type: .trends),
    TabItem(imageName: "plus.circle", type: .post),
    TabItem(imageName: "magnifyingglass", type: .search),
    TabItem(imageName: "gearshape", type: .profile)
  ]
  
  @State var selectedTab: TabItemKind = .home
  @State private var scale = 1.0
  
  private let tabItemWidth = 60.0
  private let indicatorColor = 
         Color(red: 224/255.0, green: 103/255.0, blue: 111/255.0)
  
  var body: some View {
    
    ZStack {
      HStack {
        Spacer()
        
        ForEach(items, id: \.type) { item in
          Image(systemName: item.imageName)
            .frame(width: tabItemWidth, height: tabItemWidth)
            // to make the full frame tappable, not just the image
            .contentShape(Rectangle())
            .scaleEffect(selectedTab == item.type ? scale : 1.0)
            .symbolVariant(selectedTab == item.type ? .fill : .none)
            .foregroundStyle(selectedTab == 
                            item.type ? .primary : .secondary)
            .onTapGesture {
              withAnimation(.spring(response: 0.3, 
                                     dampingFraction: 0.6)) {
                selectedTab = item.type
                scale = 1.1
              }
            }
          
          Spacer()
        }
      }
      
      VStack(alignment: .leading) {
        // Push indicator to the bottom
        Spacer()
        HStack {
          // Leading dynamic spacing
          leadingSpacers()
          
          Capsule()
            .frame(width: 32, height: 3)
            .offset(y: -3)
            .foregroundStyle(indicatorColor)
            .padding(.horizontal, 14)
            .shadow(color: indicatorColor, radius: 5, x: 0, y: -1)
          
          // Trailing dynamic spacing
          trailingSpacers()
        }
      }
      .frame(maxWidth: .infinity)
    }
    .frame(maxWidth: .infinity)
    .frame(height: 64)
    .background(.thinMaterial, in: .capsule)
    .shadow(color: .black.opacity(0.6), radius: 0.0, y: 0.5)
    .padding()
  }
  
  @ViewBuilder
  func leadingSpacers() -> some View {
    let leadingMaxIndex = selectedTab.rawValue + 1
    ForEach(0..<leadingMaxIndex, id: \.self) { _ in
      Spacer()
    }
    Spacer().frame(width: tabItemWidth * CGFloat((leadingMaxIndex - 1)))
  }
  
  @ViewBuilder
  func trailingSpacers() -> some View {
    let trailingMaxIndex = items.count - selectedTab.rawValue
    ForEach(0..<trailingMaxIndex, id: \.self) { _ in
      Spacer()
    }
    Spacer().frame(width: tabItemWidth * CGFloat(trailingMaxIndex - 1))
  }
}

#Preview {
  CustomTabbarDemoView()
}

Discussion about this podcast