Open registry vs closed Codable enum
A closed Codable enum of widgets cannot learn a type the binary does not name. An open registry can, and unknown types degrade instead of failing the page.
A closed Codable enum of widgets cannot learn a type the binary does not name. An open registry can, and unknown types degrade instead of failing the page. Noah Little's Medium posts on enum Codable synthesis and Jacob Bartlett's switch-shaped SDUI are the industry tutorials. Flow-UI refused that shape on purpose.
What the closed enum looks like#
Synthesis is tidy. A switch in the view is tidy. Production traffic is not tidy. Marketing ships hero_v3. 12% of sessions are a version behind. The enum either fails decode or you add unknown that throws away data without a key path.
Associated values also fight Swift 6 sendability when the enum grows closures and views.
What the open registry looks like#
The table is [String: Entry]. Keys are widgetType strings. Values are decode plus AnyView builder. Unknown keys are not a compile error. They are a runtime policy.
Last writer wins. Override is not a new enum case.
Degradation is the point#
Resilience is the contract: unknown type, malformed payload, dropped array element, duplicate id. The page renders. Debug placeholder, release skip. DecodeDiagnostics records the rest.
A closed enum that throws on unknown is the opposite contract. You will blank home. You will then add try? and silently drop without diagnostics. You will have built a worse registry.
When a closed enum is still fine#
A prototype. A hack day. A single surface with two types that will never grow. The moment a third team wants a widget without editing the shared enum, you lost.
If you must start with an enum, put it in the app, not in a package other apps import. Migrating later is copying cases into WidgetView types.
Last writer wins versus enum exhaustiveness#
Exhaustiveness is a compiler feature. It is also a coupling feature. Every app that imports your enum recompiles when you add a case. A registry lets a feature module register order_card without touching a central enum file. That is how large iOS orgs actually ship.
Lossy arrays are the other half#
Even with a registry, a malformed data object for a known type must not kill the section. LossyArray drops the child. The enum approach often wraps the whole list in one decode that fails together. Pair open lookup with lossy lists.
any WidgetView.Type is an existential of types you registered. The set is whatever launch code added, not a frozen enum in FlowRender. You could decode unknown types into a generic JSON dump. Default policy is skip or placeholder. A generic JSON view is how you accidentally build HTML. Prefer skip.
What Flow-UI refused#
Noah Little's Medium posts show how Swift synthesises Codable for enums with associated values. That is a good lesson for domain models. It is a bad catalogue for UI types that arrive over the network from a backend that ships weekly. Jacob Bartlett's backend-driven SwiftUI writing uses a switch-shaped renderer as the teaching tool, and is honest that specialised SDUI is useful while a generic HTML-in-JSON framework is hubris. Flow-UI took the specialised path: open table, native WidgetView, unknown types degrade.
Last writer wins on entries[V.Content.widgetType]. Exhaustiveness is a compiler feature and a coupling feature. Every app that imports a shared enum recompiles when you add a case. A registry lets a feature module register order_card without touching a central file.
Unknown policy: debug .placeholder, release .skip. Diagnostics: unknownType, malformedPayload, droppedElement, duplicateID. Nested accordion ids are not unique page-wide. None of that falls out of a closed enum for free.
Read adding a widget, resilience, and the registry article.
A closed enum cannot version additively#
Additive JSON is the Mobile Native Foundation conversation in miniature: old binaries must survive new fields and new type strings. An enum case is not additive. It is a source change. A new associated value is a source change. A new WidgetView plus register is also a source change, but only for binaries that should render the type. Everyone else skips. That is schema versioning without a max_schema_version header in the package. If your host wants a capability header, send one. Flow-UI will not invent it.
Swift 6 makes the enum worse, not better#
WidgetView is @MainActor because View is. A giant enum that holds view types, payloads, and maybe closures fights sendability. The registry stores @Sendable decode closures and @MainActor view builders behind a lock (@unchecked Sendable on the class). You do not have to solve that puzzle in your feature module. You write a WidgetContent struct and a WidgetView and you register.
A default enum case that erases data to JSONValue without diagnostics is a silent registry. You will debug it with print statements. DecodeDiagnostics already has unknownType and malformedPayload. Use the type that exists.
Closed enums freeze the catalogue. Open registries degrade. Production SDUI needs the second.
Gist authors will keep shipping enum tutorials because they fit in one file. That is fine for a weekend. The day a second team needs a type without editing a shared enum, move. Flow-UI is the package-shaped version of that move: open table, last writer wins, lossy unknown types. You can copy the seams without the brand. You should not copy the enum.
Migration from a gist without a rewrite week#
If you already shipped an enum, you do not have to delete it on Tuesday. Wrap each case in a WidgetContent plus WidgetView. Register them. Stop adding cases. Point PageStore at the registry. Keep the enum only as a decoder for old fixtures until they drain. The dangerous moment is leaving both paths live: some pages switch, some pages look up. Pick a date. After that date, unknown strings must hit policy, not default: EmptyView().
What "open" does not mean#
It does not mean the payload can run Swift. It does not mean a script field. It does not mean downloaded executable code. Guideline 2.5.2 conversations stay in the production post. Open means the catalogue is a table filled at launch. Types you did not register cannot paint product UI. They skip. That is containment, not an interpreter.
Airbnb Ghost is the industry pattern of one schema and native renderers, not a Flow-UI case study. iOS still writes SwiftUI. The JSON still cannot invent a gesture you did not compile. The enum versus registry choice is only about how the iOS renderer fails when the string is new.
If you remember one contrast: a closed enum fails closed, an open registry fails open, and production traffic is mostly the second. Gist authors will keep shipping enum tutorials because they fit in one file. Use them as a weekend. Ship the table. Pair it with LossyArray or you will still blank a section when one data object is wrong. Open lookup without lossy lists is half a design.
Related documentation
Related articles
- Components6 min read
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.
- Components7 min read
Nested widgets and accordion
Nesting is a widget that holds more widgets. Accordion is the starter example: decode children through the same registry.
- Components8 min read
Starter widgets as worked examples
Starter widgets are examples you can override: title block, image text card, banner, tag rail, stepper, accordion, separator, button row.
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