forked from ole/swiftui-layout-inspector
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathDebugLayoutLog.swift
90 lines (78 loc) · 3.43 KB
/
DebugLayoutLog.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
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 }
}
}