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
This commit is contained in:
osy
2026-09-20 03:45:31 -05:00
parent 9d00a54833
commit d6306ed8c3
+34
View File
@@ -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
}
}