Printed on: June 11, 2024
In iOS 18, SwiftUI’s ScrollView
has gotten plenty of love. We’ve a number of new options for ScrollView
that give tons of management to us as builders. One in every of my favourite interactions with scroll views is once I can drag on a listing an a header picture animates together with it.
In UIKit we might implement a UIScrollViewDelegate
and skim 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 attainable 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 non-public var offset: CGFloat = 0
var physique: some View {
ZStack(alignment: .prime) {
Picture(.photograph)
.resizable()
.aspectRatio(contentMode: .fill)
.body(peak: 300 + max(0, -offset))
.clipped()
.transformEffect(.init(translationX: 0, y: -(max(0, offset))))
ScrollView {
Rectangle()
.fill(Coloration.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
})
}
}
}
We’ve an @State non-public var
to maintain monitor of the ScrollView
‘s present content material offset. I am utilizing a ZStack
to layer the Picture
under the ScrollView
. I’ve seen that including the Picture
to the ScrollView
ends in a reasonably stuttery animation most likely as a result of now we have parts altering dimension whereas the scroll view scrolls. As an alternative, we add a transparent Rectangle
to the ScrollView
to push or content material down by an acceptable 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 unfavorable. To stop resizing the picture once we’re scrolling down within the listing, we use the max
operator.
.body(peak: 300 + max(0, -offset))
Subsequent, we additionally must offset the picture when the consumer scrolls down within the listing. 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 wish to push our picture up what that occurs. When the offset is unfavorable, we wish to use 0
as an alternative so we once more use the max
operator to ensure we do not offset our picture within the incorrect 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 kind of worth we intend to calculate based mostly on its geometry. On this case, we’re calculating a CGFloat
. This worth may be no matter you need and will match the return sort from the of
closure that you just move 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 wish to take. We’ll obtain the earlier and the newly calculated worth. For this impact, we wish 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 aware of the consumer’s contact!