Authoring screens¶
Murti renders screens from JSON. MurtiBuilder is a Swift DSL that produces that
JSON, so you can write screens in typed Swift instead of by hand, and MurtiGen
turns authored screens into validated JSON files at build time.
Both run on Apple platforms and depend on MurtiCore.
The DSL¶
import MurtiBuilder
let dashboard = Screen("dashboard") {
VStack(spacing: 12, alignment: .leading) {
Text("Hello, {{user.name}}", style: .title)
Button("View details",
action: .navigate("productDetail", params: ["productId": "abc123"]))
Button("Load balance",
action: .api("getAccountBalance", onSuccess: .navigate("balanceDetail")))
}
}
let json = try dashboard.validated().jsonData()
{{user.name}} stays in the output verbatim; the app resolves tokens at render
time. validated() runs the schema validator and throws if the screen is invalid;
jsonData() encodes the payload (with sorted keys, so output is stable).
Built-in components¶
Text, Image, VStack, HStack, Button, and Card cover the built-in
component set. Containers take a child closure; the closure supports if and
for:
VStack(spacing: 8) {
if showHeader { Text("Recent", style: .headline) }
for item in items { Text(item.title) }
}
Any other component¶
The built-ins mirror the renderer's built-in set, but the registry is open. Use
component(_:_:) to author a third-party or custom type by its type string:
Actions¶
Button takes an Action: .navigate(_:params:onSuccess:onError:),
.api(_:params:onSuccess:onError:), .dismiss, .refresh, or .openURL(_:).
Targets are named references (a screen key, a named request, a named link), never
a raw URL.
A note on names¶
Text, VStack, Button, and Image share names with SwiftUI. Write
screen-authoring files that import MurtiBuilder but not SwiftUI; if a file
needs both, qualify the DSL type as MurtiBuilder.Text.
Export as DSL¶
DSLExport runs the DSL in reverse: given a decoded tree (a MurtiNode or a full
MurtiPayload), it generates MurtiBuilder source that rebuilds an equivalent tree.
The visual editor uses it for its Export DSL button, so a screen
imported as JSON — or built by direct manipulation — can come back out as Swift.
A node is written with its typed initializer (Text, VStack, Button, …) when
its props fit that initializer exactly; anything else falls back to the
open-vocabulary component("type", [props]) { … } so no props are lost. Node ids
aren't emitted (the DSL doesn't set them), matching JSON export. An action on a
non-button has no DSL form, so it's omitted with a note in the output.
Build-time generation¶
MurtiGen writes each authored screen to <key>.json, validating every screen
first so a bad bundle never ships. Author screens in
Sources/MurtiGen/main.swift
(loop over data for lists), then run:
Serve the resulting JSON from any backend or CDN as opaque bytes. The structure is
fixed at generation time; dynamic values stay at runtime through {{token}}s and
named API requests. To ship signed, production-ready payloads, wrap the output in
a signed envelope (see the security model) as part of your build.