Revealed on: June 11, 2024
In iOS 18, SwiftUI’s ScrollView has gotten plenty of love. Now we have a number of new options for ScrollView that give tons of management to us as builders. Certainly one of my favourite interactions with scroll views is once I can drag on an inventory an a header picture animates together with it.
In UIKit we might implement a UIScrollViewDelegate and browse the content material offset on scroll. In SwiftUI we may obtain the stretchy header impact with GeometryReader however that is by no means felt like a pleasant answer.
In iOS 18, it is doable to realize a stretchy header with little to no workarounds by utilizing the onScrollGeometryChange view modifier.
To implement this stretchy header I am utilizing the next arrange:
struct StretchingHeaderView: View {
@State personal var offset: CGFloat = 0
var physique: some View {
ZStack(alignment: .prime) {
Picture(.picture)
.resizable()
.aspectRatio(contentMode: .fill)
.body(peak: 300 + max(0, -offset))
.clipped()
.transformEffect(.init(translationX: 0, y: -(max(0, offset))))
ScrollView {
Rectangle()
.fill(Colour.clear)
.body(peak: 300)
Textual content("(offset)")
LazyVStack(alignment: .main) {
ForEach(0..<100, id: .self) { merchandise in
Textual content("Merchandise at (merchandise)")
}
}
}
.onScrollGeometryChange(for: CGFloat.self, of: { geo in
return geo.contentOffset.y + geo.contentInsets.prime
}, motion: { new, outdated in
offset = new
})
}
}
}
Now we have an @State personal var to maintain monitor of the ScrollView‘s present content material offset. I am utilizing a ZStack to layer the Picture beneath the ScrollView. I’ve seen that including the Picture to the ScrollView ends in a fairly stuttery animation in all probability as a result of we’ve parts altering measurement whereas the scroll view scrolls. As a substitute, we add a transparent Rectangle to the ScrollView to push or content material down by an applicable quantity.
To make our impact work, we have to improve the picture’s peak by -offset in order that the picture improve when our scroll is destructive. To stop resizing the picture after we’re scrolling down within the record, we use the max operator.
.body(peak: 300 + max(0, -offset))
Subsequent, we additionally have to offset the picture when the consumer scrolls down within the record. Here is what makes that work:
.transformEffect(.init(translationX: 0, y: -(max(0, offset))))
When the offset is optimistic the consumer is scrolling downwards. We need to push our picture up what that occurs. When the offset is destructive, we need to use 0 as a substitute so we once more use the max operator to ensure we do not offset our picture within the flawed route.
To make all of it work, we have to apply the next view modifier to the scroll view:
.onScrollGeometryChange(for: CGFloat.self, of: { geo in
return geo.contentOffset.y + geo.contentInsets.prime
}, motion: { new, outdated in
offset = new
})
The onScrollGeometryChange view modifier permits us to specify which sort of worth we intend to calculate primarily based on its geometry. On this case, we’re calculating a CGFloat. This worth could be no matter you need and may match the return kind from the of closure that you simply cross subsequent.
In our case, we have to take the scroll view’s content material offset on the y axis and increment that by the content material inset’s prime. By doing this, we calculate the suitable “zero” level for our impact.
The second closure is the motion that we need to take. We’ll obtain the earlier and the newly calculated worth. For this impact, we need to set our offset variable to be the newly calculated scroll offset.
All this collectively creates a enjoyable strechy and bouncy impact that is tremendous attentive to the consumer’s contact!

