1. Introduction: the gap between a file format and a screen
iOS ships with a lot of built-in image support. Point a UIImageView at a
PNG, a JPEG, a HEIF photo, and it just works: the file is decoded and its
pixels show up on screen with a single line of code. Given that, it is
reasonable to expect a similarly simple answer for SVG. Surely there is a
UISVGView, or a method on UIImage that accepts an SVG file the same way
it accepts a PNG.
There is not. iOS has no built-in call that means "parse this SVG and show it." That gap is the subject of this post. Not as a complaint, but as a question worth answering properly: what does iOS actually give you for putting things on screen, and why does that toolkit stop short of a format like SVG? Answering that requires a tour of how iOS draws anything at all, starting from first principles and assuming no prior iOS background.
2. What "drawing on iOS" even means
Every visible thing on an iPhone or iPad screen, a button, a photo, a line of text, a curve in a chart, ends up as the same thing underneath: a grid of colored pixels that the system composites and hands to the display. There is no special-cased path for "vector content" versus "raster content" at the hardware level; the screen only ever shows pixels.
What differs is how those pixels get produced. An app does not usually reach down and set individual pixel values itself. Instead, it works through a small set of frameworks that sit between the app's code and the final image, each one responsible for a different part of the journey from "here is what I want drawn" to "here are the pixels for that."
2a. The layers of the graphics stack, top to bottom
It helps to hold a simple map of that stack in mind, because later sections will keep referring back to it:
- SwiftUI or UIKit: the layer an app developer actually writes code against.
A
Text, aButton, a customViewthat overrides its own drawing. This is the vocabulary of "what should appear." - Core Animation, and specifically its
CALayertree: the layer that actually gets composited onto the screen. Every visible SwiftUI or UIKit view is, underneath, backed by one or more of these layers. - Core Graphics: the 2-D drawing engine that produces the pixel content those layers display, when a layer's content is something an app draws itself rather than, say, a pre-decoded photo.
- The GPU and the display: where composited layers are finally turned into an actual image on the physical screen.
An app developer mostly lives at the top of that stack. But a library that wants to render an arbitrary SVG file has to think about all four levels, because the format it is rendering does not map onto any one of them for free.
3. Core Graphics: the 2-D drawing engine underneath
The layer worth understanding first is Core Graphics (also known by its older name, Quartz), because it is the actual drawing engine: the thing that takes shape and color instructions and turns them into pixels. Core Graphics is a C-level, immediate-mode API. "Immediate-mode" means that when you issue a drawing instruction, it is carried out right away, producing pixels in that moment, rather than being recorded into some retained structure to be drawn later.
The central object in Core Graphics is CGContext, and the easiest mental
model for it is a canvas you paint into with a pen. You do not describe a
finished picture all at once; you issue a sequence of drawing commands into
a context, one at a time, and the context accumulates the resulting pixels
as you go.
3a. CGContext is a stateful pen
That pen has state, and the state persists between calls. You set a fill
color, and it stays set until you change it again. You set a line width, and
every stroke after that uses it, until something changes it. A typical
sequence looks like: set the fill color, add a path describing a shape's
outline, then tell the context to fill that path using the current fill
color. Order matters here in a very literal sense: filling before adding the
path fills nothing (there is no path yet), and setting the fill color after
the fill call has no effect on a fill that already happened. CGContext is
not a description of a final image; it is closer to a sequence of physical
actions carried out on a physical canvas, where the order of actions changes
the result.
3b. Paths, transforms, and the graphics-state stack
The shapes you feed into a CGContext are represented by CGPath, which is
Core Graphics' own vocabulary for geometry: move to a point, add a line
segment, add a curve, close the shape. This is the level every higher-level
description of a shape eventually has to be translated into. A rectangle, a
circle, an arbitrarily complex outline: all of it, by the time it reaches
Core Graphics, is expressed as a CGPath built from these same primitive
moves.
Alongside paths, a context tracks a current transformation matrix (a mathematical description of translation, scaling, and rotation currently in effect), which is applied to everything drawn while it is active. And because a context's state, fill color, line width, current transform, and more, can get elaborate, Core Graphics provides a save and restore mechanism: push the current state onto a stack, do some drawing under a temporarily modified state, then pop back to exactly how things were before. This is what lets one part of a drawing routine change, say, the current transform for its own purposes without permanently disturbing the state that surrounding code depends on.
3c. Core Graphics' coordinate system
One detail about Core Graphics is worth flagging early, because it will matter later for any format that was not designed with Core Graphics in mind: its default coordinate system has its origin at the bottom-left, with y increasing upward. That is the ordinary Cartesian convention from mathematics, but it is not the only convention in common use for describing on-screen graphics; plenty of formats and APIs instead put the origin at the top-left with y increasing downward, which matches how a screen is physically scanned row by row from the top. Whenever content authored under one convention has to be drawn through Core Graphics' API, something has to reconcile the difference, because Core Graphics itself will not do it for you.
4. Core Animation and layers: how views actually reach the screen
Core Graphics can produce pixels, but pixels alone do not explain how a
screen full of buttons, images, and text all end up composited together,
each capable of moving, fading, or being redrawn independently without the
whole screen being repainted from scratch. That job belongs to Core
Animation, and specifically to CALayer.
Everything visible on screen lives, underneath its SwiftUI or UIKit surface,
in a tree of CALayer objects. A layer is a retained, persistent object:
once it has content (a bitmap, or a description of how to draw itself), the
system holds onto that content and can composite it repeatedly, move it,
animate its opacity, without needing that content to be produced again from
scratch each time. The system's compositor walks this layer tree and
combines all of it into the final image sent to the display.
One particular kind of layer matters for vector content specifically:
CAShapeLayer. Instead of holding a pre-rendered bitmap, a CAShapeLayer is
handed a CGPath (the same path vocabulary Core Graphics uses) and a fill or
stroke color, and the system takes responsibility for rasterizing that path
into pixels on the layer's behalf, as a retained object the compositor can
reuse across frames.
4a. Two ways to put vectors on screen
That gives two genuinely different ways to get vector shapes onto an iOS
screen, and the distinction between them recurs throughout this whole area.
One is to issue path-drawing commands directly into a CGContext: this is
immediate-mode, it produces a finished set of pixels the moment the drawing
call runs, and once drawn, the result is just a bitmap with no memory of the
shapes that produced it. The other is to build a tree of CAShapeLayers (or
other layers), handing the system a CGPath to hold onto: this is retained,
the system keeps the path around and can recomposite or animate it without
re-running any drawing code. Both roads ultimately depend on the same
underlying primitive, a CGPath interpreted by Core Graphics' fill and
stroke logic, but they differ in whether the result is a one-time bitmap or
a persistent, re-usable object in the compositor's tree.
5. What iOS gives you for images, and where it stops
Given all of that, it is worth taking stock of what iOS actually offers for
images specifically. UIImage is the standard container for image data,
and it can load and display the common raster formats out of the box: PNG,
JPEG, HEIF, and a handful of others. Core Image sits alongside it, offering
filters and processing operations for that same raster pixel data, things
like blurring, color adjustment, and compositing effects, once an image has
already been decoded into pixels.
All of that is genuinely useful, and it covers a lot of what an app needs
from "images" in the everyday sense. But it is worth stating the boundary of
that support plainly, because it is easy to assume it extends further than
it does: none of it parses or renders SVG. There is no SVG decoder anywhere
in that stack. UIImage cannot be initialized from raw SVG text and produce
a scalable, re-renderable result the way it can from PNG bytes. Core Image's
filters operate on pixels that already exist; they have nothing to say
about a document that has not been rasterized yet.
5a. The one asterisk: SVG as a static asset
There is one place SVG shows up in ordinary iOS development, and it is narrower than it looks. Xcode lets a developer drop an SVG file into an asset catalog (the bundled collection of images an app ships with), and at build time, that SVG is converted into a fixed, pre-rendered form baked into the app. The app never parses SVG text at runtime in this flow; by the time the app is running, the conversion has already happened, once, at a size fixed at build time, and what actually loads at runtime is that pre-baked result, not the original document.
That is a genuinely useful feature for a fixed set of app icons or toolbar glyphs known in advance. But it is not runtime SVG rendering in the sense this whole post is about: a document loaded from an arbitrary source, at a size not known in advance, parsed and painted while the app is running. The asset-catalog path answers "how do I ship an SVG-authored icon as part of my app," not "how do I take an SVG my app receives at runtime and show it." Those are different problems, and only the second one is the subject here.
6. Why there is no native SVG renderer
Given the stack described above, the absence of a runtime SVG renderer is
not an oversight so much as a reasonable consequence of what SVG actually
is. A raster codec, a PNG or JPEG decoder, has a comparatively narrow job:
decode compressed bytes into a grid of pixel values. SVG asks for
considerably more. It is not just a shape format; it is a specification
that includes a CSS-like styling cascade, inheritance, gradients, clipping,
masking, group-level compositing, and reference mechanisms like <use> that
let one part of a document point at another. Supporting all of that means
supporting something closer to a small, self-contained document-rendering
system than a codec.
Shipping that as a platform-level, general-purpose runtime renderer is a
substantially bigger commitment than shipping a codec for a fixed pixel
format, and iOS has, as a matter of fact, never done it. What iOS does ship
is the lower-level drawing machinery described above: Core Graphics for
immediate-mode 2-D drawing, and CAShapeLayer for retained vector content.
Those are general enough that an SVG renderer can be built on top of them,
but the platform stops there and leaves the actual building to whoever wants
that capability.
7. The consequence: everything funnels through Core Graphics
That leaves a clear, if slightly unglamorous, conclusion. Whatever a
particular SVG library's public API looks like, however it is called or
what it returns, the pixels it eventually produces on an iOS screen have to
come from somewhere, and that somewhere is Core Graphics. Either the library
issues drawing commands directly into a CGContext (the immediate-mode
route from section 3), or it builds a tree of CAShapeLayers that
themselves wrap CGPaths and let the system rasterize them (the retained
route from section 4). There is no third option, because Core Graphics and
Core Animation are, between them, the entirety of what the platform offers
for turning shape descriptions into on-screen pixels.
SVG itself has no native path through this stack; nothing in Core Graphics or Core Animation understands SVG's XML syntax, its cascade, or its reference semantics directly. Every SVG renderer that exists on iOS has to build its own road down to one of these two primitives, because the platform did not build one for it.
8. So the real design question is: what do you build on the way down?
Once it is clear that the destination is fixed, Core Graphics or
CAShapeLayers wrapping CGPaths, and nothing else, the interesting
engineering questions turn out to live entirely in the middle of the
journey, not at either end. Every renderer has to start from the same kind
of text and end at the same kind of drawing call. What differs, and what
actually distinguishes one library's approach from another's, is what
in-memory model the parsed document is turned into, and how that model gets
walked and translated into the sequence of Core Graphics calls that produce
the final picture. That is where the real design decisions live, and it is
where this series picks up next.