How to build server driven UI in SwiftUI
Build server driven UI in SwiftUI with four pieces: a JSON envelope, a widget registry, a renderer, and host networking. Worked path using Flow-UI.
Build server driven UI in SwiftUI with four pieces: a JSON envelope, a widget registry, a renderer, and host networking. Tutorials on Medium and Pyramid almost always show a closed switch on a Codable enum. That is a weekend. This path is the one that still works when the backend ships a type you have not named yet.
Flow-UI is the worked example. You can copy the seams into your own package. The seams matter more than the brand.
The four pieces#
- Envelope. A page document: id, sections, widgets, optional nav, header, footer, pagination, refresh.
- Registry. A map from
typestring to a SwiftUI view. Open. Last writer wins. - Renderer. Walks sections, contains unknown types, applies layout chrome in a fixed order.
- Loader. Host code that returns
Data. The framework does not fetch.
Skip any one of these and you will rebuild it under a different name six months later.
Minimal JSON page#
Start with one section and one widget you already understand. A title block is enough.
Do not design thirty widget types before the first page renders. Agree the envelope. Add types when a real surface needs them.
Register one widget#
Starter widgets exist so you can see a page immediately. FlowWidgets.register(on:) puts title block, image text card, banner, button row, tag rail, stepper row, accordion, and separator into the registry.
Your product widget is three files in the app target: payload model (WidgetContent), view (WidgetView), one register call.
The payload model owns static let widgetType = "order_card" and the data shape. Reuse TextData and friends from FlowCore so fonts, colors, and dark hex work without a custom theme on day one.
Do not apply padding, background, or corners inside the view. The backend sends those on layout. The renderer applies them around your body. If you pad twice, every page looks wrong and nobody knows which layer to fix.
Render FlowPageView#
PageStore owns the decoded page, load state, refresh, and pagination. FlowPageView draws nav, bars, sections, sheets, and toasts.
iOS 17 is the floor. Observation is how the view tree stays in sync. There is no iOS 16 shim in the package.
Host PageLoader#
Return raw bytes. Decoding is FlowCore's job. If you decode twice, you will fight diagnostics.
What you still cannot do without a release#
A new widget type is Swift. Ship the binary, then let the backend place it.
A new action type is an ActionHandler you register on ActionDispatcher. Built-ins already cover toast, dismiss, refresh, bottom sheet, and api mutations. Deeplinks are yours. There is no router in the package.
A WebView is still allowed as a destination. It is not the page.
A note on tests#
You can unit test decode without a window: FlowCore does not import SwiftUI. Feed fixture Data through the same decoder the store uses. UI tests still belong on FlowPageView for gestures. Do not skip decode tests because "it is just JSON". Lossy behaviour is a contract.
Closed enum versus this path#
A closed enum cannot decode hologram_projector. You either crash, fail the whole page, or add a default case that throws away the payload with no diagnostics.
An open registry plus LossyArray drops the unknown child, records a key path, and renders the rest. Debug shows a placeholder. Release skips. That is the behaviour you want on old binaries.
Questions this article should close#
Can I skip FlowWidgets?#
Yes. Register only your types. Starters are examples. Last writer wins if you keep them and override.
Where do images load from?#
Host FlowImageLoader. The package is not Kingfisher and not a CDN.
How do I preview in Xcode?#
Hand a fixture PageLoader that returns Data from a JSON file. WidgetContext.inert() exists for widget previews. You still want one FlowPageView preview that loads a full envelope.
Next#
- Quick start is this article as a checklist.
- Adding a widget is the three-step guide.
- Installation is the package product list.
- How to implement SDUI on iOS
How to build server driven UI in SwiftUI: four seams, one registry call, bytes from the host. Everything else is product taste.
Related documentation
Get started
Quick start
Build your first server driven UI screen in SwiftUI: register widgets, implement a page loader, and render backend JSON as native iOS views.
Get started
Installation
Add the Flow-UI server driven UI package to an iOS or macOS app with Swift Package Manager. Requires iOS 17, no third party dependencies.
Guides
Adding a widget
Teach Flow-UI a new server driven UI component in three steps, entirely from your own app: model the payload, write the SwiftUI view, register it.
Related articles
- Server-Driven UI5 min read
SDUI vs WebViews on iOS
SDUI renders native SwiftUI from JSON. A WebView renders HTML in a browser process. Teams pick SDUI when the page should feel like the rest of the app.
- Server-Driven UI4 min read
API driven UI and remote UI, explained
API driven UI and remote UI are names for the same idea as server driven UI: the API returns page structure, the iOS app paints SwiftUI.
- Server-Driven UI4 min read
Backend driven UI vs server driven UI
Backend driven UI, server driven UI, BDU, and SDUI are the same split: the server describes the page, the client renders native views.
More on the blog.
Read the source
Flow-UI is MIT licensed. The schema, renderer and starter widgets live on GitHub.
GitHubGet started with Flow-UI
Install the Swift package, register a widget, and render a page from JSON.
Get started