Spatial SwiftUI: spatialOverlay

We can add secondary content within the bounds of views.

Overview

Each view in visionOS has a bounds. Most of the time, we think of SwiftUI content as having width and height, but these views can also have depth. spatialOverlay allows us to place secondary views within the bounds of a parent view. For example, imagine a simple BoxView with a 2D card placed to the front.

BoxView()
  .spatialOverlay(alignment:  .front) {
    CardView()
  }

I find it helpful to think of spatialOverlay sort of as an ornament-like feature for individual views. In fact, the alignment options are similar. We can use values like .front, .back, leading, .top, etc. to place content around the view.

spatialOverlay is a modifier we place on another view. The bounds of that parent view make up area we can draw content into. We can’t exceed those bounds, so with spatialOverlay we don’t have a contentAlignment option like we do with ornaments. Instead, SwiftUiIwill adjust the content alignment to keep the view aligned within the parent.

We can even use multiple spatialOverlay modifiers on a view. The example for this post places a 2D view relative to the Earth model. Then we add some padding to create some space around that and add another spatialOverlay for the Moon.

HStackLayout().depthAlignment(.center) {
    
    ModelView(name: "Earth")
        .frame(width: 260, height: 260)
        // Add an overlay view to the Earth model
        .spatialOverlay(alignment: alignmentSign) {
            VStack {
                Text("Earth & Luna")
                    .font(.headline)
            }
            .padding()
            .background(.black)
            .cornerRadius(24)
        }
        // Add some padding after the first overlay to create some space
        .padding(36)

        // Add a second spatial overlay to place the moon model
        .spatialOverlay(alignment: alignmentMoon) {
            ModelView(name: "Moon")
                .frame(width: 60, height: 60)
        }
    
}

Video Demo

Example Code

struct Example093: View {

    @State private var alignmentSign: Alignment3D = .bottomFront
    @State private var alignmentMoon: Alignment3D = .topLeading
    @State private var showDebugLines = true

    var body: some View {

        HStackLayout().depthAlignment(.center) {

            ModelView(name: "Earth")
                .frame(width: 260, height: 260)
                .debugBorder3D(showDebugLines ? .white : .clear)
            // Add an overlay view to the Earth model
                .spatialOverlay(alignment: alignmentSign) {
                    VStack {
                        Text("Earth & Luna")
                            .font(.headline)
                    }
                    .padding()
                    .background(.black)
                    .cornerRadius(24)
                }
            // Add some padding after the first overlay to create some space around the first example
                .padding(36)
                .debugBorder3D(showDebugLines ? .white : .clear)
            // Add a second spatial overlay to place the moon model
                .spatialOverlay(alignment: alignmentMoon) {
                    ModelView(name: "Moon")
                        .frame(width: 60, height: 60)
                }

        }
        // Controls to modify the example
        .ornament(attachmentAnchor: .scene(.trailing), contentAlignment: .trailing, ornament: {
            VStack(alignment: .center, spacing: 8) {
                Button(action: {
                    withAnimation {
                        alignmentSign = .topTrailingFront
                        alignmentMoon = .bottomLeading
                    }
                }, label: {
                    Text("Demo 1")
                })
                Button(action: {
                    withAnimation {
                        alignmentSign = .front
                        alignmentMoon = .trailingFront
                    }
                }, label: {
                    Text("Demo 2")
                })
                Button(action: {
                    withAnimation {
                        alignmentSign = .bottomLeadingFront
                        alignmentMoon = .topLeadingBack
                    }
                }, label: {
                    Text("Demo 3")
                })

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

        })


    }
}

#Preview {
    Example093()
}

// Adapted from Example 051 - Spatial SwiftUI: Model3D
fileprivate struct ModelView: View {

    @State var name: String = ""

    var body: some View {
        Model3D(named: name, bundle: realityKitContentBundle)
        { phase in
            if let model = phase.model {
                model
                    .resizable()
                    .aspectRatio(contentMode: .fit)
            } else if phase.error != nil {
                Text("Could not load model \(name).")
            } else {
                ProgressView()
            }
        }
    }
}

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?