import SwiftUI @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) func logLayoutStep(_ label: String, step: LogEntry.Step) { DispatchQueue.main.async { guard let prevEntry = LogStore.shared.log.last else { // First log entry → start at indent 0. LogStore.shared.log.append(LogEntry(label: label, step: step, indent: 0)) return } var newEntry = LogEntry(label: label, step: step, indent: prevEntry.indent) let isSameView = prevEntry.label == label switch (isSameView, prevEntry.step, step) { case (true, .proposal(let prop), .response(let resp)): // Response follows immediately after proposal for the same view. // → We want to display them in a single row. // → Coalesce both layout steps. LogStore.shared.log.removeLast() newEntry = prevEntry newEntry.step = .proposalAndResponse(proposal: prop, response: resp) LogStore.shared.log.append(newEntry) case (_, .proposal, .proposal): // A proposal follows a proposal → nested view → increment indent. newEntry.indent += 1 LogStore.shared.log.append(newEntry) case (_, .response, .response), (_, .proposalAndResponse, .response): // A response follows a response → last child returns to parent → decrement indent. newEntry.indent -= 1 LogStore.shared.log.append(newEntry) default: // Keep current indentation. LogStore.shared.log.append(newEntry) } } } /// A custom layout that clears the DebugLayout log /// at the point where it's placed in the view tree. @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) struct ClearDebugLayoutLog: Layout { func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { assert(subviews.count == 1) DispatchQueue.main.async { LogStore.shared.log.removeAll() LogStore.shared.viewLabels.removeAll() } return subviews[0].sizeThatFits(proposal) } func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { assert(subviews.count == 1) subviews[0].place(at: bounds.origin, proposal: proposal) } } @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) public final class LogStore: ObservableObject { public static let shared: LogStore = .init() @Published public var log: [LogEntry] = [] var viewLabels: Set<String> = [] func registerViewLabelAndWarnIfNotUnique(_ label: String, file: StaticString, line: UInt) { DispatchQueue.main.async { if self.viewLabels.contains(label) { let message: StaticString = "Duplicate view label '%s' detected. Use unique labels in debugLayout() calls" runtimeWarning(message, [label], file: file, line: line) } self.viewLabels.insert(label) } } } @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) struct DebugLayoutSelectedViewID: EnvironmentKey { static var defaultValue: String? { nil } } @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) extension EnvironmentValues { var debugLayoutSelectedViewID: String? { get { self[DebugLayoutSelectedViewID.self] } set { self[DebugLayoutSelectedViewID.self] = newValue } } }