ios: open new windows through SwiftUI and hide the item when unsupported

"New Window…" called requestSceneSessionActivation with no error
handler whenever UIApplication reports multiple scenes. On iPhone Duo
that stays true although new scenes are only allowed on the inner
display, so the request could fail silently on the outer one. Give the
window group an identifier and use openWindow from iOS 16, gated on the
supportsMultipleWindows environment value which follows the system's
current answer; the iOS 15 path keeps the UIKit call but now reports a
refusal in an alert.

Two windows of one session could also pick the same display, and both
would then request its resolution with the last one winning. The window
and external monitor pickers only offer devices that no other window
already shows.

Assisted-by: Claude:claude-fable-5-1
This commit is contained in:
osy
2026-09-24 20:52:49 -05:00
parent 501b1244a5
commit 14bb9d11ed
2 changed files with 59 additions and 13 deletions
+4 -1
View File
@@ -36,8 +36,11 @@ struct UTMApp: App {
}
}
/// Identifies the window group for `openWindow`.
static let windowID = "main"
var body: some Scene {
WindowGroup {
WindowGroup(id: Self.windowID) {
UTMSingleWindowView(data: data)
.externalDisplayAccessory()
}.commands {
+55 -12
View File
@@ -23,18 +23,22 @@ struct VMToolbarDisplayMenuView: View {
#if os(visionOS)
@Environment(\.supportsMultipleWindows) private var supportsMultipleWindows
@Environment(\.openWindow) private var openWindow
#else
private var supportsMultipleWindows: Bool {
UIApplication.shared.supportsMultipleScenes
}
#endif
/// Two views of one display would both request its resolution, so a window only offers what no
/// other window shows, and what it shows itself so that its selection is always in the list.
private func availableDevices(for windowID: VMSessionState.WindowID?, current: VMWindowState.Device?) -> [VMWindowState.Device] {
session.devices.filter { device in
device == current || !session.windowDeviceMap.contains { $0.key != windowID && $0.value == device }
}
}
var body: some View {
Menu {
Menu {
Picker("", selection: $state.device) {
MenuLabel("None", systemImage: "rectangle.dashed").tag(nil as VMWindowState.Device?)
ForEach(session.devices) { device in
ForEach(availableDevices(for: state.id, current: state.device)) { device in
switch device {
case .serial(_, let index):
MenuLabel("Serial \(index): \(session.qemuConfig.serials[index].target.prettyValue)", systemImage: "rectangle.connected.to.line.below").tag(device as VMWindowState.Device?)
@@ -55,7 +59,7 @@ struct VMToolbarDisplayMenuView: View {
}
Picker("", selection: $externalDevice) {
MenuLabel("None", systemImage: "rectangle.dashed").tag(nil as VMWindowState.Device?)
ForEach(session.devices) { device in
ForEach(availableDevices(for: externalWindowBinding.wrappedValue.id, current: externalWindowBinding.wrappedValue.device)) { device in
switch device {
case .serial(_, let index):
MenuLabel("Serial \(index): \(session.qemuConfig.serials[index].target.prettyValue)", systemImage: "rectangle.connected.to.line.below").tag(device as VMWindowState.Device?)
@@ -68,19 +72,29 @@ struct VMToolbarDisplayMenuView: View {
MenuLabel("External Monitor", systemImage: "rectangle.on.rectangle")
}
}
#if os(visionOS)
if supportsMultipleWindows {
Divider()
Button {
#if os(visionOS)
openWindow(value: session.newWindow())
#else
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: nil, options: nil, errorHandler: nil)
#endif
} label: {
MenuLabel("New Window…", systemImage: "plus.rectangle.on.rectangle")
NewWindowLabel()
}
}
#else
if #available(iOS 16, *) {
NewWindowMenuItem()
} else if UIApplication.shared.supportsMultipleScenes {
Divider()
Button {
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: nil, options: nil) { error in
state.alert = .nonfatalError(error.localizedDescription)
}
} label: {
NewWindowLabel()
}
}
#endif
} label: {
Label("Display", systemImage: "rectangle.on.rectangle")
}.overlay(Badge(count: session.devices.count), alignment: .topTrailing)
@@ -90,6 +104,35 @@ struct VMToolbarDisplayMenuView: View {
}
}
private struct NewWindowLabel: View {
var body: some View {
MenuLabel("New Window…", systemImage: "plus.rectangle.on.rectangle")
}
}
#if !os(visionOS)
/// Opens another scene through SwiftUI, which also knows when the system will not allow one.
///
/// `UIApplication.supportsMultipleScenes` stays true on iPhone Duo even though new scenes can
/// only be created on the inner display, so the item hides itself from the environment instead.
@available(iOS 16, *)
private struct NewWindowMenuItem: View {
@Environment(\.supportsMultipleWindows) private var supportsMultipleWindows
@Environment(\.openWindow) private var openWindow
var body: some View {
if supportsMultipleWindows {
Divider()
Button {
openWindow(id: UTMApp.windowID)
} label: {
NewWindowLabel()
}
}
}
}
#endif
private struct Badge: View {
let count: Int