Product9/14/2026·5 min read

The Widget Is a Document, Not a View

WidgetCraft stopped shipping a second UI implementation: a document package now feeds the creator preview, the extension, and the renderer from one tested tree.

The preview that lied

WidgetCraft started with the usual shape for a widget builder: a creator app assembled a widget, a preview showed what the creator had assembled, and the extension had its own code for producing the widget that the system actually installed. The three surfaces looked related. They were not the same program.

That distinction only matters when they disagree, which is exactly when a widget builder becomes expensive to debug. A preview can show the object the creator has in memory while the extension loads a stale archive. A data provider can return a value in the app process while the extension has a different registration table. A layout change can be visible in the editor and absent from the Home Screen. Each side is locally plausible, and the user sees only that the widget is wrong.

The first design decision was therefore a boundary rather than a screen:

The widget is a document that two runtimes interpret.

The creator does not hand the extension a view object. It writes a package. The package has a manifest describing the container and a document.json describing the element tree. The extension does not know how the creator produced that file. It loads the same document and interprets it with the same runtime contract.

That sounds like serialization. It is more specific than that. The document is the product's durable output, so its shape is a user-facing compatibility boundary. The interpreter is mechanism: it knows how a text node, image node, stack, timeline, data provider or action becomes a real widget view. The product owns which nodes it offers and how it names them. The mechanism owns how a valid node behaves on the device.

The package is the seam

The package system gives each side a clean responsibility.

The creator can edit a document without importing the extension's lifecycle. It can preview a draft without pretending that the user installed it. The installer can validate the manifest, copy the package into the container's storage boundary, and publish an installation event. The extension can ask for the selected document and render it without knowing whether it came from the creator, a built-in card, or a future package source.

That last point is why the selection mechanism had to be separate from the product's configuration. The extension owns the system-facing intent and the runtime owns the document. WidgetCraft supplies the available choices; the Foundation widget runtime resolves the selected package. If the renderer imports a product's WidgetConfigurationIntent, the mechanism has learned about one product and the boundary has already failed.

The package also makes lifecycle visible. Creating a widget is not installing one. The creator writes a candidate package, the user chooses Install, and only then does the installer make it available to the extension. After installation or removal, the app tells WidgetKit that the set of available timelines changed. Without that signal the file can be correct on disk while the Home Screen keeps rendering the old snapshot. The user action and the system refresh are two parts of one state transition.

Moving the mechanism out of the product

The document was the visible seam. The larger change was moving the code behind it to Foundation.

The old WidgetCraft tree contained data providers, device information, storage, JavaScript APIs, actions, timeline support and container management. They were product-owned mostly because the first product needed them there. By September, that directory had grown to roughly 28.5k lines of code that had no reason to know WidgetCraft existed. Keeping it there made every future widget product either copy the runtime or depend on a product target, both of which turn a mechanism into a product dependency.

The migration kept the ownership rule strict:

  • Foundation owns the document model, package storage, interpreter, providers, actions and lifecycle.
  • WidgetCraft owns the catalog of built-in widgets, the creator UI and the choices it offers.
  • The extension owns the system entry point and asks the runtime to render the selected document.

This is not a directory cleanup. It changes what can be expressed. A new data provider can be tested against the runtime without launching WidgetCraft. A new creator card can be added without changing the extension's renderer. And a second product can consume the mechanism without making the first product a library.

Render the thing that ships

The preview tests were the place where the architecture had to prove itself. A test that instantiates a view controller can pass while the document is empty, a node is ignored, or the extension uses a different path. The test therefore builds a real element tree, feeds it to the real SwiftUI renderer, and writes a PNG to the test output.

That choice makes the assertion about content rather than existence. The test can inspect the generated image, but it also keeps the document and renderer in the same path the extension uses. A preview is useful only if it answers the question the user will ask after installation: what will appear on the Home Screen?

The document tests sit below that rendering test. They cover decoding, unknown fields, container metadata and storage behaviour. The renderer tests cover the visible tree. The integration path covers package installation and WidgetKit refresh. Each test owns one boundary; no single snapshot is asked to prove storage, selection, rendering and system refresh at once.

What the document buys us

There is a cost. A document format needs versioning, validation and migration. A view written directly in Swift can use any private implementation detail; a document must say what it means. We accepted that cost because it is paid once at the package boundary, where a failed decode can name the field and refuse installation. A malformed widget should never become a half-rendered widget on a user's Home Screen.

The payoff is that the creator, preview and extension no longer have to agree by convention. They agree by reading the same artifact. The runtime can evolve behind that artifact, and the product can evolve its catalog without teaching the runtime which product it belongs to.

When you build a widget in WidgetCraft, the useful thing is the editor. The engineering decision that makes the editor trustworthy is quieter: the thing you create is a document, and the system renders that document all the way to the Home Screen.

Takeaways

Treat a generated UI as an artifact with a contract. If the creator and the runtime each reconstruct the result, they will eventually disagree. A manifest and a document make the boundary inspectable and testable.

Keep the interpreter product-independent. Moving the mechanism into Foundation lets a product own its catalog and lets another consumer reuse the renderer without importing the first product's composition root.

Make previews execute the shipping path. A view existence assertion can pass for an empty widget; a real element tree rendered by the real SwiftUI path gives the test a visible, user-level claim.

Model installation as a lifecycle transition. Writing a package, selecting it, installing it and refreshing WidgetKit are separate operations with one owner for each state change. Treating them as one file copy is how a correct widget stays stale on the Home Screen.

Products in this post

Keep reading