What You Will Build
A custom navigation bar featuring a greeting header with notification bell and profile avatar, a search field with material background, and a scrollable list of items. This replaces the default NavigationStack title bar when you need full creative control over the header design.
Why This Pattern Matters
Most production apps (Spotify, Airbnb, Instagram) use custom navigation bars rather than the system default. Building your own gives you control over layout, animations, and branding. This tutorial shows how to compose a header from scratch inside a ScrollView.
Key SwiftUI Concepts
- .ultraThinMaterial backgrounds for a frosted glass look on buttons and search fields.
- LazyVStack for efficient rendering of scrollable list items.
- SF Symbols with badge variants like
bell.badge.
Step 1: Build the Header Bar
The header uses an HStack with a greeting on the left and action icons on the right:
HStack {
VStack(alignment: .leading, spacing: 2) {
Text("Good Morning")
.font(.caption)
.foregroundStyle(.secondary)
Text("Explorer")
.font(.title2.bold())
}
Spacer()
Image(systemName: "bell.badge")
.font(.title3)
.padding(10)
.background(.ultraThinMaterial, in: Circle())
Image(systemName: "person.circle.fill")
.font(.title)
.foregroundStyle(.blue)
}
.padding()
Step 2: Add the Search Field
HStack {
Image(systemName: "magnifyingglass")
.foregroundStyle(.secondary)
TextField("Search...", text: $searchText)
}
.padding(12)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 12))
.padding(.horizontal)
Step 3: Build the List Items
Each row uses an HStack with a colored circle avatar, text content, and a chevron disclosure indicator:
LazyVStack(spacing: 12) {
ForEach(0..<10) { i in
HStack {
Circle()
.fill([Color.blue, .green, .orange, .purple, .red][i % 5].gradient)
.frame(width: 44, height: 44)
.overlay {
Image(systemName: "star.fill")
.foregroundStyle(.white)
}
VStack(alignment: .leading) {
Text("Item \(i + 1)")
.fontWeight(.medium)
Text("Description for item")
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
Image(systemName: "chevron.right")
.foregroundStyle(.tertiary)
}
.padding()
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 12))
}
}
.padding()
Tips and Pitfalls
- Material backgrounds adapt to light/dark mode automatically, so you do not need separate color definitions.
- Use LazyVStack (not VStack) for lists with more than 20 items. Lazy loading prevents all rows from being created at once.
- Hide the system nav bar: If you embed this in a
NavigationStack, use.navigationBarHidden(true)to avoid a double header. - Add scroll offset tracking: Use a
GeometryReaderinside the ScrollView to collapse the header as the user scrolls, creating a sticky compact header effect.
iOS Version: iOS 15+