visionOS: An Introduction

Dec 16 2025 · Swift 6, visionOS 26, Xcode 26

Lesson 03: Building an Immersive View

Demo Part 1

Episode complete

Play next episode

Next
Transcript

Start with the app in the Starter folder, or carry on with the build from lesson 2.

From the Project Navigator, select the ContentListView and embed the Immersive Tab in a NavigationSplitView. In the detail closure, set up a button with the label Open ImmersiveSpace.

NavigationSplitView {
  Text("Immersive Tab")
    .font(.largeTitle)
    .foregroundColor(.orange)
} detail: {
    Button("Open ImmersiveSpace") {
      //
    }
}
.tabItem {
  Image(systemName: "globe")
  Text("Immersive")
}

Start the Preview in the Canvas and check the split view.

In the Views folder, create a new SwiftUI view named ImmersiveView.

You can use the toy biplane from Lesson 2 to recreate a banking, circular flight. Switch over to Reality Composer Pro, where you’ll create our own USD scene file for the flight simulation.

In the Project Browser at the bottom, click the cube icon with the plus sign to create a new scene. Double-click the name and change it to ImmersiveScene.

Note: If the name doesn’t change in the Navigator, click the x to close it. Then, double-click it in the Project Browser again, and it’ll reopen in the Navigator.

Click the + in the top right corner of the screen to open the Content Library. Alternatively, you can choose Show Content Library from the View menu or press Shift-Command-L.

Choose a primitive sphere and drag it to the center of the editor. Use the Inspector to set the position to (0,0,0). Tip: If you can’t see the sphere, double-click its name in the Navigator.

Set the Scale of the sphere to 0.01, 0.01, 0.01. From the Project Browser, drag a toy_biplane_idle.usdz reference into the editor. If it’s too big to see, double-click the Root or the plane in the Navigator. Set the plane’s Z Rotation to 90°, and the biplane will point to the right. Also, set the z position to 60 to move the plane toward the viewer. Make sure you Save the file after editing.

Now switch back to Xcode. Notice that the ImmersiveScene.usda file is now in Sources. Select the ImmersiveView file from the Project Navigator where you can load the scene with a RealityView.

Import RealityKit and RealityKitContent at the top of the file.

Replace the Text("Hello World") view with a RealityView. As you did in the Volume view, unwrap a scene and asynchronously load the entity by the ImmersiveScene from the realityContentBundle.

RealityView { content in
  if let scene = try? await Entity(named: "ImmersiveScene", in: realityKitContentBundle) {
    content.add(scene)
  }
}

Pro Tip: If you see Cannot preview in this file. at any time, you may need to choose Clean Build Folder from the Product menu. The app is getting pretty resource-intensive at this point.

There’s nothing to see here yet since you need to choose the mode of ImmersiveView you want. Generally, you’ll only be able to see Immersive views in the Simulator.

Go to the Vision101App file and add a reference to load the ImmersiveSpace named "ImmersiveScene", that you created in Reality Composer Pro in the body, just below the WindowGroup:

ImmersiveSpace(id: "ImmersiveScene") {
  ImmersiveView()
}

While you’re here, add a state variable for the ImmersionStyle protocol, which you’ll need to open and dismiss the ImmersiveView, with the associated Environment Variables. You’ll see more on this later. The state variable here is used during opening and refers to the type of presented data this immersive space accepts.

@State private var currentStyle: ImmersionStyle = .full

Modify the ImmersiveSpace below in the body with an immersionStyle modifier.

.immersionStyle(selection: $currentStyle, in: .full)

Now go to the ContentListView and stub out another button in the detail: to exit the immersion. Add some color modifiers to make them easier to find in the simulator. Embed them in a HStack.

Button("Open ImmersiveSpace") {

}.foregroundColor(.blue)
Button("Close ImmersiveSpace") {

}.foregroundColor(.red)

Add a couple of environment variables to open and dismiss the Immersive Space at the top of the file. These are the protocol methods that the RealityKit framework provides as EnvironmentVariables.

@Environment(\.openImmersiveSpace) var openImmersiveScene
@Environment(\.dismissImmersiveSpace) var dismissImmersiveScene

To load the ImmersiveSpace, initiate a Task in the button’s action closure and use a unique identifier to refer to the scene to load, in this case, the name you provided in Reality Composer Pro.

Task {
  let result =  await openImmersiveScene(id: "ImmersiveScene")
  if case .error = result {
    print("An error occurred")
  }
}

Also, while you’re here, add the task to the close button action to dismiss the scene.

Task {
  await dismissImmersiveScene()
  print("Dismissing Complete")
}

Build and run to try out the buttons. Press Open ImmersiveView. Note that visionOS shows a dialog to warn the user to be aware of their surroundings.

While in the Simulator, use the Dolly to move away from the scene. Notice that the biplane is around 1.5 meters from the buttons and appears below and behind the user.

Try the dismiss button to return to the room environment you started from.

Pro Tip: if you ever get stuck in an Immersive space, you can tap the Digital Crown on the Vision Pro or tap the Home icon at the top of the screen in the Simulator to jump back to the Shared Space.

To fix the plane’s placement, add an offset modifier to both the y and z axis in the ImmersiveView‘s RealityView. You could do this in Reality Composer Pro, but you’ll soon add a norbit animation around the (0,0,0) origin.

Reminder: If you can’t locate the closing brace for the RealityView, double-click the opening brace, and Xcode will select everything, including the closing brace.

.offset(y: -2000)
.offset(z: -1500)

Build and run to see the adjusted position.

You fixed the immersive scene’s position, but the main window is still visible. Fix that by changing the opacity.

At the top of ContentListView, add a state variable isShowingImmersive with a default false value.

@State private var isShowingImmersive = false

Next, add an opacity toggle with a ternary operator to the bottom of the TabView beside its closing brace.

.opacity(isShowingImmersive ? 0 : 1)

You’ll need to move the dismiss button out of the TabView:

  • Collapse the TabView with the Code Folding Ribbon.
  • Right-click it and Embed in a VStack.
  • Un-collapse the TabView.
  • Move the dismiss button below the TabView but still inside the VStack.

Tip: You can select the button code and use Command-Option-] to move the selected code down line by line.

With the dismiss button below the TabView, add the modifier to reverse the opacity to make the dismiss button visible while immersed. Notice the values in the operator are reversed. Also, add the logic to reverse the value of isShowingImmersive to false.

Button("Close ImmersiveSpace") {
  Task {
    await dismissImmersiveScene()
    print("Dismissing Complete")
    isShowingImmersive = false
  }
}.foregroundColor(.red)
  .opacity(isShowingImmersive ? 1 : 0)

Finally, set isShowingImmersive to true in the “Open ImmersiveSpace” button action.

Button("Open ImmersiveSpace") {
    Task {
      let result =  await openImmersiveScene(id: "ImmersiveScene")
      if case .error = result {
        print("An error occurred")
      }
      isShowingImmersive = true
    }
}.foregroundColor(.blue)

Build and run to see the effect of setting the window’s opacity.

Enable the Toy Biplane animations by adding an update: closure on the RealityView in ImmersiveView file:

update: { content in
  if let scene = content.entities.first {
    scene.availableAnimations.forEach { animation in
      scene.playAnimation(animation.repeat(), transitionDuration: 3, startsPaused: false)
    }
  }
}

Build and run, then open the ImmersiveScene to see the animations. Notice the plane bobs and the propeller turns.

That’s all for part one. In the next demo, you’ll add a flightpath for the biplane, a virtual background panorama and adjust the lighting with a skybox.

See forum comments
Cinema mode Download course materials from Github
Previous: Instruction Next: Demo Part 2