Revealed on: June 11, 2024
In iOS 18, SwiftUI’s ScrollView
has gotten plenty of love. We’ve got a number of new options for ScrollView
that give tons of management to us as builders. One among 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 would implement a UIScrollViewDelegate
and browse the content material offset on scroll. In SwiftUI we might 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 attain a stretchy header with little to no workarounds through the use of 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: .high) {
Picture(.photograph)
.resizable()
.aspectRatio(contentMode: .fill)
.body(peak: 300 + max(0, -offset))
.clipped()
.transformEffect(.init(translationX: 0, y: -(max(0, offset))))
ScrollView {
Rectangle()
.fill(Shade.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.high
}, motion: { new, previous in
offset = new
})
}
}
}
We’ve got an @State personal var
to maintain observe 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 reasonably stuttery animation in all probability as a result of we’ve got parts altering dimension 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 enhance the picture’s peak by -offset
in order that the picture enhance when our scroll is detrimental. To forestall resizing the picture once we’re scrolling down within the checklist, we use the max
operator.
.body(peak: 300 + max(0, -offset))
Subsequent, we additionally must offset the picture when the person scrolls down within the checklist. This is what makes that work:
.transformEffect(.init(translationX: 0, y: -(max(0, offset))))
When the offset is optimistic the person is scrolling downwards. We need to push our picture up what that occurs. When the offset is detrimental, we need to use 0
as an alternative so we once more use the max
operator to verify we do not offset our picture within the mistaken course.
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.high
}, motion: { new, previous 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 will be no matter you need and will match the return sort from the of
closure that you just go 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 high
. 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 conscious of the person’s contact!