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.

Questions or feedback?