Skip to content
Flow-UILive
On this page
Scheduled 4 Sept 2026

How to implement SDUI on iOS

Implement SDUI on iOS by decoding a page envelope, registering widgets, containing unknown types, and dispatching actions as data.

Ayush MishraPublished 4 min read

Implement SDUI on iOS by decoding a page envelope, registering widgets, containing unknown types, and dispatching actions as data. The SwiftUI tutorial in this series named the four seams. This post is the iOS-shaped version: UIKit hosting, iOS 17, Observation, and the decode path that must not crash a session.

Decode the envelope, not a private struct per page#

One PageModel type is the contract. Hand-written Decodable with decodeIfPresent defaults is how FlowCore stays additive. If every team invents HomePayload and CartPayload as unrelated structs, you will never share widgets or mutations.

The walk is page, then section, then AnyWidget. Layout, actions, and tracking ride beside data. Arrays are lossy: one bad child does not empty the section.

AppBootstrap.swiftswift
import FlowUI
 
let registry = WidgetRegistry()
FlowWidgets.register(on: registry)
registry.register(OrderCardWidget.self)

Pass that registry into PageStore. The store calls your PageLoader, decodes, and holds the page. A slower response does not overwrite a newer one.

swift
let store = PageStore(pageID: "home", loader: APILoader(), registry: registry)

Register widgets from the app target#

iOS 17+ is required. The renderer uses Observation and modern scroll APIs. There is no compatibility layer.

Widgets are @MainActor because View is. Register at launch, on the main actor, before the first FlowPageView appears.

UIKit apps do not rewrite first. FlowHostingController(store:dispatcher:) is a UIHostingController. Push it. The widgets are still SwiftUI.

Contain unknown types#

Old App Store binaries are a production fact. The backend will ship type strings this binary does not know.

UnknownWidgetPolicy defaults to .placeholder in debug and .skip in release. Diagnostics record type and coding path. Duplicate ids get a suffix and a report, because ForEach and mutations both break when two widgets share an id.

This is not optional polish. It is the difference between "SDUI crashed the home page" and "one promo is missing on last month's build".

Dispatch actions as data#

Taps are not NavigationLink trees hardcoded in the widget. The backend sends ActionData. ActionDispatcher is a chain. Built-ins: toast, dismiss, refresh, bottom sheet, api mutations.

Deeplinks are host handlers. Flow-UI ships no router on purpose. Your app already has tabs, a stack, or a coordinator. Forward the URL there.

API actions round-trip through PageLoader as PageRequest.Kind.action. The response can mutate the page (replace a widget, patch a section) without fetching a whole new envelope.

Networking stays in the host#

PageLoader.loadPage(_:) async throws -> Data is the whole HTTP story. Flow-UI performs no network calls. Image loading is the same idea: FlowImageLoader is a host seam.

That is how you keep URLSession configuration, certificates, and auth in the app that already owns them.

Observation and state#

PageStore is the page. WidgetStateStore is ephemeral per-widget state (stepper counts, expansion). SwiftUI Observation is the invalidation path. Do not duplicate page JSON into a second ObservableObject unless you enjoy desync.

Stable widget ids matter on refresh. Send real ids for anything interactive. Positional fallbacks exist so ForEach does not reshuffle, but they are the floor.

Cancellation and races#

PageStore retains the in-flight fetch so a newer request can cancel an older one. Without that, a slow refresh can overwrite a fresh api mutation. You do not have to reimplement this if you use the store. If you roll your own, you will rediscover it in production.

Pagination appends by re-reading current state after await, and it ignores a next-page response when the cursor no longer matches. That is how a replace_widget during a fetch does not get clobbered.

A worked iOS checklist#

StepOwner
Add the Swift packageApp
Register starter + product widgetsApp launch
Implement PageLoaderExisting API client
Present FlowPageView or FlowHostingControllerFeature
Register deeplink ActionHandlerNavigation
Point telemetry at DecodeDiagnosticsDebug + production logs
The Flow-UI mark: two nodes joined through a filled centre

Questions this article should close#

UIKit or SwiftUI first?#

If the app is UIKit, host FlowHostingController and keep your coordinators. If the app is SwiftUI, present FlowPageView. Do not rewrite the app as a prerequisite.

What about iPad and Mac Catalyst?#

SwiftUI widgets run where SwiftUI runs. You still have to design layouts that make sense in a regular-width size class. The envelope can send different pages per size class from the backend if you send that signal in PageLoader. The package does not invent a breakpoint system.

How to implement SDUI on iOS: decode, register, contain, dispatch. Networking stays yours. The router stays yours.

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