@@ -2,15 +2,15 @@ import SwiftUI
2
2
3
3
@available ( macOS 13 . 0 , iOS 16 . 0 , tvOS 16 . 0 , watchOS 9 . 0 , * )
4
4
struct InspectLayout : ViewModifier {
5
+ @StateObject private var logStore : LogStore = . init( )
5
6
@State private var selectedView : String ? = nil
6
7
@State private var generation : Int = 0
7
8
@State private var frame : CGRect = CGRect ( x: 0 , y: 0 , width: 300 , height: 300 )
8
- @ObservedObject private var logStore = LogStore . shared
9
9
10
10
private static let coordSpaceName = " InspectLayout "
11
11
12
12
func body( content: Content ) -> some View {
13
- ClearDebugLayoutLog {
13
+ ClearDebugLayoutLog ( logStore : logStore ) {
14
14
content
15
15
. id ( generation)
16
16
. environment ( \. debugLayoutSelectedViewID, selectedView)
@@ -39,6 +39,7 @@ struct InspectLayout: ViewModifier {
39
39
. offset ( x: frame. minX, y: frame. minY)
40
40
. coordinateSpace ( name: Self . coordSpaceName)
41
41
}
42
+ . environmentObject ( logStore)
42
43
}
43
44
}
44
45
@@ -160,21 +161,40 @@ struct Draggable: ViewModifier {
160
161
}
161
162
}
162
163
164
+ @available ( macOS 13 . 0 , iOS 16 . 0 , tvOS 16 . 0 , watchOS 9 . 0 , * )
165
+ struct DebugLayoutModifier : ViewModifier {
166
+ var label : String
167
+ var file : StaticString
168
+ var line : UInt
169
+ @EnvironmentObject private var logStore : LogStore
170
+
171
+ func body( content: Content ) -> some View {
172
+ DebugLayout ( label: label, logStore: logStore) {
173
+ content
174
+ }
175
+ . onAppear {
176
+ logStore. registerViewLabelAndWarnIfNotUnique ( label, file: file, line: line)
177
+ }
178
+ . modifier ( DebugLayoutSelectionHighlight ( viewID: label) )
179
+ }
180
+ }
181
+
163
182
/// A custom layout that saves the layout proposals and responses for a view
164
183
/// to a log.
165
184
@available ( macOS 13 . 0 , iOS 16 . 0 , tvOS 16 . 0 , watchOS 9 . 0 , * )
166
185
struct DebugLayout : Layout {
167
186
var label : String
187
+ var logStore : LogStore
168
188
169
189
func sizeThatFits(
170
190
proposal: ProposedViewSize ,
171
191
subviews: Subviews ,
172
192
cache: inout ( )
173
193
) -> CGSize {
174
194
assert ( subviews. count == 1 )
175
- logLayoutStep ( label, step: . proposal( proposal) )
195
+ logStore . logLayoutStep ( label, step: . proposal( proposal) )
176
196
let response = subviews [ 0 ] . sizeThatFits ( proposal)
177
- logLayoutStep ( label, step: . response( response) )
197
+ logStore . logLayoutStep ( label, step: . response( response) )
178
198
return response
179
199
}
180
200
@@ -192,15 +212,17 @@ struct DebugLayout: Layout {
192
212
/// placed in the view tree.
193
213
@available ( macOS 13 . 0 , iOS 16 . 0 , tvOS 16 . 0 , watchOS 9 . 0 , * )
194
214
struct ClearDebugLayoutLog : Layout {
215
+ var logStore : LogStore
216
+
195
217
func sizeThatFits(
196
218
proposal: ProposedViewSize ,
197
219
subviews: Subviews ,
198
220
cache: inout ( )
199
221
) -> CGSize {
200
222
assert ( subviews. count == 1 )
201
223
DispatchQueue . main. async {
202
- LogStore . shared . log. removeAll ( )
203
- LogStore . shared . viewLabels. removeAll ( )
224
+ logStore . log. removeAll ( )
225
+ logStore . viewLabels. removeAll ( )
204
226
}
205
227
return subviews [ 0 ] . sizeThatFits ( proposal)
206
228
}
@@ -216,59 +238,56 @@ struct ClearDebugLayoutLog: Layout {
216
238
}
217
239
}
218
240
219
- @available ( macOS 13 . 0 , iOS 16 . 0 , tvOS 16 . 0 , watchOS 9 . 0 , * )
220
- func logLayoutStep( _ label: String , step: LogEntry . Step ) {
221
- DispatchQueue . main. async {
222
- guard let prevEntry = LogStore . shared. log. last else {
223
- // First log entry → start at indent 0.
224
- LogStore . shared. log. append ( LogEntry ( label: label, step: step, indent: 0 ) )
225
- return
226
- }
227
-
228
- var newEntry = LogEntry ( label: label, step: step, indent: prevEntry. indent)
229
- let isSameView = prevEntry. label == label
230
- switch ( isSameView, prevEntry. step, step) {
231
- case ( true , . proposal( let prop) , . response( let resp) ) :
232
- // Response follows immediately after proposal for the same view.
233
- // → We want to display them in a single row.
234
- // → Coalesce both layout steps.
235
- LogStore . shared. log. removeLast ( )
236
- newEntry = prevEntry
237
- newEntry. step = . proposalAndResponse( proposal: prop, response: resp)
238
- LogStore . shared. log. append ( newEntry)
239
-
240
- case ( _, . proposal, . proposal) :
241
- // A proposal follows a proposal → nested view → increment indent.
242
- newEntry. indent += 1
243
- LogStore . shared. log. append ( newEntry)
244
-
245
- case ( _, . response, . response) ,
246
- ( _, . proposalAndResponse, . response) :
247
- // A response follows a response → last child returns to parent → decrement indent.
248
- newEntry. indent -= 1
249
- LogStore . shared. log. append ( newEntry)
250
-
251
- default :
252
- // Keep current indentation.
253
- LogStore . shared. log. append ( newEntry)
254
- }
255
- }
256
- }
257
-
258
241
@available ( macOS 13 . 0 , iOS 16 . 0 , tvOS 16 . 0 , watchOS 9 . 0 , * )
259
242
public final class LogStore : ObservableObject {
260
- public static let shared : LogStore = . init( )
261
-
262
243
@Published public var log : [ LogEntry ] = [ ]
263
244
var viewLabels : Set < String > = [ ]
264
245
265
246
func registerViewLabelAndWarnIfNotUnique( _ label: String , file: StaticString , line: UInt ) {
266
- DispatchQueue . main. async {
267
- if self . viewLabels. contains ( label) {
247
+ DispatchQueue . main. async { [ self ] in
248
+ if viewLabels. contains ( label) {
268
249
let message : StaticString = " Duplicate view label '%s' detected. Use unique labels in layoutStep() calls "
269
250
runtimeWarning ( message, [ label] , file: file, line: line)
270
251
}
271
- self . viewLabels. insert ( label)
252
+ viewLabels. insert ( label)
253
+ }
254
+ }
255
+
256
+ func logLayoutStep( _ label: String , step: LogEntry . Step ) {
257
+ DispatchQueue . main. async { [ self ] in
258
+ guard let prevEntry = log. last else {
259
+ // First log entry → start at indent 0.
260
+ log. append ( LogEntry ( label: label, step: step, indent: 0 ) )
261
+ return
262
+ }
263
+
264
+ var newEntry = LogEntry ( label: label, step: step, indent: prevEntry. indent)
265
+ let isSameView = prevEntry. label == label
266
+ switch ( isSameView, prevEntry. step, step) {
267
+ case ( true , . proposal( let prop) , . response( let resp) ) :
268
+ // Response follows immediately after proposal for the same view.
269
+ // → We want to display them in a single row.
270
+ // → Coalesce both layout steps.
271
+ log. removeLast ( )
272
+ newEntry = prevEntry
273
+ newEntry. step = . proposalAndResponse( proposal: prop, response: resp)
274
+ log. append ( newEntry)
275
+
276
+ case ( _, . proposal, . proposal) :
277
+ // A proposal follows a proposal → nested view → increment indent.
278
+ newEntry. indent += 1
279
+ log. append ( newEntry)
280
+
281
+ case ( _, . response, . response) ,
282
+ ( _, . proposalAndResponse, . response) :
283
+ // A response follows a response → last child returns to parent → decrement indent.
284
+ newEntry. indent -= 1
285
+ log. append ( newEntry)
286
+
287
+ default :
288
+ // Keep current indentation.
289
+ log. append ( newEntry)
290
+ }
272
291
}
273
292
}
274
293
}
0 commit comments