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.

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?