Skip to content

Instantly share code, notes, and snippets.

@bcardarella
Created September 5, 2025 16:20
Show Gist options
  • Save bcardarella/0abb363347b25c353674fd84331b567e to your computer and use it in GitHub Desktop.
Save bcardarella/0abb363347b25c353674fd84331b567e to your computer and use it in GitHub Desktop.

VML Specification (Canonical)

VML (View Markup Language) is a declarative markup for describing SwiftUI-compatible UI hierarchies. It encodes structure, attributes, and modifiers (via a style string). VML does not encode closures, actions, or control flow; the client runtime handles events and data flow outside of this spec.

This document supersedes earlier drafts and incorporates all refinements, including Templates, attr(...) with CSS‑aligned type(<…>) hints, strict formatting, serialization rules, and an EBNF grammar. It also contains an exhaustive SwiftUI → VML conversion cookbook with before/after examples.


1. Core Principles

  1. Element parity: Element names map 1:1 to SwiftUI view types (Text, Image, VStack, etc.), matching exact Swift casing.

  2. Attributes:

    • Map to SwiftUI initializer parameters (e.g., <Image systemName="star"/>).
    • Arbitrary data-carrier attributes are allowed for binding via attr(...) inside style.
    • The only modifier serialized as a root-level attribute is id.
  3. Modifiers: Encoded inside a style attribute, applied left → right exactly like SwiftUI chaining.

  4. Templates: Immediate children annotated with template="SlotName" can be hoisted into named/default slots expected by SwiftUI (e.g., background(content:), overlay(content:), Label(title:icon:), NavigationLink(destination:label:), toolbar(content:)).

  5. Events: VML never encodes closures or actions; the client detects gestures, taps, etc., and dispatches events.

  6. No control flow: if / switch / ForEach are outside VML. Templates are static.


2. Modifiers (style) — Grammar & Formatting

2.1 Application Order

Modifiers are applied strictly left-to-right.

<Text style="padding(8), background(.red), padding(.horizontal, 12)"/>

Equivalent SwiftUI:

Text("...")
  .padding(8)
  .background(.red)
  .padding(.horizontal, 12)

2.2 Canonical Formatting

Required spacing:

  • Between modifiers: , (comma + single space)
  • Between arguments: ,
  • Labeled arguments: label: value (colon + single space)

Enums in style:

  • Always keep the leading dot: .topLeading, .purple
  • Match SwiftUI case names exactly (case-sensitive)

Strings:

  • Always double-quoted; escape inner quotes and serialize as entities (e.g., &quot;).

Arrays & Tuples:

  • Arrays: [item1, item2] with spaces after commas.
  • Tuples: (x: 1, y: 0) with spaces after commas and around :.

Angles & Numbers:

  • Angles may be expressed as .degrees(45) or 45deg.
  • Bare numbers are coerced to expected SwiftUI types (e.g., points).

Dictionaries:

  • Not supported inside style.

Nested calls:

  • Use Swift-style calls, obeying the same spacing rules.

2.3 Gradients — Canonicalization

  • Evenly spaced stops → colors form:

    style="background(.linearGradient(colors: [.blue, .purple], startPoint: .topLeading, endPoint: .bottomTrailing))"
    
  • Uneven stop positions → stops form (you may use Gradient.Stop(...); .init equivalent is permitted but not required).


3. Attributes & attr(...) Binding

3.1 Purpose

attr(...) binds an element’s own attribute value into a modifier parameter on the same element, enabling runtime-updated values to flow into SwiftUI modifiers.

<Text title="Welcome" style="navigationTitle(attr(title type(<string>), &quot;Untitled&quot;))"/>

3.2 CSS‑Aligned Grammar

Allowed forms (spacing is canonical):

  • attr(name)
  • attr(name, fallback)
  • attr(name type(<type>))
  • attr(name type(<type>), fallback)

Supported <type>s:

  • <string>, <number>, <integer>, <length>, <angle>, <color>, <url>, <boolean>

Color inputs (<color>) accept both CSS values (#RRGGBB, rgb(...), hsl(...)) and SwiftUI color enums (with leading dot: .red, .secondary, .blue.opacity(0.5)).

3.3 Scope & Constraints

  • attr(...) resolves attributes only on the same element.
  • attr(...) is only valid inside style. It is not allowed as a raw attribute value.

3.4 Fallbacks & Coercion

  • If the attribute is missing or fails coercion, the fallback is used (if provided), else the client logs a warning and SwiftUI defaults.
  • Empty attribute values are treated as missing, except type(<string>) which resolves to an empty string.

3.5 Examples

<Rectangle style="frame(width: attr(w type(<number>), 200), height: attr(h type(<number>), 100))"/>
<Circle fill="#ff0000" style="background(attr(fill type(<color>), .blue))"/>
<Text degrees="45deg" style="rotationEffect(attr(degrees type(<angle>), .degrees(0)))"/>

4. Templates — Slots & Hoisting

Templates connect immediate children to slot parameters (named or default) on their parent view. They provide a structural equivalent to SwiftUI @ViewBuilder arguments such as content, label, title, icon, destination, and multi-valued toolbar.

4.1 Declaring & Referencing

  • Child declares its target with template="SlotName".
  • Parent references the child in its style using a symbol :slot (only valid inside style).
<Text style="background(alignment: .center, content: :bg)">
  Hello
  <Star template="bg"/>
</Text>

4.2 Scope, Discovery & Order

  • Templates are scoped to the immediate parent. Only immediate children are eligible for collection.
  • Forward references (child appears later) are allowed.
  • For single-valued slots (e.g., background(content:)), if multiple children declare the same template, the first in DOM order is used.
  • For multi-valued slots (e.g., toolbar(content:)), collect all matching immediate children in DOM order, even if interleaved with other children.

4.3 Default vs Named Slots — Precedence

  • If a parent has a default (unnamed) slot and you provide both:

    • Unlabeled children and
    • An explicit child for that slot (e.g., template="label")

    → The explicit template wins. Unlabeled children for that slot are ignored.

  • If only unlabeled children exist for a default slot, pass them directly into the SwiftUI @ViewBuilder (SwiftUI forms a TupleView). VML does not add Group/HStack/VStack implicitly.

4.4 Consumption & Hoisting

  • A matched templated child is hoisted into its slot and removed from the parent’s normal child list.
  • The child’s own modifiers remain intact and apply inside the slot, left-to-right.

4.5 Reuse (Discouraged)

  • Reusing the same template in multiple slots is permitted but discouraged; the same element instance (state/identity) is rendered in multiple places.

4.6 Warnings

  • Unmatched templates (names not supported by parent) → ignored; log a warning.
  • Missing expected slotslog a warning; SwiftUI fallback behavior applies.
  • Nested templates (not immediate children) → ignored; may warn.

5. Events — Out of Scope

  • VML does not encode closures or actions (e.g., onTapGesture {}, Button(action:)).
  • The client runtime owns event detection, dispatch, payloads, and app wiring.
  • Elements imply interaction (e.g., Button, Toggle, TextField, NavigationLink), but VML carries no event attributes.

6. Serialization & Canonicalization

6.1 Emitters must:

  • Preserve modifier order and the exact spacing rules (", " and ": ").

  • Keep leading dots on enums inside style; attribute enums do not use the dot (current rule; may change later).

  • Quote all attribute values with double quotes and escape as HTML/XML entities.

  • Attribute order recommendation:

    1. id
    2. Initializer-backed attributes (alphabetical)
    3. Data attributes used by attr(...) (alphabetical)
    4. style (last)
  • Default slots: serialize unlabeled children as-is unless an explicit template for that slot exists (in which case omit unlabeled siblings for that slot).

  • Gradients: canonicalize evenly spaced stops to colors:; preserve explicit stops: when uneven.

6.2 Logging (non-fatal)

  • Missing required slots; unmatched templates; multiple matches for single-valued slots; failed attr(...) coercion; template reuse across slots; nested templates.

7. EBNF Grammar (Extract)

Lexical (tokens)

letter      = "A"…"Z" | "a"…"z" | "_" ;
digit       = "0"…"9" ;
hexdigit    = digit | "A"…"F" | "a"…"f" ;
IDENT       = letter , { letter | digit } ;
SYMBOL      = ":" , IDENT ;            (* template reference; only inside style *)
SP          = " " ;                     (* exactly one space *)
SEP         = "," , SP ;                (* ", " *)
COL         = ":" , SP ;                (* ": " *)
INT         = digit , { digit } ;
FLOAT       = INT , "." , { digit } | "." , digit , { digit } ;
NUMBER      = FLOAT | INT ;
ANGLE_DEG   = NUMBER , "deg" ;
STRING      = '"' , { …escaped chars… } , '"' ;
CSSHEX      = "#" , hexdigit{6} , [ hexdigit{2} ] ;
RGBFUNC     = "rgb" , "(" , … , ")" ;
HSLFUNC     = "hsl" , "(" , … , ")" ;
CSSCOLOR    = CSSHEX | RGBFUNC | HSLFUNC ;
MEMBER_CALL = "." , IDENT , [ "(" , [ ArgList ] , ")" ] ;
MEMBER_CHAIN= MEMBER_CALL , { "." , IDENT , [ "(" , [ ArgList ] , ")" ] } ;
FUNC_CALL   = IDENT , "(" , [ ArgList ] , ")" ;

style grammar

Style       = Modifier , { SEP , Modifier } ;
Modifier    = FUNC_CALL ;
ArgList     = Arg , { SEP , Arg } ;
Arg         = [ Label , COL ] , Expr ;
Label       = IDENT ;
Expr        = NUMBER | ANGLE_DEG | STRING | CSSCOLOR | MEMBER_CHAIN
            | FUNC_CALL | Array | Tuple | SYMBOL | AttrCall ;
Array       = "[" , [ Expr , { SEP , Expr } ] , "]" ;
Tuple       = "(" , TupleField , { SEP , TupleField } , ")" ;
TupleField  = ( IDENT , COL , Expr ) | Expr ;

attr(...) grammar

AttrCall    = "attr" , "(" , AttrInner , ")" ;
AttrInner   = AttrName , [ SP , TypeHint ] , [ SEP , Fallback ] ;
AttrName    = IDENT ;
TypeHint    = "type" , "(" , "<" , AttrType , ">" , ")" ;
AttrType    = "string" | "number" | "integer" | "length" | "angle"
            | "color" | "url" | "boolean" ;
Fallback    = Expr ;

8. SwiftUI → VML Conversion Cookbook (Before/After)

Conventions used below

  • Enums in style use leading dot (.red, .top).
  • Labeled parameters use : spacing.
  • Modifier lists use , spacing.
  • Where a SwiftUI initializer parameter exists, prefer an element attribute.

8.1 Basic Text & Modifiers

SwiftUI

Text("Hello")
  .font(.system(size: 17, weight: .semibold))
  .foregroundColor(.purple)
  .padding(.horizontal, 12)

VML

<Text style="font(.system(size: 17, weight: .semibold)), foregroundColor(.purple), padding(.horizontal, 12)">
  Hello
</Text>

8.2 Frame, Background, Corner Radius

SwiftUI

Text("Badge")
  .frame(width: 100, height: 40)
  .background(.blue)
  .foregroundColor(.white)
  .cornerRadius(8)

VML

<Text style="frame(width: 100, height: 40), background(.blue), foregroundColor(.white), cornerRadius(8)">
  Badge
</Text>

8.3 Linear Gradient (Canonical colors form)

SwiftUI

Rectangle()
  .fill(.linearGradient(colors: [.blue, .purple], startPoint: .topLeading, endPoint: .bottomTrailing))

VML

<Rectangle style="fill(.linearGradient(colors: [.blue, .purple], startPoint: .topLeading, endPoint: .bottomTrailing))"/>

8.4 Background/Overlay with View Content (Template)

SwiftUI

Text("Hello")
  .background(alignment: .center) {
    Star()
  }

VML

<Text style="background(alignment: .center, content: :bg)">
  Hello
  <Star template="bg"/>
</Text>

Overlay variant

<Text style="overlay(alignment: .topTrailing, content: :ov)">
  Hello
  <Badge template="ov"><Text>NEW</Text></Badge>
</Text>

8.5 Label(title:icon:)

SwiftUI

Label {
  Text("Downloads")
} icon: {
  Image(systemName: "arrow.down.circle")
}

VML

<Label>
  <Text template="title">Downloads</Text>
  <Image template="icon" systemName="arrow.down.circle"/>
</Label>

Alternate shorthand SwiftUI (Label("Downloads", systemImage: "arrow.down.circle")) becomes the same VML as above.


8.6 Button with Default Slot (multiple unlabeled children)

SwiftUI

Button {
  Text("Play")
  Image(systemName: "play")
} action: { /* client-owned */ }

VML

<Button>
  <Text>Play</Text>
  <Image systemName="play"/>
</Button>
  • Unlabeled children go directly to the default label slot.
  • VML does not encode the action; the client handles events.

Precedence example — explicit wins:

<Button>
  <Text>Ignored</Text>
  <Text template="label">Used</Text>
</Button>

8.7 NavigationLink (URL destination, templated placeholder)

SwiftUI

NavigationLink(destination: { DetailView() }) {
  Text("More Information")
}

VML

<NavigationLink destination={"/products/1"}>
  <Text>More Information</Text>
  <ProgressView
    template="destination"
    title="Product 1"
    style="navigationTitle(attr(title type(<string>), &quot;Loading&quot;))"
  />
</NavigationLink>
  • The client may replace the templated placeholder with server-provided content after navigation.
  • Symbols are forbidden outside style; therefore destination=:detail is invalid.

8.8 Section Header/Footer

SwiftUI

Section {
  Text("Row")
} header: {
  Text("Header")
} footer: {
  Text("Footer")
}

VML

<Section>
  <Text>Row</Text>
  <Text template="header">Header</Text>
  <Text template="footer">Footer</Text>
</Section>

8.9 Toolbar (Multi-valued Slot)

SwiftUI

List {
  Text("Row 1")
}
.toolbar {
  ToolbarItem(placement: .topBarLeading) { Text("Filter") }
  ToolbarItem(placement: .topBarTrailing) { Text("Edit") }
}

VML

<List style="toolbar(content: :toolbar)">
  <Text>Row 1</Text>
  <ToolbarItem template="toolbar" placement="topBarLeading"><Text>Filter</Text></ToolbarItem>
  <ToolbarItem template="toolbar" placement="topBarTrailing"><Text>Edit</Text></ToolbarItem>
</List>
  • All immediate children with template="toolbar" are collected in DOM order.

8.10 Image (SF Symbols)

SwiftUI

Image(systemName: "heart.fill")
  .foregroundColor(.red)

VML

<Image systemName="heart.fill" style="foregroundColor(.red)"/>

8.11 Opacity & Shadow

SwiftUI

Text("Faded")
  .opacity(0.5)
  .shadow(color: .black.opacity(0.2), radius: 8, x: 0, y: 4)

VML

<Text style="opacity(0.5), shadow(color: .black.opacity(0.2), radius: 8, x: 0, y: 4)">
  Faded
</Text>

8.12 Rotation & 3D Rotation (angles)

SwiftUI

Image(systemName: "arrow.right")
  .rotationEffect(.degrees(45))
  .rotation3DEffect(.degrees(90), axis: (x: 1, y: 0, z: 0))

VML

<Image systemName="arrow.right"
       style="rotationEffect(.degrees(45)), rotation3DEffect(.degrees(90), axis: (x: 1, y: 0, z: 0))"/>

Alternate angle literal:

<Image systemName="arrow.right" style="rotationEffect(45deg)"/>

8.13 Dynamic Binding with attr(...)

SwiftUI

Text(model.title)
  .navigationTitle(model.title.isEmpty ? "Untitled" : model.title)

VML

<Text title="Home" style="navigationTitle(attr(title type(<string>), &quot;Untitled&quot;))">
  Home
</Text>

Numeric with fallback

<Rectangle style="frame(width: attr(w type(<number>), 200), height: attr(h type(<number>), 100))"/>

Color from attribute or fallback

<Circle fill="#00ffcc" style="background(attr(fill type(<color>), .blue))"/>

8.14 ZStack with Overlay (Template)

SwiftUI

ZStack {
  Circle().fill(.blue)
  Text("42").font(.largeTitle).foregroundColor(.white)
}

VML

<ZStack>
  <Circle style="fill(.blue)"/>
  <Text style="font(.largeTitle), foregroundColor(.white)">42</Text>
</ZStack>

Overlay as a modifier:

<Circle style="fill(.blue), overlay(content: :ov)">
  <Text template="ov" style="font(.largeTitle), foregroundColor(.white)">42</Text>
</Circle>

8.15 List Row Background (Template)

SwiftUI

Text("Row")
  .listRowBackground(Color.yellow)

VML

<Text style="listRowBackground(content: :bg)">
  Row
  <Rectangle template="bg" style="fill(.yellow)"/>
</Text>

8.16 Explicit Stops Gradient (non-even)

SwiftUI

Rectangle()
  .fill(.linearGradient(
    Gradient(stops: [
      .init(color: .red, location: 0.0),
      .init(color: .yellow, location: 0.3),
      .init(color: .green, location: 1.0)
    ]),
    startPoint: .leading,
    endPoint: .trailing
  ))

VML

<Rectangle style="fill(LinearGradient(gradient: Gradient(stops: [
  Gradient.Stop(color: .red, location: 0.0),
  Gradient.Stop(color: .yellow, location: 0.3),
  Gradient.Stop(color: .green, location: 1.0)
]), startPoint: .leading, endPoint: .trailing))"/>

Note: While .init(...) is valid in Swift, Gradient.Stop(...) is the explicit canonical name in VML examples. Either form is acceptable.


9. Invalid / Warning Examples

Nested template (ignored, warn)

<List style="toolbar(content: :toolbar)">
  <VStack>
    <ToolbarItem template="toolbar"><Text>Ignored</Text></ToolbarItem>
  </VStack>
</List>

Symbol outside style (invalid)

<NavigationLink destination=:detail>…</NavigationLink>  <!-- invalid -->

Explicit default template beats unlabeled

<Button>
  <Text>Unlabeled (ignored)</Text>
  <Text template="label">Used</Text>
</Button>

Multiple matches for single-valued slot (first wins)

<Text style="background(content: :bg)">
  <Circle template="bg"/>
  <Rectangle template="bg"/>  <!-- ignored -->
</Text>

attr(...) used as attribute value (invalid)

<Text title="attr(name)">Hello</Text>  <!-- invalid -->

10. Quick Reference Tables

10.1 Common Slots (names must match SwiftUI labels exactly)

SwiftUI API Slot Names Cardinality
background(alignment:content:) content 1
overlay(alignment:content:) content 1
Label(title:icon:) title, icon 1 each
NavigationLink(destination:label:) destination, label 1 each
Section(header:footer:) header, footer 1 each
toolbar(content:) toolbar many
listRowBackground(_:) (as content) content 1
buttonStyle(label:) (when view-style) label (default) 1

10.2 Spacing Rules (recap)

  • Between modifiers: ,
  • Between args: ,
  • Labeled args: :
  • Arrays: commas with single space; Tuples: commas and : with single spaces
  • Enums inside style: leading dot

11. Implementation Notes

  • Parsing may be permissive, but serialization must be canonical (spacing, quoting, ordering).
  • Client is responsible for: event dispatch, data updates, coercion of attr(...) types, warnings, and SwiftUI type validation.
  • Reuse of templated child across slots is discouraged; duplicate with distinct template names for unique instances.

12. End-to-End Canonical Example

<VStack id="home" title="Home"
        style="padding(16), background(.linearGradient(colors: [.blue, .purple], startPoint: .topLeading, endPoint: .bottomTrailing))">

  <Text style="font(.system(size: 17)), navigationTitle(attr(title type(<string>), &quot;Untitled&quot;))">
    Welcome
  </Text>

  <Card style="background(alignment: .center, content: :bg)">
    <Text>Body</Text>
    <Star template="bg"/>
  </Card>

  <List style="toolbar(content: :toolbar)">
    <ToolbarItem template="toolbar" placement="topBarLeading"><Text>Filter</Text></ToolbarItem>
    <ToolbarItem template="toolbar" placement="topBarTrailing"><Text>Edit</Text></ToolbarItem>
  </List>
</VStack>

This specification is the canonical source of truth for VML.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment