From d6306ed8c337e3e663c26ca33cb302b2e3fb2b5b Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sun, 20 Sep 2026 03:45:31 -0500 Subject: [PATCH] vm(iOS): confirm before closing the window of a running VM On iPad, closing the last window quits the app: the system ends the process about 100 ms after the scene disconnects, so a running VM was lost without any warning. macOS already asks through windowShouldClose. iOS 27 lets a scene ask for confirmation first, so do that for the last interactive window unless the VM is stopped or paused with its state saved. Stop replaces the Close button. There is no Suspend button because the process does not live long enough after the window closes to save the state; a button that started the save was tried and the state was never written. Not available on visionOS, and not needed for UTM Remote where the VM keeps running on the server. Assisted-by: Claude:claude-fable-5-1 --- Platform/iOS/VMWindowView.swift | 34 +++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/Platform/iOS/VMWindowView.swift b/Platform/iOS/VMWindowView.swift index 688ff6d0..906b3ad5 100644 --- a/Platform/iOS/VMWindowView.swift +++ b/Platform/iOS/VMWindowView.swift @@ -107,6 +107,7 @@ struct VMWindowView: View { #endif } .modifier(VMToolbarOrnamentModifier(state: $state)) + .modifier(VMWindowCloseConfirmationModifier(isInteractive: isInteractive)) .statusBarHidden(true) .alert(item: $state.alert, content: { type in switch type { @@ -319,3 +320,36 @@ private extension View { } } } + +/// Asks before the system closes the last window of a running VM, which would end it without saving. +private struct VMWindowCloseConfirmationModifier: ViewModifier { + let isInteractive: Bool + + @EnvironmentObject private var session: VMSessionState + + private var isLastWindowOfRunningVM: Bool { + let externalWindows = session.externalWindowBinding == nil ? 0 : 1 + // a VM that is still pausing or saving would lose its state as well + let isSafeToClose = session.vmState == .stopped || (session.vmState == .paused && session.vm.registryEntry.isSuspended) + return isInteractive && !isSafeToClose && session.windows.count - externalWindows <= 1 + } + + func body(content: Content) -> some View { + #if os(visionOS) || WITH_REMOTE + content + #else + if #available(iOS 27, *) { + content.dismissalConfirmationDialog("This virtual machine is still running.", shouldPresent: isLastWindowOfRunningVM) { + Button("Stop", role: .destructive) { + session.powerDown() + } + } message: { + Text("Closing the window stops the virtual machine. Any unsaved changes will be lost.") + } + } else { + content + } + #endif + } +} +