Home > database >  .clipShape lose alignment when padding | SwiftUI | iOS15
.clipShape lose alignment when padding | SwiftUI | iOS15

Time:02-10

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.

CodePudding user response:

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

CodePudding user response:

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)
    }
}
}
  • Related