Using scene phase to provide an exit for an immersive space
Building on the Scene Phase concepts we learned with Windows, with an anchored twist.
When working with immersive spaces, it can be hard to know what to do with the main window. Some apps may want to keep it open as a sort of control panel, while other may want to hide or close it.
Using Scene Phase to manage window state
We can use scene phase to observe the state of the main window. Then, we provide a means to restore it if the user closes it. I’ve used this patten to provide an exit to a space or to reopen a utility window.
In this example, we create a hand anchored attachment. It will show when the user is in the space and when the main window has been closed.
// Track the state of our scenes
class AppModel {
var mainWindowOpen: Bool = false
var gardenOpen: Bool = false
}
In the Content View, we can update the app model based on scene phase.
.onChange(of: scenePhase, initial: true) {
switch scenePhase {
case .inactive, .background:
appModel.mainWindowOpen = false
case .active:
appModel.mainWindowOpen = true
@unknown default:
appModel.mainWindowOpen = false
}
}In the Immersive Space, we can watch this value and conditionally show an attachment.
struct ImmersiveView: View {
@Environment(AppModel.self) private var appModel
@Environment(\.scenePhase) private var scenePhase
@Environment(\.openWindow) private var openWindow
@State var handTrackedEntity: Entity = {
let handAnchor = AnchorEntity(.hand(.left, location: .aboveHand))
return handAnchor
}()
var body: some View {
RealityView { content, attachments 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)
}
content.add(handTrackedEntity)
if let attachmentEntity = attachments.entity(for: "AttachmentContent") {
attachmentEntity.components[BillboardComponent.self] = .init()
handTrackedEntity.addChild(attachmentEntity)
}
}
} update: { content, attachments in
} attachments: {
Attachment(id: "AttachmentContent") {
HStack(spacing: 12) {
Button(action: {
openWindow(id: "MainWindow")
}, label: {
Image(systemName: "arrow.2.circlepath.circle")
})
}
.opacity(appModel.mainWindowOpen ? 0 : 1)
}
}
.preferredSurroundingsEffect(.colorMultiply(.stepBack02))
.gesture(tap)
.onChange(of: scenePhase, initial: true) {
switch scenePhase {
case .inactive, .background:
appModel.gardenOpen = false
case .active:
appModel.gardenOpen = true
@unknown default:
appModel.gardenOpen = false
}
}
}
var tap: some Gesture {...}
func createClones(_ root: Entity, glassSphere: Entity) {...}
}
Notice that we also use scene phase to update the app model about the state of the space. We can observe this in the Content View and update our button accordingly.
Button(action: {
Task {
if(appModel.gardenOpen) {
await dismissImmersiveSpace()
return
} else if (!appModel.gardenOpen) {
await openImmersiveSpace(id: "GardenScene")
}
}
}, label: {
Text(appModel.gardenOpen ? "Close Immersive Space" :"Open Immersive Space")
})
Video demo
A video recorded on Apple Vision Pro. The user enters a space and taps a few objects. They close the window and a new control appears on their left hand. Tapping this control reopens the window.
This is only one example of how to provide an exit in an immersive space. I like reopening the main window, which will provide the actual means of closing the space. You could also show a 3D object in the scene that will open a window, the close the space. Just make sure that your window has actually finished opening before you try to close the space!
Sample code is available in Garden13 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.

Follow Step Into Vision