Lab 087 – Standing inside a Spatial Layout

Documenting a workaround for using SwiftUI Spatial Layouts in Immersive Spaces.

The Spatial Layout features in visionOS 26 can be a great way to create a complex 3D layout outside of RealityKit. I’ve used these features in several labs, but always in a Window or Volume.

See also

I ran into an interesting issue when trying to use a Radial Layout in an Immersive Cpace. The ideas was to surround myself with SwiftUI panels that all face inwards. This was easy to do using the RadialLayout that we looked at in the previous labs. All I had to do was increase the size of the layout and the content.

But when I stepped inside this lab I could not see my layout. As I moved around it showed up. Then I realized what was happening. SwiftUI has a feature that will fade out any View that we’re too close to. I added a debugBorder3D around my view to show its bounds. This made it clear that the view was drawing where I wanted it, but when standing inside it I could not see anything.

The workaround I came up with is simple. I positioned the view somewhere we won’t step into, then offset the content back to where we want. In this lab I moved the entire view for the layout above my head. Then I offset each card to hang in the air below the view. Check out the video to see this in action.

Video Demo

Lab Code

struct Lab087: View {

    @State private var angleOffset: Angle = .zero

    @State var nodes: Int = 12
    @State var previousNodes: Int = 3
    @State var arcDegrees: Double = 360
    @State var angleOffsetDegrees: Double = -90
    @State var shouldAutoCenter = true

    var body: some View {

        VStack {
            RadialLayout(angleOffset: angleOffset) {
                ForEach(0..<nodes, id: \.self) { index in
                    Rectangle()
                        .foregroundColor(.clear)
                        .frame(width: 500, height: 800)
                        .glassBackgroundEffect()
                        .rotation3DLayout(Rotation3D(angle: .degrees(360 - 90), axis: .x))
                        .rotation3DLayout(Rotation3D(angle: .degrees(inwardZRotation(for: index, total: nodes)), axis: .z))
                        .offset(z: -1200)
                }
            }
            .debugBorder3D(.white)
            .frame(width: 3600, height: 3600)
            .frame(depth: 3600, alignment: .front)
            .rotation3DLayout(Rotation3D(angle: .degrees(90), axis: .x))
            .offset(y: -1800)
            .offset(z: -1200)

        }

    }

    // Some helper functions to Orient each card towards the center (WIP)
    private func inwardZRotation(for index: Int, total: Int) -> Double {
        // Compute the placement angle used by ArcLayout for this index
        let angleDeg = arcPlacementAngleDegrees(for: index, total: total)
        // Rotate the card so its “top” points toward the circle center
        return angleDeg + 90.0
    }

    private func arcPlacementAngleDegrees(for index: Int, total: Int) -> Double {
        // Match ArcLayout’s math
        let arc = arcDegrees
        let startDeg = -90.0 // start from top
        let isFull = arc >= 359.9
        let incrementDeg: Double = {
            if isFull {
                return 360.0 / Double(max(1, total))
            } else {
                return arc / Double(max(1, total - 1))
            }
        }()

        // Auto-center offset (same as ArcLayout.autoCenterOffset)
        let offsetNeeded: Double = {
            guard shouldAutoCenter else { return 0 }
            let targetCenter = 0.0
            let arcCenter = startDeg + (arc / 2.0)
            return targetCenter - arcCenter
        }()

        return startDeg + (incrementDeg * Double(index)) + angleOffsetDegrees + offsetNeeded
    }
}

Download the Xcode project with this and many more labs from Step Into Vision.

Questions or feedback?