How to open and dismiss immersive spaces in visionOS

Covering the basics of using openImmersiveSpace and dismissImmersiveSpace.

App structure

Let’s start with the visionOS project template in Xcode 16, then delete most of the boiler plate. In the app file, we can define an ImmersiveSpace, assign it an id, and pass it a view. When launched, the app will open a regular visionOS window with a button to open the space.

import SwiftUI

@main
struct Garden12App: App {

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .defaultSize(CGSize(width: 600, height: 600))

        // Give the space an ID and a view
        ImmersiveSpace(id: "GardenScene") {
            ImmersiveView()
        }
    }
}

Open a space

Opening a space is similar to opening a window or volume. We need to import openImmersiveSpace from the environment.

    @Environment(\.openImmersiveSpace) private var openImmersiveSpace

We can create a button to open the space. It provides an id value that matches the one defined in the app file. Opening an immersive space is an asynchronous operation, so we need to do this in a task using the await keyword

Button(action: {
    Task {
        await openImmersiveSpace(id: "GardenScene")
        isImmersiveSpacePresented = true
    }
}, label: {
    Text("Open Immersive Space")
})

When we tap the button, the new space will open. The window will still be there. We’ll explore how to close and reopen windows when moving into spaces in another post.

Dismiss a space

The process to dismiss (or exit) a space is the same as opening one. However, we no longer need the id. VisionOS only allows one immersive space to be open. I guess the developers at Apple decided a simple function would suffice.

Import dismissImmersiveSpace from the environment.

@Environment(\.dismissImmersiveSpace) private var dismissImmersiveSpace

Create a button to call dismissImmersiveSpace.

Button(action: {
    Task {
        await dismissImmersiveSpace()
        isImmersiveSpacePresented = false
    }
}, label: {
    Text("Close Immersive Space")
})

A note about the space

When coming up with an idea for the space, I wanted something that could work with passthrough or in a fully immersive space. I decide on a simple bubble garden. The Reality Composer Pro file has a bubble that I created with a sphere and a physically based material. In the ImmersiveView, we set up a RealityView that imports the scene and clones the bubble 100 times, assigning random positions around the player.

I also added preferredSurroundingsEffect with a color value to change the appearance of passthrough. When we tap a bubble, we change the color to a random value, then remove the tapped entity.

struct ImmersiveView: View {

    @State var sceneColor: Color = .pink

    let colors: [Color] = [
        .red,
        .blue,
        .pink,
        .green,
        .purple,
        .orange,
        .cyan,
        .indigo
    ]

    var body: some View {
        RealityView { content in
            if let root = try? await Entity(named: "Immersive", in: realityKitContentBundle) {
                content.add(root)

                if let glassSphere = root.findEntity(named: "GlassSphere") {
                    glassSphere.components[HoverEffectComponent.self] = .init()
                    createClones(root, glassSphere: glassSphere)
                }
            }
        }
        .preferredSurroundingsEffect(.colorMultiply(sceneColor))
        .gesture(tap)
    }

    var tap: some Gesture {
        TapGesture()
            .targetedToAnyEntity()
            .onEnded { value in
                // Ensure the new color is different from the current sceneColor
                let availableColors = colors.filter { $0 != sceneColor }
                if let newColor = availableColors.randomElement() {
                    sceneColor = newColor
                }
                value.entity.removeFromParent()
            }
    }


    func createClones(_ root: Entity, glassSphere: Entity) {
        let centerPos = SIMD3<Float>(0, 1.5, 0)
        for _ in 1...100 {
            let clone = glassSphere.clone(recursive: true)
            // Random spherical coordinates - thanks ChatGPT!
            let distance = Float.random(in: 1...3) // Radius between 1 and 3
            let theta = Float.random(in: 0...(2 * .pi)) // Random angle for the horizontal plane
            let phi = Float.random(in: 0...(Float.pi)) // Random angle for the vertical plane

            // Convert spherical coordinates to Cartesian
            let x = distance * sin(phi) * cos(theta)
            let y = distance * sin(phi) * sin(theta)
            let z = distance * cos(phi)
            clone.position = centerPos + SIMD3(x, y, z)
            root.addChild(clone)
        }
    }
}

It’s not much, but it was fun to build and fun to play with.

Video Demo Warning: Flashing Lights

A video recorded on Apple Vision Pro, showing a button tapped in a window to open an immersive space.

Sample code is available in Garden12 in the Step Into Example Projects repo.

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?