Skip to content

Instantly share code, notes, and snippets.

@avielg
Last active September 26, 2024 03:04
Show Gist options
  • Save avielg/2e706b57896d2a27be5a5929680da5c4 to your computer and use it in GitHub Desktop.
Save avielg/2e706b57896d2a27be5a5929680da5c4 to your computer and use it in GitHub Desktop.
SwiftUI View Tree Graph
#!/opt/homebrew/bin/python3
import re
import glob
import os
import graphviz
##################################################################################
######### USAGE #########
# 1. Place this file at the topmost folder that contains all of the SwiftUI
# files you wish to be included. The script will recuresively find files in
# any subfolder.
# 2. Change this to the path to your main SwiftUI view file, relative to the
# script. If the script is in the same folder as the file, just put the filename.
MAIN_FILE = 'AppView.swift'
# 3. Get need dot and graphviz. Easiest way is to run:
# $ pip install graphviz
# 4. Ensure `dot` is in PATH. If you use homebrew, this should do it:
os.environ["PATH"] += os.pathsep + "/opt/homebrew/bin"
# Ignore any `PreferenceKey` and `EnvironmentKey` types
DROP_KEYS = True
# Ignore anything that isn't a `View`
DROP_NON_VIEWS = True
# Style subgraphs differently
STYLE_SUBGRAPH = False
##################################################################################
class Node:
def __init__(self, name, children, inline_children):
self.name = name
self.children = children
self.inline_children = inline_children
found = []
def analyze(file):
# print(name)
found.append(file)
base = os.path.basename(file)
viewname = os.path.splitext(base)[0]
children = []
inline_structs = []
with open(file) as f:
lines = f.readlines()
for rawline in lines:
line = rawline.strip()
if line.strip("private").strip("public").strip().startswith("struct"):
regex_result = re.search("struct ([A-Za-z0-9_]*)", line)
if regex_result:
name = regex_result.group(1)
if name != viewname and name not in inline_structs and "_" not in name:
isView = "View" in line
if not DROP_NON_VIEWS or isView:
iskey = "PreferenceKey" in line or "EnvironmentKey" in line
if not DROP_KEYS or not iskey:
inline_structs.append(name)
in_view = False
for rawline in lines:
line = rawline.strip()
is_comment = line.startswith("/*") or line.startswith("*") or line.startswith("//") or line.startswith("import")
is_property_decl = " var " in line or " let " in line
if not is_comment and not is_property_decl:
regex_result = re.search("(?!\.)([A-Z][a-zA-z0-9]*)(?!\.)\s*(\(|{)", line)
if regex_result:
name = regex_result.group(1)
if name not in inline_structs and "_" not in name:
for f in glob.glob('**/' + name + '.swift', recursive=True):
if f != file:
child = analyze(f)
children.append(child)
return Node(viewname, children, inline_structs)
def print_node(node, prefix):
print(prefix + node.name)
for inline in node.inline_children:
print(prefix + " (" + inline + ")")
for child in node.children:
print_node(child, prefix + " ")
# keep track of already created edges since we don't care about multiple edges between the same nodes
edgesIDs = []
def graph(dot, node):
dot.node(node.name)
for child in node.children:
id = node.name + child.name
if not id in edgesIDs:
edge = dot.edge(node.name, child.name)
edgesIDs.append(id)
graph(dot, child)
if len(node.inline_children) > 0:
with dot.subgraph(name='cluster_' + node.name) as c:
if STYLE_SUBGRAPH:
c.node_attr['shape'] = 'egg'
c.node_attr['fontcolor'] = 'darkgray'
c.node_attr['style'] = ''
c.graph_attr['style'] = 'dotted'
for inline in node.inline_children:
id = node.name + inline
if id not in edgesIDs:
edgesIDs.append(id)
c.node(inline)
c.edge(node.name, inline)
def run():
main_node = analyze(MAIN_FILE)
print_node(main_node, "")
dot = graphviz.Digraph(comment='View Tree From: ' + main_node.name, node_attr={'shape': 'box', 'style': 'filled'})
graph(dot, main_node)
# print(dot.source)
if STYLE_SUBGRAPH:
source = dot
else:
source = dot.unflatten(stagger=3)
source.render('./graph', format='png', view=True)
run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment