SDUI on iOS: native SwiftUI, not WebViews
SDUI on iOS is native SwiftUI rendered from a JSON page. A WebView is a different product with different scrolling, gestures, and accessibility.
SDUI on iOS means the server describes a page and the client draws native SwiftUI. A WebView is a browser inside your app. Those two products share a marketing sentence ("the backend controls the UI") and almost nothing else in the view tree.
The confusion is old. Hacker News threads under "What Is Server-Driven UI?" fill up with "why not WKWebView?" because HTML is already a UI schema. The answer is not FPS tables. The answer is how the page feels next to the rest of a native app.
The confusion#
Product people hear "server driven" and picture a CMS that publishes a page. Engineering hears "remote UI" and pictures HTML. On iOS those collapse into WKWebView unless you name the renderer.
SDUI, as Flow-UI implements it, is a JSON envelope decoded into PageModel, then rendered by FlowPageView. Each widget is a SwiftUI View that conforms to WidgetView. The server never sends HTML. The client never asks WebKit to layout the page.
A WebView is still the right tool for a document that is genuinely a document: a help article authored in HTML, a payment flow a vendor only offers as a web checkout, a legal page that must match a website pixel for pixel. Those are not product surfaces. They are documents.
Native view tree vs embedded browser#
A native SDUI page is a SwiftUI tree. ScrollView, NavigationStack (or your UIKit nav), swipe-back, large titles, and the keyboard all belong to UIKit/SwiftUI. Hit testing is the same as the settings page you wrote by hand.
A WebView is a separate process with its own scroll physics. Rubber-banding fights the outer ScrollView unless you spend a sprint on nested scroll. WKWebView accessibility is a web accessibility tree, not the UIKit one. Dynamic Type becomes CSS. Dark mode becomes a second stylesheet you have to keep honest.
| Concern | Native SDUI | WKWebView |
|---|---|---|
| Scrolling | SwiftUI / UIKit | Web process |
| Swipe back | System interactive pop | Easy to break |
| VoiceOver | UIKit elements | Web AX tree |
| Dynamic Type | FontData + SwiftUI | CSS you maintain |
| New widget types | App Store binary | CSS/JS you already shipped |
That table is qualitative. It is not a benchmark.
Accessibility, Dynamic Type, swipe-back#
VoiceOver users on iOS expect a button to be a button. Flow-UI widgets are SwiftUI views, so traits come from the view you wrote. The schema still has to send alt text on images and labels on controls. Native does not mean automatic copy.
Dynamic Type is the same story. Atoms like TextData and FontData exist so the backend can describe type without shipping a new font file. The view still uses SwiftUI text, which scales.
Swipe-back is the tell. If your "server driven home" eats the interactive pop gesture, users feel a website. If it does not, they feel an app. Native SDUI keeps the gesture because there is no web view stealing touches.
What still requires an App Store binary#
Native does not mean the server can invent a widget. A map, a camera preview, a custom chart: those are Swift types you compile. You model a payload, write a WidgetView, and register it.
After that registration, the backend can place order_card anywhere a widget is legal: a section, a header, a sheet, nested in an accordion.
Unknown types are a different problem. Old binaries will see new strings. Debug builds default to UnknownWidgetPolicy.placeholder. Release builds default to .skip. The rest of the page still renders. That is the opposite of a WebView, where an unknown HTML tag is usually just ignored by the parser with no diagnostics in your telemetry unless you built that yourself.
Flow-UI widgets are SwiftUI views#
The protocol is @MainActor because View is. The registry is the only AnyView erasure point. Your widget stays generic. WidgetContext is how it talks to page state and to ActionDispatcher.
Flow-UI does not fetch. PageLoader is a host protocol: loadPage(_ request: PageRequest) async throws -> Data. Put URLSession in the host, or a JSON file, or a GraphQL client. The renderer only decodes.
Deeplinks are the same seam. There is no router in the package. A deeplink action is data. Your handler decides where to go. That keeps SDUI from becoming a second navigation framework fighting UIKit.
How teams get this wrong in production#
The failure is rarely "we chose WebView". It is "we chose both on one scrolling page". A native header, a WKWebView body, a native tab bar: nested scroll, double bounce, and a VoiceOver rotor that jumps between trees. If you must host web checkout, push it as a destination (deeplink or modal) so the merchandising page stays one renderer.
Another failure is snapshot SDUI: the server returns images of UI. That can look native in a screenshot and still fail Dynamic Type, localisation, and hit targets. Flow-UI does not snapshot. Widgets are views.
JSON is not the differentiator#
Both stacks can be "server driven" in a slide. HTML is a UI schema. JSON is a UI schema. Ask what process paints pixels. If the answer is WebKit, you have a WebView product. If the answer is SwiftUI, you have SDUI as this site uses the word.
UIKit hosts#
You do not have to be a SwiftUI-only app. FlowHostingController embeds FlowPageView in a UIHostingController. The widgets are still SwiftUI. The surrounding navigation can stay UIKit. That is still native SDUI, not a WebView.
When a hybrid still happens#
Checkout webviews, OAuth, and partner content will stay. Host them as a destination of a deeplink or an api action, not as the body of every merchandising page.
Questions this article should close#
Is a hybrid native shell plus WebView body "good enough"?#
For a document, yes. For a home feed that should feel like the rest of the app, you will spend the savings on nested scroll and accessibility. Measure before you standardise on hybrid.
What to read#
- FAQ answers the WebView question in one accordion item.
- Introduction is the product path into the package.
- Server driven UI for SwiftUI is the category definition this post specialises.
SDUI on iOS is native SwiftUI from JSON. Call it remote UI if you want. Do not call it a WebView.
Related documentation
Resources
FAQ
Is Flow-UI free, is server driven UI just WebViews, what happens on old app versions? Straight answers about SDUI on iOS and SwiftUI.
Get started
Introduction
Flow-UI is an open source server driven UI framework for SwiftUI. Your backend describes an iOS screen as JSON and Flow-UI renders it as native views.
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