Most iOS SVG libraries parse a document into a retained graph of objects, a DOM, a scene graph, or a tree of layers, and keep it alive in memory to render from and to poke at later. ThinPath makes the opposite bet: a parsed document is a flat arena of value-type structs, and rendering is a single walk over that arena straight into a CGContext. There is no DOM, no scene graph, no tree of layer objects standing in memory after the parse finishes. If you stop reading here, that is the whole idea: a flat, index-addressed intermediate representation instead of a node graph, traded for the interactivity a node graph would buy. The rest of this post earns that claim by looking at what everyone else does, what ThinPath does instead, and why the difference should cost less memory.
What an SVG actually is (the short version)
An SVG is not a grid of pixels. It is a text document: an XML tree of shape
elements and <path> commands that read like instructions for a pen. To put
one on a screen, a renderer has to parse that tree into an in-memory model and
then paint the model, and nearly all the difficulty lives in that translation
from recipe to pixels. If you want the ground-up version of this, from vectors
versus rasters through the cascade, geometry conversion, and reference cycles,
we unpack it in
What an SVG actually is, and why rendering one is hard.
How iOS draws anything (the short version)
The second thing worth knowing is where those pixels have to come from. iOS has
no built-in way to render an SVG; no "just show this SVG" call exists.
Underneath everything an app draws sits Core Graphics and its CGContext, the
low-level 2-D engine that actually turns instructions into pixels, and every
SVG has to reach it somehow. So any SVG library, whatever its public API looks
like, is really just building its own road down to CGContext. We trace how
iOS draws and why SVG has no home there in
How iOS draws anything, and why SVG has no home there.
Those two posts establish the shared problem every iOS SVG library faces: turn a tree of text into Core Graphics calls. What separates one library from the next is the in-memory model they build in between. So it is worth looking closely at the model nearly all of them actually reach for.
How other iOS SVG libraries do it
The shared pattern: parse into a retained object model, then render from it
Look across the existing iOS SVG libraries and a common shape emerges. Almost all of them parse the SVG document into a persistent, reference-type object model first, and then render by walking that model: a DOM, a scene graph, or at minimum a tree of layer objects. The model outlives a single render pass and sits in memory as a graph of heap-allocated objects, each one addressable, each one capable of being looked up, mutated, and re-rendered later. Call this the node-graph (or DOM-mirroring) pattern. It shows up in libraries with very different ages, languages, and design goals (Objective-C and Swift, CoreAnimation-based and SwiftUI-based), which suggests it isn't an accident of any one implementation but close to the default answer to "how do you represent a parsed SVG in memory."
SVGKit: the dual-tree DOM + CALayer model
SVGKit is the maximal version of this approach: Objective-C, class-based, CoreAnimation-oriented, and long-standing in the iOS SVG space.
Two parallel trees in memory
An SVGKImage holds not one but two full retained object graphs for a single parsed
image: a DOMTree, described in its header as "the root element of a tree of
SVGElement subclasses," and a separate CALayerTree, "the root element of a tree of
CALayer subclasses." A third property, DOMDocument, sits alongside them. Parsing an
SVG with SVGKit means building two parallel trees, not one.
One class per element type
The DOM tree is a full Objective-C class hierarchy, one class per SVG element type:
SVGGElement, SVGPathElement, SVGRectElement, SVGCircleElement,
SVGEllipseElement, SVGLineElement, SVGPolygonElement, SVGPolylineElement,
SVGImageElement, gradient elements, SVGUseElement, SVGStyleElement,
SVGClipPathElement, SVGDefsElement, and more, each with its own .h/.m pair.
The DOM tree retains all parsed tags, including ones SVGKit doesn't otherwise
support rendering; the CALayer tree only contains layers for the features it can
actually draw.
Two render backends with a tradeoff
SVGKit ships two view classes that read from this model differently.
SVGKFastImageView rasterizes the image via renderInContext:, producing a
flattened bitmap. Its own header warns that CAAnimations are not supported this way,
"because of the way this class works, any animations you add to the SVGKImage's
CALayerTree will be ignored," and that the render call "MAY BE artificially slow."
SVGKLayeredImageView instead mounts the live CALayerTree as real sublayers, which
does support CoreAnimation but is heavier. SVGKit's own guidance is that the fast,
rasterized view "is the one you want 9 times in 10," implying the layered view and
its live tree are the exception, reached for when you specifically need what a
retained layer tree provides.
What the retained model buys: hit-testing
That "specifically need" case is hit-testing and per-element addressability. Because
SVGKLayeredImageView mounts real CALayer objects, standard UIKit and CoreAnimation
hit-testing resolves down to individual element layers, and layerWithIdentifier:
lets code fetch a specific layer by the element's id. SVGKit's own demo advertises
this directly: "Tap the images to see bounding-boxes / hit detection." It's worth
noting the caveat runs the other way too: the fast, rasterized view flattens
everything into a bitmap, so this per-element hit-testing applies only to the
layered path, not to the view SVGKit recommends by default.
SVGView (exyte): an observable Swift node tree rendered as SwiftUI
SVGView is the modern Swift and SwiftUI incarnation of the same idea, and the closest architectural relative to ThinPath in this survey.
SVGNode is a class, and the tree is reference types all the way down
The base node type, SVGNode, is declared as a Swift class, not a struct, and its
fields (transform, opaque, opacity, clip, mask, id, a gestures array)
are @Published, making the node itself observable. SVGGroup, a subclass of
SVGNode conforming to ObservableObject, holds @Published var contents: [SVGNode], an array of child node references. SVGShape adds @Published fill and
stroke properties, and SVGPath adds a @Published array of path segments. The
result is a scene graph built entirely out of heap-allocated, reference-type,
independently observable objects, one instance per SVG element.
Two-stage parse: XML to DOM to node tree
Parsing goes through two object graphs in sequence. A DOMParser first builds a
tree of generic xmlNodes from the raw XML, and then a separate SVGParser walks
that tree to build the actual SVGNode tree. Two full graphs are constructed, one
handed off to build the other.
Rendered as live SwiftUI views
Once built, the node tree renders directly as SwiftUI. SVGNode.toSwiftUI() is a
@ViewBuilder that switches on node type and returns the matching SwiftUI view type:
groups become SVGGroupView, which iterates model.contents inside a ZStack using
@ObservedObject, and shapes become SVGPathView, which wraps a SwiftUI Path.
There is no CALayer tree here and no bitmap flatten step; the node graph stays live
inside the SwiftUI view hierarchy for as long as the view exists.
What the retained model buys: interactivity
Because the tree is retained and observable, SVGView gets first-class interactivity
almost for free. getNode(byId:) walks the tree (recursively, through
SVGGroup.contents) to find a specific node by its SVG id. Individual nodes accept
gestures directly through onTapGesture and addGesture, and because those same
nodes are @Published/ObservableObject, mutating a looked-up node's opacity or
fill after the fact drives a normal SwiftUI re-render. The README's own example is
exactly this shape: part.onTapGesture { part.opacity = 0.2 }. As with SVGKit, the
capability tracks directly to the fact that the graph is retained, addressable, and
alive rather than discarded after a single render pass.
PocketSVG: no DOM, but a flat array of styled Bezier paths
PocketSVG is a partial step toward flattening the model: it drops the element and DOM hierarchy but still produces one retained object per path.
Output is [SVGBezierPath], each carrying its own style bag
Parsing a PocketSVG file produces a flat NSArray<SVGBezierPath*>. There is no
element/DOM object graph and no shared style or rule graph sitting above the paths;
each SVGBezierPath simply carries its own svgAttributes dictionary (fill,
stroke, transform, opacity, fill rule, line cap, and so on), and that dictionary is
read and applied per-path at layer-build time.
Deliberately limited scope
PocketSVG is explicit that it isn't trying to be a compliant SVG renderer. Its
stated goal is "to use SVG as a format for serializing CG/UIPaths," which means it
only supports the subset of SVG expressible as such paths: path, line,
polyline, polygon, rect, circle, ellipse.
Rendering: one CAShapeLayer per path (or direct CGContext)
SVGLayer, a CALayer subclass, builds one CAShapeLayer per parsed path and
inserts each as a sublayer, applying that path's own style attributes during
layoutSublayers. On iOS it sets shouldRasterize = YES by default (with the
README noting this should be turned off if you intend to animate the layer's
transform). So for an SVG with many paths, this is many per-path CAShapeLayer
objects, each with its own backing store. A separate, lower-level entry point also
exists: free functions like SVGDrawPaths(...) that draw straight into a
CGContextRef without building any layer tree at all.
No built-in hit-testing
PocketSVG's public headers (SVGLayer.h, SVGImageView.h, SVGBezierPath.h)
expose no tap or gesture API. What they expose instead is raw access: "Access every
shape within your SVG as a CGPath for more fine-grained manipulation." Hit-testing
or interactivity, if you want it, is something you build yourself on top of the
returned CGPaths or CAShapeLayers.
SwiftSVG: layers, not a node tree
SwiftSVG is lighter still. It's a lightweight, layer-oriented library: its README
describes it as producing UIBezierPath, CAShapeLayer, UIView, and CALayer
output, creating layers rather than a retained node tree. It renders flat icons
well, but by its own README doesn't support gradients, text, or animation, and makes
no mention of a hit-testing or interactivity API.
Macaw: the scene-graph predecessor, now deprecated
Worth noting briefly for lineage: Macaw, also from exyte, used a scene-graph model
rendered inside a MacawView, and its README advertised user events and animation
support. It's explicitly deprecated now, with its own README recommending SwiftUI
for declarative UI and SVGView, covered above, for SVG support specifically. Its
internal node and shape class structure isn't independently verified at the file
level here, so it's included mainly to show that the scene-graph approach in this
space predates SVGView and was carried forward into it, not invented by it.
The pattern and its price
Lay the field side by side and the same shape repeats: a full DOM plus a parallel CALayer tree (SVGKit), an observable Swift class tree (SVGView), a scene graph (Macaw), or, in a lighter form, one retained object per path or per layer (PocketSVG, SwiftSVG). Nearly every library in this survey keeps a graph of reference-type objects around after parsing, rather than discarding its intermediate representation once a render pass is done.
And in the two libraries that build out that model fully, SVGKit's layered view and SVGView's node tree, that retained graph is exactly what makes hit-testing and interactivity possible: a specific element can be found by id, addressed individually, and mutated in place because it exists as a standing object somewhere in memory, not because of some separate mechanism bolted on afterward. The libraries that skip the full graph (PocketSVG, SwiftSVG) also skip built-in hit-testing; the ones that build it (SVGKit's layered path, SVGView) get it as a consequence of the same objects that cost the per-node allocations and reference-type overhead in the first place. That's the tradeoff this pattern represents: capability bought with a retained heap graph, not a flaw in any one library's design. It's the baseline the rest of this post sets ThinPath's approach against.
How ThinPath does it
The bet: no retained node graph at all
Every library in the previous section makes the same starting choice: parse into a
persistent, reference-type object model, then render by walking that model later.
ThinPath makes the opposite bet. A parsed document is a flat arena of value-type
structs, and rendering is a single walk over that arena straight into a CGContext.
There is no DOM, no scene graph, no tree of layer objects standing in memory after
the parse finishes, waiting to be looked up, mutated, or re-rendered. The document
exists for exactly as long as the arrays that hold it exist, and nothing more.
The flat-arena IR
One document, a handful of contiguous arrays
Where SVGKit builds two parallel object graphs and SVGView builds one tree of
ObservableObject classes, SVGDocument is a set of contiguous Arrays: nodes,
pathCommands, points, gradientStops, transforms, plus a StringPool and a
classNames array. There is no class hierarchy underneath it, one class per element
type the way SVGKit's DOM tree works. A node is a value written into an array slot,
not an object allocated on the heap.
SVGNode is a fixed-size value type
SVGNode itself is a struct, and every instance is the same fixed size. Anything
variable-length (a path's commands, a polygon's points, a gradient's stops) does not
live inline on the node; it lives in a side arena, and the node holds only a
reference to its slice of that arena. Compare this to SVGView's SVGPath, which
holds its own @Published array of segments directly on the object, or PocketSVG's
SVGBezierPath, which carries its own attributes dictionary per instance. In
ThinPath every node is the same shape and the same size, no matter what kind of
element it represents or how much content it holds.
Addressing by integer index, not pointer
Cross-references between nodes (parent, first-child, next-sibling, a paint-server
reference, an href target, a clip or mask reference) are never embedded copies and
never object pointers. They're integer indices into the node array: NodeIndex,
StringRef, and TransformRef are all typealiases for Int32. This is where the
value-type choice pays off structurally, not just in allocation count: a "reference"
in ThinPath's IR is a 4-byte integer, not an 8-byte pointer to a heap-allocated
object with its own retain count. Int32 rather than the platform-native Int is a
deliberate choice, since each node carries several of these links and halving their
width halves that part of the footprint; -1 serves as the null sentinel
(NodeIndex.none).
The tree as an intrusive index linked list
The tree shape itself is stored the same way: each node carries a first-child index
and a next-sibling index, an intrusive linked list threaded through the nodes
array rather than a separate container of child references. Adding a child allocates
nothing beyond appending the new node, and walking a subtree needs no data structure
beyond nodes itself. There's no separate contents: [SVGNode] array per group the
way SVGView's SVGGroup holds one; the tree structure and the node storage are the
same array.
Side arenas via (start, count) windows
The general pattern for anything variable-length is an ArenaRange, a (start, count) window into a shared side arena rather than a per-node owned array: path
commands live in a shared pathCommands arena, polygon points in points, gradient
stops in gradientStops, class tokens in classNames. A node with a long path and a
node with a short one are still the same fixed size; only the window they point to
differs.
Paying nothing for the common case
The transform arena carries this further: it stores only non-identity matrices.
Identity, the common case for most nodes in most documents, is represented by the
same -1 sentinel used elsewhere for "no reference," so a node with no transform of
its own costs nothing beyond the sentinel value it already had to store for other
reasons.
Interned strings and packed colors
Strings, ids, class names, font families, href targets, text runs, are interned once
through a deduplicating StringPool and referenced everywhere as a 4-byte
StringRef; interning is idempotent, so a repeated string returns the existing
reference rather than storing a duplicate. Colors are packed as RGBA, four UInt8
values (4 bytes total), rather than four CGFloats. Neither of these is unusual on
its own, but both follow from the same premise as the rest of the IR: store the
value once, reference it everywhere it's needed.
Shared definitions stored once
The same premise applies to paint servers. A paint that references a gradient or a pattern stores only a reference to it, never an embedded copy, so a gradient shared across many shapes exists exactly once in the arena regardless of how many shapes paint with it.
Append-only and immutable downstream
The arenas are append-only during parsing, and every downstream pass (style
resolution, coordinate math, rendering) treats them as immutable: nothing removes or
reorders an arena element once it's written. Element id resolution reflects the same
discipline, going through one idMap: [StringRef: NodeIndex] where the last
definition of a given id wins. Once parsing finishes, the document is a fixed,
frozen shape that every later pass reads but never rewrites.
Parsing straight into the arena
The parser is built directly on top of this model rather than staging through an
intermediate one. It's built on Foundation's XMLParser, an event-driven,
libxml2-backed SAX parser, not a retained DOM: nodes are appended to doc.nodes as
tags open and close during the SAX walk. This is the opposite of SVGView's two-stage
parse, where a DOMParser first builds a full tree of generic xmlNodes and only
then a separate SVGParser walks that tree to build the SVGNode tree it actually
uses. ThinPath never holds a second full object graph alongside the IR at any point,
because there's never a first one either; the arena is the only thing that gets
built.
Reference resolution happens in the same single pass, with no fix-up step
afterward. A backward reference, one pointing at an id already seen, is resolved
immediately against idMap as it's parsed. A forward reference is left as .none
and resolved on demand later, also through idMap, rather than requiring the parser
to walk the tree a second time once every id is known.
The parser is also deliberately narrow in what it captures at parse time. It records
only each element's own declared presentation state, RawStyle, as a set of
optional fields where nil specifically means "not specified on this element."
Inheritance and initial values are left to the style resolver; the parser's job ends
at capturing what was written on the tag. Within an element, the inline style=""
block is applied after presentation attributes, so inline style wins, matching CSS
cascade order at the single-element level.
CSS <style> support follows the same transient-scratch-state principle as the rest
of the parse. <style> element text, including CDATA, is accumulated during the SAX
walk and tokenized into CSS rules only when the element closes; those rules cover
type, class, id, universal, compound, and descendant selectors (child, sibling,
attribute, and pseudo selectors are unsupported and dropped rather than erroring).
The cascade itself runs as one linear post-parse pass that matches every rule
against every node and writes the winning declarations directly into
doc.nodes[i].style. Once that pass completes, none of the CSS machinery, the
parsed rules, the selectors, the declaration blocks, is retained anywhere: it's
scratch state local to the parse delegate, released when the delegate deallocates.
SVGDocument has no CSS-related field at all. The cascade happens, leaves its
result baked into the node arena, and then disappears.
Two more choices in the parser defer work rather than doing it eagerly, and both
follow the arena's general shape. <image> elements store only their href in the
IR; decoding the actual image data is deferred to render time. Elliptical arcs are
stored in the raw endpoint parameterization as authored (A rx ry rot large sweep x y) rather than converted to the center parameterization SVG rendering actually
needs; that conversion happens later, in the render or flattening pass. In both
cases the parser writes down what it was told rather than computing something
derived from it, keeping parse-time work, and the arena itself, as small as what the
source document actually specifies.
The result of all of this is a document that, once parsed, is a handful of flat
arrays and nothing else: no DOM sitting behind it the way SVGKit keeps one behind
its CALayer tree, no observable object graph the way SVGView keeps one behind its
SwiftUI views. Rendering, covered next, is a single walk over that arena straight
into a CGContext, with no intermediate node graph and no intermediate bitmap on
the into-context path. What that walk does and doesn't buy is worth being direct
about. The retained graphs in the previous section exist because SVGKit's layered
view and SVGView's node tree need a standing, addressable object to hit-test against
or to mutate after the fact; that's the whole reason those graphs get built and kept
alive. ThinPath doesn't build one, so it doesn't have one to hit-test against or
mutate: no per-element lookup by id after the parse, no gesture handling on a node,
no reaching into a rendered SVG to change one shape's opacity later. That isn't a
gap to be closed later; it falls directly out of the arena design, and the tradeoff
runs in ThinPath's favor for the case it's built for, rendering a document once,
efficiently, and dropping it, at the cost of the case it deliberately isn't built
for, keeping a document around to poke at.
Why this should use less memory
That architecture is the whole reason to expect a lighter memory profile, so it is worth making the reasoning explicit. A note before this section starts: everything below is an argument from architecture, not a result from a benchmark. There is no profiling session behind these paragraphs, no measured byte count, no "Nx lighter" figure to report. What follows is a case built from how each design allocates, built by reading the source of ThinPath and the libraries surveyed earlier in this post. Treat it as a set of claims you can go verify yourself, in Instruments, against your own documents, not as a number to take on faith.
The foundational contrast: structs in arrays versus classes in a graph
Every library in the previous section (SVGKit's SVGElement/CALayer pair, SVGView's
@Published SVGNode classes, PocketSVG's per-path objects) represents a parsed document as
reference-type instances: one heap allocation per element, at minimum, sometimes two. ThinPath's
SVGNode is a struct, and every parsed document lives in a handful of contiguous Arrays
(nodes, pathCommands, points, gradientStops, transforms) rather than as objects scattered
across the heap.
That distinction is the root of everything else in this section, so it's worth being precise about what it does and doesn't imply. A class instance costs more than its fields: it needs its own heap allocation, separate from whatever holds the reference to it, plus whatever bookkeeping the runtime attaches to that allocation (an isa pointer, a retain count). A struct in an array costs exactly its fields, laid out contiguously with its neighbors. None of this is a claim about how many bytes either approach uses on a given document; it's a claim about what kind of cost each model incurs per node, structurally, before a single byte of actual content is counted. A document with a few elements might not show a difference either way. A document with tens of thousands of nodes is exactly where a per-node fixed cost, paid once by a class allocator and never by an array, starts to matter, and it's exactly the case worth checking for yourself.
No second graph kept alive to render from
SVGKit doesn't just build one retained model, it builds two: a DOMTree of SVGElement
subclasses and a parallel CALayerTree, both alive for the life of the image. SVGView builds two
in sequence: an xmlNode DOM first, then a class tree of SVGNodes built from it. In both cases,
there's a full object graph standing in memory that the render step reads from, and in SVGKit's
case a second one alongside it.
ThinPath keeps none. Parsing appends into the arena's arrays as the SAX parser fires events, and
rendering is a single depth-first walk over that same arena straight into a CGContext. There is
no scene graph materialized between parse and render, and nothing survives the render call that a
scene graph would: no second tree, no per-element view objects, no parallel layer hierarchy. The
structural point isn't "ThinPath's graph is smaller," it's that there's no separate graph at all,
walking is done directly on the same flat storage the parser produced.
No retain traffic while walking the tree
Reference types come with a cost that's easy to forget because it's invisible in the source: every
time a reference is copied, retained, or released, ARC does work. A tree of class instances (an
SVGNode class holding an array of child SVGNode references, say) means every traversal, every
lookup, every intermediate variable that holds onto a child is a retain, and letting go of it is a
release. Walk a big tree of reference types and you've generated a large amount of retain/release
traffic that has nothing to do with the actual work of drawing pixels.
ThinPath's cross-references (parent, first-child, next-sibling, paint-server references, href
targets, clip and mask references) are Int32 indices, not references to objects. Following a
"pointer" in this IR means reading an integer out of an array and using it to index into another
array. Copying that integer, holding onto it, letting it go: none of that touches ARC, because
there's no retained object on the other end of it. This is a claim about a pattern of ownership,
not a count of retain calls avoided; the way to see it directly is to profile a walk over a large
document and look at what shows up (or doesn't) under Swift's ARC/retain-release instrumentation.
Fewer, larger allocations instead of many small ones
Because variable-length data (a path's commands, a polygon's points, a gradient's stops) lives in
shared side arenas addressed by (start, count) windows rather than in an array or dictionary
owned by each individual node, a parsed document's storage is a small, fixed number of buffers:
nodes, pathCommands, points, gradientStops, and so on, each grown as parsing proceeds.
Contrast this with PocketSVG, where each path carries its own svgAttributes dictionary, a
separate heap object per path rather than a shared table. A handful of large contiguous buffers
is a different allocation shape than one small object per node or per path: fewer calls to the
allocator, and elements that are near each other in the document tend to be near each other in
memory, rather than scattered wherever the allocator happened to place each individual object.
Again, this is a description of the allocation pattern, and the way to see whether it matters for
a particular document is to open Instruments and look at allocation counts directly, not to take
a general argument as a substitute for measuring your own case.
Sharing instead of copying, and paying nothing for the common case
A few smaller decisions compound in the same direction. Strings (ids, class names, font families,
href targets, text runs) are interned once through a deduplicating StringPool and referenced
everywhere as a 4-byte handle, so a repeated string doesn't cost a second copy. A gradient or
pattern referenced by many shapes is stored once in the arena and referenced, never duplicated
per shape that uses it. Colors are packed into four bytes rather than stored as four CGFloats.
And the transform arena stores only matrices that aren't the identity: a node with no transform of
its own costs nothing beyond a sentinel value it already had to store for other reasons, rather
than allocating or storing an identity matrix it will never actually use. None of these are large
effects in isolation, and none of them are quantified here; they're each a place the design
declined to pay a cost that a more literal, per-node-owns-its-data model would pay by default.
What this doesn't mean
None of this is a claim that ThinPath renders in less time or holds fewer bytes than any specific competitor on any specific document; that would take a benchmark, and this roadmap doesn't include one. It's also worth being honest about what buys the lightness described above: it's the same decision that gives up a retained, mutable, addressable graph. SVGKit's layered view and SVGView's node tree get real hit-testing and per-element interactivity precisely because an object exists somewhere in memory to look up, address, and mutate after the parse is done. ThinPath's arena is walked once and produces pixels; there's no node to hand back to a caller that wants to tap an element or animate its opacity later. Lighter by construction is not a strictly better design, it is the specific tradeoff of a render-only library, made deliberately and paid for in the capability it gives up.
If you take one thing from this section, take the invitation rather than the argument: none of
this needs to be believed secondhand. The arena types are in SVGModel.swift, the walk is in
RenderContext.swift, and Instruments will show you exactly what each design costs on a document
you actually care about.
Who should and shouldn't use ThinPath
The whole argument comes down to one tradeoff, so the recommendation is simple. Use ThinPath if what you need is to render an SVG to pixels once, efficiently, and then drop it: displaying icons, illustrations, or documents received at runtime, where the SVG is content to show rather than a UI to interact with. That is the case the flat-arena design is built for, and the case its lighter footprint follows from.
Do not reach for ThinPath if you need to tap individual elements, look them up by id after parsing, or animate one shape's opacity or fill in place. Those capabilities require exactly the retained, addressable node graph that ThinPath deliberately doesn't build, and SVGKit's layered view or exyte's SVGView will serve you far better there. ThinPath gives up interactivity on purpose; that isn't a missing feature, it's the other side of the same design decision that makes it light.
If your case is the first one, the source, install instructions, and API docs live in: ThinPath. And if you want the fundamentals underneath all of this, What an SVG actually is, and why rendering one is hard How iOS draws anything, and why SVG has no home there are there for the reader who wants to go deeper; everything the argument itself needs is above.