Add this PreferenceKey to track the vertical scroll offset of a View:
struct VerticalScrollOffsetKey: PreferenceKey {
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
Add this ViewModifier to allow tracking of a View's vertical offset and call a scrollPostionUpdate closure when scrolling has stopped:
extension View {
func onScrollEnded(in coordinateSpace: CoordinateSpace, onScrollEnded: @escaping (CGFloat) -> Void) -> some View {
modifier(OnVerticalScrollEnded(coordinateSpace: coordinateSpace, scrollPostionUpdate: onScrollEnded))
}
}
final class OnVerticalScrollEndedOffsetTracker: ObservableObject {
let scrollViewVerticalOffset = CurrentValueSubject<CGFloat, Never>(0)
func updateOffset(_ offset: CGFloat) {
scrollViewVerticalOffset.send(offset)
}
}
struct OnVerticalScrollEnded: ViewModifier {
let coordinateSpace: CoordinateSpace
let scrollPostionUpdate: (CGFloat) -> Void
@StateObject private var offsetTracker = OnVerticalScrollEndedOffsetTracker()
func body(content: Content) -> some View {
content
.background(
GeometryReader(content: { geometry in
Color.clear.preference(key: VerticalScrollOffsetKey.self, value: abs(geometry.frame(in: coordinateSpace).origin.y))
})
)
.onPreferenceChange(VerticalScrollOffsetKey.self, perform: offsetTracker.updateOffset(_:))
.onReceive(offsetTracker.scrollViewVerticalOffset.debounce(for: 0.1, scheduler: DispatchQueue.main).dropFirst(), perform: scrollPostionUpdate)
}
}
Usage: Add the .onScrollEnded modifier to the content of the ScrollView and give the ScrollView a coordinateSpace name:
struct ScrollingEndedView: View {
private let coordinateSpaceName = "scrollingEndedView_coordinateSpace"
var body: some View {
ScrollView {
VStack {
ForEach(0...100, id: \.self) { rowNum in
Text("Row \(rowNum)")
.frame(maxWidth: .infinity)
.padding(.vertical)
.background(Color.orange)
}
}
.onScrollEnded(in: .named(coordinateSpaceName), onScrollEnded: updateScrollPosition(_:))
}
.coordinateSpace(name: coordinateSpaceName) // add the coordinateSpaceName to the ScrollView itself
}
private func updateScrollPosition(_ position: CGFloat) {
print("scrolling ended @: \(position)")
}
}