The onGeometryChange view modifier observes a view's size or frame without a GeometryReader
wrapper, so it cannot break your layout. It back-deploys to iOS 16, macOS 13, tvOS 16, watchOS 9,
and visionOS 1, which makes it the compatible way to track geometry.
The Three Parameters
Provide an equatable result type, a transformation that reduces the GeometryProxy to that type,
and an action that receives the value. Geometry changes constantly during scrolling, so SwiftUI
runs the action only when the transformed value changes; keep the value small.
@State private var size: CGSize = .zero
ScrollView {
Color.red.onGeometryChange(for: CGSize.self) { geometry in
geometry.size
} action: { newValue in
size = newValue
}
}
Backward-Compatible Scroll Offset
A zero-height marker view plus a named coordinate space tracks the scroll offset on iOS 16, where the iOS 18 scroll geometry APIs do not exist.
@State private var offset: CGFloat = 0
ScrollView {
Color.clear
.frame(height: 0)
.onGeometryChange(for: CGFloat.self) { geometry in
geometry.frame(in: .named("scrollView")).minY
} action: { newValue in
offset = newValue
}
// scroll content
}
.coordinateSpace(.named("scrollView"))
GeometryProxy.frame(in:) resolves the frame in any coordinate space you have declared with
coordinateSpace.
The Old-and-New Overload
An action closure taking (old, new) values exists too, but only on the iOS 18 generation of
platforms; it does not back-deploy. Stick to the single-value action for compatible code.
} action: { old, new in
offset = min(old, new)
}