SwiftUI Custom Tab Bar with Badge
A custom tab bar with animated badge indicators for unread notifications or messages.
import SwiftUI
struct CustomTabBar: View {
@State private var selectedTab = 0
var body: some View {
TabView(selection: $selectedTab) {
HomeView()
.tabItem {
Image(systemName: "house")
Text("Home")
}
.tag(0)
MessagesView(badgeCount: 3)
.tabItem {
Image(systemName: "message")
Text("Messages")
}
.tag(1)
SettingsView()
.tabItem {
Image(systemName: "gear")
Text("Settings")
}
.tag(2)
}
.accentColor(.blue)
}
}
struct MessagesView: View {
var badgeCount: Int
var body: some View {
NavigationView {
Text("Messages Screen")
.navigationTitle("Messages")
.overlay(
Group {
if badgeCount > 0 {
ZStack {
Circle()
.fill(Color.red)
.frame(width: 24, height: 24)
Text("$badgeCount)")
.foregroundColor(.white)
.font(.caption)
.padding(.horizontal, 4)
}
.offset(x: 70, y: -10)
.transition(.scale)
.animation(.easeInOut(duration: 0.3), value: badgeCount)
}
}
)
}
}
}
#Preview {
CustomTabBar()
}How It Works
This implementation demonstrates a custom tab bar with an animated badge on one of the tabs. It uses TabView with a state variable selectedTab to manage the currently selected tab. Each tab item has a label and an icon, and the Messages tab includes a badge count indicating unread messages.
The badge is implemented using a ZStack that contains a red circle and a Text view showing the number of unread messages. The badge appears only when the badge count is greater than zero. The offset modifier positions the badge relative to the tab item label, and a smooth scale animation is applied using .transition(.scale) and .animation(.easeInOut(duration: 0.3), value: badgeCount) for a polished visual effect when the badge appears or updates.
The NavigationView inside the MessagesView allows for future navigation stack integration, and the overlay pattern keeps the badge visually attached to the tab bar rather than the content area. This makes the badge appear as a natural extension of the tab bar itself.
Customization
| Parameter | Where to change | Example values |
|---|---|---|
badgeCount |
In MessagesView initializer |
0, 3, 10 |
| Badge color | .fill(Color.red) |
.blue, .green, .orange |
| Badge size | .frame(width: 24, height: 24) |
20, 30, 40 |
| Animation duration | .animation(.easeInOut(duration: 0.3), value: badgeCount) |
0.2, 0.5, 0.7 |
| Badge offset | .offset(x: 70, y: -10) |
(x: 60, y: -5), (x: 80, y: -15) |
Accessibility Notes
To improve accessibility, consider adding .accessibilityLabel("Unread messages: $badgeCount)") to the badge container. This ensures screen readers convey the number of unread messages to users who rely on assistive technologies.