Skip to content
Flow-UILive
On this page

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.

Ayush MishraPublished 6 min read

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.

ConcernNative SDUIWKWebView
ScrollingSwiftUI / UIKitWeb process
Swipe backSystem interactive popEasy to break
VoiceOverUIKit elementsWeb AX tree
Dynamic TypeFontData + SwiftUICSS you maintain
New widget typesApp Store binaryCSS/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.

The Flow-UI mark: two nodes joined through a filled centre

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.

OrderCardWidget.swiftswift
struct OrderCardWidget: WidgetView {
    let content: OrderCardContent
    let context: WidgetContext
 
    var body: some View {
        Text(content.orderNumber.text)
    }
}

After that registration, the backend can place order_card anywhere a widget is legal: a section, a header, a sheet, nested in an accordion.

swift
registry.register(OrderCardWidget.self)

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#

SDUI on iOS is native SwiftUI from JSON. Call it remote UI if you want. Do not call it a WebView.

More on the blog.

Read the source

Flow-UI is MIT licensed. The schema, renderer and starter widgets live on GitHub.

GitHub

Get started with Flow-UI

Install the Swift package, register a widget, and render a page from JSON.

Get started