๐ง Building the Apple Music-Style Profile Header in SwiftUI (With Scroll Shrink Animation)
Apple Musicโs profile screen has a smooth shrinking header: ย when you scroll down, the big profile banner gracefully collapses. ย When youโฆ
๐ง Building the Apple Music-Style Profile Header in SwiftUI (With Scroll Shrink Animation)
Apple Musicโs profile screen has a smooth shrinking header: when you scroll down, the big profile banner gracefully collapses. When you scroll up, the header expands back again.
In this tutorial, weโll recreate that in SwiftUI, complete with:
- Parallax background blur
- Profile image scaling
- Smooth text scaling
- Sticky animated tab bar
- Scroll-driven header shrink and expand behavior
๐ง Core Idea Behind the Shrinking Header
The key is to read the scroll offset continuously and adjust the headerHeight state property.
- Scroll Down โ Collapses Header
- Scroll Up โ Expands Header
To achieve this, we use:
ScrollView { ... }
.coordinateSpace(name: "scroll")
Then read the scroll movement using a GeometryReader:
let offset = geo.frame(in: .named("scroll")).minY
We track the difference between the current and previous offsets. This tells us the scroll direction:
Scroll Direction Offset Diff Action Up (finger moves down) diff > 0 Expand header Down (scroll further) diff < 0 Collapse header
โ Full Working Code (Just Paste & Run)
import SwiftUI
struct AppleMusicProfileHeaderView: View {
@State private var headerHeight: CGFloat = 260
@State private var lastScrollOffset: CGFloat = 0
@State private var selectedTab: String = "Overview"
@Namespace private var animationNamespace
let minHeight: CGFloat = 120
let maxHeight: CGFloat = 320
private let tabs = ["Overview", "Playlists", "Artists"]
var body: some View {
VStack(spacing: 0) {
header
ScrollView {
headerOffsetReader
stickyTabs
content
}
.coordinateSpace(name: "scroll")
}
.ignoresSafeArea(edges: .top)
.background(Color.black)
}
}
๐ 1. The Header (Parallax + Gradient + Profile Scaling)
We compute a collapse progress factor (0 โ 1) to animate:
- Profile Image Size
- Title Font Size
- Background Blur
extension AppleMusicProfileHeaderView {
private var header: some View {
ZStack(alignment: .bottomLeading) {
Image("music_bg")
.resizable()
.scaledToFill()
.frame(height: headerHeight)
.clipped()
.overlay(
LinearGradient(
colors: [.black.opacity(0.1), .black.opacity(0.7)],
startPoint: .top,
endPoint: .bottom
)
)
.blur(radius: 8 * collapseProgress)
HStack(spacing: 16) {
Image("profile_pic")
.resizable()
.scaledToFill()
.frame(width: profileImageSize, height: profileImageSize)
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 3))
.shadow(color: .black.opacity(0.4), radius: 8, y: 4)
VStack(alignment: .leading, spacing: 4) {
Text("Suraj Kumar")
.foregroundColor(.white)
.font(.system(size: titleSize, weight: .bold))
Text("Apple Music โข Artist / Listener")
.foregroundColor(.white.opacity(0.85))
.font(.subheadline)
}
}
.padding(.leading, 24)
.padding(.bottom, 22)
}
.frame(height: headerHeight)
.animation(.spring(response: 0.35, dampingFraction: 0.8), value: headerHeight)
}
private var collapseProgress: CGFloat { max(0, min(1, (headerHeight - minHeight) / (maxHeight - minHeight))) }
private var profileImageSize: CGFloat { 50 + (60 * collapseProgress) }
private var titleSize: CGFloat { 16 + (10 * collapseProgress) }
}
๐ 2. Sticky Tab Bar with Animated Selection Highlight
extension AppleMusicProfileHeaderView {
private var stickyTabs: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 18) {
ForEach(tabs, id: \.self) { tab in
Text(tab)
.fontWeight(selectedTab == tab ? .bold : .regular)
.foregroundColor(selectedTab == tab ? .white : .white.opacity(0.6))
.padding(.vertical, 8)
.padding(.horizontal, 14)
.background(
ZStack {
if selectedTab == tab {
RoundedRectangle(cornerRadius: 12)
.fill(Color.white.opacity(0.25))
.matchedGeometryEffect(id: "TAB_HIGHLIGHT", in: animationNamespace)
}
}
)
.onTapGesture { withAnimation(.spring()) { selectedTab = tab } }
}
}
.padding(.horizontal, 16)
}
.background(.ultraThinMaterial)
.padding(.top, -8)
}
}
๐ถ 3. Music List Content (Mock UI)
extension AppleMusicProfileHeaderView {
private var content: some View {
VStack(spacing: 20) {
ForEach(1...40, id: \.self) { index in
HStack {
RoundedRectangle(cornerRadius: 8)
.fill(Color.gray.opacity(0.3))
.frame(width: 60, height: 60)
VStack(alignment: .leading) {
Text("Track \(index)")
.foregroundColor(.white)
Text("Artist โข Album Name")
.foregroundColor(.white.opacity(0.6))
.font(.caption)
}
Spacer()
}
.padding()
.background(Color.white.opacity(0.06))
.cornerRadius(14)
}
}
.padding()
}
}
๐ 4. Scroll Offset Logic (The Heart of the Header Animation)
extension AppleMusicProfileHeaderView {
private var headerOffsetReader: some View {
GeometryReader { geo in
let offset = geo.frame(in: .named("scroll")).minY
Color.clear.onAppear { lastScrollOffset = offset }
Color.clear.onChange(of: offset) { updateHeader(offset: offset) }
}
.frame(height: 0)
}
private func updateHeader(offset: CGFloat) {
let diff = offset - lastScrollOffset
if diff < 0 { headerHeight = max(minHeight, headerHeight + diff) } // collapse
if diff > 0 { headerHeight = min(maxHeight, headerHeight + diff) } // expand
lastScrollOffset = offset
}
}
๐ฏ Final Thoughts
This pattern is lightweight, SwiftUI-native, and fully customizable. No UIKit hacks. No scroll listeners. Everything stays declarative.
You now have:
โ Smooth scrolling header collapse โ Animated profile scaling โ Sticky tab bar with matched geometry animation โ Clean architecture split into view extensions
๋ฉํ๋ฐ์ดํฐ
- post_id
- c3ffa2fbf86d
- slug
- building-the-apple-music-style-profile-header-in-swiftui-with-scroll-shrink-animation-c3ffa2fbf86d
- url
- https://medium.com/@kumarsuraj19111997/building-the-apple-music-style-profile-header-in-swiftui-with-scroll-shrink-animation-c3ffa2fbf86d
- canonical_url
- https://medium.com/@kumarsuraj19111997/building-the-apple-music-style-profile-header-in-swiftui-with-scroll-shrink-animation-c3ffa2fbf86d
- author_url
- https://medium.com/@kumarsuraj19111997
- status
- ok
- fetched_at
- 2026-08-11 04:34:46