visionOS: Unable to programmatically close child WindowGroup when parent window closes

Hi , I'm struggling with visionOS window management and need help with closing child windows programmatically.

App Structure

My app has a Main-Sub window hierarchy:

  • AWindow (Home/Main)
  • BWindow (Main feature window)
  • CWindow (Tool window - child of BWindow)

Navigation flow:

  • AWindow → BWindow (switch, 1 window on screen)
  • BWindow → CWindow (opens child, 2 windows on screen)

I want BWindow and CWindow to be separate movable windows (not sheet/popover) so users can position them independently in space.

The Problem

  1. CWindow doesn't close when BWindow closes by tapping the X button below the app (next to the window bar)

  • User clicks X on BWindow → BWindow closes but CWindow remains
  • CWindow becomes orphaned on screen
  • Can close CWindow programmatically when switching BWindow back to AWindow
  1. App launch issue
    • After closing both windows, CWindow is remembered as last window
    • Reopening app shows only CWindow instead of BWindow
    • User gets stuck in CWindow with no way back to BWindow

I've Tried Environment dismissWindow in cleanup but its not working.

// In BWindow.swift
.onDisappear {
        if windowManager.isWindowOpen("cWindow") {
        dismissWindow(id: "cWindow")
    }
}

My App Structure Code Now

// in MyNameApp.swift
@main
struct MyNameApp: App {
    var body: some Scene {
        WindowGroup(id: "aWindow") {
            AWindow()
        }
        WindowGroup(id: "bWindow") {
            BWindow()
        }
        WindowGroup(id: "cWindow") {
            CWindow()
        }
    }
}
// WindowStateManager.swift
class WindowStateManager: ObservableObject {
    static let shared = WindowStateManager()
    @Published private var openWindows: Set<String> = []
    @Published private var windowDependencies: [String: String] = [:] 
    private init() {}
    func markWindowAsOpen(_ id: String) {
        markWindowAsOpen(id, parent: nil)
    }
    func markWindowAsClosed(_ id: String) {
        openWindows.remove(id)
        windowDependencies[id] = nil
    }
    func isWindowOpen(_ id: String) -> Bool {
        let isOpen = openWindows.contains(id)
        return isOpen
    }
    func markWindowAsOpen(_ id: String, parent: String? = nil) {
        openWindows.insert(id)
        if let parentId = parent {
            windowDependencies[id] = parentId
        }
    }
    func getParentWindow(of childId: String) -> String? {
        let parent = windowDependencies[childId]
        return parent
    }
    func getChildWindows(of parentId: String) -> [String] {
        let children = windowDependencies.compactMap { key, value in
            value == parentId ? key : nil
        }
        return children
    }
    func setNextWindowParent(_ parentId: String) {
        UserDefaults.standard.set(parentId, forKey: "nextWindowParent")
    }
    func getAndClearNextWindowParent() -> String? {
        let parent = UserDefaults.standard.string(forKey: "nextWindowParent")
        UserDefaults.standard.removeObject(forKey: "nextWindowParent")
        return parent
    }
    func forceCloseChildWindows(of parentId: String) {
        let children = getChildWindows(of: parentId)
        for child in children {
            markWindowAsClosed(child)
            NotificationCenter.default.post(
                name: Notification.Name("ForceCloseWindow"),
                object: nil,
                userInfo: ["windowId": child]
            )
            forceCloseChildWindows(of: child)
        }
    }
    func hasMainWindowOpen() -> Bool {
        let mainWindows = ["main", "bWindow"]
        return mainWindows.contains { isWindowOpen($0) }
    }
    func cleanupOrphanWindows() {
        for (child, parent) in windowDependencies {
            if isWindowOpen(child) && !isWindowOpen(parent) {
                NotificationCenter.default.post(
                    name: Notification.Name("ForceCloseWindow"),
                    object: nil,
                    userInfo: ["windowId": child]
                )
                markWindowAsClosed(child)
            }
        }
    }
}
// BWindow.swift
struct BWindow: View {
    @Environment(\.dismissWindow) private var dismissWindow
    @ObservedObject private var windowManager = WindowStateManager.shared
    var body: some View {
        VStack {
            Button("Open C Window") {
                windowManager.setNextWindowParent("bWindow")
                openWindow(id: "cWindow")
            }
        }
        .onAppear {
            windowManager.markWindowAsOpen("bWindow")
        }
        .onDisappear {
            windowManager.markWindowAsClosed("bWindow")
            windowManager.forceCloseChildWindows(of: "bWindow")
        }
        .onChange(of: scenePhase) { oldValue, newValue in
            if newValue == .background || newValue == .inactive {
                 windowManager.forceCloseChildWindows(of: "bWindow")
            }
        }
    }
}
// CWindow.swift
import SwiftUI
struct cWindow: View {
    @ObservedObject private var windowManager = WindowStateManager.shared
    @State private var shouldClose = false
    var body: some View {
     // Content
        }
        .onDisappear {
            windowManager.markWindowAsClosed("cWindow")
            NotificationCenter.default.removeObserver(
                self,
                name: Notification.Name("ForceCloseWindow"),
                object: nil
            )
        }
        .onChange(of: scenePhase) { oldValue, newValue in
            if newValue == .background {
            }
        }
        .onAppear {
            let parent = windowManager.getAndClearNextWindowParent()
            windowManager.markWindowAsOpen("cWindow", parent: parent)
            NotificationCenter.default.addObserver(
                forName: Notification.Name("ForceCloseWindow"),
                object: nil, queue: .main) { notification in
                if let windowId = notification.userInfo?["windowId"] as? String, windowId == "cWindow" {
                    shouldClose = true
                }
            }
        }
        .onChange(of: shouldClose) { _, newValue in
            if newValue {
                dismissWindow()
            }
        }
}

The logs show everything executes correctly, but CWindow remains visible on screen.

Questions

  1. Why doesn't dismissWindow(id:) work in cleanup scenarios?
  2. Is there a proper way to create a window relationships like parent-child relationships in visionOS?
  3. How can I ensure main windows open on app launch instead of tool windows?
  4. What's the recommended pattern for dependent windows in visionOS?

Environment: Xcode 16.2, visionOS 2.0, SwiftUI

Hello,

You could try making use of scene phases to alert the app when the parent window is being closed.

See https://developer.apple.com/documentation/swiftui/scenephase Unfortunately, this approach doesn't work for us consistently in Immersive Space.

In Vision 26, we have some additional tools available. Check out .restorationBehavior and .defaultLaunchBehavior described in this document. https://developer.apple.com/documentation/visionos/adopting-best-practices-for-scene-restoration/

Good luck!

Hello @Jir_253,

Thanks for your questions. I’d recommend that you start by watching Set the scene with SwiftUI in visionOS from this year’s WWDC. Your “CWindow” sounds similar to the Tools window covered in this talk:

WindowGroup("Tools", id: "tools") {
    ToolsView()
}
.restorationBehavior(.disabled)
.defaultLaunchBehavior(.suppressed)

Similar to iOS where you cannot quit the application yourself, you cannot dismiss the last window of your application. When a user goes to close the main window when the secondary window is open, you cannot close this secondary window if it’s the only window group that is open in your application. Consider adding an affordance to the secondary window to reopen the main window in this case.

There's no supported way for you to create parent/child window relationships with the APIs currently available. If you'd like us to consider adding the necessary functionality, please file an enhancement request using Feedback Assistant. Once you file the request, please post the FB number here.

If you're not familiar with how to file enhancement requests, take a look at Bug Reporting: How and Why?

Let me know if you have any additional questions,
Michael

visionOS: Unable to programmatically close child WindowGroup when parent window closes
 
 
Q