Spatial SwiftUI: SpatialContainer

A Layout that can align overlapping views, allowing multiple views to exist in one space.

Overview

visionOS 26 brings a new type of Layout: SpatialContainer. This lets multiple views exist in the same space. The container will be shaped like a bounding box, sized to fit the largest child on each axis. We can align the content with an Alignment3D value.

Let’s create a SwiftUI box and place a 3D model inside, then align them.

SpatialContainer(alignment: .top) {
    // A helper function to create a SwiftUI box using ZStacks and colors.
    debugBorder3DView(showDebugLines ? .white : .clear)
        .frame(width: 200, height: 200)
        .frame(depth: 200)

    ModelViewSimple(name: "Earth", bundle: realityKitContentBundle)
        .frame(width: 100, height: 100)
}

We can do something similar with multiple 3D models. We’ll adjust the opacity of the larger view so we can see the small one inside.

SpatialContainer(alignment: .top) {
    ModelViewSimple(name: "Earth", bundle: realityKitContentBundle)
        .opacity(0.22)
        .frame(width: 200, height: 200)

    ModelViewSimple(name: "Moon", bundle: realityKitContentBundle)
        .frame(width: 100, height: 100)
}

We already learned about spatialOverlay. That modifier uses SpatialContainer when we pass it more than one view. From the docs:

Multiple views provided by content are organized into a SpatialContainer.

Of course, we could pass spatialOverlay our own `SpatialContainer if we need to define alignment.

I find it helpful to think of this as a 3D Stack. The SwiftUI stacks that we use let us lay out views along an axis in a rectangle. SpatialContainer lets us lay out views in a 3D bounding box.

One powerful use case that comes to mind is transitioning from a Model3D to a RealityView. We size both views to fill the same space. We’ll look at an example of that in a future lab.

Video Demo

Example Code

struct Example094: View {

    @State private var alignment: Alignment3D = .center
    @State private var showDebugLines = true
    
    var body: some View {
        HStackLayout().depthAlignment(.center) {

            // A container with a SwiftUI box and a 3D model
            SpatialContainer(alignment: alignment) {
                debugBorder3DView(showDebugLines ? .white : .clear)
                .frame(width: 200, height: 200)
                .frame(depth: 200)

                ModelViewSimple(name: "Earth", bundle: realityKitContentBundle)
                    .frame(width: 100, height: 100)
                    .debugBorder3D(showDebugLines ? .blue : .clear)
            }

            // A container with two 3D models
            SpatialContainer(alignment: alignment) {
                ModelViewSimple(name: "Earth", bundle: realityKitContentBundle)
                    .opacity(0.22)
                    .frame(width: 200, height: 200)
                    .debugBorder3D(showDebugLines ? .blue : .clear)

                ModelViewSimple(name: "Moon", bundle: realityKitContentBundle)
                    .frame(width: 100, height: 100)
                    .debugBorder3D(showDebugLines ? .green : .clear)
            }
            
        }
        // Controls to modify the example
        .ornament(attachmentAnchor: .scene(.trailing), contentAlignment: .trailing, ornament: {
            VStack(alignment: .center, spacing: 8) {
                Button(action: {
                    withAnimation {
                        alignment = .center
                    }
                }, label: {
                    Text("Demo 1")
                })
                Button(action: {
                    withAnimation {
                        alignment = .top
                    }
                }, label: {
                    Text("Demo 2")
                })
                Button(action: {
                    withAnimation {
                        alignment = .bottomLeadingFront
                    }
                }, label: {
                    Text("Demo 3")
                })

                Button(action: {
                    showDebugLines.toggle()
                }, label: {
                    Text("Debug")
                })
            }
            .padding()
            .controlSize(.small)
            .glassBackgroundEffect()

        })

    }
}

Support our work so we can continue to bring you new examples and articles.

Questions or feedback?