SwiftOpenUI uses a pluggable backend architecture. The core framework defines views, state, and layout — backends render them to native platform elements.
SwiftOpenUI (core)
├── View protocol, @State, @Binding, @ObservedObject, ...
├── ViewBuilder, App, Scene, WindowGroup
└── RenderBackend protocol
├── GTK4Backend → GtkWidget (Linux)
├── Win32Backend → HWND (Windows)
├── WebBackend → DOM elements (Browser/Wasm)
└── real SwiftUI → macOS (no backend needed)
public protocol RenderBackend {
func run<A: App>(_ appType: A.Type)
}Each backend implements run() to:
- Create the platform's application/event loop
- Instantiate the
App - Walk the scene tree, rendering
WindowGroupcontent to native widgets - Enter the run loop
Each backend has three key parts:
| Component | Role | GTK4 | Win32 | Web |
|---|---|---|---|---|
| Backend | App lifecycle, window creation | GTK4Backend |
Win32Backend |
WebBackend |
| Renderer | View → native element mapping | GTKRenderer |
WinRenderer |
WebRenderer |
| ViewHost | Reactive rebuilds on state change | GTKViewHost |
Win32ViewHost |
WebViewHost |
Each renderer follows the same pattern:
- Primitive views (Text, Button, etc.) — direct native element creation via a
Renderableprotocol extension - Stateful composite views — wrapped in a
ViewHostfor reactive rebuilds - Stateless composite views — recurse through
.body
// Pseudocode — same pattern in all backends
func renderView<V: View>(_ view: V) -> NativeElement {
if let renderable = view as? PlatformRenderable {
return renderable.createNativeElement()
}
if hasReactiveProperties(view) {
return renderStatefulView(view) // ViewHost wrapper
}
return renderView(view.body) // recurse
}Each ViewHost coalesces state changes into a single rebuild per frame:
| Platform | Coalescing mechanism |
|---|---|
| GTK4 | g_idle_add (next main loop iteration) |
| Win32 | PostMessage + custom message ID |
| Web | requestAnimationFrame |
The Window scene type provides single-instance windows opened via OpenWindowAction (an environment key). Each backend manages window lifecycle differently:
| Platform | Window Management |
|---|---|
| GTK4 | GTK4WindowRegistry — tracks live GtkWindow* pointers per scene ID. open(id:) refocuses existing windows or creates via factory. destroy signal clears pointers to prevent use-after-free. All Window scenes register factories at render time. |
| Win32 | Win32WindowRegistry — tracks HWND per scene ID. open(id:) calls SetForegroundWindow on existing handle or invokes factory. WM_DESTROY clears handle. Separate windowSceneWndProc with message forwarding (WM_HSCROLL, WM_VSCROLL, WM_NOTIFY). hasMainWindow flag for app termination when no windows remain. |
| Web | Not yet implemented. |
AppBundle provides platform-independent resource lookup for packaged apps:
| Mode | Discovery | Resources path |
|---|---|---|
| Production (.app bundle) | macOS: Foundation.Bundle; Linux: /proc/self/exe + Info.json walk-up; Windows: GetModuleFileNameW + Info.json walk-up |
macOS: Contents/Resources/; Linux/Windows: Resources/ |
Development (swift run) |
Walk up from executable, find Package.swift + Resources/ |
<packageRoot>/Resources/ |
The create-bundle SPM plugin (swift package create-bundle <product>) automates packaging into .build/bundles/<Product>.app with platform-appropriate layout and generated metadata.
Icons / Image(systemName:) are resolved backend-specifically:
- macOS uses SF Symbols natively via SwiftUI.
- Non-macOS backends bundle a Material Symbols font in a dedicated SwiftPM target (
SwiftOpenUISymbols), gated per-platform inPackage.swift. Each backend registers the font process-locally at startup (FontConfig on Linux,AddFontResourceExW+FR_PRIVATEon Win32,document.fonts.addon Web,Typeface.createFromAsseton Android). - macOS builds never pull the symbols target — zero icon-font weight in macOS app bundles.
See icon-symbols.md for the full design: bundling, license compliance, process-local loading, and the phased Image(systemName:) compatibility roadmap.
On macOS, examples use real SwiftUI directly (import SwiftUI + App.main()). No SwiftOpenUI backend is needed — the framework compiles for testing but rendering uses Apple's native implementation.