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!