To add a new platform backend to SwiftOpenUI, you need three components:
- Backend — implements
RenderBackend, manages app lifecycle - Renderer — maps SwiftOpenUI views to native elements
- ViewHost — handles reactive rebuilds when
@Statechanges
Sources/Backend/YourPlatform/
└── Rendering/
├── YourBackend.swift
├── YourRenderer.swift
└── YourViewHost.swift
import SwiftOpenUI
public struct YourBackend: RenderBackend {
public init() {}
public func run<A: App>(_ appType: A.Type) {
let instance = A()
// Render instance.body (a Scene) into your platform's window
// Enter your platform's event/run loop
}
}Handle WindowGroup by extending it with a rendering protocol:
protocol YourWindowRenderable {
func render()
}
extension WindowGroup: YourWindowRenderable {
func render() {
// Create a window, render self.content into it
}
}Define a protocol for native element creation, then extend each SwiftOpenUI view type:
public protocol YourRenderable {
func createNativeElement() -> NativeElement
}
extension Text: YourRenderable {
public func createNativeElement() -> NativeElement {
// Create a native text/label element with self.content
}
}
extension VStack: YourRenderable { ... }
extension HStack: YourRenderable { ... }
extension Button: YourRenderable { ... }
// ... etcThe dispatch function follows this pattern:
public func renderView<V: View>(_ view: V) -> NativeElement {
if let renderable = view as? YourRenderable {
return renderable.createNativeElement()
}
if hasReactiveProperties(view) {
return renderStatefulView(view)
}
return renderView(view.body)
}The ViewHost manages a stable native container that persists across rebuilds:
public class YourViewHost: AnyViewHost {
let container: NativeElement
let buildBody: () -> NativeElement
public func scheduleRebuild() {
// Coalesce: use platform idle callback / requestAnimationFrame / PostMessage
}
func rebuild() {
// 1. Remove old children from container
// 2. Save/restore environment context
// 3. Call buildBody() to get new element
// 4. Add new element to container
}
public func suppressNextFocusRestore() {
// Optional — suppress focus restoration if your platform needs it
}
}Add your backend target and append it to example dependencies:
// Conditionally for platform-specific backends:
#if os(YourPlatform)
targets += [
.target(
name: "BackendYour",
dependencies: ["SwiftOpenUI"],
path: "Sources/Backend/YourPlatform/Rendering"
),
]
exampleDeps.append("BackendYour")
#endifFor cross-compilable backends (like Web), declare unconditionally.
Add canImport checks to each example:
#if canImport(BackendYour)
import BackendYour
#endif
// ... at the bottom:
#elseif canImport(BackendYour)
YourBackend().run(MyApp.self)At minimum, implement rendering for these core views:
| Priority | Views |
|---|---|
| Must | Text, Button, VStack, HStack, EmptyView |
| Should | ZStack, Spacer, Divider, Color, Group, ForEach |
| Modifiers | PaddedView, FrameView, ForegroundColorView, BackgroundView, FontModifiedView, BorderView |
| State | AnyView, _ConditionalView, Optional, TupleView (variadic) |
| Environment | EnvironmentObjectModifierView, EnvironmentModifierView |
- GTK4:
Sources/Backend/GTK4/Rendering/— C interop, GObject lifecycle - Win32:
Sources/Backend/Win32/Rendering/— HWND management, custom layout engine - Web:
Sources/Backend/Web/Rendering/— simplest, good starting point