Skip to content

Instantly share code, notes, and snippets.

@ralfebert
Last active February 25, 2025 09:52
Show Gist options
  • Save ralfebert/a90e2b9d0a1aefd5468f4ed7609b1e4a to your computer and use it in GitHub Desktop.
Save ralfebert/a90e2b9d0a1aefd5468f4ed7609b1e4a to your computer and use it in GitHub Desktop.
A modifier to mark SwiftUI views in the view hierarchy and get a hierarchical textual description of the View - meant for unit testing
import SwiftUI
/**
A modifier to do textual asserts for SwiftUI Views in unit tests.
Thought to be used in tandem with https://github.com/pointfreeco/swift-snapshot-testing , but with a self-built textual description.
Mark SwiftUI views with the .trace("Example") modifier:
HStack {
Text("Foo")
.trace("Foo")
Text("Bar")
.trace("Foo")
}
.trace("HStack")
Get a hierarchical textual description from that:
view
.onPreferenceChange(ViewTracePreferenceKey.self) { value in
print(value.description)
}
"""
HStack
Foo
Bar
"""
(for this to work, the View needs to be brought to a UIWindow)
*/
#if DEBUG
public struct ViewTraceElement: Equatable {
var name: String
var indent: Int
}
public struct ViewTracePreferenceKey: PreferenceKey {
public static let defaultValue: [ViewTraceElement] = []
public static func reduce(value: inout [ViewTraceElement], nextValue: () -> [ViewTraceElement]) {
value = value + nextValue()
}
}
public struct ViewTraceModifier: ViewModifier {
let name: String
public init(name: String) {
self.name = name
}
public func body(content: Content) -> some View {
content.transformPreference(ViewTracePreferenceKey.self) { value in
if value.isEmpty {
value = [ViewTraceElement(name: name, indent: 0)]
} else {
// This captures the View hierarchy
value = [ViewTraceElement(name: name, indent: 1)] + value + [ViewTraceElement(name: name, indent: -1)]
}
}
}
}
public extension [ViewTraceElement] {
var description: String {
var result = ""
let indent = " "
var currentIndent = 0
for e in self {
if e.indent >= 0 {
if !result.isEmpty {
result += "\n"
}
result += String(repeating: indent, count: currentIndent) + e.name
}
currentIndent = currentIndent + e.indent
assert(currentIndent >= 0)
}
return result
}
}
#endif
public extension View {
@inline(__always) func trace(_ name: String) -> some View {
#if DEBUG
self.modifier(ViewTraceModifier(name: name))
#else
self
#endif
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment