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
- Lab 067 – Exploring custom Layouts in SwiftUI
- Lab 068 – Adding an axis to custom layouts
- Lab 069 – More fun with RadialLayout
- Lab 072 – More fun with HoneycombLayout
- Lab 074 – Mocking up a clock using Radial Layout
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.


Follow Step Into Vision