DEV Community

Quang
Quang

Posted on

layout with traffic light on iPad

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)

safe area vs corner adaptation horizontal layout guide

What if we use layoutGuide(for: .margins(cornerAdaptation: .vertical))? It goes right underneath the traffic light.

safe area vs corner adaptation vertical layout guide

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()
}

Enter fullscreen mode Exit fullscreen mode

Top comments (0)