4
4
import SwiftUI
5
5
6
6
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 {
9
12
self
10
13
}
11
- . modifier ( DebugLayoutWrapper ( label : label ) )
14
+ . environment ( \ . debugLayoutSelectedViewID , selection )
12
15
}
13
- }
14
16
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) )
29
23
}
30
24
}
31
25
26
+ /// A custom layout that adds the layout proposals and responses for a view to a log for display.
32
27
struct DebugLayout : Layout {
33
28
var label : String
34
29
35
30
func sizeThatFits( proposal: ProposedViewSize , subviews: Subviews , cache: inout ( ) ) -> CGSize {
36
31
assert ( subviews. count == 1 )
37
- log ( label, action : . proposal( proposal) )
32
+ logLayoutStep ( label, step : . proposal( proposal) )
38
33
let response = subviews [ 0 ] . sizeThatFits ( proposal)
39
- log ( label, action : . response( response) )
34
+ logLayoutStep ( label, step : . response( response) )
40
35
return response
41
36
}
42
37
@@ -45,6 +40,25 @@ struct DebugLayout: Layout {
45
40
}
46
41
}
47
42
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
+
48
62
extension CGFloat {
49
63
var pretty : String {
50
64
String ( format: " %.1f " , self )
@@ -70,149 +84,3 @@ extension ProposedViewSize {
70
84
return " \( width. pretty) \( thinSpace) × \( thinSpace) \( height. pretty) "
71
85
}
72
86
}
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