-
-
Save bjorng/3219fa109d302306b804e84b61eb68f4 to your computer and use it in GitHub Desktop.
View Mirror
This file contains hidden or 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 | |
struct ContentView: View { | |
var body: some View { | |
HStack { | |
Text("Hello") | |
.padding() | |
.background(.blue) | |
.overlay { | |
Color.yellow | |
} | |
Image(systemName: "globe") | |
} | |
.mirror() | |
} | |
} | |
#Preview { | |
ContentView() | |
} | |
// Usage: take any view, and call `.mirror()` on it. | |
extension View { | |
func mirror() -> some View { | |
MirrorView(content: self) | |
} | |
} | |
// The rest of this file is "internal" and not very beautiful yet. | |
/// A simple Tree datastructure that holds nodes with `A` as the value. | |
struct Tree<A> { | |
var value: A | |
var children: [Tree<A>] = [] | |
init(_ value: A, children: [Tree<A>] = []) { | |
self.value = value | |
self.children = children | |
} | |
} | |
extension Tree { | |
func map<B>(_ transform: (A) -> B) -> Tree<B> { | |
return Tree<B>(transform(value), children: children.map({ $0.map(transform) })) | |
} | |
} | |
extension Tree: Equatable where A: Equatable { } | |
extension Tree: Hashable where A: Hashable { } | |
private let specialChars: Set<Character> = Set("<()>, ") | |
// Parsing the type into a tree | |
extension Substring { | |
fileprivate mutating func dropSpaces() { | |
while first?.isWhitespace == true { | |
removeFirst() | |
} | |
} | |
fileprivate mutating func parseName() -> String { | |
var result: String = "" | |
while let f = self.first, !specialChars.contains(f) { | |
result.append(removeFirst()) | |
} | |
dropSpaces() | |
return result | |
} | |
fileprivate mutating func eat(_ character: Element) -> Bool { | |
guard first == character else { return false } | |
removeFirst() | |
dropSpaces() | |
return true | |
} | |
fileprivate mutating func parseHierarchy() -> Tree<String> { | |
var children: [Tree<String>] = [] | |
if eat("(") { | |
while !eat(")") { | |
children.append(parseHierarchy()) | |
_ = eat(",") | |
} | |
return Tree("Tuple", children: children) | |
} | |
let name = parseName() | |
assert(!name.isEmpty) | |
if eat("<") { | |
while !eat(">") { | |
children.append(parseHierarchy()) | |
_ = eat(",") | |
} | |
} | |
return Tree(name, children: children) | |
} | |
} | |
extension Tree where A == String { | |
/// Construct a tree structure that reflects the generic structure of a type. | |
init<X>(reflecting value: X) { | |
let m = Mirror(reflecting: value) | |
let input = "\(m.subjectType)" | |
var remainder = input[...] | |
let hierarchy = remainder.parseHierarchy() | |
assert(remainder.isEmpty) | |
self = hierarchy | |
} | |
} | |
struct Identified<A>: Identifiable { | |
var id = UUID() | |
var value: A | |
init(_ value: A) { self.value = value } | |
} | |
extension Identified: Equatable where A: Equatable { } | |
extension Identified: Hashable where A: Hashable { } | |
struct CollectDict<Key: Hashable, Value>: PreferenceKey { | |
static var defaultValue: [Key:Value] { [:] } | |
static func reduce(value: inout [Key:Value], nextValue: () -> [Key:Value]) { | |
value.merge(nextValue(), uniquingKeysWith: { $1 }) | |
} | |
} | |
/// Draws an edge from `from` to `to` | |
struct EdgeShape: Shape { | |
var from: CGPoint | |
var to: CGPoint | |
func path(in rect: CGRect) -> Path { | |
Path { p in | |
p.move(to: self.from) | |
p.addLine(to: self.to) | |
} | |
} | |
} | |
extension CGRect { | |
subscript(_ point: UnitPoint) -> CGPoint { | |
return .init(x: minX + point.x * width, y: minY + point.y * height) | |
} | |
} | |
// It'd be nicer to do this with a `Layout` because that works much better with animations. | |
struct Diagram<A: Identifiable & Hashable, V: View>: View { | |
let tree: Tree<A> | |
var strokeWidth: CGFloat = 1 | |
let node: (A) -> V | |
init(tree: Tree<A>, strokeWidth: CGFloat = 1, node: @escaping (A) -> V) { | |
self.tree = tree | |
self.strokeWidth = strokeWidth | |
self.node = node | |
} | |
private typealias Key = CollectDict<A.ID, Anchor<CGRect>> | |
var body: some View { | |
return VStack(alignment: .center) { | |
node(tree.value) | |
.fixedSize() | |
.anchorPreference(key: Key.self, value: .bounds, transform: { | |
[self.tree.value.id: $0] | |
}) | |
.padding() | |
HStack(alignment: .top, spacing: 8) { | |
ForEach(tree.children, id: \.value, content: { child in | |
Diagram(tree: child, strokeWidth: self.strokeWidth, node: self.node) | |
}) | |
} | |
}.backgroundPreferenceValue(Key.self, { (rects: [A.ID: Anchor<CGRect>]) in | |
GeometryReader { proxy in | |
ForEach(self.tree.children, id: \.value, content: { | |
child in | |
EdgeShape(from: | |
proxy[rects[self.tree.value.id]!][.bottom], | |
to: proxy[rects[child.value.id]!][.top]) | |
.stroke(lineWidth: self.strokeWidth) | |
}) | |
} | |
}) | |
} | |
} | |
struct MirrorView<Content>: View { | |
let content: Content | |
var collapsed: Bool = false | |
var body: some View { | |
let tree = Tree(reflecting: content).simplified().map(Identified.init) | |
return Diagram(tree: tree, strokeWidth: 1, node: { value in | |
Text(value.value) | |
.padding(8) | |
.background { | |
RoundedRectangle(cornerRadius: 8) | |
.fill(.white) | |
.strokeBorder(Color.black, lineWidth: 1) | |
} | |
}) | |
} | |
} | |
// Simplify the tree | |
extension Tree<String> { | |
func simplified() -> Tree<A> { | |
if value == "Optional" && children.count == 1 { | |
let child = children[0] | |
return Tree(child.value + "?", children: child.children.map { $0.simplified() }) | |
} else if value == "TupleView" && children.first?.value == "Tuple" { | |
return children[0].simplified() | |
} else if value == "ModifiedContent" { | |
if children.count == 2 { | |
let child = children[1] | |
let name = child.value.simplerName | |
if basicModifiers.contains(child.value) { | |
return Tree<A>(name, children: [ | |
children[0].simplified() | |
]) | |
} | |
if inlineSecondChildModifiers.contains(child.value) { | |
let n = children[1].children[0].simplified().value | |
return Tree<A>(n, children: [ | |
children[0].simplified(), | |
]) | |
} | |
if twoChildModifiers.contains(child.value) { | |
return Tree(name, children: [ | |
children[0].simplified(), | |
children[1].children[0].simplified() | |
]) | |
} | |
} | |
} | |
return Tree(value.simplerName, children: children.map { $0.simplified() }) | |
} | |
} | |
let basicModifiers: Set<String> = [ | |
"_FixedSizeLayout", | |
"_PaddingLayout", | |
"_FrameLayout", | |
"_AspectRatioLayout", | |
] | |
let twoChildModifiers: Set<String> = [ | |
"_BackgroundStyleModifier", | |
"_BackgroundModifier", | |
"_OverlayModifier", | |
] | |
let inlineSecondChildModifiers: Set<String> = [ | |
"_EnvironmentKeyWritingModifier", | |
"_PreferenceWritingModifier", | |
"_PreferenceActionModifier" | |
] | |
extension String { | |
var simplerName: String { | |
switch self { | |
case "_FixedSizeLayout": return ".fixedSize" | |
case "_PaddingLayout": return ".padding" | |
case "_BackgroundModifier": return ".background" | |
case "_OverlayModifier": return ".overlay" | |
case "_BackgroundStyleModifier": return ".background" | |
case "_FrameLayout": return ".frame" | |
case "_PreferenceWritingModifier": return ".preference" | |
case "_PreferenceActionModifier": return ".onPreferenceChange" | |
case "_AspectRatioLayout": return ".aspectRatio" | |
case _ where first == "_": return String(dropFirst()) | |
default: return self | |
} | |
} | |
} | |
extension Tree<String> { | |
private var isPreference: Bool { | |
value == "_PreferenceWritingModifier" | |
} | |
var containsPreference: Bool { | |
children.contains { | |
$0.isPreference || $0.containsPreference | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment