Skip to content

Commit 90863e1

Browse files
committed
Add UI variant based on Table and use it on macOS
Closes ole#4. I’m not sure if I’m gonna stick to this. Is having to maintain two implementations of the UI worth it? And Table is unusable on iOS because it only shows the first column on iPhones (always) and on iPads (in Split View).
1 parent d678f7e commit 90863e1

File tree

5 files changed

+298
-128
lines changed

5 files changed

+298
-128
lines changed

DemoApp/ContentView.swift

+6-1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ struct Inspector<Subject: View>: View {
5050
@State private var height: CGFloat = 100
5151
@State private var selectedView: String? = nil
5252
@State private var generation: Int = 0
53+
@ObservedObject private var logStore = LogStore.shared
5354

5455
private var roundedWidth: CGFloat { width.rounded() }
5556
private var roundedHeight: CGFloat { height.rounded() }
@@ -98,7 +99,11 @@ struct Inspector<Subject: View>: View {
9899
}
99100
.padding()
100101

101-
DebugLayoutLogView(selection: $selectedView)
102+
#if os(macOS)
103+
LogEntriesTable(logEntries: logStore.log, highlight: $selectedView)
104+
#else
105+
LogEntriesGrid(logEntries: logStore.log, highlight: $selectedView)
106+
#endif
102107
}
103108
}
104109
}

Sources/LayoutInspector/DebugLayoutLog.swift

+1-127
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ struct ClearDebugLayoutLog: Layout {
6262
public final class LogStore: ObservableObject {
6363
public static let shared: LogStore = .init()
6464

65-
@Published var log: [LogEntry] = []
65+
@Published public var log: [LogEntry] = []
6666
var viewLabels: Set<String> = []
6767

6868
func registerViewLabelAndWarnIfNotUnique(_ label: String, file: StaticString, line: UInt) {
@@ -76,36 +76,6 @@ public final class LogStore: ObservableObject {
7676
}
7777
}
7878

79-
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
80-
struct LogEntry: Identifiable {
81-
enum Step {
82-
case proposal(ProposedViewSize)
83-
case response(CGSize)
84-
case proposalAndResponse(proposal: ProposedViewSize, response: CGSize)
85-
}
86-
87-
var id: UUID = .init()
88-
var label: String
89-
var step: Step
90-
var indent: Int
91-
92-
var proposal: ProposedViewSize? {
93-
switch step {
94-
case .proposal(let p): return p
95-
case .response(_): return nil
96-
case .proposalAndResponse(proposal: let p, response: _): return p
97-
}
98-
}
99-
100-
var response: CGSize? {
101-
switch step {
102-
case .proposal(_): return nil
103-
case .response(let r): return r
104-
case .proposalAndResponse(proposal: _, response: let r): return r
105-
}
106-
}
107-
}
108-
10979
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
11080
struct DebugLayoutSelectedViewID: EnvironmentKey {
11181
static var defaultValue: String? { nil }
@@ -118,99 +88,3 @@ extension EnvironmentValues {
11888
set { self[DebugLayoutSelectedViewID.self] = newValue }
11989
}
12090
}
121-
122-
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
123-
public struct DebugLayoutLogView: View {
124-
@Binding var selection: String?
125-
@ObservedObject var logStore: LogStore
126-
127-
private static let tableRowHorizontalPadding: CGFloat = 8
128-
private static let tableRowVerticalPadding: CGFloat = 4
129-
130-
public init(selection: Binding<String?>? = nil, logStore: LogStore = LogStore.shared) {
131-
if let binding = selection {
132-
self._selection = binding
133-
} else {
134-
var nirvana: String? = nil
135-
self._selection = Binding(get: { nirvana }, set: { nirvana = $0 })
136-
}
137-
self._logStore = ObservedObject(wrappedValue: logStore)
138-
}
139-
140-
public var body: some View {
141-
ScrollView(.vertical) {
142-
Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 0, verticalSpacing: 0) {
143-
// Table header row
144-
GridRow {
145-
Text("View")
146-
Text("Proposal")
147-
Text("Response")
148-
}
149-
.font(.headline)
150-
.padding(.vertical, Self.tableRowVerticalPadding)
151-
.padding(.horizontal, Self.tableRowHorizontalPadding)
152-
153-
// Table header separator line
154-
Rectangle().fill(.secondary)
155-
.frame(height: 1)
156-
.gridCellUnsizedAxes(.horizontal)
157-
.padding(.vertical, Self.tableRowVerticalPadding)
158-
.padding(.horizontal, Self.tableRowHorizontalPadding)
159-
160-
// Table rows
161-
ForEach(logStore.log) { item in
162-
let isSelected = selection == item.label
163-
GridRow {
164-
HStack(spacing: 0) {
165-
indentation(level: item.indent)
166-
Text(item.label)
167-
.font(.body)
168-
}
169-
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
170-
171-
Text(item.proposal?.pretty ?? "")
172-
.monospacedDigit()
173-
.fixedSize()
174-
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
175-
176-
Text(item.response?.pretty ?? "")
177-
.monospacedDigit()
178-
.fixedSize()
179-
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
180-
}
181-
.font(.callout)
182-
.padding(.vertical, Self.tableRowVerticalPadding)
183-
.padding(.horizontal, Self.tableRowHorizontalPadding)
184-
.foregroundColor(isSelected ? .white : nil)
185-
.background(isSelected ? Color.accentColor : .clear)
186-
.contentShape(Rectangle())
187-
.onTapGesture {
188-
selection = isSelected ? nil : item.label
189-
}
190-
}
191-
}
192-
.padding(.vertical, 8)
193-
}
194-
.background {
195-
#if os(macOS)
196-
Color(white: 0.8)
197-
#else
198-
Color(uiColor: .secondarySystemBackground)
199-
#endif
200-
}
201-
}
202-
203-
private func indentation(level: Int) -> some View {
204-
ForEach(0 ..< level, id: \.self) { _ in
205-
Color.clear
206-
.frame(width: 16)
207-
.overlay(alignment: .leading) {
208-
Rectangle()
209-
.frame(width: 1)
210-
.padding(.leading, 4)
211-
// Compensate for cell padding, we want continuous vertical lines.
212-
.padding(.vertical, -Self.tableRowVerticalPadding)
213-
}
214-
}
215-
}
216-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import SwiftUI
2+
3+
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
4+
public struct LogEntriesGrid: View {
5+
var logEntries: [LogEntry]
6+
@Binding var highlight: String?
7+
8+
private static let tableRowHorizontalPadding: CGFloat = 8
9+
private static let tableRowVerticalPadding: CGFloat = 4
10+
11+
public init(logEntries: [LogEntry], highlight: Binding<String?>? = nil) {
12+
self.logEntries = logEntries
13+
if let binding = highlight {
14+
self._highlight = binding
15+
} else {
16+
var nirvana: String? = nil
17+
self._highlight = Binding(get: { nirvana }, set: { nirvana = $0 })
18+
}
19+
}
20+
21+
public var body: some View {
22+
ScrollView(.vertical) {
23+
Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 0, verticalSpacing: 0) {
24+
// Table header row
25+
GridRow {
26+
Text("View")
27+
Text("Proposal")
28+
Text("Response")
29+
}
30+
.font(.headline)
31+
.padding(.vertical, Self.tableRowVerticalPadding)
32+
.padding(.horizontal, Self.tableRowHorizontalPadding)
33+
34+
// Table header separator line
35+
Rectangle().fill(.secondary)
36+
.frame(height: 1)
37+
.gridCellUnsizedAxes(.horizontal)
38+
.padding(.vertical, Self.tableRowVerticalPadding)
39+
.padding(.horizontal, Self.tableRowHorizontalPadding)
40+
41+
// Table rows
42+
ForEach(logEntries) { item in
43+
let isSelected = highlight == item.label
44+
GridRow {
45+
HStack(spacing: 0) {
46+
indentation(level: item.indent)
47+
Text(item.label)
48+
.font(.body)
49+
}
50+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
51+
52+
Text(item.proposal?.pretty ?? "")
53+
.monospacedDigit()
54+
.fixedSize()
55+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
56+
57+
Text(item.response?.pretty ?? "")
58+
.monospacedDigit()
59+
.fixedSize()
60+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
61+
}
62+
.font(.callout)
63+
.padding(.vertical, Self.tableRowVerticalPadding)
64+
.padding(.horizontal, Self.tableRowHorizontalPadding)
65+
.foregroundColor(isSelected ? .white : nil)
66+
.background(isSelected ? Color.accentColor : .clear)
67+
.contentShape(Rectangle())
68+
.onTapGesture {
69+
highlight = isSelected ? nil : item.label
70+
}
71+
}
72+
}
73+
.padding(.vertical, 8)
74+
}
75+
.background {
76+
#if os(macOS)
77+
Color(white: 0.8)
78+
#else
79+
Color(uiColor: .secondarySystemBackground)
80+
#endif
81+
}
82+
}
83+
84+
private func indentation(level: Int) -> some View {
85+
ForEach(0 ..< level, id: \.self) { _ in
86+
Color.clear
87+
.frame(width: 16)
88+
.overlay(alignment: .leading) {
89+
Rectangle()
90+
.frame(width: 1)
91+
.padding(.leading, 4)
92+
// Compensate for cell padding, we want continuous vertical lines.
93+
.padding(.vertical, -Self.tableRowVerticalPadding)
94+
}
95+
}
96+
}
97+
}
98+
99+
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
100+
struct LogEntriesGrid_Previews: PreviewProvider {
101+
static var previews: some View {
102+
LogEntriesGrid(logEntries: sampleLogEntries)
103+
}
104+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import SwiftUI
2+
3+
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
4+
public struct LogEntriesTable: View {
5+
var logEntries: [LogEntry]
6+
@Binding var highlight: String?
7+
@State private var selectedRow: LogEntry.ID? = nil
8+
9+
public init(logEntries: [LogEntry], highlight: Binding<String?>? = nil) {
10+
self.logEntries = logEntries
11+
if let binding = highlight {
12+
self._highlight = binding
13+
} else {
14+
var nirvana: String? = nil
15+
self._highlight = Binding(get: { nirvana }, set: { nirvana = $0 })
16+
}
17+
}
18+
19+
public var body: some View {
20+
Table(logEntries, selection: $selectedRow) {
21+
TableColumn("View") { item in
22+
let shouldHighlight = highlight == item.label
23+
HStack {
24+
indentation(level: item.indent)
25+
Text(item.label)
26+
Image(systemName: "circle.fill")
27+
.font(Font.caption2)
28+
.foregroundStyle(.tint)
29+
.opacity(shouldHighlight ? 1 : 0)
30+
}
31+
}
32+
TableColumn("Proposal") { item in
33+
Text(item.proposal?.pretty ?? "")
34+
.monospacedDigit()
35+
.fixedSize()
36+
.foregroundStyle(.primary)
37+
}
38+
TableColumn("Response") { item in
39+
Text(item.response?.pretty ?? "")
40+
.monospacedDigit()
41+
.fixedSize()
42+
.foregroundStyle(.primary)
43+
}
44+
}
45+
.onChange(of: highlight) { viewLabel in
46+
let selectedLogEntry = logEntries.first { $0.id == selectedRow }
47+
if viewLabel != selectedLogEntry?.label {
48+
selectedRow = nil
49+
}
50+
}
51+
.onChange(of: selectedRow) { rowID in
52+
let selectedLogEntry = logEntries.first { $0.id == rowID }
53+
highlight = selectedLogEntry?.label
54+
}
55+
.font(.callout)
56+
}
57+
58+
private func indentation(level: Int) -> some View {
59+
ForEach(0 ..< level, id: \.self) { _ in
60+
Color.clear
61+
.frame(width: 12)
62+
}
63+
}
64+
}
65+
66+
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
67+
struct LogEntriesTable_Previews: PreviewProvider {
68+
static var previews: some View {
69+
LogEntriesTable(logEntries: sampleLogEntries)
70+
}
71+
}

0 commit comments

Comments
 (0)