← ALL WRITING

iOS Development

How iOS draws anything, and why SVG has no home there

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, a Button, a custom View that overrides its own drawing. This is the vocabulary of "what should appear."
  • Core Animation, and specifically its CALayer tree: 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.