I am new to SwiftUI and have completed half of my TabBar UI. However, I am stuck on adding a minor curve to both sides of the center button, similar to the example image.
I'm unsure how to add a minor curve on both sides of the center button? This is what I have achieved so far:
Below is my code for TabBar:
import SwiftUI
struct ContentView: View {
// MARK: - HIDING NATIVE TAB BAR
init(){
UITabBar.appearance().isHidden = true
}
var body: some View {
VStack {
Spacer()
TabBarShape()
.fill(Color.white)
.frame(height: 80)
.shadow(color: Color.black.opacity(0.4), radius: 2, x: 0, y: -1)
.overlay(
ZStack {
Button(action: {
print("Create Button Action")
}, label: {
Image("plus_icon")
.frame(width: 60, height: 60, alignment: .center)
.background(Color.custom64B054Color)
.cornerRadius(30)
}).offset(x: 0, y: -36)
Text("Create")
.padding(.top, 32)
HStack(spacing: 0) {
TabBarItem(iconName: "house.fill", action: {})
TabBarItem(iconName: "person.fill", action: {})
}
.frame(height: 80)
}
)
}.ignoresSafeArea()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
// MARK: - TabBar Shape
struct TabBarShape: Shape {
// Constants used for the shape
private enum Constants {
static let cornerRadius: CGFloat = 20
static let buttonRadius: CGFloat = 30
static let buttonPadding: CGFloat = 9
}
// Function to define the shape's path
func path(in rect: CGRect) -> Path {
let path = UIBezierPath()
// Move to the starting point at the bottom-left corner
path.move(to: .init(x: 0, y: rect.height))
// Add a line to the upper-left corner, leaving space for the corner radius
path.addLine(to: .init(x: 0, y: rect.height - Constants.cornerRadius))
// Add a quarter-circle in the upper-left corner
path.addArc(withCenter: .init(x: Constants.cornerRadius, y: Constants.cornerRadius),
radius: Constants.cornerRadius,
startAngle: CGFloat.pi,
endAngle: -CGFloat.pi/2,
clockwise: true)
// Calculate the end point for the line before the first button
let lineEnd = rect.width/2 - 2 * Constants.buttonPadding - Constants.buttonRadius
// Add a line to the calculated end point
path.addLine(to: .init(x: lineEnd, y: 0))
// Add a quarter-circle for the first button
path.addArc(withCenter: .init(x: lineEnd, y: Constants.buttonPadding),
radius: Constants.buttonPadding,
startAngle: 0,
endAngle: -CGFloat.pi/2,
clockwise: true)
// Add a half-circle for the first button
path.addArc(withCenter: .init(x: rect.width/2, y: 0),
radius: Constants.buttonPadding + Constants.buttonRadius,
startAngle: 0,
endAngle: CGFloat.pi,
clockwise: false)
// Calculate the start point for the line after the first button
let lineStart = rect.width/2 + 2 * Constants.buttonPadding - Constants.buttonRadius
// Add a quarter-circle for the second button
path.addArc(withCenter: .init(x: lineStart, y: Constants.buttonPadding),
radius: Constants.buttonPadding,
startAngle: -CGFloat.pi/2,
endAngle: -CGFloat.pi/2,
clockwise: true)
// Add a line to the calculated start point for the second button
path.addLine(to: .init(x: rect.width - Constants.cornerRadius, y: 0))
// Add a quarter-circle in the upper-right corner
path.addArc(withCenter: .init(x: rect.width - Constants.cornerRadius, y: Constants.cornerRadius),
radius: Constants.cornerRadius,
startAngle: -CGFloat.pi/2,
endAngle: 0,
clockwise: true)
// Add a line to the bottom-right corner
path.addLine(to: .init(x: rect.width, y: rect.height))
// Close the path to complete the shape
path.close()
// Convert the UIBezierPath to a SwiftUI Path
return Path(path.cgPath)
}
}
// MARK: - TabBar Item
struct TabBarItem: View {
let iconName: String
let action: () -> Void
var body: some View {
Button(action: action) {
Image(systemName: iconName)
.font(.system(size: 24))
.foregroundColor(.blue)
.padding(20)
}
.frame(maxWidth: .infinity)
}
}
It would be great if anyone could help. Also Could anyone guide me if this is the correct approach for my expected TabBar UI?
Thank You!
I had a go at implementing this shape using the same technique of building the path as was used in the answer to TabBar Customisation in SwiftUI (it was my answer).
I am not familiar with a UIBezierPath
, so I stuck to Path
instead.
Here is the revised TabBarShape
:
struct TabBarShape: Shape {
// Constants used for the shape
private enum Constants {
static let cornerRadius: CGFloat = 20
static let smallCornerRadius: CGFloat = 15
static let buttonRadius: CGFloat = 30
static let buttonPadding: CGFloat = 9
}
// Function to define the shape's path
func path(in rect: CGRect) -> Path {
var path = Path()
// Move to the starting point at the bottom-left corner
var x = rect.minX
var y = rect.maxY
path.move(to: CGPoint(x: x, y: y))
// Add the rounded corner on the top-left corner
x += Constants.cornerRadius
y = Constants.buttonRadius + Constants.cornerRadius
path.addArc(
center: CGPoint(x: x, y: y),
radius: Constants.cornerRadius,
startAngle: .degrees(180),
endAngle: .degrees(270),
clockwise: false
)
// Add a small corner leading to the main half-circle
x = rect.midX - Constants.buttonRadius - (Constants.buttonPadding / 2) - Constants.smallCornerRadius
y = Constants.buttonRadius - Constants.smallCornerRadius
path.addArc(
center: CGPoint(x: x, y: y),
radius: Constants.smallCornerRadius,
startAngle: .degrees(90),
endAngle: .degrees(35), // 0
clockwise: true
)
// Add the main half-circle
x = rect.midX
y += Constants.smallCornerRadius + Constants.buttonPadding
path.addArc(
center: CGPoint(x: x, y: y),
radius: Constants.buttonRadius + Constants.buttonPadding,
startAngle: .degrees(215), // 180
endAngle: .degrees(325), // 0
clockwise: false
)
// Add a trailing small corner
x += Constants.buttonRadius + (Constants.buttonPadding / 2) + Constants.smallCornerRadius
y = Constants.buttonRadius - Constants.smallCornerRadius
path.addArc(
center: CGPoint(x: x, y: y),
radius: Constants.smallCornerRadius,
startAngle: .degrees(145), // 180
endAngle: .degrees(90),
clockwise: true
)
// Add the rounded corner on the top-right corner
x = rect.maxX - Constants.cornerRadius
y = Constants.buttonRadius + Constants.cornerRadius
path.addArc(
center: CGPoint(x: x, y: y),
radius: Constants.cornerRadius,
startAngle: .degrees(270),
endAngle: .degrees(0),
clockwise: false
)
// Connect the bottom corner
x = rect.maxX
y = rect.maxY
path.addLine(to: CGPoint(x: x, y: y))
// Close the path to complete the shape
path.closeSubpath()
return path
}
}
You will notice that there is no need to draw lines between arcs, because they are added automatically.
The smaller corners leading in and out of the main half-circle are drawn as separate arcs. To smooth the join, these arcs and the main half-circle don't go all the way to 90 degrees. These partial angles were found with a little trial-and-error. I also noticed that the height of the half circle in your target screenshot was less than half the height of the button with padding, which is why the center of the + does not align with the top edge of the background area. I used the size of the button padding for this height adjustment and half of the padding for adjusting the x-positions where the small corners begin and end.
Here is how the shape can be used in an adapted version of your main body
. Some notes:
HStack
with bottom alignment is used to contain the regular tab bar items.HStack
.HStack
to allow for the safe area insets. The insets are measured using a GeometryReader
.var body: some View {
GeometryReader { proxy in
VStack {
Spacer()
HStack(alignment: .bottom) {
TabBarItem(label: "Schedule", iconName: "house.fill") {}
Text("Create")
TabBarItem(label: "Profile", iconName: "person.fill") {}
}
.font(.footnote)
.padding(.top, 42)
.overlay(alignment: .top) {
Button {
print("Create Button Action")
} label: {
Image(systemName: "plus") // "plus_icon"
.resizable()
.scaledToFit()
.padding()
.frame(width: 60, height: 60)
.foregroundStyle(.white)
.background {
Circle()
.fill(.green) // custom64B054Color
.shadow(radius: 3)
}
}
.padding(9)
}
.padding(.bottom, max(8, proxy.safeAreaInsets.bottom))
.background {
TabBarShape()
.fill(.white)
.shadow(radius: 3)
}
}
.ignoresSafeArea(edges: .bottom)
}
}
I also made some small changes to TabBarItem
, to include a label and omit the padding:
struct TabBarItem: View {
let label: String
let iconName: String
let action: () -> Void
var body: some View {
Button(action: action) {
VStack(spacing: 4) {
Image(systemName: iconName)
.font(.system(size: 24))
.foregroundColor(.blue)
Text(label)
}
}
.frame(maxWidth: .infinity)
}
}
Here's how it all looks:
Hope it helps.