Skip to content

Commit b30077a

Browse files
committed
Refactoring, renaming, documentation
1 parent 3702b87 commit b30077a

File tree

4 files changed

+221
-187
lines changed

4 files changed

+221
-187
lines changed

LayoutInspector.xcodeproj/project.pbxproj

+4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
5D2245F428D8FDB500E84C7D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5D2245F328D8FDB500E84C7D /* Assets.xcassets */; };
1313
5D2245F828D8FDB500E84C7D /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5D2245F728D8FDB500E84C7D /* Preview Assets.xcassets */; };
1414
5D2245FF28D9015300E84C7D /* DebugLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D2245FE28D9015300E84C7D /* DebugLayout.swift */; };
15+
5D22463928DA2E7500E84C7D /* DebugLayoutLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D22463828DA2E7500E84C7D /* DebugLayoutLog.swift */; };
1516
/* End PBXBuildFile section */
1617

1718
/* Begin PBXFileReference section */
@@ -22,6 +23,7 @@
2223
5D2245F528D8FDB500E84C7D /* LayoutInspector.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = LayoutInspector.entitlements; sourceTree = "<group>"; };
2324
5D2245F728D8FDB500E84C7D /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
2425
5D2245FE28D9015300E84C7D /* DebugLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugLayout.swift; sourceTree = "<group>"; };
26+
5D22463828DA2E7500E84C7D /* DebugLayoutLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugLayoutLog.swift; sourceTree = "<group>"; };
2527
/* End PBXFileReference section */
2628

2729
/* Begin PBXFrameworksBuildPhase section */
@@ -59,6 +61,7 @@
5961
5D2245EF28D8FDB400E84C7D /* App.swift */,
6062
5D2245F128D8FDB400E84C7D /* ContentView.swift */,
6163
5D2245FE28D9015300E84C7D /* DebugLayout.swift */,
64+
5D22463828DA2E7500E84C7D /* DebugLayoutLog.swift */,
6265
5D2245F328D8FDB500E84C7D /* Assets.xcassets */,
6366
5D2245F528D8FDB500E84C7D /* LayoutInspector.entitlements */,
6467
5D2245F628D8FDB500E84C7D /* Preview Content */,
@@ -147,6 +150,7 @@
147150
5D2245FF28D9015300E84C7D /* DebugLayout.swift in Sources */,
148151
5D2245F228D8FDB400E84C7D /* ContentView.swift in Sources */,
149152
5D2245F028D8FDB400E84C7D /* App.swift in Sources */,
153+
5D22463928DA2E7500E84C7D /* DebugLayoutLog.swift in Sources */,
150154
);
151155
runOnlyForDeploymentPostprocessing = 0;
152156
};

LayoutInspector/ContentView.swift

+34-21
Original file line numberDiff line numberDiff line change
@@ -11,47 +11,60 @@ struct ContentView: View {
1111
.debugLayout("Text")
1212
.aspectRatio(1, contentMode: .fit)
1313
.debugLayout("aspectRatio")
14+
.padding()
15+
.debugLayout("padding")
16+
.background {
17+
Color.yellow
18+
.debugLayout("yellow")
19+
}
20+
.debugLayout("background")
1421
}
1522

1623
var body: some View {
1724
VStack {
1825
VStack {
1926
subject
20-
.clearConsole()
21-
.environment(\.debugLayoutSelection, selectedView)
27+
.startDebugLayout(selection: selectedView)
28+
.id(generation)
2229
.frame(width: width, height: height)
2330
.overlay {
2431
Rectangle()
2532
.strokeBorder(style: StrokeStyle(dash: [5]))
2633
}
27-
.id(generation)
2834
.padding(.bottom, 16)
2935

30-
LabeledContent {
31-
Slider(value: $width, in: 50...500, step: 1)
32-
} label: {
33-
Text("Width: \(width, format: .number.precision(.fractionLength(0)))")
34-
.monospacedDigit()
35-
}
36+
VStack {
37+
LabeledContent {
38+
HStack {
39+
Slider(value: $width, in: 50...500, step: 1)
40+
Stepper("Width", value: $width)
41+
}
42+
.labelsHidden()
43+
} label: {
44+
Text("W \(width, format: .number.precision(.fractionLength(0)))")
45+
.monospacedDigit()
46+
}
3647

37-
LabeledContent {
38-
Slider(value: $height, in: 50...500, step: 1)
39-
} label: {
40-
Text("Height: \(height, format: .number.precision(.fractionLength(0)))")
41-
.monospacedDigit()
42-
}
48+
LabeledContent {
49+
HStack {
50+
Slider(value: $height, in: 50...500, step: 1)
51+
Stepper("Height", value: $height)
52+
}
53+
.labelsHidden()
54+
} label: {
55+
Text("H \(height, format: .number.precision(.fractionLength(0)))")
56+
.monospacedDigit()
57+
}
4358

44-
Button("Reset layout cache") {
45-
generation += 1
59+
Button("Reset layout cache") {
60+
generation += 1
61+
}
4662
}
4763
.buttonStyle(.bordered)
4864
}
4965
.padding()
5066

51-
ConsoleView()
52-
.onPreferenceChange(Selection.self) { selection in
53-
selectedView = selection
54-
}
67+
DebugLayoutLogView(selection: $selectedView)
5568
}
5669
}
5770
}

LayoutInspector/DebugLayout.swift

+34-166
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,34 @@
44
import SwiftUI
55

66
extension View {
7-
func debugLayout(_ label: String) -> some View {
8-
DebugLayout(label: label) {
7+
/// Start debugging the layout algorithm for this subtree.
8+
///
9+
/// This clears the debug layout log.
10+
func startDebugLayout(selection: String? = nil) -> some View {
11+
ClearDebugLayoutLog {
912
self
1013
}
11-
.modifier(DebugLayoutWrapper(label: label))
14+
.environment(\.debugLayoutSelectedViewID, selection)
1215
}
13-
}
1416

15-
struct DebugLayoutWrapper: ViewModifier {
16-
var label: String
17-
@Environment(\.debugLayoutSelection) private var selection: String?
18-
19-
func body(content: Content) -> some View {
20-
content
21-
.overlay {
22-
let isSelected = label == selection
23-
if isSelected {
24-
Rectangle()
25-
.strokeBorder(style: StrokeStyle(lineWidth: 1, dash: [5]))
26-
.foregroundColor(.pink)
27-
}
28-
}
17+
/// Monitor the layout proposals and responses for this view and add them to the log.
18+
func debugLayout(_ label: String) -> some View {
19+
DebugLayout(label: label) {
20+
self
21+
}
22+
.modifier(DebugLayoutSelectionHighlight(viewID: label))
2923
}
3024
}
3125

26+
/// A custom layout that adds the layout proposals and responses for a view to a log for display.
3227
struct DebugLayout: Layout {
3328
var label: String
3429

3530
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
3631
assert(subviews.count == 1)
37-
log(label, action: .proposal(proposal))
32+
logLayoutStep(label, step: .proposal(proposal))
3833
let response = subviews[0].sizeThatFits(proposal)
39-
log(label, action: .response(response))
34+
logLayoutStep(label, step: .response(response))
4035
return response
4136
}
4237

@@ -45,6 +40,25 @@ struct DebugLayout: Layout {
4540
}
4641
}
4742

43+
/// Draws a highlight (dashed border) around the view that's selected
44+
/// in the DebugLayout log table.
45+
fileprivate struct DebugLayoutSelectionHighlight: ViewModifier {
46+
var viewID: String
47+
@Environment(\.debugLayoutSelectedViewID) private var selection: String?
48+
49+
func body(content: Content) -> some View {
50+
content
51+
.overlay {
52+
let isSelected = viewID == selection
53+
if isSelected {
54+
Rectangle()
55+
.strokeBorder(style: StrokeStyle(lineWidth: 2, dash: [5]))
56+
.foregroundColor(.pink)
57+
}
58+
}
59+
}
60+
}
61+
4862
extension CGFloat {
4963
var pretty: String {
5064
String(format: "%.1f", self)
@@ -70,149 +84,3 @@ extension ProposedViewSize {
7084
return "\(width.pretty)\(thinSpace)×\(thinSpace)\(height.pretty)"
7185
}
7286
}
73-
74-
struct ClearConsole: Layout {
75-
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
76-
assert(subviews.count == 1)
77-
DispatchQueue.main.async {
78-
Console.shared.log.removeAll()
79-
}
80-
return subviews[0].sizeThatFits(proposal)
81-
}
82-
83-
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
84-
subviews[0].place(at: bounds.origin, proposal: proposal)
85-
}
86-
}
87-
88-
extension View {
89-
func clearConsole() -> some View {
90-
ClearConsole { self }
91-
}
92-
}
93-
94-
final class Console: ObservableObject {
95-
static let shared: Console = .init()
96-
97-
@Published var log: [LogItem] = []
98-
99-
struct LogItem: Identifiable {
100-
enum Action {
101-
case proposal(ProposedViewSize)
102-
case response(CGSize)
103-
case proposalAndResponse(proposal: ProposedViewSize, response: CGSize)
104-
}
105-
106-
var id: UUID = .init()
107-
var label: String
108-
var action: Action
109-
110-
var proposal: ProposedViewSize? {
111-
switch action {
112-
case .proposal(let p): return p
113-
case .response(_): return nil
114-
case .proposalAndResponse(proposal: let p, response: _): return p
115-
}
116-
}
117-
118-
var response: CGSize? {
119-
switch action {
120-
case .proposal(_): return nil
121-
case .response(let r): return r
122-
case .proposalAndResponse(proposal: _, response: let r): return r
123-
}
124-
}
125-
}
126-
}
127-
128-
func log(_ label: String, action: Console.LogItem.Action) {
129-
DispatchQueue.main.async {
130-
if var lastLogItem = Console.shared.log.last,
131-
lastLogItem.label == label,
132-
case .proposal(let proposal) = lastLogItem.action,
133-
case .response(let response) = action
134-
{
135-
Console.shared.log.removeLast()
136-
lastLogItem.action = .proposalAndResponse(proposal: proposal, response: response)
137-
Console.shared.log.append(lastLogItem)
138-
} else {
139-
Console.shared.log.append(.init(label: label, action: action))
140-
}
141-
}
142-
}
143-
144-
struct DebugLayoutSelection: EnvironmentKey {
145-
static var defaultValue: String? { nil }
146-
}
147-
148-
extension EnvironmentValues {
149-
var debugLayoutSelection: String? {
150-
get { self[DebugLayoutSelection.self] }
151-
set { self[DebugLayoutSelection.self] = newValue }
152-
}
153-
}
154-
155-
struct Selection<Value: Equatable>: PreferenceKey {
156-
static var defaultValue: Value? { nil }
157-
158-
static func reduce(value: inout Value?, nextValue: () -> Value?) {
159-
value = value ?? nextValue()
160-
}
161-
}
162-
163-
struct ConsoleView: View {
164-
@ObservedObject var console = Console.shared
165-
@State private var selection: String? = nil
166-
167-
var body: some View {
168-
ScrollView(.vertical) {
169-
Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 0, verticalSpacing: 0) {
170-
GridRow {
171-
Text("View")
172-
Text("Proposal")
173-
Text("Response")
174-
}
175-
.font(.headline)
176-
.padding(.vertical, 4)
177-
.padding(.horizontal, 8)
178-
179-
Rectangle().fill(.secondary)
180-
.frame(height: 1)
181-
.gridCellUnsizedAxes(.horizontal)
182-
.padding(.vertical, 4)
183-
.padding(.horizontal, 8)
184-
185-
ForEach(console.log) { item in
186-
let isSelected = selection == item.label
187-
GridRow {
188-
Text(item.label)
189-
.font(.body)
190-
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
191-
192-
Text(item.proposal?.pretty ?? "")
193-
.monospacedDigit()
194-
.fixedSize()
195-
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
196-
197-
Text(item.response?.pretty ?? "")
198-
.monospacedDigit()
199-
.fixedSize()
200-
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
201-
}
202-
.font(.callout)
203-
.padding(.vertical, 4)
204-
.padding(.horizontal, 8)
205-
.foregroundColor(isSelected ? .white : nil)
206-
.background(isSelected ? Color.accentColor : .clear)
207-
.contentShape(Rectangle())
208-
.onTapGesture {
209-
selection = isSelected ? nil : item.label
210-
}
211-
}
212-
}
213-
.padding(.vertical, 8)
214-
}
215-
.background(Color(uiColor: .secondarySystemBackground))
216-
.preference(key: Selection.self, value: selection)
217-
}
218-
}

0 commit comments

Comments
 (0)