swiftswiftuishapes

How to make this shape?


enter image description here

I want to achieve a custom tab bar shape in SwiftUI that looks like the image above. The top corners (top-left and top-right) should be rounded normally, and the bottom corners (bottom-left and bottom-right) should have an "inverted" curve, giving it a unique outward curve look.

I've tried using clipShape with a simple RoundedRectangle, but that doesn't allow for inward curves at the bottom corners. How can I create a custom SwiftUI shape that allows me to have standard rounded corners at the top and inverted (outward) curves at the bottom, similar to the shape in the image?

My Attempt:

//
//  CustomTabBar.swift
//  skill
//
//  Created by Chetan Patil on 06/12/24.
//

import SwiftUI

struct CustomTabBar: View {
    @State private var selectedTab: Tab = .favourites // State to track selected tab
    
    enum Tab {
        case favourites, accounts, payments, deposits
    }

    var body: some View {
        VStack(spacing: 0) {
            // Tabs Content
            
            
            // Custom Tab Bar
            HStack {
                tabItem(title: "Favourites", isSelected: selectedTab == .favourites) {
                    selectedTab = .favourites
                }
                
                tabItem(title: "Accounts", isSelected: selectedTab == .accounts) {
                    selectedTab = .accounts
                }
                tabItem(title: "Payments", isSelected: selectedTab == .payments) {
                    selectedTab = .payments
                }
                tabItem(title: "Deposits", isSelected: selectedTab == .deposits) {
                    selectedTab = .deposits
                }
            }
            .background(Color(red: 0/255, green: 64/255, blue: 143/255))
           
            // Blue background
            TabView(selection: $selectedTab) {
                FavouritesView()
                    .tag(Tab.favourites)
                AccountsView()
                    .tag(Tab.accounts)
                PaymentsView()
                    .tag(Tab.payments)
                DepositsView()
                    .tag(Tab.deposits)
            }
            .frame(maxWidth: .infinity, maxHeight: 400)
            
        }
        .ignoresSafeArea(edges: .bottom) // Ignore safe area for the tab bar
    }

    // Tab Item View
    private func tabItem(title: String, isSelected: Bool, action: @escaping () -> Void) -> some View {
        Button(action: action) {
            Text(title)
                .font(.system(size: 14, weight: isSelected ? .bold : .regular))
                .foregroundColor(isSelected ? .black : .gray)
                .padding(.vertical, 12)
                .frame(maxWidth: .infinity)
        }
        .background(isSelected ? Color.white : Color.clear) // Highlight selected tab
        .clipShape(
            .rect(
                topLeadingRadius: 10,
                bottomLeadingRadius: -10,
                bottomTrailingRadius: -10,
                topTrailingRadius: 10
            )
        )
    }
}


// Example Content Views for Each Tab
struct FavouritesView: View {
    var body: some View {
        Text("Favourites View")
            .frame(maxWidth: .infinity, maxHeight: 400)
            .background(Color.white)
    }
}


struct AccountsView: View {
    var body: some View {
        Text("Accounts View")
            .frame(maxWidth: .infinity, maxHeight: 400)
            .background(Color.white)
    }
}

struct PaymentsView: View {
    var body: some View {
        Text("Payments View")
            .frame(maxWidth: .infinity, maxHeight: 400)
            .background(Color.white)
    }
}

struct DepositsView: View {
    var body: some View {
        Text("Deposits View")
            .frame(maxWidth: .infinity, maxHeight: 400)
            .background(Color.white)
    }
}


#Preview {
    CustomTabBar()
}

Solution

  • I would suggest using a custom Shape for this.

    struct TabShape: Shape {
        let cornerRadius: CGFloat = 12
        let slopeWidth: CGFloat = 20
    
        func path(in rect: CGRect) -> Path {
            let bottomLeft = CGPoint(x: rect.minX, y: rect.maxY)
            let topLeft = CGPoint(x: rect.minX, y: rect.minY)
            let slopeBegin = CGPoint(x: rect.maxX - (slopeWidth / 2), y: rect.minY)
            let slopeEnd = CGPoint(x: rect.maxX + (slopeWidth / 2), y: rect.maxY)
            let bottomRight = CGPoint(x: rect.maxX + (slopeWidth / 2) + cornerRadius, y: rect.maxY)
            return Path { path in
                path.move(to: bottomLeft)
                path.addArc(tangent1End: topLeft, tangent2End: slopeBegin, radius: cornerRadius)
                path.addArc(tangent1End: slopeBegin, tangent2End: slopeEnd, radius: cornerRadius)
                path.addArc(tangent1End: slopeEnd, tangent2End: bottomRight, radius: cornerRadius)
                path.closeSubpath()
            }
        }
    }
    

    Testing this shape in isolation:

    TabShape()
        .frame(width: 100, height: 40)
        .border(.red)
    

    Screenshot

    To use it in your custom tab bar, just replace the background behind the tab items:

    private func tabItem(title: String, isSelected: Bool, action: @escaping () -> Void) -> some View {
        Button(action: action) {
            // ...as before
        }
        .background {
            TabShape()
                .fill(isSelected ? .white : .clear)
        }
    }
    

    Screenshot

    To make space for the slope on the last tab, you probably want to add some padding to the HStack that contains the tab items:

    // Custom Tab Bar
    HStack {
        // ...as before
    }
    .padding(.trailing, 16) // 👈 here
    .background(Color(red: 0/255, green: 64/255, blue: 143/255))
    

    Screenshot