ios: keep the Start/Resume button and headless notice out of the fold

A partially folded iPhone Duo reports the fold as a reserved division
region. Alerts, menus and sheets already move out of it, but a control
centred in the VM window lands right in the curve: the Start/Resume
button shown while the VM is stopped or paused, and the notice shown
when no device is selected. Place them in the largest part of the
window that no active division runs through; without a fold that is the
whole window, so nothing changes elsewhere.

The query only exists in the iOS 27.1 SDK, so the code is compiled in
only when the SwiftUI module is at least that version and the project
still builds with Xcode 27.0.

Assisted-by: Claude:claude-fable-5-1
This commit is contained in:
osy
2026-09-24 20:52:49 -05:00
parent c61e6f8611
commit 3cd0f945f0
+60
View File
@@ -98,6 +98,7 @@ struct VMWindowView: View {
.font(.system(size: 128))
.vibrancyEffect()
.vibrancyEffectStyle(.label)
.clearOfReservedDivisions()
}
}.background(Color.black)
.ignoresSafeArea()
@@ -298,10 +299,69 @@ private struct HeadlessView: View {
.clipShape(RoundedRectangle(cornerRadius: 25.0, style: .continuous))
.vibrancyEffect()
.vibrancyEffectStyle(.label)
.clearOfReservedDivisions()
}
}
}
// MARK: - Folding devices
#if !os(visionOS) && canImport(SwiftUI, _version: 8.0.85)
@available(iOS 27.1, *)
extension GeometryProxy {
/// The largest part of the view that no active fold runs through, or all of it.
var frameClearOfDivisions: CGRect {
var frame = CGRect(origin: .zero, size: size)
for region in reservedRegions(kind: .division) where region.isActive {
let division = region.frame
let parts = [
frame.part(maxY: division.minY),
frame.part(minY: division.maxY),
frame.part(maxX: division.minX),
frame.part(minX: division.maxX),
]
frame = parts.max { $0.width * $0.height < $1.width * $1.height }!
}
return frame
}
}
private extension CGRect {
/// The rectangle cut down to the given edges, empty when nothing is left.
func part(minX: CGFloat? = nil, minY: CGFloat? = nil, maxX: CGFloat? = nil, maxY: CGFloat? = nil) -> CGRect {
let left = max(self.minX, minX ?? self.minX)
let top = max(self.minY, minY ?? self.minY)
let right = min(self.maxX, maxX ?? self.maxX)
let bottom = min(self.maxY, maxY ?? self.maxY)
return CGRect(x: left, y: top, width: max(0, right - left), height: max(0, bottom - top))
}
}
#endif
private extension View {
/// Keeps centred interactive content out of the fold of a partially folded device.
///
/// Alerts, menus and sheets move out of the fold by themselves; a control centred in the
/// window does not.
@ViewBuilder
func clearOfReservedDivisions() -> some View {
#if !os(visionOS) && canImport(SwiftUI, _version: 8.0.85)
if #available(iOS 27.1, *) {
GeometryReader { proxy in
let frame = proxy.frameClearOfDivisions
self
.frame(width: frame.width, height: frame.height)
.offset(x: frame.minX, y: frame.minY)
}
} else {
self
}
#else
self
#endif
}
}
#if !os(visionOS)
/// Stub for non-Vision platforms
fileprivate struct VMToolbarOrnamentModifier: ViewModifier {