Skip to content
Flow-UILive
On this page

Overriding a starter widget

Last registration wins. Override a starter widget by registering the same type string with your view. No fork of the package.

Ayush MishraPublished 6 min read

Last registration wins. Override a starter widget by registering the same type string with your view. No fork of the package. That rule is the whole customization story for hosts that like the JSON contract of image_text_card or banner but not the default chrome.

Teams that skip this and vendor the FlowWidgets sources create a private fork they must merge forever. Teams that invent a parallel product_row type for every visual tweak force the backend to send two schemas. The registry already has an overwrite. Use it.

This follows starter widgets as worked examples. The catalogue stays the contract. The view becomes yours. How to add a brand new type is still Adding a widget. Override is the case where the type string already exists.

Last writer wins is a dictionary put#

WidgetRegistry.register stores an entry keyed by V.Content.widgetType. A later call with the same key replaces decode, view builder, and optional skeleton thunk. There is no merge, no “partial override,” no subclass hook.

HostBootstrap.swiftswift
let registry = WidgetRegistry()
FlowWidgets.register(on: registry)
registry.register(ProductRowWidget.self)

If ProductRowWidget.Content.widgetType is "image_text_card", every JSON widget of that type now builds ProductRowWidget. Accordion children, section bodies, sticky headers, footers, and bottom sheets all follow, because they all ask the same table.

swift
registry.register(ProductRowWidget.self)

Register after the library if you are replacing a starter. Register before the library if you want the starter to win, which is almost never what you meant. FlowWidgets.register(on:) documents the rule in source: call once at startup, before or after your own widgets; last registration per type wins.

The Content type must still decode the JSON#

Override replaces the view and the decode closure together. Your WidgetContent is what the registry will decode from data. If you keep the starter’s JSON shape, copy the starter’s content struct (or decode the same keys). If you change keys, the backend has to change too, and you no longer have an override. You have a new type wearing an old name, which will break old payloads.

The clean override: same widgetType, same data keys, different body. Reuse ImageTextCardContent if it is public and sufficient. If you need extra host-only view logic with no new keys, you can still wrap that content type.

If you need extra JSON fields, make them optional with decodeIfPresent so old documents keep working. Additive fields are the schema rule in Resilience. Required new keys are a breaking change, not an override.

What you copy, what you leave#

Copy layout discipline. Do not apply page margins inside the view. Envelope layout already wraps you.

Copy dispatch discipline. Envelope taps are attached by WidgetRowView. Inner buttons call context.dispatch. Do not reach for a dispatcher or a store.

Copy atom use. FlowText, FlowImage, FlowTag honor theme and dark colors. Reimplementing Text with a hardcoded font is how overrides drift from Dynamic Type.

Leave the type string alone. The backend, the playground fixtures, and every nested accordion already send image_text_card. Your view’s job is to look like the rest of the app when that string arrives.

Skeletons override too#

If the starter provided WidgetSkeletonProviding and your type does not, loading states fall back to the default block. If your row is visually heavy, conform and return a shaped placeholder. The registry stores whichever thunk the last registration supplied.

Previews of the override#

Preview ProductRowWidget with WidgetContext.inert() and a fixture ImageTextCardContent. You do not need to boot a page to see that the trailing chevron is now your asset and the tag style matches checkout. Then load a real envelope in the playground against a registry that includes the override, because nested and sheet paths are how you find the one surface you forgot.

When not to override#

Do not override title_block to mean “product hero with video.” That is a new type. Overloading a heading widget until it is a media player is how a specialised SDUI stack turns into hubris.

Do not override separator to hide a full checkout widget. Separators are dividers.

Do not override one type in debug and another in release unless you enjoy diagnosing “works on my machine” with a registry dump. If you need a debug-only chrome, use UnknownWidgetPolicy and the debug overlay, not a shadow catalogue.

Do not register from a view body. Registration is launch work on the main actor, once. Doing it lazily from the first page can race a decode that already ran with the starter type.

Forking versus overlay#

A fork of FlowWidgets is justified when you are changing decode rules for everyone, or contributing a generally useful widget upstream. An overlay (register after) is justified when your app’s visual language disagrees with the starter body. Most product work is overlay.

The FAQ says you do not have to use starter widgets at all. Skipping FlowWidgets.register and registering only product types is valid. Override is the middle path: keep the public type strings so the backend stays stable, swap the views so the app stays on brand.

How last-wins interacts with other last-wins#

ActionDispatcher.register is also last writer wins, newest handler first. A host handler can override toast or api. That is a different table. Do not try to override a widget by installing an action handler. Do not try to override an action by registering a widget.

Unknown types are not an override target. If the backend sends hologram_projector and you have not registered it, policy is skip or placeholder. Register a real view under that type to support it. That is adding a widget, not overriding a starter.

Nested containers do not get a say. Accordion cannot pin the starter image_text_card while the rest of the page uses yours. One table, one winner per string.

A concrete overlay checklist#

  1. Pick the starter type whose JSON you want to keep. Read its WidgetContent keys from source or from the catalog.
  2. Write a WidgetView that decodes those keys (reuse the content struct when you can).
  3. Set static var widgetType to the same string as the starter.
  4. Call FlowWidgets.register(on:) then registry.register(YourWidget.self) at launch.
  5. Preview the view. Then hit a page, a sheet, and an accordion that include that type.
  6. Keep optional new keys additive. Do not rename title because your designer said headline unless the backend renamed it too.

Product nouns still belong as new types#

order_card should not be an override of image_text_card. It has order numbers, status tags, line items. That is a new widgetType. Override is for “same noun, our chrome.” New nouns go through the add-a-widget path, which How to implement SDUI on iOS already listed as app-launch work.

The protocol rules in WidgetView, WidgetContext, and AnyView erasure still apply. Your overlay is a generic WidgetView. The registry remains the only widget AnyView wall. You are not type-erasing in the overlay body to “beat” the starter.

Where this sits#

Starter widgets are examples. The registry is a dictionary. Last put wins. Forking the package to change a chevron is wasted work. Changing a type string without changing the backend is a silent unknown widget in production.

Register yours second. Keep the JSON. Ship the chrome your app already has.

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

Same type string, your view, no fork. That is an override.

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