Spatial SwiftUI: onGeometryChange3D
We can use this modifier to access data from GeometryProxy3D without using GeometryReader3D.
Overview
We learned about GeometryReader3D in a previous example. This provides access to GeometryProxy3D. We can use it modify our content based on changes to that proxy. For example, scaling the content of a RealityView when the size of a window or volume changes.
There is another way we can access GeometryReader3D. We can use onGeometryChange3D instead of wrapping our view in a geometry reader. This lets us track only the data we need, instead of all changes to the proxy.
This modifier is a bit more complex than other SwiftUI modifiers. We provide the type of data we want from the geometry. Then extract the value we need from the proxy to make it available in the action closure. With those two steps done, we can use the action block to work with the data.
SomeView()
// Set the type we want to work with. In this case, we'll use Rext3D
.onGeometryChange3D(for: Rect3D.self) { proxy in
return proxy.frame(in: .global) // extract the Rect3D from the GeometryProxy
} action: { newValue in // newValue is a Rect3D
volumeSize = newValue.size // We can read the size of the Rect3D as a Size3D
}See GeometryProxy and GeometryProxy3D for a list of characteristics.
See also: Spatial SwiftUI: GeometryReader3D, Using onGeometryChange3D to scale RealityView content when a Volume is resized, and Lab 080 – First Look at Unified Coordinate Conversion.
Video Demo
We track two changes in this example. When the volume size is changed, we scale the content inside of a RealityView. When the volume is moved, we capture the position of the volume in world space. Volumes use their back bottom corner as an origin so we’ll add an ornament there to visualize this change.
Example Code
struct Example134: View {
@Environment(\.physicalMetrics) var physicalMetrics
@State private var volumeSize: Size3D = .zero
@State private var volumeRootEntity = Entity()
// A place to store the bounds of our 3D content
@State private var baseExtents: SIMD3<Float> = .zero
// A place to store the Point3D for the volume position in world space
@State private var volumePosition: Point3D = .zero
var body: some View {
RealityView { content in
content.add(volumeRootEntity)
guard let baseRoot = try? await Entity(named: "ToyBiplane", in: realityKitContentBundle) else { return }
volumeRootEntity.addChild(baseRoot, preservingWorldTransform: true)
baseExtents = baseRoot.visualBounds(relativeTo: nil).extents / baseRoot.scale
baseRoot.position.y = -baseExtents.y / 2
scaleContent(by: volumeSize)
}
.ornament(attachmentAnchor: .scene(.bottomLeadingBack), ornament: {
VStack(alignment: .leading, spacing: 12) {
Text("X: \(volumePosition.x)")
Text("Y: \(volumePosition.y)")
Text("Z: \(volumePosition.z)")
}
.font(.largeTitle)
.padding(24)
.glassBackgroundEffect()
})
.debugBorder3D(.white)
// Example 01: Anytime the volume changes in size we'll scale the RealityView content
.onGeometryChange3D(for: Rect3D.self) { proxy in
return proxy.frame(in: .global)
} action: { new in
volumeSize = new.size
scaleContent(by: volumeSize)
}
// Example 02: Capture the position of the volume when it changes
.onGeometryChange3D(for: Point3D.self) { proxy in try! proxy
.coordinateSpace3D()
.convert(value: Point3D.zero, to: .worldReference)
} action: { _, new in
volumePosition = new // We'll just show this in an ornament
}
}
/// Scale the 3D content based on the size of the Volume
/// See this article for details: https://stepinto.vision/example-code/using-ongeometrychange3d-to-scale-realityview-content-when-a-volume-is-resized/
func scaleContent(by volumeSize: Size3D) {
let scale = Float(physicalMetrics.convert(volumeSize.width, to: .meters)) / baseExtents.x
volumeRootEntity.setScale(.init(repeating: scale), relativeTo: nil)
}
}Download the Xcode project with this and many more examples from Step Into Vision.
Some examples are provided as standalone Xcode projects. You can find those here.


Follow Step Into Vision