Skip to content
Flow-UILive
On this page

SwiftUI component registry

A SwiftUI component registry maps a backend type string to a payload model and a view. One register call. No framework enum of widgets.

Ayush MishraPublished 6 min read

A SwiftUI component registry maps a backend type string to a payload model and a view. One register call. No framework enum of widgets. Every gist that teaches SDUI with a closed switch is competing with this idea. The ranking wedge is not a claim about Google. It is that an open table survives types the compiler did not name last quarter.

Flow-UI's type is WidgetRegistry. Searchers say component registry. The vocabulary on this site is still widget. The registry is where the search phrase and the product meet.

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

One generic call#

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

register<V: WidgetView>(_ viewType: V.Type) reads V.Content.widgetType, stores a decode of V.Content from data, and stores a view builder that wraps V(content:context:) in AnyView. The pair is created in one generic scope so the cast cannot fail.

swift
registry.register(PromoBannerWidget.self)

Last write wins. Override a starter by registering the same type string later. No fork of FlowWidgets.

Why not a framework enum#

An enum of associated values cannot decode hologram_projector. You add a case, you ship a binary, you still cannot load that case on last month's binary without a default that throws away the payload.

The registry plus unknown policy plus LossyArray drops the unknown child and keeps the page. Diagnostics name the type and path. That is the production behaviour gist tutorials skip.

What gets registered#

Starter widgets: title block, image text card, banner, button row, tag rail, stepper row, accordion, separator. See the catalog. They are examples. You can register none of them.

Product widgets live in the app target. Three pieces: WidgetContent model, WidgetView, register. Guide: Adding a widget.

Optional WidgetSkeletonProviding for loading shapes. The registry stores a skeleton thunk if the type conforms.

Policy#

UnknownWidgetPolicy.placeholder default in debug. .skip default in release. Pass a policy into WidgetRegistry(unknownPolicy:) if you want placeholders in a staging release build.

registeredTypes is a sorted list of strings for debug UI. Use it. Do not guess what launched.

Threading and actors#

WidgetRegistry is @unchecked Sendable with a lock around the table. makeView is @MainActor. Views are main actor. Do not call makeView off the main actor. PageStore is already main actor.

Search language: component versus widget#

Apple says View. Many SDUI posts say component. Flow-UI JSON says type on a widget. This article's title uses component registry because that is the query. The code you write is WidgetView and WidgetRegistry.

You can register from a Swift package of your own. A design-system package can expose register(on:). Last writer still wins if the app registers again. register does not do I/O. It writes a dictionary. Keep it launch-cheap.

Open table versus gist switch#

A gist switch on a Codable enum compiles only the types you named. The next backend string is a compile error or a silent default. The registry is a [String: Entry] behind a lock. Unknown strings are policy, not a broken build. That is the ranking wedge against those tutorials: not a claim about search results, a claim about old binaries.

makeView is @MainActor. WidgetView is @MainActor. PageStore is @MainActor. Do not call view(for:context:) off the main actor. Decode can run detached because WidgetDecoding is Sendable and FlowCore stays UI-free.

Environment#

FlowPageView and FlowSheetView install flowRegistry. Reads outside them see an empty registry. That empty default is how a preview without a page view shows nothing. Pass the same instance into every PageStore for a session.

Starter catalogue: title_block, image_text_card, banner, button_row, tag_rail, stepper_row, accordion, separator via FlowWidgets.register(on:). Override by registering the same widgetType later. The catalog documents each starter. The adding a widget guide is the three-step product path. Earlier posts in this cluster: JSON to SwiftUI and architecture.

How a gist switch fails in production#

Tutorials titled "dynamic component rendering SwiftUI" almost always show an enum of associated values and a switch in a view builder. That compiles. It even previews. Then the backend adds a type the enum does not name. You have three bad options: fail the page, hide the child with no key path, or ship a binary the same day as the payload. The registry is the fourth option. decodeContent returns nil for unknown strings. AnyWidget stores UnknownWidgetContent. WidgetRowView skips or placeholders. DecodeDiagnostics records unknownType with a coding path. Marketing can preview a type that 40 percent of binaries do not have. Home still paints.

What one register call actually connects#

Three things, documented in the registry comment: the type string, the WidgetContent model, the WidgetView. Decode and view creation are closed over together so the internal cast cannot fail. Optional WidgetSkeletonProviding adds a fourth: a skeleton thunk for loading. registeredTypes is a sorted list of strings. Surface it in a debug overlay. Guessing what launched is how you spend an afternoon on a "missing" row that was never registered on the TestFlight scheme.

Host a UIKit stack with FlowHostingController and the same registry. There is not a UIKit catalogue and a SwiftUI catalogue. There is one table.

A SwiftUI widget registry is one generic register call and an open table. Skip the framework enum. Let unknown types drop out.

Register order, starters, and overrides#

Call FlowWidgets.register(on:) if you want the eight starters. Then register product types. Then register again to override a starter you dislike. Last write to entries[V.Content.widgetType] is the only override API. There is no unregister. There is no fork-the-package path. If two feature flags register different views for the same string, the later call during launch wins. Make that order explicit in one bootstrap file.

Unknown policy can be passed into WidgetRegistry(unknownPolicy:). Staging release builds sometimes want placeholders. Production release wants skip. Debug defaults already match those instincts.

register(_ viewTypes: [any WidgetView.Type]) exists for bulk registration. FlowWidgets.all is computed, not a stored global array, because a stored array of existential metatypes is shared mutable state under Swift 6. Copy that idea if you write your own catalogue package.

The search phrase is component registry. The type you import is WidgetRegistry. The JSON key is type. Keep saying widget in code review. Let search engines have the other noun.

Register on the main actor before the first page appears. A registry mutated from a background queue will race the lock, which exists, but your WidgetView initializers will not. Launch is the right time. A lazily registered type that appears after decode has already run will look unknown until the next fetch. Keep bootstrap in one file so code review can see the catalogue: starters, product types, then overrides. If registeredTypes in the debug overlay disagrees with the JSON you expected, you registered on a different instance. Pass one registry. One table at launch is the whole operational story of a SwiftUI widget registry.

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