visionOS 26: is there any way yet to distinguish a user-initiated window close from system out-of-FoV backgrounding

This was confirmed as a framework gap in an accepted answer from an Apple Vision Pro engineer in June 2024 (https://developer.apple.com/forums/thread/758014?answerId=792769022#792769022): .background fires identically whether the user taps a window's close button or the system backgrounds a window that's been out of the field of view for ~61 seconds, and there's no app-visible signal that distinguishes the two. The recommendation at the time was to use a gesture/affordance to reopen the window, and to file an enhancement request.

Two years on, with the window-management APIs that have shipped since, I want to confirm whether the situation has changed as of visionOS 26.

My case: VisionBlazer, a native spatial 3D creation tool (TestFlight beta, August 2026 launch). Users routinely work with several SwiftUI WindowGroup tool windows open at once — drawing tools, materials, timeline, properties, lighting — parked spatially around an ImmersiveSpace. Parking a window behind or beside you is core to the workflow. The app should terminate when the user closes the primary window, but must not terminate when a secondary window (or the primary) is simply parked out of view.

What I've verified on-device (visionOS 26.5):

scenePhase == .background fires identically for a user close tap and for the ~61s out-of-FoV backgrounding (SurfBoard: "…is out of FOV after 60.99 seconds. Backgrounding"). The phase sequence (active → inactive → background) and timing (~0.2–0.4s gap) are indistinguishable between the two cases. scenePhase on visionOS reflects visibility, not focus — a parked window stays .active while the user edits elsewhere, until the out-of-FoV timer fires. The session identifier reachable from the window's view hierarchy (view.window!.windowScene.session.persistentIdentifier) never matches the identifier reported by application(_:didDiscardSceneSessions:) or UIScene.didDisconnectNotification for that same close. The view-visible session stays in UIApplication.shared.openSessions indefinitely after the close. Those disconnect/discard callbacks arrive ~10–15s late and only ever carry foreign session identifiers, so they can't be attributed to a specific window. Stale-session discards from prior launches pollute the signal further. onDisappear does not fire on user close.

Five strategies tried, all failed: (1) scene-object identity captured at didMoveToWindow; (2) session.persistentIdentifier matching against openSessions; (3) live re-capture of scene/session from the view hierarchy on every lifecycle change; (4) temporal correlation of didEnterBackground/didDisconnect; (5) a focus-recency heuristic on scenePhase transitions. All fail on the identifier mismatch and the visibility-not-focus semantics above.

Questions:

As of visionOS 26, is there now any supported way to detect that the user intentionally closed a specific window, distinct from system backgrounding? Is there a SwiftUI or scene-delegate callback tied to a window's own scene that fires only on user close? Is there a dismissalReason (or equivalent) anywhere on the close path? If none of the above exists, is an explicit in-app Quit button still the intended pattern for "quit when the main window is closed"?

I have a focused test project reproducing all of this and can link it. Thanks.

Hey @RichBlum,

There is no API to determine what triggered a scene change to the background. Please file an enhancement request using Feedback Assistant with details as to why and how you'd like to use an API for this feature.

I'd love to learn more about your use case and understand if I can point you to a current API that might be able to serve you better. I'd certainly love to look at your test project reproducing the issue.

The app should terminate when the user closes the primary window, but must not terminate when a secondary window (or the primary) is simply parked out of view.

This sentence concerns me as it's up to the user to close the last scene of your app. Your app can dismiss its own scenes as long as it’s not the last scene. I'd love to see an example of the complete flow of your app from launching the app, to leaving the experience to better understand what APIs you should be using.

I would encourage you to checkout how this is handled in the following samples that use multiple scenes:

Thanks,
Michael

Thanks Michael — enhancement request filed as FB24076974, and a focused test project is here: https://www.icloud.com/iclouddrive/0fcptDNbtKk0CCcs-EYMS_2bA#WindowCloseRepro Building it produced results worth reporting, including a correction to my original post.

Complete flow: Launch opens the primary window (the editor menu) which immediately opens a fully immersive space — the space IS the product: users build and animate 3D scenes inside it in .full immersion. They open and close secondary tool windows (materials, timeline, lighting, …) around the space as they work, and park any window — including the primary — beside or behind them. There is also a player flow: tapping a scene file in Files launches straight into the immersive space with a single compact transport-bar window.

Correction first: in the focused repro (visionOS 26.5, on-device), the session-identifier mismatch I reported does NOT reproduce. While another regular window is open, closing a window delivers didDisconnect + didDiscardSceneSessions within ~30 ms, carrying an identifier that matches the closed window's view-reachable windowScene.session.persistentIdentifier, plus root-view onDisappear — attribution works, exactly as your window-life-cycle article describes. My earlier in-app observations of unmatched identifiers were evidently stale-session discards misattributed to the close. I'm happy to be wrong about that half.

The gap that DOES reproduce, cleanly: closing the LAST regular window while the fully immersive space remains open. On-device, that close produces didEnterBackground + scenePhase → background and then nothing — no disconnect, no discard, no onDisappear, session still in openSessions — for the entire time the user remains in the immersive space. That signature is identical to the ~61 s out-of-view backgrounding, so the app cannot distinguish "the user closed my last window and is now standing inside my environment with no chrome at all" from "my window is parked behind the user". Your doc states this case directly ("The last closed nonimmersive scene enters the ScenePhase.background phase but doesn't immediately receive the onDisappear() callback") — for a fully-immersive app it is the one lifecycle moment we most need to observe (dismiss the space, save state, end the session), and the one close that produces no signal.

On your concern — the app never dismisses its own last scene. The problem is the reverse direction: after the USER closes the last window, they're still inside our fully immersive environment, which has no close affordance of its own. From the user's perspective that close should end the session; the app just has nothing to act on.

The enhancement filed: deliver onDisappear/disconnect (or a dismissal callback with a reason) to the last nonimmersive scene when an immersive space remains open — or extend the association model from your "Associating a window with an immersive space" sample (which pairs a pushed window with a space) to a primary window.

One question meanwhile: is there a recommended pattern today for a fully-immersive app whose last window closes — is "Embedding controls in an immersive space" (persistent in-space chrome) the intended fallback?

Hey @RichBlum,

Thanks for this additional information.

I want to directly address your comment from FB24076974 which I feel summarizes your request.

For an app whose fully immersive space outlives its windows, the close of its last regular window is the one lifecycle moment it most needs to observe — the user is now standing inside the app's environment with no chrome at all, and the app should be able to respond (dismiss the space, save state, or end the session). It is the one close that produces no signal.

The app can respond to this state and we showcase this behavior in our sample code. The app cannot dismiss the immersive space when it is the app's only open scene – it is up to the user to close the application. It seems like you want to programmatically quit your app (from a custom button or based off scene state), but this is unsupported. On iOS, swiping closes an app; on visionOS there is the system provided close button or the Digital Crown.

Hello World does a great job of showcasing the expected behavior for three different types of experiences:

  • the Planet Earth module shows the behavior with a volume.
  • the Objects in Orbit module shows the behavior of an immersive space that doesn't require a window. In this immersive space a ViewAttachmentComponent is placed in front of the user when the main window is closed. The user has the option to leave the immersive space or reopen the main window. Depending on your experience you could reopen the main window by tapping on an entity in your scene or attach this component to another entity rather than placing it statically in the space.
  • the Solar System module presents a new window that only exists within the Immersive Space. That window and the immersive space always close together. Use this behavior for when these two scenes should not be displayed independently from each other.

If the user might want to be in the immersive space without a window from another scene, follow the behavior outlined in Embedding controls in an immersive space. In this case the user can press the Digital Crown to close the app (and the immersive space) at any time. When the user relaunches the app from the home screen the system opens the preferred scene of the default scene type; this can be configured by the defaultLaunchBehavior method.

If the immersive space should exist alongside another window, follow the example set in Associating a window with an immersive space.

The example project that you included provides a great illustration of the issue from a API standpoint, but I'd like to get to know the flow of your application better.

  • Why does launching the primary window immediately open an immersive space?
  • Is there a case where someone using your app might want to navigate your app side-by-side with other applications?
  • Why is there a difference when launching from a scene file which doesn't provide the primary editor menu window?
  • Have you considered an experience that starts with a window previewing the different immersive content? From that window the user could choose to open the immersive space with the editor menu or the compact menu.

Thanks,
Michael

visionOS 26: is there any way yet to distinguish a user-initiated window close from system out-of-FoV backgrounding
 
 
Q