After fixing these 2 issues for Focus browser & Firefox iOS, I decided to ask Codex to make me a small demo for these 2 layout guide:
- blue = safe area layout (
safeAreaLayoutGuide) - orange =
layoutGuide(for: .margins(cornerAdaptation: .horizontal))(introduced in iOS 26 SDKs)
What if we use layoutGuide(for: .margins(cornerAdaptation: .vertical))? It goes right underneath the traffic light.
As we can see, safe area layout can help with the notch or home bar but won't help us avoid UI components getting overlapped with iPadOS traffic light in window mode. .margins(cornerAdaptation: on the other hand, is introduced for that scenario.
Source code for the small demo:
import SwiftUI
struct BlogWindowControlLayoutSimplifiedDemo: View {
@State private var safeAreaFrame = CGRect.zero
@State private var windowControlFrame = CGRect.zero
var body: some View {
GeometryReader { proxy in
ZStack(alignment: .topLeading) {
LayoutRegionOutline(
frame: safeAreaFrame,
color: .blue,
dash: []
)
LayoutRegionOutline(
frame: windowControlFrame,
color: .orange,
dash: [8, 5]
)
WindowLayoutGuideReader(
safeAreaFrame: $safeAreaFrame,
windowControlFrame: $windowControlFrame
)
.allowsHitTesting(false)
}
.frame(width: proxy.size.width, height: proxy.size.height)
}
.ignoresSafeArea()
.overlay(alignment: .bottomLeading) {
LayoutLegend()
.padding(16)
}
}
}
private struct LayoutRegionOutline: View {
let frame: CGRect
let color: Color
let dash: [CGFloat]
var body: some View {
Rectangle()
.stroke(color, style: StrokeStyle(lineWidth: 2, dash: dash))
.frame(width: max(0, frame.width), height: max(0, frame.height))
.position(x: frame.midX, y: frame.midY)
.allowsHitTesting(false)
}
}
private struct LayoutLegend: View {
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Label("safeAreaLayoutGuide", systemImage: "rectangle.dashed")
.foregroundStyle(.blue)
Label("window-control margins", systemImage: "rectangle.dashed")
.foregroundStyle(.orange)
}
.font(.caption)
.padding(10)
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 10))
}
}
private struct WindowLayoutGuideReader: UIViewRepresentable {
@Binding var safeAreaFrame: CGRect
@Binding var windowControlFrame: CGRect
func makeCoordinator() -> Coordinator {
Coordinator(
safeAreaFrame: $safeAreaFrame,
windowControlFrame: $windowControlFrame
)
}
func makeUIView(context: Context) -> LayoutProbeView {
let view = LayoutProbeView()
view.onFramesChanged = { [weak coordinator = context.coordinator] safeArea, windowControl in
coordinator?.report(safeAreaFrame: safeArea, windowControlFrame: windowControl)
}
return view
}
func updateUIView(_: LayoutProbeView, context _: Context) {}
final class Coordinator {
private var safeAreaFrame: Binding<CGRect>
private var windowControlFrame: Binding<CGRect>
private var pendingFrames: (safeArea: CGRect, windowControl: CGRect)?
private var updateScheduled = false
init(safeAreaFrame: Binding<CGRect>, windowControlFrame: Binding<CGRect>) {
self.safeAreaFrame = safeAreaFrame
self.windowControlFrame = windowControlFrame
}
func report(safeAreaFrame: CGRect, windowControlFrame: CGRect) {
pendingFrames = (safeAreaFrame, windowControlFrame)
guard !updateScheduled else { return }
updateScheduled = true
Task { @MainActor [weak self] in
guard let self else { return }
updateScheduled = false
guard let frames = pendingFrames else { return }
pendingFrames = nil
if self.safeAreaFrame.wrappedValue != frames.safeArea {
self.safeAreaFrame.wrappedValue = frames.safeArea
}
if self.windowControlFrame.wrappedValue != frames.windowControl {
self.windowControlFrame.wrappedValue = frames.windowControl
}
}
}
}
}
private final class LayoutProbeView: UIView {
var onFramesChanged: ((CGRect, CGRect) -> Void)?
override func layoutSubviews() {
super.layoutSubviews()
let safeAreaFrame = safeAreaLayoutGuide.layoutFrame
if #available(iOS 26.0, *) {
let windowControlLayoutGuide = layoutGuide(for: .margins(cornerAdaptation: .vertical))
onFramesChanged?(safeAreaFrame, windowControlLayoutGuide.layoutFrame)
} else {
onFramesChanged?(safeAreaFrame, safeAreaFrame)
}
}
}
#Preview {
BlogWindowControlLayoutSimplifiedDemo()
}


Top comments (0)