Search code examples
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.

    • A simple way to add the rounded corners is to use the Path function addArc(tangent1End:tangent2End:radius:transform:). Here you just pass in the points that would be at the corners if you were to use straight lines to draw the shape (in other words, a shape drawn with square corners instead of rounded corners).

    • You might want to let the sloping part go outside (beyond) the drawing area. Alternatively, you could stay within the drawing area, but then you will probably need to enlarge the area for the shape in some other way when you use it as background to the tab items.

    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