DEV Community

Sebastien Lato
Sebastien Lato

Posted on

How to Build Modern Parallax & Scroll Effects in SwiftUI

Parallax and scroll-reactive interfaces are everywhere in modern iOS design.

You see them in:

  • Apple Music
  • TV app
  • App Store
  • Photos
  • Stocks
  • Safari

These effects give depth, motion, and personality to your UI — and with SwiftUI’s modern APIs, they’re easy to build without hacks.

In this tutorial, we’ll build three clean, modern parallax effects using only pure SwiftUI.


🎯 What We’ll Build

  1. Hero Image Parallax

    Background image moves slower than foreground as you scroll.

  2. Fade + Scale Header

    Perfect for content-heavy pages.

  3. Stretchy Pull-Down Header

    Classic iOS pattern that feels light and natural.

All using GeometryReader, scroll offsets, and lightweight animations.


🧠 Key Technique: Scroll Offset Reading

We'll track scroll position using this simple helper:

struct OffsetKey: PreferenceKey {
    static var defaultValue: CGFloat = 0
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}
Enter fullscreen mode Exit fullscreen mode

Then we attach:

GeometryReader { geo in
    Color.clear
        .preference(key: OffsetKey.self,
                    value: geo.frame(in: .named("scroll")).minY)
}
.frame(height: 0)
Enter fullscreen mode Exit fullscreen mode

This gives us a live scroll value.


🖼 1) Hero Image Parallax

struct ParallaxHeader: View {
    let height: CGFloat
    @Binding var offset: CGFloat

    var body: some View {
        let parallax = offset > 0 ? offset : offset / 4

        Image("hero")
            .resizable()
            .scaledToFill()
            .frame(height: height + (offset > 0 ? offset : 0))
            .offset(y: parallax)
            .clipped()
    }
}
Enter fullscreen mode Exit fullscreen mode

Use inside a ScrollView:

struct ParallaxDemo: View {
    @State private var offset: CGFloat = 0

    var body: some View {
        ScrollView {
            OffsetReader(offset: $offset)

            ParallaxHeader(height: 300, offset: $offset)

            content
        }
        .coordinateSpace(name: "scroll")
    }
}
Enter fullscreen mode Exit fullscreen mode

This creates a deep, smooth parallax effect similar to Apple TV or Music.


🌫 2) Fade + Scale Header (Dynamic Title)

struct FadingHeader: View {
    @Binding var offset: CGFloat

    var body: some View {
        let progress = max(0, min(1, 1 - (offset / 120)))

        Text("Dashboard")
            .font(.largeTitle.bold())
            .scaleEffect(0.8 + (0.2 * progress))
            .opacity(progress)
    }
}
Enter fullscreen mode Exit fullscreen mode

Combine it with your scroll value:

.overlay(alignment: .topLeading) {
    FadingHeader(offset: $offset)
        .padding(.top, 40)
        .padding(.leading, 20)
}
Enter fullscreen mode Exit fullscreen mode

This matches the feel of many native system screens.


🫧 3) Stretchy Pull-Down Header

struct StretchyHeader: View {
    let height: CGFloat
    @Binding var offset: CGFloat

    var body: some View {
        Image("cover")
            .resizable()
            .scaledToFill()
            .frame(height: height + max(offset, 0))
            .offset(y: -max(offset, 0))
            .clipped()
    }
}
Enter fullscreen mode Exit fullscreen mode

This creates the same stretchy behavior used in:

  • Photos
  • App Store product pages
  • Safari reader mode

✔️ Final Result

You now have:

  • parallax hero images
  • fading/scaling titles
  • stretchy pull-down headers

All powered by a clean scroll-offset technique and pure SwiftUI.
These effects instantly make your screens feel more alive, expressive, and modern — without adding complexity.

Top comments (0)