Getting started

Rendering a document takes five pieces: a vocabulary (the JSON artifact declaring which component types and actions exist), renderers (your code, one per component type), an engine (holds vocabulary, registry, policy, and limits), a builder (per document, injects context, state data, and the action handler), and a host (shows a loading view, then the built view or the failure).

To try vocabularies, documents, and expressions before installing anything, open the Playground: it validates and renders in the browser against the live specification.

Install

SwiftUI

Source, from the main branch:

dependencies: [
    .package(url: "https://github.com/get-milano/sdk.git", branch: "main")
]

Then depend on the MilanoSDK product. Tagged releases ship a prebuilt XCFramework through the same package coordinates.

Compose

Source, as a composite build in settings.gradle.kts:

includeBuild("path/to/sdk/engine/compose")

Or the binary from GitHub Packages (tagged releases; requires a GitHub token with read:packages):

repositories {
    maven("https://maven.pkg.github.com/get-milano/sdk")
}
dependencies {
    implementation("dev.get-milano:engine-compose:0.1.0")
}

Render a first document

The vocabulary declares one component and one action:

{
  "milano": "0.1.0",
  "name": "starter",
  "version": "0.1.0",
  "components": {
    "Greeting": { "properties": { "text": "string" }, "events": { "tap": null } }
  },
  "actions": {
    "openUrl": { "parameters": { "url": "string" } }
  }
}

The document uses it:

{
  "version": "0.1.0",
  "context": { "userName": "string" },
  "root": {
    "type": "Greeting",
    "id": "hello",
    "properties": { "text": { "$expr": "concat('Hello, ', context.userName)" } },
    "on": { "tap": [ { "action": "openUrl", "url": "https://get-milano.dev" } ] }
  }
}

SwiftUI

import MilanoSDK
import SwiftUI

final class GreetingRenderer: MilanoRenderer {
    func render(_ node: MilanoNode) -> AnyView {
        AnyView(
            Text(node.property("text").stringValue ?? "")
                .onTapGesture { node.emit("tap") }
        )
    }
}

var registry = MilanoRegistry()
registry.register(GreetingRenderer(), for: "Greeting")

let engine = try MilanoEngine(
    vocabularyJSON: vocabularyData,
    registry: registry,
    defaultUnknownTypePolicy: .skip
)

let builder = engine.viewBuilder(document: documentData)
    .context(["userName": .string("Ada")])
    .actionHandler { action in
        if action.name == "openUrl" { /* route to your URL opener */ }
    }

struct PromoScreen: View {
    var body: some View {
        MilanoHost(builder: builder) {
            ProgressView()
        } failure: { _ in
            EmptyView()
        }
    }
}

Compose

import dev.getmilano.*

class GreetingRenderer : MilanoRenderer {
    @Composable
    override fun Render(node: MilanoNode) {
        Text(
            text = node.property("text").stringOrNull ?: "",
            modifier = Modifier.clickable { node.emit("tap") },
        )
    }
}

val registry = MilanoRegistry().apply {
    register("Greeting", GreetingRenderer())
}

val engine = MilanoEngine(
    vocabularyJson = vocabularyJson,
    registry = registry,
    defaultUnknownTypePolicy = MilanoUnknownTypePolicy.SKIP,
)

val builder = engine.viewBuilder(documentText)
    .context(mapOf("userName" to MilanoValue.StringValue("Ada")))
    .dispatcher(MilanoMainDispatcher())
    .actionHandler { action ->
        if (action.name == "openUrl") { /* route to your URL opener */ }
    }

@Composable
fun PromoScreen() {
    MilanoHost(
        builder = builder,
        loading = { CircularProgressIndicator() },
        failure = { /* decide what the screen shows instead */ },
    )
}

On Android, pass MilanoMainDispatcher() so events and view updates serialize on the main thread. On SwiftUI the main dispatcher is the default.

What happens at build

build() is asynchronous and all-or-nothing. The document is parsed and validated in full: schema, vocabulary conformance, expression type checking, limits. If the document declares state, your state data provider is awaited and its values are validated against the declarations. Only a document that passes every check produces a view; anything else throws one typed error. See Guardrails for the full taxonomy.

Working samples

The repository contains two complete sample apps, samples/swiftui (Tuist project) and samples/compose, demonstrating banners with three layouts, an interstitial, a Milano view embedded between native components, a form with conditional visibility, required markers, and expression-driven errors, and a screen that merges app-wide and per-screen context from a live API. See them side by side, with screenshots from both platforms, in Samples; they follow the architecture described in Guidelines.


This site uses Just the Docs, a documentation theme for Jekyll.