Constrain position with ManipulationComponent
Using DidUpdateTransform to constrain the position of a manipulated entity.
Overview
The new ManipulationComponent in visionOS 26 is a huge time saver. It has some pretty great default behavior and offers a bit of customization. When I first tried it with a volume one major issue was that is was possible to drag entities outside of the bounds of the volume. When using the .reset release behavior, that isn’t a big deal. But when using .stay you can end up with lost entities that get clipped by the bounds as soon as the gesture ends.
I want to use this in Project Graveyard so I set out to find a way to constrain the position / translation changes. There are a few events that come with this component and one of them is just what I needed. We can capture the position the gesture is trying to apply, then clamp it within some set bounds.
// Listen to the transform change event and constrain the position within a fixed volume
_ = content.subscribe(to: ManipulationEvents.DidUpdateTransform.self) { event in
let newPostion = event.entity.position
// An arbitrary value to constrain movement with the hardcoded volume size
// Ideally, this should be read from the current size of the volume
let limit: Float = 0.34
let posX = min(max(newPostion.x, -limit), limit)
let posY = min(max(newPostion.y, -limit), limit)
let posZ = min(max(newPostion.z, -limit), limit)
event.entity.position = .init(x: posX, y: posY, z: posZ)
}
I’m sure there is a cleaner way to do this, but I just needed a quick way to test this out. It works really well. The entity is still responsive and all other features of the component still work.
Example Code
struct Example086: View {
var body: some View {
RealityView { content in
// An entity we can manipulate
let sphere = ModelEntity(
mesh: .generateSphere(radius: 0.1),
materials: [SimpleMaterial(color: .stepRed, isMetallic: false)])
// We'll use configureEntity to set up input and collision
ManipulationComponent.configureEntity(sphere, collisionShapes: [.generateSphere(radius: 0.1)])
// Create the component and add it to the entity
var mc = ManipulationComponent()
mc.releaseBehavior = .reset
sphere.components.set(mc)
// Listen to the transform change event and constrain the position within a fixed volume
_ = content.subscribe(to: ManipulationEvents.DidUpdateTransform.self) { event in
let newPostion = event.entity.position
// An arbitrary value to constrain movement with the hardcoded volume size
// Ideally, this should be read from the current size of the volume
let limit: Float = 0.34
let posX = min(max(newPostion.x, -limit), limit)
let posY = min(max(newPostion.y, -limit), limit)
let posZ = min(max(newPostion.z, -limit), limit)
event.entity.position = .init(x: posX, y: posY, z: posZ)
}
content.add(sphere)
}
.debugBorder3D(.stepGreen)
}
}
// See WWDC 2025 Session: Meet SwiftUI spatial layout
// https://developer.apple.com/videos/play/wwdc2025/273
extension View {
func debugBorder3D(_ color: Color) -> some View {
spatialOverlay {
ZStack {
Color.clear.border(color, width: 4)
ZStack {
Color.clear.border(color, width: 4)
Spacer()
Color.clear.border(color, width: 4)
}
.rotation3DLayout(.degrees(90), axis: .y)
Color.clear.border(color, width: 4)
}
}
}
}Support our work so we can continue to bring you new examples and articles.
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