# Building a Stretchy Header in SwiftUI with visualEffect()

Callum Matthews 4 min read
Table of Contents

A familiar design you’ll see in a lot of modern iOS apps is the stretchy header image: a big image at the top of a scroll view that grows when you pull down, instead of leaving behind blank space. Think of it as giving your header a bit of life — when users overscroll, the image expands dynamically, filling that gap with motion rather than emptiness.

Many tutorials out there tackle this by tracking scroll offset with onScrollGeometryChange() and passing that state around. While it works, I find it adds extra bookkeeping that isn’t always necessary. There’s a cleaner way, and it involves one of SwiftUI’s more underrated tools: the visualEffect() modifier.


Why visualEffect() Works So Well

The magic of visualEffect() is that it gives you two things right inside its closure:

  • An EmptyVisualEffect — essentially a container that you can apply transforms and effects to.
  • A GeometryProxy — which hands over size and coordinate information in the scroll view’s space.

That’s everything we need to create a stretchy effect without touching any external state. By looking at the geometry, we can calculate the overscroll and scale the view accordingly.


The Modifier

Here’s a simple stretchy() extension you can drop onto any view:

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
)
}
}
}

Here’s the breakdown:

  1. We grab the view’s height and its position in scroll view space.
  2. Overscrolling shows up as a positive offset, so we clamp negative values to 0.
  3. That gives us a new height and a scale factor.
  4. Finally, we apply a scaleEffect() anchored at the bottom, which stretches the view upward while leaving the rest of the layout intact.

Putting It Into Practice

Let’s say we have a detail view for a flower. Applying our new modifier to the header image is as simple as this:

struct FlowerView: View {
let flower: Flower
var body: some View {
ScrollView {
VStack {
Image(flower.name)
.resizable()
.scaledToFill()
.stretchy()
FlowerInfo(flower: flower)
}
}
.ignoresSafeArea(edges: .top)
}
}

The result? A smooth, reusable stretchy header that animates beautifully whenever the user pulls down on the scroll view.


How It Works Under the Hood

When you pull down on the scroll view, the visualEffect() modifier receives updated geometry information. The key insight is that geometry.frame(in: .scrollView).minY gives us the view’s position relative to the scroll view’s coordinate system.

  • Normal scrolling: minY is negative (view is scrolled up)
  • Overscrolling: minY becomes positive (view is pulled down beyond its natural position)

By scaling the view based on this positive offset, we create the stretchy effect. The .bottom anchor ensures the scaling happens from the bottom edge, maintaining the visual connection with the content below.


Customization Options

You can easily extend this pattern for more sophisticated effects:

extension View {
func stretchy(
maxScale: CGFloat = 1.5,
anchor: UnitPoint = .bottom
) -> 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 = min(newHeight / currentHeight, maxScale)
return effect.scaleEffect(
x: scaleFactor, y: scaleFactor,
anchor: anchor
)
}
}
}

This version adds:

  • Maximum scale limit to prevent over-stretching
  • Customizable anchor point for different scaling behaviors

Performance Considerations

The visualEffect() modifier is designed to be efficient. It only recalculates when the geometry actually changes, and SwiftUI handles the animation interpolation automatically. However, if you’re applying this to very large images, consider:

  • Using .scaledToFill() instead of .scaledToFit() for better performance
  • Ensuring your images are appropriately sized for their display area
  • Testing on older devices to ensure smooth 60fps scrolling

Wrapping Up

This approach is self-contained, lightweight, and reusable. You don’t need to pass scroll offsets through environment objects or manage extra state. Just tack .stretchy() onto any image at the top of a scroll view, and you’ve got yourself a modern, polished header effect.

If you’re digging deeper into SwiftUI, mastering little patterns like this helps you understand the framework’s declarative mindset and how geometry can drive fluid layouts. The visualEffect() modifier is a powerful tool that often gets overlooked, but it’s perfect for these kinds of scroll-based interactions.

The stretchy header effect adds that extra bit of polish that makes your app feel native and responsive. It’s the kind of detail that users might not consciously notice, but it contributes to the overall feeling of quality and attention to detail that separates great apps from good ones.


This post demonstrates how SwiftUI’s visualEffect() modifier can create engaging user experiences without the complexity of manual scroll tracking. The pattern is reusable across different views and provides a foundation for more sophisticated scroll-based animations.

My avatar

Thanks for reading! Check out my GitHub for more projects and contributions.


More Posts