Using Push Window to hide the main app window in an immersive space

Sometimes we want to keep an existing window, but show another window while presenting an immersive space.

The Problem

We have a main app window with a complex navigation stack, data, content, controls etc. We don’t want that window showing while the user is in an immersive space. Closing the main window will lose any context our user has in that window.

The Solution

We can use Push Window to replace the main window. This option came with visionOS 2.0 and it can be very useful. If you have ever used the Photos app on visionOS, then you have already seen Push Window in action.

Considering the case above, we want to hide the main window. We can use Push Window to present another window it the same place. The main window is hidden and is restored as soon as the pushed window is closed. I often use this approach to provide controls in the immersive space.

  • Media controls
  • Maps to navigate a space
  • Editing control for immersive content

See also:
Hide a window when presenting an immersive space
Hide a window with an invisible Push Window while presenting an immersive space

Advantages

  • We don’t have to modify our main window for this technique to work.
  • Easy to set up.

Disadvantages

  • This approach only works with one window.
  • Moving the pushed window will change the position of the main window, which may not be desirable in some cases.

Let’s see it in action

A video tour of the example. A main window is replaced by the pushed window when the immersive space is opened. The user moves the pushed window, then exits the space. The main window is in the location of the pushed window.

Example Code

In our app view, we defined a couple of window groups.

struct Garden026App: App {

    @State private var appModel = AppModel()

    var body: some Scene {
        WindowGroup(id: "MainWindow") {
            ContentView()
                .environment(appModel)
        }
        .defaultSize(CGSize(width: 600, height: 600))

        WindowGroup(id: "PushWindow") {
            PushWindowContent()
                .environment(appModel)
        }
        .defaultSize(CGSize(width: 300, height: 300))

        ImmersiveSpace(id: "GardenScene") {
            ImmersiveView()
                .environment(appModel)

        }
        .immersionStyle(selection: .constant(.full), in: .full)
    }
}

In our ContentView we can add pushWindow

@Environment(\.pushWindow) private var pushWindow

When we present the immersive space, we can also call pushWindow

Task {
    if(appModel.gardenOpen) {
        await dismissImmersiveSpace()
        return
    } else if (!appModel.gardenOpen) {
        await openImmersiveSpace(id: "GardenScene")
        // Call Push Window
        pushWindow(id: "PushWindow")
    }
}

When we close the immersive space, we can dismiss the pushed window

dismissWindow(id: "PushWindow")

Or, if we close the pushed window, we can dismiss the immersive space

if(appModel.gardenOpen) {
  await dismissImmersiveSpace()
}

You can find the rest of the code in Garden 026 in this repo.

Support our work so we can continue to bring you new examples and articles.

Questions or feedback?