Last active
February 25, 2025 09:52
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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