Here's the observation this whole post hangs on: a SwiftUI body never draws anything. It describes — "a VStack containing this text above that button" — and hands the description to a runtime that does the actual rendering elsewhere, later. SwiftUI is already a description handed to a renderer; it's just not a serializable one. Making it serializable is the entire trick.

We make it leave the process. Patch ships OTA updates to native Swift apps by running changed code in a WebAssembly interpreter on the device — and "changed code" very often means views. So we needed body logic to execute inside a WASM sandbox while real, native SwiftUI does the rendering outside it. (If your first reaction is "this is how JSPatch died" — interpreted code in an embedded runtime is a different mechanism than the runtime-patching Apple banned, and the compliance call is ultimately yours; the actual rules are written up here.) The mechanism turns out to be a clean idea with sharp edges, and it's the same idea under every server-driven UI system — with one twist worth understanding even if you never touch WASM.

Can you serialize a SwiftUI view? Only as a mirror IR

No — not directly. some View is an opaque nest of generics whose identity lives entirely in the type system, and it isn't Codable. What you can serialize is a mirror of it: lightweight value types with SwiftUI's names and shapes that build a tree of plain nodes — kind, properties, children — instead of Apple's types. (import SwiftUI doesn't exist inside a WASM module anyway.)

swift
// inside the sandbox: looks like SwiftUI, builds data
var body: some PatchView {
    VStack(spacing: 12) {
        Text("Order #" + orderID)
        Button("Reorder") { dispatch(.reorderTapped) }
    }
}

The result-builder syntax is ordinary Swift, so view code mostly compiles against the mirror DSL untouched (the compile-time exceptions are the same things that can't ride an IR at all — custom Canvas drawing, UIViewRepresentable; more below). What falls out the bottom is an IR: Text is a node with a string, the VStack is a node with spacing and two children. That tree encodes to bytes, crosses the boundary, and a renderer on the native side walks it and constructs the real SwiftUI.Text, the real VStack, the real modifiers.

If that sounds like server-driven UI — Airbnb-style JSON-describes-the-screen — it is, structurally. Same render half. The difference is the other half:

Server-driven UISerialized SwiftUI (this approach)
Who builds the descriptionThe serverYour Swift code, executing in a sandbox on-device
Where interaction logic runsShipped native code or a server round-tripIn the sandbox, next to the tree
What crosses the boundaryScreen layoutsView trees out, event tokens in
New behavior requiresNew shipped componentsNew code in the sandbox

Server-driven UI in SwiftUI — except the logic travels too

Server-driven UI's ceiling is that the description is produced by the server — it can rearrange screens and choose which predefined actions fire, but the logic behind those actions lives in shipped native code or a network round-trip (Airbnb's Ghost Platform is the canonical writeup).

In our setup the description is produced by executing the developer's actual Swiftif statements, ForEach over real models, computed properties, formatting — inside the sandbox. Which raises the question that makes this architecture interesting: where does @State live?

Answer: in the sandbox, next to the logic. The mirror DSL ships its own @State-named property wrapper, so @State var count = 0 compiles unchanged; what differs is where the storage lives. The whole thing runs as an Elm-style loop (The Elm Architecture, if you want the literature):

  1. Guest builds a view tree from current state → native renders it.

  2. User taps → native sends an event token across the sandbox boundary (a native closure can't reference code inside the guest's isolated memory, so the "action" in the IR is an ID).

  3. Guest runs the handler — real Swift mutating sandbox-resident state.

  4. Guest emits a fresh tree → native diffs and re-renders.

State mutation, validation, the Stepper math — all of it executes in the interpreter. Sandbox state is session-scoped; anything that touches the native world — networking, persistence, your existing services — crosses via explicit host bridges. Native SwiftUI does what it's genuinely irreplaceable for: layout, text, accessibility, scrolling physics, animations. Each side keeps the half it's best at.

What doesn't serialize: closures, Canvas, UIViewRepresentable

Where this stops — every "UI as data" system earns its scars in the same places:

  • The IR is a contract, and contracts version badly. The guest-side builder and the native-side renderer must agree on every node kind and property — ours are hand-synced copies with a fingerprint gate so a stale renderer never meets a newer tree. This is the part that looks like bookkeeping and is actually the product.

  • Closures don't serialize. Anything that's fundamentally code attached to a view — custom Canvas drawing, gesture recognizers with continuous handlers, UIViewRepresentable — can't ride an IR. Those views stay native; the system detects that and degrades per-view (an unsupported node demotes that view to native, not the whole screen).

  • Identity and animation. SwiftUI's diffing keys off view identity — structural (type and position) by default, explicit IDs for dynamic content — and a regenerated tree must preserve stable explicit IDs or every update becomes a teardown. Our first list update tore down every row: scroll position gone, every transition a cross-fade, until the IR carried stable per-item IDs mirroring ForEach's identity rules.

  • Chatty boundaries are slow boundaries. One tree per interaction is fine — interaction-rate rebuilds haven't shown up on a profile, though we owe a numbers post rather than an adjective. Anything per-frame (drag positions, scroll-linked effects) must never cross; those stay native by construction. (TextField is the stress test: the native field owns the live edit buffer, the guest receives text-changed events and owns the canonical value — keystrokes don't rebuild trees per frame.)

In coverage terms: 98.5% of view elements across our test-corpus measurements lower to the IR, counted per node — and interactivity covers the standard control vocabulary (Button, Toggle, Slider, Stepper, TextField). The remainder is exactly the list above, and it should stay native.

React Server Components, SDUI, and SwiftUI: the same pattern

Strip away our use case and the pattern stands alone: declarative UI frameworks are serialization formats that nobody serializes. The moment you do, location becomes a free variable — build the tree on a server (SDUI), in a WASM sandbox (us), in a plugin, in a test harness that asserts on trees instead of screenshots. React Server Components is the same insight wearing JavaScript. SwiftUI made the description/render split native-respectable; the description was always data, and data travels.


The render half of this system ships in Patch's public SDK; the OTA pipeline around it is at patchrelease.com. Related: What Apple actually allows · Running Swift async/await inside WebAssembly · How OTA updates work on iOS