swiftuilayoutuitabbartabbar

Custom TabBar with center rounded button SwiftUI


Here is what I am trying to do:

Screenshot

The screenshot is taken from a 14 iPhone.

Screenshot

I want to manage the layout like the first image, where all the spacing and circle spacing are reduced. How can I achieve the exact TabBar UI?

Code:-

import SwiftUI

struct TestReorderTabItemsPerSelection: View {

    @State private var selection: String = "house"
    @State private var tbHeight = CGFloat.zero
    
    struct Item {
        let title: String
        let color: Color
        let icon: String
    }
    @State var items = [
        Item(title: "folder", color: .red, icon: "folder"),
        Item(title: "eraser", color: .red, icon: "eraser.fill"),
        Item(title: "cart", color: .red, icon: "cart"),
        Item(title: "house", color: .blue, icon: "house"),
        Item(title: "car", color: .green, icon: "car"),
    ]
    
    var selected: Item {
        items.first { $0.title == selection } ?? items[0]
    }
    
    var body: some View {
        ZStack(alignment: .bottom) {
            TabView(selection: $selection) {
                ForEach(items, id: \.title) { item in
                    TabContent(height: $tbHeight) {
                        item.color
                    } .tabItem {
                            Image(systemName: item.icon)
                            Text(item.title)
                    }
                }
            }
            TabSelection(height: tbHeight, item: selected)
        }
    }
    
    struct TabSelection: View {
        let height: CGFloat
        let item: Item
        
        var body: some View {
            VStack {
                Spacer()
                Curve()
                    .frame(maxWidth: .infinity, maxHeight: height)
                    .foregroundColor(item.color)
            }
            .ignoresSafeArea()
            .overlay(
                Circle().foregroundColor(.black)
                    .frame(height: height).aspectRatio(contentMode: .fit)
                    .shadow(radius: 4)
                    .overlay(Image(systemName: item.icon)
                        .font(.title)
                        .foregroundColor(.white))
                , alignment: .bottom)
        }
    }
    
    struct TabContent<V: View>: View {
        @Binding var height: CGFloat
        @ViewBuilder var content: () -> V
        var body: some View {
            GeometryReader { gp in
                content()
                    .onAppear {
                        height = gp.safeAreaInsets.bottom
                    }
                    .onChange(of: gp.size, {
                        height = gp.safeAreaInsets.bottom
                    })
            }
        }
    }
    
    struct Curve: Shape {
        func path(in rect: CGRect) -> Path {
            let h = rect.maxY * 0.7
            return Path {
                $0.move(to: .zero)
                $0.addLine(to: CGPoint(x: rect.midX / 2.0, y: rect.minY))
                $0.addCurve(to: CGPoint(x: rect.midX, y: h), control1: CGPoint(x: rect.midX * 0.8, y: rect.minY), control2: CGPoint(x: rect.midX * 0.7, y: h))
                $0.addCurve(to: CGPoint(x: rect.midX * 3.0 / 2.0, y: rect.minY), control1: CGPoint(x: rect.midX * 1.3, y: h), control2: CGPoint(x: rect.midX * 1.2, y: rect.minY))
                $0.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
            }
        }
    }
}

struct TestReorderTabItemsPerSelection_Previews: PreviewProvider {
    static var previews: some View {
        TestReorderTabItemsPerSelection()
    }
}

Solution

  • Thank You Benzy Neez And Andrei G. for your response. Here is my Updated Answer according to my requirement:-

    import SwiftUI
    
    struct TabItem: Identifiable, Equatable {
       let id: UUID = UUID()
       let title: String
       let color: Color
       let icon: String
    }
    
    struct TestReorderTabItemsPerSelection: View {
    
    @State private var selectedTab: TabItem?
    private let tabHeight: CGFloat = 50
    
    @State var items = [
        TabItem(title: "House", color: .orange, icon: "house.fill"),
        TabItem(title: "Deposits", color: .cyan, icon: "wallet.pass.fill"),
        TabItem(title: "Scan", color: .purple, icon: "qrcode.viewfinder"),
        TabItem(title: "Finance", color: .blue, icon: "cart.circle.fill"),
        TabItem(title: "Card", color: .green, icon: "creditcard.fill")
    ]
    
    var body: some View {
        ZStack(alignment: .bottom) {
            Color.clear.ignoresSafeArea()
            // Display the custom view corresponding to the selected tab
            VStack(spacing: 5) {
                if let selectedTab = selectedTab {
                    switch selectedTab.title {
                    case "House":
                        HouseView()
                    case "Deposits":
                        DepositsView()
                    case "Scan":
                        ScanView()
                    case "Finance":
                        FinanceView()
                    case "Card":
                        CardView()
                    default:
                        Text("Unknown View")
                    }
                } else {
                    Text("Select a Tab")
                        .font(.largeTitle)
                        .foregroundColor(.gray)
                }
                
                HStack {
                    HStack {
                        TabItemView(item: items[0], selectedTab: $selectedTab, tabHeight: tabHeight)
                        TabItemView(item: items[1], selectedTab: $selectedTab, tabHeight: tabHeight)
                    }
                    
                    Spacer()
                        .frame(maxWidth: .infinity, alignment: .center)
                    
                    HStack {
                        TabItemView(item: items[3], selectedTab: $selectedTab, tabHeight: tabHeight)
                        TabItemView(item: items[4], selectedTab: $selectedTab, tabHeight: tabHeight)
                    }
                }
                .background(Color.white) // Set red background for tab bar
                .padding(.horizontal, 10)
            }
            
            .onAppear {
                selectedTab = items[0]
            }
            
            TabSelection(item: selectedTab ?? items[0], height: 80, tabHeight: tabHeight, selectedTab: $selectedTab)
                .offset(y: -10)
        }.background(Color.white) // Set red background for tab bar
       }
    }
    
    struct TabItemView: View {
    
    let item: TabItem
    @Binding var selectedTab: TabItem?
    var tabHeight: CGFloat
    
    private var isSelected: Bool {
        selectedTab == item
    }
    
    var body: some View {
        Button {
            selectedTab = item
        } label: {
            VStack {
                Image(systemName: item.icon)
                    .imageScale(.large)
                    .symbolVariant(.fill)
                Text(item.title)
                    .font(.caption)
            }
            .foregroundStyle(isSelected ? .orange : .gray)
        }
        .frame(maxWidth: .infinity, maxHeight: tabHeight)
          }
      }
    
     struct TabSelection: View {
    
    //Parameters
    let item: TabItem
    let height: CGFloat
    var tabHeight: CGFloat
    @Binding var selectedTab: TabItem?
    
    //Body
    var body: some View {
        Circle()
            .foregroundColor(.black)
            .frame(maxWidth: .infinity, maxHeight: height)
            .aspectRatio(contentMode: .fit)
            .shadow(radius: 4)
            .overlay {
                Image(systemName: "qrcode.viewfinder")
                    .font(.title)
                    .foregroundColor(.white)
            }
            .background(alignment: .bottom) {
                Curve()
                    .frame(maxWidth: .infinity, maxHeight: height, alignment: .bottom)
                    .foregroundColor(Color(UIColor.lightGray.withAlphaComponent(0.2)))
                .offset(y: height-(tabHeight - 5))
            }.onTapGesture {
                selectedTab = TabItem(title: "Scan", color: .purple, icon: "qrcode.viewfinder")
           }
         }
     }
    
    struct Curve: Shape {
    func path(in rect: CGRect) -> Path {
        let h = rect.maxY * 0.7
        return Path {
            $0.move(to: .zero)
            $0.addLine(to: CGPoint(x: rect.midX / 2.0, y: rect.minY))
            $0.addCurve(to: CGPoint(x: rect.midX, y: h), control1: CGPoint(x: rect.midX * 0.8, y: rect.minY), control2: CGPoint(x: rect.midX * 0.7, y: h))
            $0.addCurve(to: CGPoint(x: rect.midX * 3.0 / 2.0, y: rect.minY), control1: CGPoint(x: rect.midX * 1.3, y: h), control2: CGPoint(x: rect.midX * 1.2, y: rect.minY))
            $0.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
        }
      }
    }
    
    struct HouseView: View {
    var body: some View {
        ZStack {
            Color.blue
                .ignoresSafeArea()
            Text("House View")
                .font(.largeTitle)
                .foregroundColor(.white)
        }
       }
      }
    
    struct DepositsView: View {
    var body: some View {
        ZStack {
            Color.red
                .ignoresSafeArea()
            Text("Deposits View")
                .font(.largeTitle)
                .foregroundColor(.white)
          }
       }
    }
    
    struct ScanView: View {
    var body: some View {
        ZStack {
            Color.purple
                .ignoresSafeArea()
            Text("Scan View")
                .font(.largeTitle)
                .foregroundColor(.white)
         }
       }
     }
    
     struct FinanceView: View {
    var body: some View {
        ZStack {
            Color.pink
                .ignoresSafeArea()
            Text("Finance View")
                .font(.largeTitle)
                .foregroundColor(.white)
          }
       }
     }
    
     struct CardView: View {
     var body: some View {
        ZStack {
            Color.green
                .ignoresSafeArea()
            Text("Card View")
                .font(.largeTitle)
                .foregroundColor(.white)
           }
        }
     }
    
    // Preview
    struct TestReorderTabItemsPerSelection_Previews: PreviewProvider {
    static var previews: some View {
        TestReorderTabItemsPerSelection()
        }
     }