โ† Back to list

๐ŸŽง 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โ€ฆ

suraj Kumar ยท 2025-11-01 08:13 ยท 8 claps ยท 2.6 min read
#swiftui #scrollview
Open on Medium โ†—
Wiki topics: ๐Ÿ“ฑ ยท Mobile Development ๐ŸŽฌ ยท Film & Television ๐ŸŽต ยท Music & Audio ๐Ÿ‘— ยท Fashion

๐ŸŽง 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