Skip to content

Instantly share code, notes, and snippets.

@chriseidhof
Last active September 16, 2024 06:02
Show Gist options
  • Save chriseidhof/d23f82f8a9e85e75bc02be220326199a to your computer and use it in GitHub Desktop.
Save chriseidhof/d23f82f8a9e85e75bc02be220326199a to your computer and use it in GitHub Desktop.
View Inspection
import SwiftUI
struct SizeKey: PreferenceKey {
static func reduce(value: inout CGSize?, nextValue: () -> CGSize?) {
value = value ?? nextValue()
}
}
struct ContentView: View {
@State var width: CGFloat? = nil
var body: some View {
let button = Text("Neu Starten")
.fixedSize()
.padding(10)
.background(GeometryReader { proxy in
Color.clear.preference(key: SizeKey.self, value: proxy.size)
})
.frame(width: width, height: width)
.background(
Circle()
.fill(Color.blue)
)
.onPreferenceChange(SizeKey.self) { size in
self.width = size?.width
}
return button.mirror()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello")
.padding()
.background(.blue)
.mirror()
}
}
#Preview { ContentView() }
// Usage: take any view, and call `.mirror()` on it.
extension View {
func mirror(collapsed: Bool = false, rules: [Rule] = []) -> some View {
MirrorView(content: self, rules: rules)
}
}
// 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("<()>, ")
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<A>(reflecting value: A) {
let m = Mirror(reflecting: value)
let input = "\(m.subjectType)"
print(input)
var remainder = input[...]
let hierarchy = remainder.parseHierarchy()
assert(remainder.isEmpty)
self = hierarchy
}
}
extension CGPoint: VectorArithmetic {
public static func -= (lhs: inout CGPoint, rhs: CGPoint) {
lhs = lhs - rhs
}
public static func - (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
}
public static func += (lhs: inout CGPoint, rhs: CGPoint) {
lhs = lhs + rhs
}
public mutating func scale(by rhs: Double) {
x *= CGFloat(rhs)
y *= CGFloat(rhs)
}
public static func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
}
public var magnitudeSquared: Double { return Double(x*x + y*y) }
}
class Unique<A>: Identifiable {
let value: A
init(_ value: A) { self.value = value }
}
extension Unique: Equatable where A: Equatable {
static func == (lhs: Unique<A>, rhs: Unique<A>) -> Bool {
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
}
}
extension Unique: Hashable where A: Hashable {
func hash(into hasher: inout Hasher) {
value.hash(into: &hasher)
}
}
struct Collect<A>: PreferenceKey {
static var defaultValue: [A] { [] }
static func reduce(value: inout [A], nextValue: () -> [A]) {
value.append(contentsOf: nextValue())
}
}
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
var animatableData: AnimatablePair<CGPoint, CGPoint> {
get { AnimatablePair(from, to) }
set {
from = newValue.first
to = newValue.second
}
}
func path(in rect: CGRect) -> Path {
Path { p in
p.move(to: self.from)
p.addLine(to: self.to)
}
}
}
/// A simple Diagram. It's not very performant yet, but works great for smallish trees.
struct Diagram<A: Identifiable & Hashable, V: View>: View {
let tree: Tree<A>
var strokeWidth: CGFloat = 1
let node: (Binding<Bool>, A) -> V
@State private var collapsed: Bool
let collapsedDefault: Bool
init(tree: Tree<A>, strokeWidth: CGFloat = 1, node: @escaping (Binding<Bool>, A) -> V, collapsed: Bool = false) {
self.tree = tree
self.strokeWidth = strokeWidth
self.node = node
self.collapsedDefault = collapsed
self._collapsed = State(initialValue: collapsed && !tree.children.isEmpty)
}
private typealias Key = CollectDict<A.ID, Anchor<CGPoint>>
var body: some View {
return VStack(alignment: .center) {
node($collapsed, tree.value)
.anchorPreference(key: Key.self, value: .bottom, transform: {
[self.tree.value.id: $0]
})
.padding()
if !collapsed {
HStack(alignment: .top, spacing: 10) {
ForEach(tree.children, id: \.value, content: { child in
Diagram(tree: child, strokeWidth: self.strokeWidth, node: self.node, collapsed: self.collapsedDefault)
})
}
}
}.backgroundPreferenceValue(Key.self, { (centers: [A.ID: Anchor<CGPoint>]) in
GeometryReader { proxy in
if !self.collapsed {
ForEach(self.tree.children, id: \.value, content: {
child in
EdgeShape(from:
proxy[centers[self.tree.value.id]!],
to: proxy[centers[child.value.id]!])
.stroke(lineWidth: self.strokeWidth)
})
}
}
})
}
}
/// A node used to display a type
struct MyNode: ViewModifier {
var hue: Hue = .orange
func body(content: Content) -> some View {
content
.foregroundColor(.white)
.padding(8)
.background(.blue.gradient, in: .rect(cornerRadius: 4))
.background(Color.white)
}
}
struct MirrorView<Content>: View {
let content: Content
var collapsed: Bool = false
var rules: [Rule] = []
var body: some View {
let tree = Tree(reflecting: content).simplified(rules: rules).map(Unique.init)
return Diagram(tree: tree, strokeWidth: 2, node: { _, value in
Text(value.value.text)
.bold()
.fixedSize()
.modifier(MyNode(hue: value.value.hue))
}).foregroundColor(.gray)
}
}
// For presentations, it's nicer to simplify the tree a little bit.
struct DiagramState {
var hue: Hue = .orange
}
extension Tree where A == String {
func simplified(rules: [Rule] = []) -> Tree<Colored> {
_simplified(rules: rules, .init())
}
private func _simplified(rules: [Rule], _ _state: DiagramState) -> Tree<Colored> {
let (state, childState) = rules.reduce(into: (_state, _state)) { (st, rule) in
rule.apply(self, &st.0, &st.1)
}
if value == "Optional" && children.count == 1 {
let child = children[0]
return Tree<Colored>(Colored(child.value + "?", hue: state.hue), children: child.children.map { $0._simplified(rules: rules, childState) })
} else if value == "TupleView" && children.first?.value == "Tuple" {
return children[0]._simplified(rules: rules, state)
} else if value == "ModifiedContent" {
if children.count == 2 {
let child = children[1]
let name = child.value.simplerName
if basicModifiers.contains(child.value) {
return Tree<Colored>(Colored(name, hue: state.hue), children: [
children[0]._simplified(rules: rules, childState)
])
}
if inlineSecondChildModifiers.contains(child.value) {
let n = children[1].children[0]._simplified(rules: rules, childState).value
return Tree<Colored>(Colored(name + "(\(n.text))", hue: n.hue), children: [
children[0]._simplified(rules: rules, childState),
])
}
if twoChildModifiers.contains(child.value) {
return Tree<Colored>(Colored(name, hue: state.hue), children: [
children[0]._simplified(rules: rules, childState),
children[1].children[0]._simplified(rules: rules, childState)
])
}
}
}
return Tree<Colored>(Colored(value.simplerName, hue: state.hue), children: children.map { $0._simplified(rules: rules, childState) })
}
}
struct Rule {
// The first is the state of the current node. The second the child state
let apply: (Tree<String>, inout DiagramState, inout DiagramState) -> ()
}
struct Hue: Hashable {
var value: Int // 0...255
static let orange = Hue(value: 7)
static let green = Hue(value: 150)
static let blue = Hue(value: 220)
}
struct Colored: Hashable {
var text: String
var hue: Hue = .orange // 0..<256
init(_ text: String, hue: Hue) {
self.text = text
self.hue = hue
}
}
let basicModifiers: Set<String> = [
"_FixedSizeLayout",
"_PaddingLayout",
"_FrameLayout",
"_AspectRatioLayout",
]
let twoChildModifiers: Set<String> = [
"_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 "_FrameLayout": return ".frame"
case "_PreferenceWritingModifier": return ".preference"
case "_PreferenceActionModifier": return ".onPreferenceChange"
case "_AspectRatioLayout": return ".aspectRatio"
case _ where first == "_": return String(dropFirst())
default: return self
}
}
}
// Builtin Rules
let highlightSubtreeOfEnvironment = Rule(apply: { tree, state, childState in
if tree.value == "ModifiedContent" && tree.children[1].value == "_EnvironmentKeyWritingModifier" {
childState.hue = .blue
}
})
let highlightPreferenceAncestors = Rule(apply: { tree, state, childState in
if tree.containsPreference {
state.hue = .blue
}
})
extension Tree where A == String {
private var isPreference: Bool {
value == "_PreferenceWritingModifier"
}
var containsPreference: Bool {
children.contains {
$0.isPreference || $0.containsPreference
}
}
}
@chriseidhof
Copy link
Author

This is what it looks like:

Screen Shot 2019-11-29 at 10 33 39

@naeem3d
Copy link

naeem3d commented Mar 26, 2021

Hi I did and copy the project but the drawing start from up to bottom , how can I start from bottom to up ... and thanks for you effort

@chriseidhof
Copy link
Author

Hi @naeem3d, it shouldn't be a hard change... just flip the items in the VStack and you should be set, I think

@naeem3d
Copy link

naeem3d commented Dec 31, 2021

Hi Chriseidhof, Happy new Year please I try to add branch (child) when I chose any node it will be add to that node , I mean when I select node make right click and Add child name will be add to that node .. so wherever node I chose will create that child to that node ... so please How can I do that ? and I hope you understand what I mean ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment