How to use pushWindow to replace a window in visionOS

The pushWindow presentation action works much like openWindow, with a few changes.

  • The pushed (new) window will appear in the same position as the starting window, using the same center point for alignment
  • The starting window will be hidden while the pushed window is open
  • The starting window will reappear once the pushed window is closed
  • If a user moves the pushed window before closing it, the starting window will reappear at this new location 
  • The pushed window can not call pushWindow to push more windows onto the stack

Use cases:

This presentation style is great when you need to show a view with a different or complex view hierarchy. It is also useful if you want to keep the initial window position or return to the main window.

  1. Splash screens 
  2. Advanced pickers
  3. Videos or previews

† The exception to this is if we have closed the starting window. For example

  • Window A pushes B, the closes itself
  • Window B can now push Window C

I’m not sure if this is supported behavior, so use this with caution.

Using Push Window for a Splash Screen

We define two window groups with a Splash Screen as the default view that will open when the app launches.

// By default, visionOS will use the first Window Group defined in the app file when launching an app.
WindowGroup(id: "SplashScreen") {
    SplashScreen()
}
.defaultSize(width: 500, height: 500)

WindowGroup(id: "MainWindow") {
    ContentView()
        .environment(appModel)
}
.defaultSize(width: 500, height: 500)

In the SplashScreen view we set up a timer to call pushWindow after an interval. This will also call dismissWindow on the splash screen to close it after the main window has opened.

.onAppear {
    DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
        // Open the main window with a shared id using pushWindow instead of openWindow
        pushWindow(id: "MainWindow")

        // A short delay before closing the splash screen
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
            dismissWindow(id: "SplashScreen")
        }
    }
}

Updates

Starting in visionOS 26 Apple introduce pinned and locked windows. These windows are restored by visionOS when reentering a room or after a reboot. Drew Olbrich has documented two major issues related to pushWindow and see restoration.

Drew has also shared a workaround. We can use .restorationBehavior(.disabled) to the WindowGroup of a window you intend to present with pushWindow, neither bug occurs.

visionOS simulator video showing push window used with a splash screen and a picker window.

Using Push Window as a Picker

Let’s add another window group for a Flower Picker.

WindowGroup(id: "FlowerPicker") {
    FlowerPicker()
        .environment(appModel)
}
.defaultSize(width: 500, height: 500)

Then add a button to the ContentView to open this picker.

Button(action: {
      // Open the new FlowerPicker window
      pushWindow(id: "FlowerPicker")
}, label: {
      Label("Change Flower", image: "pencil.circle")
})

The Flower Picker view presents a handful of buttons that will change the selected value and dismiss the picker.

struct FlowerPicker: View {
    @Environment(\.dismissWindow) private var dismissWindow
    @Environment(AppModel.self) private var appModel

    var body: some View {
        HStack {
            Button(action: {
                // Set the selected value in our data store
                appModel.selectedFlower = "🌸"
                // Dismiss the 'pushed' current window to revert back to the parent window
                dismissWindow(id: "FlowerPicker")
            }, label: {
                Text("🌸")
            })
            ...
        }
        .buttonStyle(.plain)
        .font(.system(size: 100))
        .padding()
    }
}

The sample code for this post can be found in Garden05 in Step Into Examples on GitHub

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?