Custom Bottom Tab Bar SwiftUI
In this article, We will explore how to implement a Custom Bottom Bar using SwiftUI
Before proceeding, please consider subscribing to our YOUTUBE CHANNEL
It gives us a lot of motivation to produce high-quality content for you guys.
if you are interested in watching the video tutorial, Check out below.
Let’s start by creating the a SwiftUI file and name it as Home.
import SwiftUI
struct Home: View {
var body: some View {
}
}
Create a constructor init() function and set isHidden to true.
Inside body, Create a ZStack and a TabView inside it.
We will create State $selectedTab next.
I’m only setting up the color of the page, when tab is changed.
import SwiftUI
struct Home: View {
init() {
UITabBar.appearance().isHidden = true
}
var body: some View {
ZStack(alignment: Alignment(horizontal: .center, vertical: .bottom)) {
TabView(selection: $selectedTab) {
Color(.red)
.ignoresSafeArea()
.tag("square.and.arrow.up.circle.fill")
Color(.cyan)
.ignoresSafeArea()
.tag("pencil.circle.fill")
Color(.gray)
.ignoresSafeArea()
.tag("trash.circle.fill")
Color(.blue)
.ignoresSafeArea()
.tag("paperplane.circle.fill")
}
}
.ignoresSafeArea()
}
}
Will be creating three variables.
selectedTab is for tab change,
animation is for animation data when shifted.
xAxis is for x axis data for shape shifting.
import SwiftUI
struct Home: View {
@State var selectedTab = "square.and.arrow.up.circle.fill"
@Namespace var animation
@State var xAxis: CGFloat = 0
init() {
UITabBar.appearance().isHidden = true
}
var body: some View {
ZStack(alignment: Alignment(horizontal: .center, vertical: .bottom)) {
TabView(selection: $selectedTab) {
Color(.red)
.ignoresSafeArea()
.tag("square.and.arrow.up.circle.fill")
Color(.cyan)
.ignoresSafeArea()
.tag("pencil.circle.fill")
Color(.gray)
.ignoresSafeArea()
.tag("trash.circle.fill")
Color(.blue)
.ignoresSafeArea()
.tag("paperplane.circle.fill")
}
}
.ignoresSafeArea()
}
}
Create CustomTab function and loop through tabs array.
import SwiftUI
var tabs = ["square.and.arrow.up.circle.fill", "pencil.circle.fill", "trash.circle.fill", "paperplane.circle.fill"]
struct Home: View {
@State var selectedTab = "square.and.arrow.up.circle.fill"
@Namespace var animation
@State var xAxis: CGFloat = 0
init() {
UITabBar.appearance().isHidden = true
}
var body: some View {
ZStack(alignment: Alignment(horizontal: .center, vertical: .bottom)) {
TabView(selection: $selectedTab) {
Color(.red)
.ignoresSafeArea()
.tag("square.and.arrow.up.circle.fill")
Color(.cyan)
.ignoresSafeArea()
.tag("pencil.circle.fill")
Color(.gray)
.ignoresSafeArea()
.tag("trash.circle.fill")
Color(.blue)
.ignoresSafeArea()
.tag("paperplane.circle.fill")
}
CustomTabBar()
}
.ignoresSafeArea()
}
@ViewBuilder
func CustomTabBar() -> some View {
VStack(alignment: .center) {
HStack {
ForEach(tabs, id: \.self) { image in
}
}
.padding(.horizontal, 50)
.padding(.vertical, 10)
}
.frame(maxWidth: .infinity)
}
}
I’ll be creating a tab bar button,
Using GeometryReader, I get the xAxis value which i ll set onClick.
import SwiftUI
var tabs = ["square.and.arrow.up.circle.fill", "pencil.circle.fill", "trash.circle.fill", "paperplane.circle.fill"]
struct Home: View {
@State var selectedTab = "square.and.arrow.up.circle.fill"
@Namespace var animation
@State var xAxis: CGFloat = 0
init() {
UITabBar.appearance().isHidden = true
}
var body: some View {
ZStack(alignment: Alignment(horizontal: .center, vertical: .bottom)) {
TabView(selection: $selectedTab) {
Color(.red)
.ignoresSafeArea()
.tag("square.and.arrow.up.circle.fill")
Color(.cyan)
.ignoresSafeArea()
.tag("pencil.circle.fill")
Color(.gray)
.ignoresSafeArea()
.tag("trash.circle.fill")
Color(.blue)
.ignoresSafeArea()
.tag("paperplane.circle.fill")
}
CustomTabBar()
}
.ignoresSafeArea()
}
@ViewBuilder
func CustomTabBar() -> some View {
VStack(alignment: .center) {
HStack {
ForEach(tabs, id: \.self) { image in
GeometryReader { reader in
Button {
withAnimation(Animation.interactiveSpring(dampingFraction: 2)) {
selectedTab = image
xAxis = reader.frame(in: .global).midX
}
} label: {
Image(systemName: image)
.resizable()
.renderingMode(.template)
.aspectRatio(contentMode: .fit)
.foregroundColor(image == selectedTab ? getIconColor(image: image) : Color.black)
.background(Color.white.opacity(selectedTab == image ? 1 : 0).clipShape(Circle()))
.overlay {
RoundedRectangle(cornerRadius: 30)
.stroke(.white, lineWidth: 4)
}
.matchedGeometryEffect(id: image, in: animation)
.offset(x: 0 , y: selectedTab == image ? -40 : 0)
.foregroundStyle(.blue)
}
.onAppear{
if image == tabs.first {
xAxis = reader.frame(in: .global).midX
}
}
}
.frame(width: 60, height: 60)
if image != tabs.last {
Spacer(minLength: 0)
}
}
}
.padding(.horizontal, 50)
.padding(.vertical, 10)
}
.frame(maxWidth: .infinity)
}
func getIconColor(image: String) -> Color {
switch image {
case "square.and.arrow.up.circle.fill":
return Color.red
case "pencil.circle.fill":
return Color.cyan
case "trash.circle.fill":
return Color.gray
case "paperplane.circle.fill":
return Color.blue
default:
return Color.red
}
}
}
Tab bar is almost done, Now to make the arc we need a shape.
Create a file, name it CustomTabBarShape and extend it to Shape.
import SwiftUI
struct CustomTabBarShape: Shape {
var xAxis: CGFloat
var animatableData: CGFloat {
get {return xAxis}
set {xAxis = newValue}
}
func path(in rect: CGRect) -> Path {
let width = rect.size.width
let height = rect.size.height
let circleWidth = width * 0.2
let point1 = CGPoint(x: 0, y: height)
let point2 = CGPoint(x: 0, y: height * 0.2)
let point3 = CGPoint(x: width * 0.4, y: 0)
let point4 = CGPoint(x: width * 0.8, y: 0)
let point5 = CGPoint(x: width, y: height)
return Path { path in
path.move(to: point1)
path.addLine(to: point2)
path.addArc(center: .init(x: width * 0.1, y: width * 0.1), radius: width * 0.1, startAngle: .init(degrees: 180), endAngle: .init(degrees: 270), clockwise: false)
path.addLine(to: point3)
path.addArc(center: .init(x: xAxis, y: 0), radius: circleWidth / 2, startAngle: .init(degrees: 180), endAngle: .init(degrees: 0), clockwise: true)
path.addLine(to: point4)
path.addArc(center: .init(x: width * 0.9, y: width * 0.1), radius: width * 0.1, startAngle: .init(degrees: 270), endAngle: .init(degrees: 0), clockwise: false)
path.addLine(to: point5)
path.closeSubpath()
}
}
}
Set this shape on VStack of CustomTabBar inside background.
Like below
.background(Color.white.clipShape(CustomTabBarShape(xAxis: xAxis)))
struct Home: View {
@State var selectedTab = "square.and.arrow.up.circle.fill"
@Namespace var animation
@State var xAxis: CGFloat = 0
init() {
UITabBar.appearance().isHidden = true
}
var body: some View {
ZStack(alignment: Alignment(horizontal: .center, vertical: .bottom)) {
TabView(selection: $selectedTab) {
Color(.red)
.ignoresSafeArea()
.tag("square.and.arrow.up.circle.fill")
Color(.cyan)
.ignoresSafeArea()
.tag("pencil.circle.fill")
Color(.gray)
.ignoresSafeArea()
.tag("trash.circle.fill")
Color(.blue)
.ignoresSafeArea()
.tag("paperplane.circle.fill")
}
CustomTabBar()
}
.ignoresSafeArea()
}
@ViewBuilder
func CustomTabBar() -> some View {
VStack(alignment: .center) {
HStack {
ForEach(tabs, id: \.self) { image in
GeometryReader { reader in
Button {
withAnimation(Animation.interactiveSpring(dampingFraction: 2)) {
selectedTab = image
xAxis = reader.frame(in: .global).midX
}
} label: {
Image(systemName: image)
.resizable()
.renderingMode(.template)
.aspectRatio(contentMode: .fit)
.foregroundColor(image == selectedTab ? getIconColor(image: image) : Color.black)
.background(Color.white.opacity(selectedTab == image ? 1 : 0).clipShape(Circle()))
.overlay {
RoundedRectangle(cornerRadius: 30)
.stroke(.white, lineWidth: 4)
}
.matchedGeometryEffect(id: image, in: animation)
.offset(x: 0 , y: selectedTab == image ? -40 : 0)
.foregroundStyle(.blue)
}
.onAppear{
if image == tabs.first {
xAxis = reader.frame(in: .global).midX
}
}
}
.frame(width: 60, height: 60)
if image != tabs.last {
Spacer(minLength: 0)
}
}
}
.padding(.horizontal, 50)
.padding(.vertical, 10)
}
.frame(maxWidth: .infinity)
.background(Color.white.clipShape(CustomTabBarShape(xAxis: xAxis)))
}
func getIconColor(image: String) -> Color {
switch image {
case "square.and.arrow.up.circle.fill":
return Color.red
case "pencil.circle.fill":
return Color.cyan
case "trash.circle.fill":
return Color.gray
case "paperplane.circle.fill":
return Color.blue
default:
return Color.red
}
}
}
That’s it. Below is the custom tab bar.
Once again Thanks for stopping by.
Do check out our YOUTUBE CHANNEL
Social Handles
Instagram : https://www.instagram.com/mobileappsacademy/
Twitter : https://twitter.com/MobileAppsAcdmy
LinkedIn : https://www.linkedin.com/company/mobile-apps-academy/