Skip to content

Repository files navigation

Concrete

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.

Quick start

// 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

Porting a Granite component from SwiftUI

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.

What's supported

ViewsText (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.)

Deliberate divergences

  • Picker(selection:options:label:) takes its options directly instead of collecting .tag()-ed children (a String-raw-value CaseIterable enum needs only Picker("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, not Angle.
  • GeometryReader, .transition, and custom ViewModifier conformances 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.

Architecture

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.

Development

$ 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.

License

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).

About

Swift HTML sites on hummingbird backed by Granite.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages