skip to Main Content

I’m working on a custom Navigation Bottom Bar. I want it to be floating but I realized that when I add .padding() on it the .clipShape() inside its background loses alignment.

I have coded a simple version of the issue.

BottomNav without padding:

enter image description here

BottomNav with padding:

enter image description here

Main View:

struct ContentView: View {
var body: some View {
        VStack{
            Spacer()
            CustomBottomNav()
                .padding() // <- THIS MAKE THE SHAPE TO LOSE ALIGN
        }
}

CustomBottomNav:

struct CustomBottomNav: View {

@State var selectedTab: Views = .HOME
@State private var xAxis: CGFloat = 0
@Namespace var animation

private let tabs: [Views] = [.SETTINGS, .RANKING, .HOME, .SALES, .SEARCHER]
private let iconSize: CGFloat = 25

private func isSelectedTab(_ tab: Views) -> Bool{
    return selectedTab == selectedTab
}

var body: some View {
    HStack(spacing: 0){
        ForEach(tabs, id: .self){ tab in
            
            GeometryReader { geo in
                Button {
                    
                    print(geo.frame(in: .global).midX)
                    
                    withAnimation(.spring()) {
                        selectedTab = tab
                        xAxis = geo.frame(in: .global).midX
                    }
                } label: {
                    getImage(tab: tab)
                        .resizable()
                        .renderingMode(.template)
                        .aspectRatio(contentMode: .fit)
                        .frame(width: 30, height: 30)
                        .padding(isSelectedTab(tab) ? 15 : 0)
                        .background(
                            Color.orange
                                .opacity(
                                    isSelectedTab(tab) ? 1 : 0
                                ).clipShape(Circle())
                        )
                        .matchedGeometryEffect(id: tab, in: animation)
                }
                .position(x: geo.frame(in: .local).midX, y: geo.frame(in: .local).midY)
                .offset(
                    y: -60
                )
                .onAppear {
                    if isSelectedTab(tab) {
                        xAxis = geo.frame(in: .global).midX
                    }
                }
            }
            .frame(width: iconSize, height: iconSize)
            .overlay(
                Rectangle()
                    .stroke(.red)
            )
            
            if tab != tabs.last{
                Spacer()
            }

        }
    }
    .padding(.horizontal, 30)
    .padding(.vertical)
    .background(
        Color.yellow
            .clipShape(BottomNavShape(xAxis: xAxis))
            .cornerRadius(15)
    )
}

func getImage(tab: Views) -> Image {
    switch tab {
        case .SETTINGS:
            return Image("house")
        case .RANKING:
            return Image("house")
        case .HOME:
            return Image("house")
        case .SALES:
            return Image("house")
        case .SEARCHER:
            return Image("house")
    }
}

enum Views{
    case SETTINGS
    case RANKING
    case HOME
    case SALES
    case SEARCHER
    
}
}

BottomNavShape:

struct BottomNavShape: Shape{

var xAxis: CGFloat

//Animating path
var animatableData: CGFloat{
    get{
        return xAxis
    }
    set{
        xAxis = newValue
    }
}

func path(in rect: CGRect) -> Path {
    return Path{ path in
        path.move(to: CGPoint(x: 0, y: 0))
       path.addLine(to: CGPoint(x: rect.width, y: 0))
       path.addLine(to: CGPoint(x: rect.width, y: rect.height))
       path.addLine(to: CGPoint(x: 0, y: rect.height))
       
       let center = CGPoint(x: xAxis, y: rect.midY)
       
        path.move(to: center)
       
        path.addArc(center: center, radius: 15, startAngle: .degrees(0), endAngle: 
.degrees(1), clockwise: true)
    }
}
}

These classes are all you need to replicate the issue.

I’m using XCode 13 and iOS 15.

2

Answers


  1. Chosen as BEST ANSWER

    The problem is that your are passing a global coordinate to BottomNavShape which uses local coordinates to draw itself.

    So global X = 30 coordinate passed to BottomNavShape will be 30 points after BottomNavShape's minX starts in global context.

    In order to solve this I changed xAxis for xAxisMultiplier.

    I made this function:

        func setAxisMultiplier(_ index: Int){
            xAxisMultiplier = CGFloat(index) / CGFloat(tabs.count - 1)
        }
    

    Where index is the index of tab inside tabs

    I added two more params to BottomNavShape: horizontalPadding and iconSize

    BottomNavShape:

    struct BottomNavShape: Shape{
    
    var xAxisMultiplier: CGFloat
    var horizontalPadding: CGFloat
    var iconSize: CGFloat
    
    //Animating path
    var animatableData: CGFloat{
        get{
            return xAxisMultiplier
        }
        set{
            xAxisMultiplier = newValue
        }
    }
    
    func path(in rect: CGRect) -> Path {
        return Path{ path in
            path.move(to: CGPoint(x: 0, y: 0))
            path.addLine(to: CGPoint(x: rect.width, y: 0))
            path.addLine(to: CGPoint(x: rect.width, y: rect.height))
            path.addLine(to: CGPoint(x: 0, y: rect.height))
    
            let xCenter = ((rect.maxX - (horizontalPadding * 2) - iconSize) * xAxisMultiplier) + horizontalPadding + (iconSize * 0.5)
    
            path.move(to: CGPoint(x: xCenter - 50, y: 0))
    
            let to1 = CGPoint(x: xCenter, y: 35)
            let control1 = CGPoint(x: xCenter - 25, y: 0)
            let control2 = CGPoint(x: xCenter - 25, y: 35)
    
            let to2 = CGPoint(x: xCenter + 50, y: 0)
            let control3 = CGPoint(x: xCenter + 25, y: 35)
            let control4 = CGPoint(x: xCenter + 25, y: 0)
    
            path.addCurve(to: to1, control1: control1, control2: control2)
    
            path.addCurve(to: to2, control1: control3, control2: control4)
        }
    }
    }
    

  2. Its a little tricky because of the hard coded values, but
    I got it to work like this:

    if you use a defined padding on the outer view, e.g.

    .padding(30)
    

    then you can subtract this value in CustomButtonNav here:

    xAxis = geo.frame(in: .global).midX - 30 // subtract your outer padding here
    

    This appear 2 times:
    in the button action & in the .onAppear

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search