layout with traffic light on iPad
DEV Community

layout with traffic light on iPad

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)

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

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()
}
Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.