Get started quickly with our GitHub template:
👉 https://github.com/getautomaapp/swiftvanbase
Use this template to bootstrap your SwiftVan project with all the necessary configuration and setup!
SwiftVan is a reactive UI framework for Swift that compiles to WebAssembly (WASM) and renders to the DOM. It provides a SwiftUI-like declarative syntax for building web applications using Swift.
SwiftVan follows a reactive, component-based architecture:
┌─────────────────┐
│ Swift Code │
│ (Components) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Elements │ ◄── State subscriptions
│ (UI Tree) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ DomRenderer │
│ (Virtual DOM) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Browser DOM │
└─────────────────┘
- Elements - Building blocks of the UI (Div, Button, Text, etc.)
- State - Reactive state management system
- Renderer - Manages the virtual DOM and updates
- ElementBuilder - Result builder for declarative syntax
All UI components conform to the Element protocol:
public protocol Element: AnyObject {
var name: String { get } // HTML tag name
var refId: UUID { get } // Unique identifier
var stateSubscribers: [UUID: AnyState] { get set }
var children: [AnyElement] { get set }
var attributes: () -> DictValue { get set }
var _attributes: DictValue { get set }
var content: () -> [AnyElement] { get set }
func unmount() -> Void
}State changes automatically trigger UI updates through a subscription system:
let count = State(0)
count.value += 1 // Automatically updates all subscribed UI elementsSwiftVan uses Swift's @resultBuilder to enable declarative syntax:
Div {
Text({ "Hello" })
Button { Text({ "Click me" }) }
}let counter = State(0)
let name = State("John")
let items = State([1, 2, 3])When you read state.value inside an element's attributes or content, the element automatically subscribes to that state:
Text({ "Count: \(counter.value)" }) // Auto-subscribes to countercounter.value += 1 // Triggers re-render of subscribed elements- Subscribe: Elements automatically subscribe when accessing
state.value - Notify: State notifies all subscribers when value changes
- Unsubscribe: Elements unsubscribe when unmounted
Generic container element (renders as <div>)
Div(attributes: { [:] }) {
// children
}Inline container element (renders as <span>)
Span(attributes: { [:] }) {
// children
}Renders text with different heading sizes
// Different sizes
Text.normal({ "Regular text" })
Text.h1({ "Heading 1" })
Text.h3({ "Heading 3" })
Text.h5({ "Heading 5" })
Text.h6({ "Heading 6" })
// With attributes
Text.h1({ "Title" }, attributes: {
["style": ["color": "blue"]]
})Clickable button element
Button(onclick: {
// Handle click
}) {
Text({ "Click me" })
}
// With attributes
Button({ ["style": ["background": "blue"]] }, onclick: {
// Handle click
}) {
Text({ "Styled Button" })
}Image element (renders as <img>)
Image(attributes: {
["src": "image.png", "alt": "Description"]
})Canvas element for drawing
Canvas(attributes: {
["width": 800, "height": 600]
}) {
// children
}Ordered list (renders as <ol>)
OrderedList(attributes: { [:] }) {
ListItem { Text({ "First" }) }
ListItem { Text({ "Second" }) }
}Unordered list (renders as <ul>)
UnorderedList(attributes: { [:] }) {
ListItem { Text({ "Item 1" }) }
ListItem { Text({ "Item 2" }) }
}List item (renders as <li>)
ListItem(attributes: { [:] }) {
Text({ "Item content" })
}Hyperlink element (renders as <a>)
HyperLink(attributes: {
["href": "https://example.com"]
}) {
Text({ "Visit Example" })
}Conditionally render elements based on state:
let isLoggedIn = State(false)
If({ isLoggedIn.value }, states: [isLoggedIn]) {
Text({ "Welcome back!" })
} Else: {
Text({ "Please log in" })
}Important: Pass the states array to ensure the conditional re-evaluates when state changes.
Render a list of items from a state array:
let items = State([1, 2, 3])
ForEach(items: items) { item in
Text({ "Item: \(item)" })
}Features:
- Automatically updates when items are added
- Each item must render exactly one child element
- Subscribes to the state array for reactive updates
Attributes are passed as dictionaries:
Div(attributes: {
["id": "container", "class": "main"]
}) {
// children
}Styles use nested dictionaries:
Div(attributes: {
["style": [
"background": "blue",
"color": "white",
"padding": "10px"
]]
}) {
// children
}Attributes can use state values:
let color = State("red")
Div(attributes: {
["style": ["background": color.value]]
}) {
// children
}Event handlers are passed as closures:
Button({ [:] }, onclick: {
print("Button clicked!")
}) {
Text({ "Click" })
}The DomRenderer manages the virtual DOM and updates:
let ui = Div {
Text({ "Hello World" })
}
let renderer = DomRenderer(root: ui)
renderer.mount()- Mount: Initial render of the element tree
- Update: Diff props and update only changed attributes
- Unmount: Clean up elements and unsubscribe from state
The renderer efficiently updates only changed attributes:
struct PropsDiff {
var added: [String: Any] // New attributes
var changed: [String: Any] // Modified attributes
var removed: [String] // Removed attributes
}- Swift 6.1+
- SwiftWasm toolchain
- Node.js (for development server)
- Clone the repository:
git clone --recurse-submodules --remote-submodules https://github.com/GetAutomaApp/SwiftVan.git
cd SwiftVan
npm install- Build the project:
swift build --triple wasm32-unknown-wasi- Run the development server:
npm run devSwiftVan/
├── Sources/
│ ├── SwiftVan/ # Core framework
│ │ ├── Element.swift
│ │ ├── State.swift
│ │ ├── Renderer.swift
│ │ ├── ElementBuilder.swift
│ │ ├── Elements/ # UI elements
│ │ └── RenderTargets/ # DomRenderer
│ └── SwiftVanExample/ # Example app
│ └── main.swift
├── Package.swift
└── index.html
let count = State(0)
let ui = Div {
Text.h1({ "Counter: \(count.value)" })
Button(onclick: { count.value += 1 }) {
Text({ "Increment" })
}
}
let renderer = DomRenderer(root: ui)
renderer.mount()let todos = State(["Buy milk", "Walk dog"])
let newTodo = State("")
let ui = Div {
Text.h1({ "Todo List" })
ForEach(items: todos) { todo in
Div {
Text({ todo })
}
}
Button(onclick: {
todos.value.append("New task")
}) {
Text({ "Add Todo" })
}
}
let renderer = DomRenderer(root: ui)
renderer.mount()let isVisible = State(true)
let ui = Div {
Button(onclick: { isVisible.value.toggle() }) {
Text({ "Toggle" })
}
If({ isVisible.value }, states: [isVisible]) {
Text({ "I'm visible!" })
} Else: {
Text({ "I'm hidden!" })
}
}
let renderer = DomRenderer(root: ui)
renderer.mount()final class Counter {
var count = State(0)
func render() -> AnyElement {
Div {
Text.h3({ "Count: \(self.count.value)" })
Button(onclick: { self.count.value += 1 }) {
Text({ "Increment" })
}
}
}
}
let counter = Counter()
let renderer = DomRenderer(root: counter.render())
renderer.mount()Components can manage their own state and lifecycle:
class MyComponent: BaseComponent {
var count = State(0)
override func render() -> AnyElement {
Div {
Text({ "Count: \(count.value)" })
}
}
}Manual state subscription (usually automatic):
let state = State(0)
let subscriptionId = UUID()
state.subscribe(subscriptionId) { id, value in
print("State changed to: \(value)")
}
// Cleanup
state.unsubscribe(subscriptionId)Create custom elements by conforming to the Element protocol:
public class CustomElement: Element {
public let name = "custom"
public let refId = UUID()
public var stateSubscribers: [UUID: AnyState] = [:]
public var children: [AnyElement] = []
public var content: () -> [AnyElement]
public var attributes: () -> DictValue
public var _attributes: DictValue = [:]
public init(
attributes: @escaping () -> DictValue = {[:]},
@ElementBuilder _ content: @escaping () -> [AnyElement]
) {
self.content = content
self.attributes = attributes
let (attributes, children) = children()
self.children = children
self._attributes = attributes
}
}- Only changed attributes are updated in the DOM
- State subscriptions are automatically managed
- Elements are unmounted and cleaned up properly
- Minimize state reads: Cache state values if used multiple times
- Use specific states: Break down large state objects
- Avoid unnecessary re-renders: Use conditional rendering wisely
- Clean up subscriptions: Elements automatically unsubscribe on unmount
The framework includes debug prints for:
- Element mounting/unmounting
- State changes
- Attribute updates
- ForEach operations
- State not updating: Ensure you're modifying
state.value, not the state itself - Multiple renders: Check for circular state dependencies
- Memory leaks: Ensure elements are properly unmounted
- JavaScriptKit: Bridge between Swift and JavaScript
- SwiftWasm: Swift to WebAssembly compiler
From the TODO list:
- Components system
- PhantomElement for If-Else (prevents parent re-renders)
- Additional convenience initializers
- More built-in elements (table, form elements, etc.)
Projects using this framework can be closed source & freely distributed without attribution.
Contribution Guide: CONTRIBUTING.md
For issues and questions, please refer to the GitHub repository.