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:
BottomNav with padding:
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
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:
Where index is the index of tab inside tabs
I added two more params to BottomNavShape: horizontalPadding and iconSize
BottomNavShape:
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.
then you can subtract this value in CustomButtonNav here:
This appear 2 times:
in the button action & in the
.onAppear