Stretchy header in SwiftUI with visualEffect()
It’s a common design pattern in modern iOS apps to have a large image at the top of a scroll view, extending all the way into the top safe area. When the user pulls down to overscroll, instead of revealing empty space above the image, the image expands, growing in size and creating a dynamic visual effect.
There are a few ways to achieve this effect in SwiftUI and I most commonly see developers using onScrollGeometryChange()
where they get the scroll offset, write it to a shared app state, and then use it to compute the frame of the image. But we can also do it in a slightly simpler way using the visualEffect() modifier.
This modifier takes a closure with two arguments: an EmptyVisualEffect
that we can apply additional effects to, and a GeometryProxy
that gives us access to the size and coordinates of the view, which we can use to get its frame in scroll view coordinate space. Given we have everything we need for our stretchy effect inside this closure, we can create a simple, self-contained view modifier that doesn’t need any additional state tracking.
extension View {
func stretchy() -> some View {
visualEffect { effect, geometry in
let currentHeight = geometry.size.height
let scrollOffset = geometry.frame(in: .scrollView).minY
let positiveOffset = max(0, scrollOffset)
let newHeight = currentHeight + positiveOffset
let scaleFactor = newHeight / currentHeight
return effect.scaleEffect(
x: scaleFactor, y: scaleFactor,
anchor: .bottom
)
}
}
}
We get the view’s current height and its scroll offset from the GeometryProxy
, and we make sure to only account for the positive offset, since we just want the image to grow when the user overscrolls and don’t want it to shrink when the user scrolls up to reveal more content below the image. We then compute the view’s new height and the scale factor, and apply a scale effect to the EmptyVisualEffect
with the bottom
anchor, so that the image stretches up without affecting the rest of the layout underneath it.
Now all that’s left is to apply our new stretchy()
modifier to the image we want to grow when the user pulls down.
struct FlowerView: View {
let flower: Flower
var body: some View {
ScrollView {
VStack {
Image(flower.name)
.resizable()
.scaledToFill()
.stretchy()
FlowerInfo(flower: flower)
}
}
.ignoresSafeArea(edges: .top)
}
}
This gives us a smooth, self-contained stretchy header effect with no extra state or offset tracking needed, and it can be easily reused throughout the app.
If you want to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.
For more resources on Swift and SwiftUI, check out my other books and book bundles.
I’m currently running a WWDC 2025 promotion with 30% off my books, plus additional savings when purchased as a bundle. Visit the books page to learn more.