Last active
September 26, 2024 03:04
-
-
Save avielg/2e706b57896d2a27be5a5929680da5c4 to your computer and use it in GitHub Desktop.
SwiftUI View Tree Graph
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
#!/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