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.
-
Element parity: Element names map 1:1 to SwiftUI view types (
Text,Image,VStack, etc.), matching exact Swift casing. -
Attributes:
- Map to SwiftUI initializer parameters (e.g.,
<Image systemName="star"/>). - Arbitrary data-carrier attributes are allowed for binding via
attr(...)insidestyle. - The only modifier serialized as a root-level attribute is
id.
- Map to SwiftUI initializer parameters (e.g.,
-
Modifiers: Encoded inside a
styleattribute, applied left → right exactly like SwiftUI chaining. -
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:)). -
Events: VML never encodes closures or actions; the client detects gestures, taps, etc., and dispatches events.
-
No control flow:
if/switch/ForEachare outside VML. Templates are static.
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)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.,
").
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)or45deg. - 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.
-
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(...);.initequivalent is permitted but not required).
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>), "Untitled"))"/>
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)).
attr(...)resolves attributes only on the same element.attr(...)is only valid insidestyle. It is not allowed as a raw attribute value.
- 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.
<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)))"/>
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.
- Child declares its target with
template="SlotName". - Parent references the child in its
styleusing a symbol:slot(only valid insidestyle).
<Text style="background(alignment: .center, content: :bg)">
Hello
<Star template="bg"/>
</Text>
- 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 sametemplate, 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.
-
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 aTupleView). VML does not addGroup/HStack/VStackimplicitly.
- 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.
- Reusing the same template in multiple slots is permitted but discouraged; the same element instance (state/identity) is rendered in multiple places.
- Unmatched templates (names not supported by parent) → ignored; log a warning.
- Missing expected slots → log a warning; SwiftUI fallback behavior applies.
- Nested templates (not immediate children) → ignored; may warn.
- 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.
-
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:
id- Initializer-backed attributes (alphabetical)
- Data attributes used by
attr(...)(alphabetical) 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 explicitstops:when uneven.
- Missing required slots; unmatched templates; multiple matches for single-valued slots; failed
attr(...)coercion; template reuse across slots; nested templates.
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 ;
Conventions used below
- Enums in
styleuse leading dot (.red,.top). - Labeled parameters use
:spacing. - Modifier lists use
,spacing. - Where a SwiftUI initializer parameter exists, prefer an element attribute.
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>
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>
SwiftUI
Rectangle()
.fill(.linearGradient(colors: [.blue, .purple], startPoint: .topLeading, endPoint: .bottomTrailing))VML
<Rectangle style="fill(.linearGradient(colors: [.blue, .purple], startPoint: .topLeading, endPoint: .bottomTrailing))"/>
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>
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.
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
labelslot. - VML does not encode the action; the client handles events.
Precedence example — explicit wins:
<Button>
<Text>Ignored</Text>
<Text template="label">Used</Text>
</Button>
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>), "Loading"))"
/>
</NavigationLink>
- The client may replace the templated placeholder with server-provided content after navigation.
- Symbols are forbidden outside
style; thereforedestination=:detailis invalid.
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>
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.
SwiftUI
Image(systemName: "heart.fill")
.foregroundColor(.red)VML
<Image systemName="heart.fill" style="foregroundColor(.red)"/>
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>
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)"/>
SwiftUI
Text(model.title)
.navigationTitle(model.title.isEmpty ? "Untitled" : model.title)VML
<Text title="Home" style="navigationTitle(attr(title type(<string>), "Untitled"))">
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))"/>
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>
SwiftUI
Text("Row")
.listRowBackground(Color.yellow)VML
<Text style="listRowBackground(content: :bg)">
Row
<Rectangle template="bg" style="fill(.yellow)"/>
</Text>
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.
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 -->
| 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 |
- Between modifiers:
, - Between args:
, - Labeled args:
: - Arrays: commas with single space; Tuples: commas and
:with single spaces - Enums inside
style: leading dot
- 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
templatenames for unique instances.
<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>), "Untitled"))">
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.