Version 1.0
Status: Definitive Specification
Date: 2025-09-05
This specification defines the View Markup Language (VML), a declarative SGML-based markup language for describing SwiftUI user interface hierarchies. VML provides a static, server-renderable representation of SwiftUI views that can be transmitted over HTTP and interpreted by compliant clients.
- Introduction
- Conformance
- Document Structure
- Elements
- Attributes
- The Style Attribute
- Templates and Slots
- The attr() Function
- Serialization Rules
- Core View Catalog
- Modifiers Reference
- Custom Extensions
- Grammar Definition
- Examples
VML is designed to provide a static, declarative representation of SwiftUI view hierarchies suitable for server-side generation and client-side rendering. It establishes a precise mapping between SwiftUI's programmatic API and an SGML-based markup format.
This specification covers:
- Declarative description of SwiftUI-compatible UI hierarchies
- Elements corresponding to SwiftUI view types
- Attributes representing SwiftUI initializer parameters
- Style serialization for SwiftUI view modifiers
- Template/slot mechanism for ViewBuilder arguments
- Attribute binding through the attr() function
The following are explicitly excluded from VML:
- Runtime closures, callbacks, or event handlers
- Control flow structures (if, switch, ForEach)
- Data bindings or state management
- Business logic or computations (except within style expressions)
- Dynamic interpolation within markup
- Static Declaration: VML documents are completely static. All values MUST be resolved at generation time.
- Direct Mapping: VML elements map 1:1 to SwiftUI view types, preserving exact naming and casing.
- No Runtime Evaluation: VML contains no executable code, closures, or runtime bindings.
- Predictable Serialization: Modifier order and formatting follow strict, deterministic rules.
A conforming VML document:
- MUST use the
<!doctype swiftui+vml>declaration - MUST have a root
<vml>element - MUST quote all attribute values with either double quotes or single quotes
- MUST NOT contain runtime interpolation or bindings
- MUST follow the serialization rules defined in Section 9
A conforming VML client:
- MUST recognize all core SwiftUI view elements defined in Section 10
- MUST process the style attribute according to Section 6
- MUST support the template/slot mechanism defined in Section 7
- MUST handle the attr() function as specified in Section 8
- MAY support custom elements and modifiers through registration
<!doctype swiftui+vml>
<vml>
<head>
<!-- Optional external styles and lifecycle templates -->
</head>
<body>
<!-- Root view hierarchy -->
</body>
</vml>Request:
ACCEPT: application/swiftui+vml[; target=ios|macos|visionos]
Response:
CONTENT-TYPE: application/swiftui+vml
The target parameter indicates the requesting device platform and is available to developers for programmatic decision-making on the server. VML does not enforce how or whether the target parameter should be used. If omitted, iOS is the default target. Clients typically send the appropriate target value when connecting.
Note: Version negotiation support is planned for future releases due to framework compatibility requirements but is not yet specified.
The head section MAY contain:
- External View Style Sheet (VSS) references:
<Style url="/path/to/styles.vss"/> - Lifecycle templates for application state management
External VSS files are fetched from the specified URL. The VSS format and processing rules are defined in a separate specification.
<head>
<Style url="/styles/main.vss"/>
<VStack template="launch" spacing="16">
<Image systemName="app.badge"/>
<Text style="font(.headline)">Starting Up</Text>
</VStack>
<VStack template="connecting" spacing="16">
<ProgressView/>
<Text style="font(.headline)">Connecting</Text>
</VStack>
<VStack template="disconnected" spacing="16">
<Image systemName="cloud.slash"/>
<Text style="font(.headline)">Connection Lost</Text>
</VStack>
<VStack template="error" spacing="16">
<Image systemName="exclamationmark.triangle.fill"/>
<Text style="font(.headline)">An Error Occurred</Text>
</VStack>
</head>The following lifecycle template names have special meaning when present in the head section:
launch- App starting up, before any connection attemptconnecting- Attempting to establish or re-establish connectionerror- Connection or loading faileddisconnected- Connection lost, not attempting to reconnect
These templates are parsed by the client and used for application state management when server-provided content is not available. Template names used in the body section do not have special lifecycle meaning.
Element names MUST exactly match their corresponding SwiftUI view type names, preserving case sensitivity.
Examples:
Text→<Text>VStack→<VStack>RoundedRectangle→<RoundedRectangle>NavigationStack→<NavigationStack>
<Text>Content</Text>
<Label title="Title" systemImage="star"/>
<Label>Title</Label> <!-- Alternative form --><VStack alignment="leading" spacing="12">...</VStack>
<HStack alignment="center" spacing="8">...</HStack>
<ZStack alignment="topLeading">...</ZStack>
<Group>...</Group>
<ScrollView axis="horizontal">...</ScrollView><Circle/>
<Rectangle/>
<RoundedRectangle cornerRadius="12"/>
<Capsule/>
<Ellipse/><Button>Label</Button>
<Toggle>Setting</Toggle>
<TextField>Placeholder</TextField>
<Slider value="0.5" in="0...1"/>
<Picker selection="option1">...</Picker>Custom SwiftUI views are represented as elements with their type name:
<ProfileCard name="John Doe" role="Developer"/>
<CustomButton variant="primary">Click</CustomButton>SwiftUI initializer parameters map directly to element attributes. The attribute name MUST match the SwiftUI parameter name exactly.
SwiftUI:
Image(systemName: "star.fill")
VStack(alignment: .leading, spacing: 12)
RoundedRectangle(cornerRadius: 10)VML:
<Image systemName="star.fill"/>
<VStack alignment="leading" spacing="12"/>
<RoundedRectangle cornerRadius="10"/>String values MUST be enclosed in either double quotes or single quotes. When using double quotes, inner double quotes MUST be escaped as ". When using single quotes, inner single quotes MUST be escaped as '.
<Text message="Hello "World""/>
<Text message='Hello "World"'/>
<Text message='Don't stop'/>Numeric values are represented as strings but interpreted as numbers by the client.
<Circle radius="50"/>
<VStack spacing="12"/>Boolean values are represented as "true" or "false" strings.
<Toggle isOn="true"/>
<ScrollView showsIndicators="false"/>Enum values in attributes support both forms:
- With leading dot:
alignment=".leading" - Without leading dot:
alignment="leading"
Both forms MUST be accepted by conforming clients.
<VStack alignment=".leading"/> <!-- Valid -->
<VStack alignment="leading"/> <!-- Also valid -->Range values use Swift range syntax:
<Slider in="0...1"/> <!-- Closed range -->
<Slider in="0..<100"/> <!-- Half-open range -->The following attributes have special handling:
id: View identity, derived from.id()modifiertag: View tag for selection, derived from.tag()modifiertemplate: Declares the element as a template for a slot
<Text id="header-text" tag="main">Content</Text>
<Button template="action">OK</Button>When a SwiftUI initializer parameter has no meaningful static value, the attribute MUST be omitted entirely rather than provided with an empty or placeholder value.
SwiftUI:
TextField("Placeholder", text: $binding) // Binding has no static valueVML:
<TextField>Placeholder</TextField> <!-- text attribute omitted -->The style attribute contains all SwiftUI view modifiers that are not initializer parameters, serialized as a comma-separated list.
- Modifiers are separated by
,(comma followed by single space) - Labeled arguments use
:(colon followed by single space) - Modifiers are applied in the order they appear (left-to-right); VML does not enforce ordering constraints
- Enum values MUST include the leading dot
- Nested function calls and method chaining are permitted
- No leading whitespace is permitted within parentheses
- Whitespace is required after commas and after named argument colons
- Array elements within square brackets
[...]use comma separation - String values that may break parsing SHOULD use
attr()function with attribute storage, or be entity-encoded if included directly in modifiers
SwiftUI:
Text("Hello")
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(.blue)
.padding(.horizontal, 12)
.background(.yellow)VML:
<Text style="font(.system(size: 17, weight: .semibold)), foregroundStyle(.blue), padding(.horizontal, 12), background(.yellow)">Hello</Text>Valid syntax:
style="background(.linearGradient(colors: [.blue, .red], startPoint: .top)), padding(.horizontal, 16)"
style="fill(.radialGradient(colors: [.blue.opacity(0.8), .purple.mix(with: .red, by: 0.5)], center: .center))"Invalid syntax:
style="padding( .horizontal , 16 )" <!-- Leading whitespace in parentheses -->
style="background(.blue),padding(16)" <!-- Missing space after comma -->
style="font(.system(size:17))" <!-- Missing space after colon -->Complex string values:
<!-- Preferred: Use attr() for complex strings -->
<Text imageName="my-image, with comma.png" style="background(.image(attr(imageName)))">Text</Text>
<!-- Alternative: Entity encoding -->
<Text style="background(.image("my-image, with comma.png"))">Text</Text>Colors, materials, and other simple values are written inline:
style="background(.blue)"
style="background(.ultraThinMaterial)"
style="foregroundStyle(.red.opacity(0.5))"Gradients use their full constructor syntax:
style="fill(.linearGradient(colors: [.blue, .purple], startPoint: .top, endPoint: .bottom))"Shapes use enum-style representation:
style="clipShape(.circle)"
style="clipShape(.rect(cornerRadius: 12))"Views that require construction use template slots (see Section 7):
style="overlay(content: :overlayContent)"The .cornerRadius() modifier is deprecated. Use .clipShape(.rect(cornerRadius:)) instead.
Deprecated:
style="cornerRadius(8)"Preferred:
style="clipShape(.rect(cornerRadius: 8))"Templates provide a mechanism for supplying view content to modifiers that accept @ViewBuilder closures in SwiftUI.
Templates are declared as immediate children with the template attribute:
<ParentView style="modifier(content: :slotName)">
<ChildView template="slotName">Content</ChildView>
</ParentView>- Parent elements ONLY render named slot templates of direct children
- Templates that are not direct children are not accessible to parent modifiers
- If a template is declared but not referenced by any modifier, it is ignored
Template names MUST match the SwiftUI API parameter name:
| SwiftUI Modifier | Parameter Name | Template Attribute |
|---|---|---|
overlay(alignment:content:) |
content | template="content" |
background(alignment:content:) |
content | template="content" |
tabItem(_:) |
(unnamed) | template="tabItem" |
safeAreaInset(edge:content:) |
content | template="content" |
.sheet(isPresented:content:) |
content | template="content" |
Templates are referenced in the style attribute using the :name syntax:
style="overlay(alignment: .topLeading, content: :badge)"The symbol syntax (:name) takes highest parsing priority and is always interpreted as a template reference, regardless of other syntactic patterns that may be present.
Some modifiers support implicit slot names where the slot name doesn't need to be referenced:
<View style="clipShape()">
<Circle template="shape"/>
</View>Some modifiers may act upon multiple templates with the same name:
<View style="toolbar(content: :content)">
<ToolbarItem template="content" placement="navigationBarLeading">
<Button>Back</Button>
</ToolbarItem>
<ToolbarItem template="content" placement="navigationBarTrailing">
<Button>Done</Button>
</ToolbarItem>
</View>VML does not enforce multi-valued slot behavior. The SwiftUI client determines whether to collect all templates with the same name or use only one.
SwiftUI:
Text("Hello")
.overlay(alignment: .topTrailing) {
Circle()
.fill(.red)
.frame(width: 10, height: 10)
}VML:
<Text style="overlay(alignment: .topTrailing, content: :indicator)">Hello
<Circle template="indicator" style="fill(.red), frame(width: 10, height: 10)"/>
</Text>The attr() function binds element attribute values into style modifiers, enabling client-side value substitution.
attr(name)
attr(name, fallback)
attr(name type(<type>))
attr(name type(<type>), fallback)
Supported type hints:
<string>- String values<number>- Numeric values (float or integer)<integer>- Integer values only<length>- Length values with units<angle>- Angle values (.degrees() or deg suffix)<color>- Color values (SwiftUI or CSS format)<url>- URL strings<boolean>- Boolean values
- attr() ONLY resolves attributes on the same element
- If the attribute exists (even as empty string), its value is used
- If the attribute is absent, the fallback is used (if provided)
- If no fallback and attribute is missing, the value returned is
niland client SHOULD log a warning
<!-- Using attribute value in modifier -->
<Circle fill="#ff0000" style="fill(attr(fill type(<color>)))"/>
<!-- With fallback -->
<Text size="20" style="font(.system(size: attr(size type(<number>), 17)))">Hello</Text>
<!-- For presentation state -->
<View showingSheet="false" style="sheet(isPresented: attr(showingSheet), content: :sheet)">
<Text template="sheet">Sheet Content</Text>
</View>The attr() function:
- MUST NOT be used for template references
- MUST NOT reference attributes from other elements
- MUST NOT be used outside the style attribute
-
Spacing:
- Between modifiers:
,(comma + single space) - Between arguments:
,(comma + single space) - Label-value separator:
:(colon + single space)
- Between modifiers:
-
Attribute Order (RECOMMENDED):
idattribute (if present)- Initializer attributes (alphabetical)
- Data attributes for attr() (alphabetical)
styleattribute (last)
-
Modifier Order: VML does not enforce constraints on modifier order; modifiers are applied in the order they appear
The following characters MUST be encoded in attribute values:
<→<>→>&→&"→"(when using double-quoted attributes)'→'(when using single-quoted attributes)
When using double-quoted attributes, only double quotes within the attribute value need to be encoded as ". When using single-quoted attributes, only single quotes within the attribute value need to be encoded as '.
- Leading/trailing whitespace in text content is preserved
- Whitespace between elements follows SGML rules
- No whitespace normalization in attribute values
SwiftUI:
Text("Hello World")
.font(.title)
.foregroundStyle(.blue)
.multilineTextAlignment(.center)VML:
<Text style="font(.title), foregroundStyle(.blue), multilineTextAlignment(.center)">Hello World</Text>SwiftUI:
Label("Home", systemImage: "house")
Label("Files", image: "folder-icon")VML:
<Label systemImage="house">Home</Label>
<Label title="Files" image="folder-icon"/>SwiftUI:
Image(systemName: "star.fill")
.resizable()
.frame(width: 50, height: 50)
.foregroundStyle(.yellow)
Image("photo")
.resizable()
.scaledToFit()VML:
<Image systemName="star.fill" style="resizable(), frame(width: 50, height: 50), foregroundStyle(.yellow)"/>
<Image name="photo" style="resizable(), scaledToFit()"/>SwiftUI:
AsyncImage(url: URL(string: "https://example.com/image.jpg")) { phase in
switch phase {
case .empty:
ProgressView()
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fit)
case .failure:
Image(systemName: "photo")
@unknown default:
EmptyView()
}
}VML:
<AsyncImage url="https://example.com/image.jpg">
<ProgressView template="phase.empty"/>
<AsyncImage image template="phase.success" style="resizable(), aspectRatio(contentMode: .fit)"/>
<Image systemName="photo" template="phase.failure"/>
</AsyncImage>SwiftUI:
VStack(alignment: .leading, spacing: 10) {
Text("Title")
Text("Subtitle")
}
HStack(spacing: 20) {
Circle().fill(.blue)
Circle().fill(.red)
}
ZStack(alignment: .topTrailing) {
Rectangle().fill(.gray)
Text("Badge")
}VML:
<VStack alignment="leading" spacing="10">
<Text>Title</Text>
<Text>Subtitle</Text>
</VStack>
<HStack spacing="20">
<Circle style="fill(.blue)"/>
<Circle style="fill(.red)"/>
</HStack>
<ZStack alignment="topTrailing">
<Rectangle style="fill(.gray)"/>
<Text>Badge</Text>
</ZStack>SwiftUI:
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(items) { item in
Text(item.name)
}
}
}VML (static content only):
<ScrollView axis="horizontal" showsIndicators="false">
<HStack>
<Text>Item 1</Text>
<Text>Item 2</Text>
<Text>Item 3</Text>
</HStack>
</ScrollView>SwiftUI:
List {
Text("Row 1")
.listRowBackground(Color.blue)
Text("Row 2")
.listRowSeparator(.hidden)
HStack {
Text("Row 3")
}
.listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20))
}VML:
<List>
<Text style="listRowBackground(.blue)">Row 1</Text>
<Text style="listRowSeparator(.hidden)">Row 2</Text>
<HStack style="listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20))">
<Text>Row 3</Text>
</HStack>
</List>SwiftUI:
Circle()
.fill(.blue)
.frame(width: 100, height: 100)
RoundedRectangle(cornerRadius: 15)
.strokeBorder(.green, lineWidth: 2)
Rectangle()
.fill(
.linearGradient(
colors: [.blue, .purple],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)VML:
<Circle style="fill(.blue), frame(width: 100, height: 100)"/>
<RoundedRectangle cornerRadius="15" style="strokeBorder(.green, lineWidth: 2)"/>
<Rectangle style="fill(.linearGradient(colors: [.blue, .purple], startPoint: .topLeading, endPoint: .bottomTrailing))"/>SwiftUI:
Button("Save") {
// Action
}
.buttonStyle(.borderedProminent)
Button(action: {}) {
HStack {
Image(systemName: "plus")
Text("Add")
}
}VML:
<Button style="buttonStyle(.borderedProminent)">Save</Button>
<Button>
<HStack>
<Image systemName="plus"/>
<Text>Add</Text>
</HStack>
</Button>SwiftUI:
TextField("Email", text: $email)
.textFieldStyle(.roundedBorder)
.keyboardType(.emailAddress)VML:
<TextField style="textFieldStyle(.roundedBorder), keyboardType(.emailAddress)">Email</TextField>SwiftUI:
Toggle("Notifications", isOn: $notificationsEnabled)
Toggle(isOn: .constant(true)) {
Label("WiFi", systemImage: "wifi")
}VML:
<Toggle>Notifications</Toggle>
<Toggle isOn="true">
<Label systemImage="wifi">WiFi</Label>
</Toggle>SwiftUI:
NavigationStack {
List {
NavigationLink("Settings", destination: SettingsView())
NavigationLink(destination: ProfileView()) {
Label("Profile", systemImage: "person")
}
}
.navigationTitle("Home")
}VML:
<NavigationStack>
<List style="navigationTitle('Home')">
<NavigationLink>
<Text template="label">Settings</Text>
<SettingsView template="destination"/>
</NavigationLink>
<NavigationLink>
<Label systemImage="person" template="label">Profile</Label>
<ProfileView template="destination"/>
</NavigationLink>
</List>
</NavigationStack>SwiftUI:
Form {
Section("Personal Information") {
TextField("Name", text: $name)
TextField("Email", text: $email)
}
Section {
Toggle("Notifications", isOn: $notifications)
} header: {
Text("Preferences")
} footer: {
Text("Choose your notification preferences")
}
}VML:
<Form>
<Section>
<Text template="header">Personal Information</Text>
<TextField>Name</TextField>
<TextField>Email</TextField>
</Section>
<Section>
<Text template="header">Preferences</Text>
<Text template="footer">Choose your notification preferences</Text>
<Toggle>Notifications</Toggle>
</Section>
</Form>SwiftUI:
TabView {
HomeView()
.tabItem {
Label("Home", systemImage: "house")
}
SettingsView()
.tabItem {
Label("Settings", systemImage: "gear")
}
}VML:
<TabView>
<HomeView style="tabItem(:home)">
<Label systemImage="house" template="home">Home</Label>
</HomeView>
<SettingsView style="tabItem(:settings)">
<Label systemImage="gear" template="settings">Settings</Label>
</SettingsView>
</TabView>SwiftUI:
DisclosureGroup("Advanced Options") {
Toggle("Option 1", isOn: $option1)
Toggle("Option 2", isOn: $option2)
}VML:
<DisclosureGroup>
<Text template="label">Advanced Options</Text>
<Toggle>Option 1</Toggle>
<Toggle>Option 2</Toggle>
</DisclosureGroup>SwiftUI:
Menu("Actions") {
Button("Edit") { }
Button("Delete") { }
Divider()
Button("Share") { }
}VML:
<Menu>
<Text template="label">Actions</Text>
<Button>Edit</Button>
<Button>Delete</Button>
<Divider/>
<Button>Share</Button>
</Menu>SwiftUI:
ViewThatFits {
Text("This is a very long text that might not fit")
Text("Shorter text")
Text("Tiny")
}VML:
<ViewThatFits>
<Text>This is a very long text that might not fit</Text>
<Text>Shorter text</Text>
<Text>Tiny</Text>
</ViewThatFits>SwiftUI:
Color.blue
Color(red: 0.5, green: 0.2, blue: 0.8)
Color("CustomColor")
Color(UIColor.systemRed)VML:
<Color name="system-blue"/>
<Color red="0.5" green="0.2" blue="0.8"/>
<Color name="CustomColor"/>
<Color name="system-red"/>
<Color name="#ff0000"/>SwiftUI:
HStack {
Text("Left")
Spacer()
Text("Right")
}
Spacer()
.frame(height: 20)VML:
<HStack>
<Text>Left</Text>
<Spacer/>
<Text>Right</Text>
</HStack>
<Spacer style="frame(height: 20)"/>SwiftUI:
VStack {
Text("Above")
Divider()
Text("Below")
}VML:
<VStack>
<Text>Above</Text>
<Divider/>
<Text>Below</Text>
</VStack>SwiftUI:
ProgressView()
ProgressView("Loading...")
ProgressView(value: 0.7)VML:
<ProgressView/>
<ProgressView>Loading...</ProgressView>
<ProgressView value="0.7"/>SwiftUI:
Link("Visit Apple", destination: URL(string: "https://apple.com")!)
ShareLink(item: URL(string: "https://example.com")!) {
Label("Share", systemImage: "square.and.arrow.up")
}VML:
<Link destination="https://apple.com">Visit Apple</Link>
<ShareLink item="https://example.com">
<Label systemImage="square.and.arrow.up">Share</Label>
</ShareLink>SwiftUI:
Gauge(value: 0.7, in: 0...1) {
Text("Battery")
} currentValueLabel: {
Text("70%")
} minimumValueLabel: {
Text("0")
} maximumValueLabel: {
Text("100")
}VML:
<Gauge value="0.7" in="0...1">
<Text template="label">Battery</Text>
<Text template="currentValueLabel">70%</Text>
<Text template="minimumValueLabel">0</Text>
<Text template="maximumValueLabel">100</Text>
</Gauge>| SwiftUI Modifier | VML Style Syntax |
|---|---|
.padding() |
padding() |
.padding(.all, 10) |
padding(.all, 10) |
.padding(.horizontal, 20) |
padding(.horizontal, 20) |
.frame(width: 100) |
frame(width: 100) |
.frame(width: 100, height: 50) |
frame(width: 100, height: 50) |
.frame(maxWidth: .infinity) |
frame(maxWidth: .infinity) |
.offset(x: 10, y: 20) |
offset(x: 10, y: 20) |
.position(x: 100, y: 200) |
position(x: 100, y: 200) |
| SwiftUI Modifier | VML Style Syntax |
|---|---|
.foregroundStyle(.blue) |
foregroundStyle(.blue) |
.background(.yellow) |
background(.yellow) |
.opacity(0.5) |
opacity(0.5) |
.blur(radius: 3) |
blur(radius: 3) |
.shadow(radius: 5) |
shadow(radius: 5) |
.shadow(color: .black.opacity(0.3), radius: 5, x: 0, y: 2) |
shadow(color: .black.opacity(0.3), radius: 5, x: 0, y: 2) |
| SwiftUI Modifier | VML Style Syntax |
|---|---|
.clipShape(Circle()) |
clipShape(.circle) |
.clipShape(RoundedRectangle(cornerRadius: 10)) |
clipShape(.rect(cornerRadius: 10)) |
.cornerRadius(8) |
Deprecated - use clipShape(.rect(cornerRadius: 8)) |
| SwiftUI Modifier | VML Style Syntax |
|---|---|
.rotationEffect(.degrees(45)) |
rotationEffect(.degrees(45)) |
.scaleEffect(1.5) |
scaleEffect(1.5) |
.scaleEffect(x: 2, y: 0.5) |
scaleEffect(x: 2, y: 0.5) |
| SwiftUI Modifier | VML Style Syntax |
|---|---|
.font(.title) |
font(.title) |
.font(.system(size: 18)) |
font(.system(size: 18)) |
.fontWeight(.bold) |
fontWeight(.bold) |
.bold() |
bold() |
.italic() |
italic() |
.textCase(.uppercase) |
textCase(.uppercase) |
.lineLimit(2) |
lineLimit(2) |
.multilineTextAlignment(.center) |
multilineTextAlignment(.center) |
| SwiftUI Modifier | VML Style Syntax |
|---|---|
.sheet(isPresented: $show) |
sheet(isPresented: attr(show), content: :content) |
.fullScreenCover(isPresented: $show) |
fullScreenCover(isPresented: attr(show), content: :content) |
.alert("Title", isPresented: $show) |
alert("Title", isPresented: attr(show), actions: :actions, message: :message) |
.confirmationDialog("Title", isPresented: $show) |
confirmationDialog("Title", isPresented: attr(show), actions: :actions, message: :message) |
.popover(isPresented: $show) |
popover(isPresented: attr(show), content: :content) |
| SwiftUI Modifier | VML Style Syntax |
|---|---|
.environment(\.colorScheme, .dark) |
environment(\.colorScheme, .dark) |
.environment(\.locale, Locale(identifier: "fr")) |
environment(\.locale, Locale(identifier: "fr")) |
| SwiftUI Modifier | VML Style Syntax |
|---|---|
.animation(.easeInOut) |
animation(.easeInOut) |
.animation(.spring(), value: size) |
animation(.spring(), value: true) |
.transition(.slide) |
transition(.slide) |
.transition(.opacity.combined(with: .scale)) |
transition(.opacity.combined(with: .scale)) |
Modifiers that accept ViewBuilder content use template slots:
SwiftUI:
Text("Content")
.overlay(alignment: .topTrailing) {
Badge()
}
.background {
RoundedRectangle(cornerRadius: 10)
.fill(.blue)
}VML:
<Text style="overlay(alignment: .topTrailing, content: :overlay), background(content: :bg)">Content
<Badge template="overlay"/>
<RoundedRectangle cornerRadius="10" template="bg" style="fill(.blue)"/>
</Text>Clients MAY support custom view types registered at runtime:
SwiftUI:
struct ProfileCard: View {
let name: String
let role: String
var body: some View {
VStack {
Image(systemName: "person.circle")
Text(name)
Text(role)
}
}
}
// Usage
ProfileCard(name: "Jane Doe", role: "Designer")VML:
<ProfileCard name="Jane Doe" role="Designer"/>Clients MAY support custom ViewModifiers registered at runtime:
SwiftUI:
struct CardStyle: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(Color.white)
.clipShape(RoundedRectangle(cornerRadius: 10))
.shadow(radius: 5)
}
}
// Usage
Text("Card").modifier(CardStyle())VML:
<Text style="cardStyle()">Card</Text>When a client encounters unknown elements or modifiers:
- Client MUST log a warning when encountering an unknown element
- Unknown elements MUST be rendered as a
Groupcontainer - All valid modifiers on the unknown element MUST be applied to the
Group - Child content MUST be rendered normally within the
Group
- Client MUST log a warning when encountering an unknown modifier
- Unknown modifier MUST be skipped
- Processing MUST continue with remaining modifiers in the style attribute
letter = "A"..."Z" | "a"..."z" | "_" ;
digit = "0"..."9" ;
hexdigit = digit | "A"..."F" | "a"..."f" ;
IDENT = letter , { letter | digit } ;
SYMBOL = ":" , IDENT ;
INT = digit , { digit } ;
FLOAT = INT , "." , { digit } | "." , digit , { digit } ;
NUMBER = FLOAT | INT ;
ANGLE_DEG = NUMBER , "deg" ;
STRING = '"' , { char } , '"' ;
(* Color formats *)
CSSHEX = "#" , hexdigit , hexdigit , hexdigit , hexdigit , hexdigit , hexdigit ,
[ hexdigit , hexdigit ] ;
RGBFUNC = "rgb(" , NUMBER , "," , NUMBER , "," , NUMBER , ")" ;
HSLFUNC = "hsl(" , NUMBER , "," , NUMBER , "%," , NUMBER , "%)" ;
CSSCOLOR = CSSHEX | RGBFUNC | HSLFUNC ;
(* Key path *)
KEYPATH = "\" , "." , IDENT , { "." , IDENT } ;
(* Enum/member access *)
MEMBER_CALL = "." , IDENT , [ "(" , [ ArgList ] , ")" ] ;
MEMBER_CHAIN = MEMBER_CALL , { MEMBER_CALL } ;Style = Modifier , { "," , SP , Modifier } ;
Modifier = IDENT , "(" , [ ArgList ] , ")" ;
ArgList = Arg , { "," , SP , Arg } ;
Arg = [ Label , ":" , SP ] , Expr ;
Label = IDENT ;
(* Expression precedence: SYMBOL has highest priority *)
Expr = SYMBOL
| NUMBER
| ANGLE_DEG
| STRING
| CSSCOLOR
| MEMBER_CHAIN
| FUNC_CALL
| Array
| AttrCall
| KEYPATH ;
Array = "[" , [ Expr , { "," , SP , Expr } ] , "]" ;
FUNC_CALL = IDENT , "(" , [ ArgList ] , ")" ;
AttrCall = "attr" , "(" , AttrInner , ")" ;
AttrInner = IDENT , [ SP , TypeHint ] , [ "," , SP , Fallback ] ;
TypeHint = "type(" , "<" , AttrType , ">" , ")" ;
AttrType = "string" | "number" | "integer" | "length"
| "angle" | "color" | "url" | "boolean" ;
Fallback = Expr ;
SP = " " ;Document = "<!doctype swiftui+vml>" , VMLElement ;
VMLElement = "<vml>" , Head? , Body , "</vml>" ;
Head = "<head>" , HeadContent* , "</head>" ;
Body = "<body>" , ViewContent , "</body>" ;
HeadContent = StyleLink | TemplateView ;
StyleLink = "<Style" , Attributes , "/>" ;
TemplateView = ViewElement ;
ViewContent = ViewElement | TextContent | ViewContent* ;
ViewElement = "<" , ElementName , Attributes? ,
( "/>" | ">" , ViewContent , "</" , ElementName , ">" ) ;
TextContent = { char } ;
Attributes = { SP , AttributeName , '="' , AttributeValue , '"' } ;
ElementName = IDENT ;
AttributeName = IDENT ;
AttributeValue = { char } ; (* With proper entity escaping *)SwiftUI:
struct LoginView: View {
@State private var email = ""
@State private var password = ""
@State private var rememberMe = false
@State private var showingAlert = false
var body: some View {
NavigationStack {
VStack(spacing: 20) {
Image(systemName: "lock.circle.fill")
.resizable()
.frame(width: 80, height: 80)
.foregroundStyle(.blue)
VStack(spacing: 16) {
TextField("Email", text: $email)
.textFieldStyle(.roundedBorder)
.keyboardType(.emailAddress)
.textContentType(.emailAddress)
SecureField("Password", text: $password)
.textFieldStyle(.roundedBorder)
.textContentType(.password)
Toggle("Remember me", isOn: $rememberMe)
}
.padding(.horizontal)
Button(action: { }) {
Text("Sign In")
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.padding(.horizontal)
HStack {
Link("Forgot Password?", destination: URL(string: "https://example.com/forgot")!)
.font(.footnote)
Spacer()
Link("Sign Up", destination: URL(string: "https://example.com/signup")!)
.font(.footnote)
}
.padding(.horizontal)
Spacer()
}
.padding(.top, 50)
.navigationTitle("Login")
.alert("Error", isPresented: $showingAlert) {
Button("OK") { }
} message: {
Text("Invalid credentials")
}
}
}
}VML:
<!doctype swiftui+vml>
<vml>
<body>
<NavigationStack>
<VStack spacing="20" style="padding(.top, 50), navigationTitle('Login'), alert('Error', isPresented: attr(showAlert), actions: :alertActions, message: :alertMessage)" showAlert="false">
<Image systemName="lock.circle.fill" style="resizable(), frame(width: 80, height: 80), foregroundStyle(.blue)"/>
<VStack spacing="16" style="padding(.horizontal)">
<TextField style="textFieldStyle(.roundedBorder), keyboardType(.emailAddress), textContentType(.emailAddress)">Email</TextField>
<SecureField style="textFieldStyle(.roundedBorder), textContentType(.password)">Password</SecureField>
<Toggle>Remember me</Toggle>
</VStack>
<Button style="buttonStyle(.borderedProminent), padding(.horizontal)">
<Text style="frame(maxWidth: .infinity)">Sign In</Text>
</Button>
<HStack style="padding(.horizontal)">
<Link destination="https://example.com/forgot" style="font(.footnote)">Forgot Password?</Link>
<Spacer/>
<Link destination="https://example.com/signup" style="font(.footnote)">Sign Up</Link>
</HStack>
<Spacer/>
<!-- Alert templates -->
<Group template="alertActions">
<Button>OK</Button>
</Group>
<Text template="alertMessage">Invalid credentials</Text>
</VStack>
</NavigationStack>
</body>
</vml>SwiftUI:
struct ProductCard: View {
let product: Product
var body: some View {
VStack(alignment: .leading, spacing: 8) {
ZStack(alignment: .topTrailing) {
Image(product.imageName)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(height: 200)
.clipped()
if product.isOnSale {
Text("SALE")
.font(.caption)
.fontWeight(.bold)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(.red)
.foregroundStyle(.white)
.clipShape(Capsule())
.padding(8)
}
}
Text(product.name)
.font(.headline)
.lineLimit(1)
Text(product.description)
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(2)
HStack {
Text("$\(product.price, specifier: "%.2f")")
.font(.title3)
.fontWeight(.bold)
if let originalPrice = product.originalPrice {
Text("$\(originalPrice, specifier: "%.2f")")
.font(.caption)
.strikethrough()
.foregroundStyle(.secondary)
}
Spacer()
Button(action: {}) {
Image(systemName: "cart.badge.plus")
}
.buttonStyle(.bordered)
}
}
.padding()
.background(.background)
.clipShape(RoundedRectangle(cornerRadius: 12))
.shadow(radius: 4)
}
}VML (for a specific product instance):
<VStack alignment="leading" spacing="8" style="padding(), background(.background), clipShape(.rect(cornerRadius: 12)), shadow(radius: 4)">
<ZStack alignment="topTrailing">
<Image name="iphone-15" style="resizable(), aspectRatio(contentMode: .fill), frame(height: 200), clipped()"/>
<Text style="font(.caption), fontWeight(.bold), padding(.horizontal, 8), padding(.vertical, 4), background(.red), foregroundStyle(.white), clipShape(.capsule), padding(8)">SALE</Text>
</ZStack>
<Text style="font(.headline), lineLimit(1)">iPhone 15 Pro</Text>
<Text style="font(.subheadline), foregroundStyle(.secondary), lineLimit(2)">The most advanced iPhone ever with titanium design</Text>
<HStack>
<Text style="font(.title3), fontWeight(.bold)">$999.00</Text>
<Text style="font(.caption), strikethrough(), foregroundStyle(.secondary)">$1099.00</Text>
<Spacer/>
<Button style="buttonStyle(.bordered)">
<Image systemName="cart.badge.plus"/>
</Button>
</HStack>
</VStack>SwiftUI:
struct DashboardView: View {
var body: some View {
ZStack {
// Gradient background
LinearGradient(
colors: [.blue.opacity(0.3), .purple.opacity(0.3)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.ignoresSafeArea()
ScrollView {
VStack(spacing: 20) {
// Header
HStack {
VStack(alignment: .leading) {
Text("Welcome back")
.font(.subheadline)
.foregroundStyle(.secondary)
Text("Dashboard")
.font(.largeTitle)
.fontWeight(.bold)
}
Spacer()
Image(systemName: "person.circle.fill")
.resizable()
.frame(width: 44, height: 44)
.foregroundStyle(.gray)
}
.padding()
// Stats Cards
HStack(spacing: 16) {
StatCard(
title: "Revenue",
value: "$12,543",
change: "+12%",
isPositive: true
)
StatCard(
title: "Orders",
value: "432",
change: "-3%",
isPositive: false
)
}
.padding(.horizontal)
// Chart placeholder
RoundedRectangle(cornerRadius: 20)
.fill(.ultraThinMaterial)
.frame(height: 200)
.overlay(
Text("Chart")
.foregroundStyle(.secondary)
)
.padding(.horizontal)
}
}
}
}
}
struct StatCard: View {
let title: String
let value: String
let change: String
let isPositive: Bool
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(title)
.font(.caption)
.foregroundStyle(.secondary)
Text(value)
.font(.title2)
.fontWeight(.bold)
HStack(spacing: 4) {
Image(systemName: isPositive ? "arrow.up.right" : "arrow.down.right")
.font(.caption)
Text(change)
.font(.caption)
}
.foregroundStyle(isPositive ? .green : .red)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(.ultraThinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}VML:
<ZStack>
<!-- Gradient background -->
<Rectangle style="fill(.linearGradient(colors: [.blue.opacity(0.3), .purple.opacity(0.3)], startPoint: .topLeading, endPoint: .bottomTrailing)), ignoresSafeArea()"/>
<ScrollView>
<VStack spacing="20">
<!-- Header -->
<HStack style="padding()">
<VStack alignment="leading">
<Text style="font(.subheadline), foregroundStyle(.secondary)">Welcome back</Text>
<Text style="font(.largeTitle), fontWeight(.bold)">Dashboard</Text>
</VStack>
<Spacer/>
<Image systemName="person.circle.fill" style="resizable(), frame(width: 44, height: 44), foregroundStyle(.gray)"/>
</HStack>
<!-- Stats Cards -->
<HStack spacing="16" style="padding(.horizontal)">
<!-- Revenue Card -->
<VStack alignment="leading" spacing="8" style="frame(maxWidth: .infinity, alignment: .leading), padding(), background(.ultraThinMaterial), clipShape(.rect(cornerRadius: 12))">
<Text style="font(.caption), foregroundStyle(.secondary)">Revenue</Text>
<Text style="font(.title2), fontWeight(.bold)">$12,543</Text>
<HStack spacing="4" style="foregroundStyle(.green)">
<Image systemName="arrow.up.right" style="font(.caption)"/>
<Text style="font(.caption)">+12%</Text>
</HStack>
</VStack>
<!-- Orders Card -->
<VStack alignment="leading" spacing="8" style="frame(maxWidth: .infinity, alignment: .leading), padding(), background(.ultraThinMaterial), clipShape(.rect(cornerRadius: 12))">
<Text style="font(.caption), foregroundStyle(.secondary)">Orders</Text>
<Text style="font(.title2), fontWeight(.bold)">432</Text>
<HStack spacing="4" style="foregroundStyle(.red)">
<Image systemName="arrow.down.right" style="font(.caption)"/>
<Text style="font(.caption)">-3%</Text>
</HStack>
</VStack>
</HStack>
<!-- Chart placeholder -->
<RoundedRectangle cornerRadius="20" style="fill(.ultraThinMaterial), frame(height: 200), overlay(content: :chartLabel), padding(.horizontal)">
<Text template="chartLabel" style="foregroundStyle(.secondary)">Chart</Text>
</RoundedRectangle>
</VStack>
</ScrollView>
</ZStack>When target is iOS:
- Default touch target minimum: 44pt
- Safe area insets apply
- Navigation bars use iOS styling
- Keyboard types are functional
When target is macOS:
- Mouse hover states may apply
- Window chrome considerations
- Menu bar integration possible
- Different control styles
When target is visionOS:
- 3D transforms may be available
- Depth and layering considerations
- Gesture requirements differ
- Spatial layout rules apply
VML parsers MUST handle:
- Malformed XML structure
- Unrecognized elements (log warning, skip)
- Invalid attribute values (use defaults)
- Missing required attributes (use defaults or skip)
Clients SHOULD handle:
- Missing template references (log warning)
- attr() resolution failures (use fallback)
- Circular template references (break cycle)
- Invalid modifier syntax (skip modifier)
When VML documents violate specification rules:
- Clients SHOULD attempt to parse and render what is possible, similar to HTML browser behavior
- VML parsing operates in strict mode by default
- Rule violations may cause parsing failures or improper SwiftUI rendering
- Clients SHOULD render all parseable content even when encountering malformed sections
GeometryReader support is marked as experimental. When supported:
<GeometryReader>
<Text style="frame(width: geometry.size.width / 2)">Half Width</Text>
</GeometryReader>- The client creates a
geometryobject when GeometryReader is present - Geometry values are available to descendant views in their style attributes only
- Geometry values are NOT accessible in view content or text
- When GeometryReaders are nested, the inner GeometryReader replaces the scope of the outer one (current limitation due to experimental nature)
The geometry object and its properties are handled entirely by the client. No template interpolation occurs in VML markup.
The following features may be considered for future versions:
- Conditional rendering directives
- Limited expression evaluation
- Animation keyframes
- Gesture recognizer hints
- Accessibility annotations
- Localization markers
End of Specification
This specification provides a complete and authoritative definition of the View Markup Language (VML) for converting SwiftUI to a static, declarative markup format suitable for server-side generation and client-side rendering.