Granite components, rendered as live web pages.
Write SwiftUI-style views with Granite's reducer architecture — serve them on localhost with full interactivity, no JavaScript authored, no WebAssembly.
Concrete is a web renderer for Granite.
It reimplements the familiar SwiftUI view surface (Text, VStack, Button,
TextField, List, NavigationStack, modifiers, @State, @Binding, …) as an
HTML DSL, reuses Granite's engine verbatim (Centers, States, Reducers, Services,
Relays), and serves the result over Hummingbird 2
in a Phoenix LiveView-style loop:
browser event ──WebSocket──▶ Hummingbird ──▶ Granite reducer (server-side)
▲ │ state commit
└────── morphdom patch ◀── re-rendered HTML ◀┘
The browser runs a ~250-line runtime plus morphdom (MIT, vendored). Everything else — view tree, state, reducers, effects — is Swift, running in one process on your machine.
// Package.swift dependency:
// .package(url: "https://github.com/riteshpakala/Concrete", branch: "main")
import Concrete
@main
struct MyApp: ConcreteApp {
var root: some View {
Counter()
}
}
struct Counter: Component {
@Command var center: Center
struct Center: GraniteCenter {
struct State: GraniteState {
var value: Int = 0
}
@Store var state: State
@Event var increment: Increment.Reducer
}
var view: some View {
VStack(spacing: 16) {
Text("\(state.value)").font(.largeTitle)
Button("+", center.increment)
}
.padding()
}
}
struct Increment: GraniteReducer {
typealias Center = Counter.Center
func reduce(state: inout Center.State) {
state.value += 1
}
}$ swift run
Concrete serving at http://127.0.0.1:8080
Open the URL. Click the button. The reducer runs in your Swift process; the DOM
patches itself. Configure host/port/title via var configuration: ConcreteConfiguration.
Try the included demo — counter, control gallery, navigation, and a shared todo list that syncs across browser tabs live:
$ swift run ConcreteDemo
Mechanical, two changes:
| SwiftUI + Granite | Concrete |
|---|---|
import SwiftUI + import Granite |
import Concrete |
struct Foo: GraniteComponent |
struct Foo: Component |
Centers, states, reducers (sync, .task, .streamingTask), @Store
(including persist:), @Payload, @Event(.onAppear) lifecycle, services,
and relays compile unchanged. Button(center.reducer), onTapGesture(reducer),
and _state-based two-way bindings (TextField("q", text: _state.query)) work
as they do in Granite's SwiftUI sugar.
Rule: never import SwiftUI in a file that imports Concrete — the view
types share names by design.
Views — Text (concat, per-segment styling), Label, Image(url:),
AsyncImage, Image(systemName:), Link, Button, Toggle, TextField,
SecureField, TextEditor, Slider, Stepper, Picker, DatePicker,
ColorPicker, ProgressView (bar + spinner), VStack/HStack/ZStack,
Spacer, Divider, List, Section, Form, ScrollView (scroll position
survives patches), LazyVGrid/LazyHGrid, ForEach (Identifiable / keyPath /
Range), Group, AnyView, EmptyView, shapes (Rectangle,
RoundedRectangle, Circle, Capsule, Ellipse with .fill/.stroke),
LinearGradient/RadialGradient/AngularGradient,
NavigationStack/NavigationLink/.navigationTitle (in-session stack),
Color (full adaptive SwiftUI palette via CSS light-dark(), plus rgb/hex/css).
Modifiers — .padding, .frame (fixed + min/ideal/max with
.infinity), .background, .overlay, .foregroundColor/Style, .font,
.bold/.italic/.underline/.strikethrough, .fontWeight, .opacity,
.cornerRadius, .border, .shadow, .blur, .grayscale, .offset,
.scaleEffect, .rotationEffect(degrees:), .zIndex, .aspectRatio,
.clipped, .fixedSize, .disabled, .hidden, .help, .id,
.multilineTextAlignment, .lineLimit, .textCase, .kerning/.tracking,
.animation (CSS transition), .onTapGesture, .onSubmit, .onChange,
.onAppear/.onDisappear, .environment, @Environment (\.colorScheme
follows the browser).
State — Concrete @State/@Binding for local view state; Granite
centers/services/relays for everything else. Structural identity mirrors
SwiftUI: state lives at a stable tree position and is discarded (with
onDisappear + Granite lifecycle teardown) when the position leaves the tree.
Multi-tab live state — every browser tab is a session with its own
components, but @Relay services are process-shared: change state in one tab
and every tab re-renders. (This falls out of Granite's architecture; it's the
demo's todo list.)
Picker(selection:options:label:)takes its options directly instead of collecting.tag()-ed children (aString-raw-valueCaseIterableenum needs onlyPicker("Title", selection: $value)).Image(systemName:)maps ~60 common SF Symbol names to Unicode glyphs and falls back to a name badge — SF Symbols themselves can't ship to the web..rotationEffect(degrees:)takes degrees, notAngle.GeometryReader,.transition, and customViewModifierconformances are not implemented (compile error rather than wrong layout).@Store(persist:)files and relay services are process-wide, shared by all sessions — the same semantics as multiple component instances in one app.
| Target | Role |
|---|---|
ConcreteHTML |
Zero-dependency view DSL + HTML renderer. Views render to an HTMLNode tree; layout maps to flexbox/CSS grid (no server-side layout math). Structural identity paths key state storage and derive stable handler/DOM ids. |
Concrete |
Granite integration + server. Component protocol, @Command (hosts a GraniteCommand outside SwiftUI), per-session mount storage with lifecycle sweep, LiveSession (16 ms-coalesced render→morph loop), Hummingbird routes, wire protocol, client runtime assets. |
ConcreteDemo |
swift run ConcreteDemo → the demo app. |
The wire protocol is JSON text frames: client sends hello and
{"t":"ev","hid":…,"k":"click|input|change|toggle|slide|submit",…}; server
sends full-HTML mount/morph frames and morphdom diffs client-side. Focus,
selection, and scroll positions are preserved across patches; the socket
auto-reconnects with backoff and resumes its session (state intact) within a
5-minute window.
$ swift test # 45 tests: renderer, controls, identity, storage, wire, HTTP
$ swift run ConcreteDemo
Concrete depends on Granite's non-SwiftUI hosting API
(GraniteCommand.hosted(...), appear()/disappear()/performTasks()), added to
Granite in commit 3897665.
Concrete is released under the license in LICENSE.
Vendored: morphdom (MIT — see
Sources/Concrete/Resources/vendor/LICENSE-morphdom.txt). Design patterns for
the view-protocol reimplementation reference
Tokamak (Apache 2.0).