SwiftUI for .NET β everywhere. Write declarative UI once in C# and render it as real native UI on each platform: SwiftUI on iOS/macOS/tvOS, Jetpack Compose on Android, GTK4 on Linux, WinUI 3 and WPF on Windows, and HTML/DOM on the Web. Not a reimplementation of each toolkit β the actual native controls, with the platform's own layout, fonts, animations, and accessibility. Plus a self-drawing SkiaSharp backend that paints the UI itself for a pixel-identical look on every platform, hosts for the MonoGame, Godot and Unity game engines, and a terminal backend for anywhere with a TTY and no display server.
One View subclass, and two families of rendering backend β native-fidelity (map to the OS's real
controls) and self-drawing (paint every pixel with SkiaSharp for a pixel-identical look everywhere):
| Platform | Renders as | Route | Status |
|---|---|---|---|
| iOS | SwiftUI | Swift shim (xcframework, P/Invoke) | β Verified on simulator |
| macOS | SwiftUI (AppKit-hosted) | Same Swift shim (#if UIKitβAppKit) |
β Verified on desktop |
| tvOS | SwiftUI | Same Swift shim (#if os(tvOS) fallbacks) |
β Verified on Apple TV sim |
| Android | Jetpack Compose | Kotlin shim (.aar, JNI) |
β Verified on emulator |
| Linux | GTK4 | Pure C# (Gir.Core, no shim) | β Verified on desktop |
| Linux | Self-drawn on a native Wayland surface | Pure C# (libwayland/xkbcommon P/Invoke) | π§© Scaffolded β builds clean; never run against a compositor |
| Windows | WinUI 3 | Pure C# (no shim) | π§© Scaffolded (needs Windows to build) |
| Windows | WPF | Pure C# (no shim) | π§© Scaffolded β compiles clean (Windows CI); never run |
| Windows | Self-drawn on a WinForms / WPF surface | Pure C# (Skia engine) | π§© Host compiles clean; never run |
| Web | HTML/DOM | Pure C# (Blazor WASM, no shim) | β Verified in Chrome |
| Any (Skia) | Self-drawn canvas | Pure C# (SkiaSharp β no native controls) | β Verified (macOS window + headless PNG) |
| Any (terminal) | Characters in a TTY | Pure C# (XenoAtom.Terminal.UI β no native controls) | β Verified headlessly (35 CI tests) |
| MonoGame | Self-drawn into a Texture2D |
Pure C# (Skia engine in a game loop) | β Verified (macOS/DesktopGL window + back buffer) |
| Godot | Godot's own 2D draw commands | Pure C# Control node β no Skia, no native library |
β Verified on Godot 4.7.2 (macOS/Metal) |
The Skia backend is a from-scratch UI toolkit: it owns layout, text shaping (HarfBuzz), scrolling,
overlays, input/focus, an animation clock, and an icon font β rendering the whole shared ContentView
identically on every OS. It's the universal renderer for a uniform look and for targets the native
backends can't reach (dependency-free desktop, embedded/framebuffer Linux). Trade-off: no native
accessibility, and WebView/Map can't be painted onto a canvas (they need a native-view overlay).
Two backend routes: SwiftUI and Compose are compiler-plugin frameworks, so they need a thin native shim (Swift/Kotlin) that reconstructs the tree; GTK, WinUI, WPF, and the Web are fully C#-bindable, so those backends are pure C# with no native code β a retained-mode interpreter that maps the node tree straight to native controls (or DOM elements) and applies the same diff patches.
public sealed class ContentView : View
{
readonly State<int> _count = State(0); // mirrors @State private var count = 0
public override View Body =>
new VStack(
new Text($"Count: {_count.Value}").Font(Font.LargeTitle),
new Text("Tap the button to increment").Font(Font.Caption).ForegroundColor(Color.Secondary),
new Button("Increment", () => _count.Value++)
).Spacing(24);
}Full docs live in docs/. Quick links:
- Getting Started Β· Architecture Β· Hot Reload
- Hosting & Dependency Injection β
SwiftProgram.CreateSwiftApp(),[Inject], lifecycle - Authoring: Views & Controls Β· Modifiers, Gestures & Animation Β· State & Binding Β· Collection View Β· Global Styles Β· Custom Controls Β· Controls Library
- Backends: Overview Β· Apple Β· Android Β· Linux/GTK Β· Windows/WinUI Β· Windows/WPF Β· Windows/WinForms Β· Web Β· Skia Β· WebGPU Β· MonoGame Β· Godot Β· Unity Β· Terminal/TUI
- Tooling: Rider Plugin & Dev Tools β run configurations, the live patch inspector, and the in-IDE Skia preview
- Live Surfaces β iOS Live Activities & the Dynamic Island, home-screen widgets, Android custom notifications & app widgets, Android 16 Live Updates
- MAUI Interop β host SwiftDotNet in a .NET MAUI app, and put real MAUI
controls back inside the tree with
MauiView - Maps Β· Roadmap
Two ways to add your own control:
- Composite (the common case) β subclass
View, compose existing views inBody. Pure C#, no native code, renders on every backend automatically. Example:sample/SharedUI/Rating.csis a β /β rating built fromHStack+Button. - Custom native primitive β for a control that isn't a composition (a native map, gauge, etc.): subclass
CustomView, emit props under aTypeName, then register a per-backend renderer. On the pure-C# backends this needs no interpreter fork:GtkRenderers.Register("NativeRating", ctx => { var scale = Gtk.Scale.NewWithRange(Gtk.Orientation.Horizontal, 0, 5, 1); scale.SetValue(ctx.Number("value") ?? 0); scale.OnValueChanged += (_, _) => ctx.Emit(((int)scale.GetValue()).ToString()); return scale; });
WinRenderers.Registeris the WinUI equivalent; SwiftUI/Compose exposeswiftDotNetRegisterRenderer/registerRendererfor native (Swift/Kotlin) extensions. Unregistered types render aβ οΈplaceholder, not a crash.
- Layout:
VStack,HStack,ZStack,ScrollView,Grid(per-track sizing + cell spans),AbsoluteLayout(point or proportional bounds),List(+List.ForEach),Form,Section,Group,Spacer,Divider - Navigation & presentation:
NavigationStack,NavigationLink,TabView(+.Paged()carousel),Tab,Sheet,Alert,ActionSheet,DisclosureGroup,Menu - Inputs (two-way bound):
TextField,SecureField,TextEditor,Toggle,Slider,Stepper,Picker,DatePicker,ColorPicker - Display:
Text,Label,Image(SF Symbols),ProgressView,Gauge,Link, and shapesRectangle/Circle/Capsule/RoundedRectangle - Modifiers (order-preserving):
.Font,.ForegroundColor,.Background,.Padding(uniform or per-Edge),.Frame(+ alignment),.CornerRadius,.Border,.Shadow(+ color/offset),.Opacity(clamped 0β1),.Disabled(dim + block interaction),.ScaleEffect(native scale transform, around an anchor),.Align(fill width + align),.NavigationTitle. Modifiers are a universal wrapper applied to any view via a single generic pass per backend β so.Opacity/.Disabled/.ScaleEffectwork on every control, not a hand-picked subset. (.ScaleEffectis a documented no-op on GTK, which has no per-widget scale transform.) - Gestures (one-shot, on any view):
.OnTapGesture(count:)(single or double-tap),.OnLongPress(press-and-hold,minimumDuration:),.OnSwipe(SwipeDirection, β¦)(a directional drag committed on release β add one call per direction). Each maps to the platform's native recognizer (SwiftUIonLongPressGesture/MagnifyGesture-family, ComposedetectTapGestures/detectDragGestures, WinUIHolding/ManipulationCompleted, GTKGestureLongPress/GestureSwipe, Web Pointer Events) and fires back through the same event channel as.OnTapGesture. Continuous pan/pinch has since landed as.OnDrag/.OnMagnifyβ see Modifiers, Gestures & Animation. - Animation:
.Animation(AnimationSpec, on: <value>)β implicit animation that interpolates a view's animatable modifiers (opacity, frame size, β¦) when theon:value changes, mirroring SwiftUI's.animation(_:value:). Specs viaAnim.Linear/EaseIn/EaseOut/EaseInOut(duration)andAnim.Spring(). Maps to real native animation β SwiftUI.animation, ComposeanimateContentSize/animateFloatAsState, WinUI theme transitions, GTK/Web CSStransition; springs are native where available and degrade to a bezier (Web) or ease-in-out (GTK)..Repeating()adds self-playing loops (shimmer/pulse). - Keyframe animations:
.Keyframes(k => k.Track(Prop.Opacity, t => t.At(0, 1).At(0.5, 0.3)).Duration(1.2))β multi-track timelines with independent per-property stops and per-segment curves, mirroring SwiftUI'skeyframeAnimator. Maps to a realKeyframeAnimator(SwiftUI), akeyframes<Float>spec (Compose), a generated@keyframesrule (Web), the frame clock (GTK), the engine's own clock (Skia/WebGPU/Unity) and a keyframedStoryboard(WinUI, uncompiled). No-op on the TUI. See Modifiers, Gestures & Animation. ExplicitAnimate.Runtransactions and enter/leave.Transition(β¦)are later phases β see the Roadmap. - Alignment:
VStack.Alignment(HorizontalAlignment),HStack.Alignment(VerticalAlignment),ZStack.Alignment(Alignment); colors also viaColor.Hex("#RRGGBB") - Maps (opt-in companion library):
new Map(cameraState).Pins(β¦).Polylines(β¦).OnTap(β¦)renders a real native map β MapLibre GL on Web, MapKit on Apple, MapLibre on Android β from one C# tree. Ships as separate packages so the SDK weight stays opt-in and Core stays dependency-free:SwiftDotNet.Maps(the view + data types),SwiftDotNet.Maps.Web(MapLibre, built & verified), plus Swift/Kotlin renderers innative/mapsfor Apple/Android. A platform with no map renderer shows the standardβ οΈplaceholder. Phase 1 (static pins/polylines + tap-to-draw) β see Maps.
SwiftUI has no stylesheet β "global styling" is the environment cascade (a value set on a container is
inherited by every descendant that doesn't set its own), style protocols (ButtonStyle & friends,
defined once and applied to every control below), and reusable ViewModifiers. SwiftDotNet offers the
same three, but resolves the cascade in C# during the render pass: each node inherits any ambient
font/foregroundColor/control-style it didn't set and ships to the backend fully resolved, using only
modifier types the backends already understand. So global styles work identically on every backend β
SwiftUI, Compose, GTK, WinUI, Web, and Skia β with no per-backend code, including the ones (Skia/GTK/WinUI)
that have no inheritance of their own. An explicit local modifier always wins over an inherited one; nothing
is injected unless you set an environment, so there's zero cost otherwise.
new ContentView()
// B β environment cascade: descendants inherit these unless they set their own (SwiftUI's `.environment`)
.Environment(e => e.Font(Font.Body).ForegroundColor(Color.Primary))
// C β control style: every Button below adopts it, no call-site changes (SwiftUI's `.buttonStyle`)
.ButtonStyle(new FilledButtonStyle())
// C β a design-token bag read by styles & bodies via EnvironmentValues.Current.Theme
.Theme(new Theme { Accent = Color.Hex("#7C4DFF"), CornerRadius = 16 });
// A β reusable bundles (SwiftUI's ViewModifier / View extension), applied explicitly per view:
new VStack(new Text("Hi")).CardStyle(); // built-in, reads the ambient Theme
new VStack(new Text("Hi")).Style(b => b.Padding().Background(Color.Secondary).CornerRadius(12));.Environment(e => β¦)sets ambientFont/ForegroundColor;.Theme(theme)injects design tokens;.ButtonStyle(style)sets the ambientIButtonStyle. Each wraps the view in a transparentEnvironmentScope(no node in the tree, no diff impact); nested scopes compose (inner overrides only what it sets). Read the active environment anywhere a view is built viaEnvironmentValues.Current..Style(style)/.CardStyle()attach a reusableIViewStylebundle. Bundles (and control styles) are authored with the same fluent modifiers you'd otherwise chain, and resolve at render time so they can read the ambientTheme. Built-ins:FilledButtonStyle,BorderedButtonStyle(IButtonStyle),CardStyle.
The Styles tab in the sample ContentView exercises all three; the cascade is covered by
GlobalStyleTests.
C# owns the view tree (React-Native style); each backend reconstructs native UI from it. A diff engine turns every re-render into a minimal patch so only changed nodes reach the renderer. The diagram below shows the iOS/SwiftUI path β the bridge is a native shim there and on Android, and an in-process interpreter on the pure-C# backends (GTK/WinUI/Web), but the patch protocol and event round-trip are identical everywhere:
C# DSL (View/State)
β ToNode() β TreeDiffer βββββββββββββββββββββββββββββββ
βΌ β SwiftDotNetBridge.xcframeworkβ
Patch ββJSONβββΊ swiftdotnet_render βββΊ apply to @Observable VNode tree βββΊ NodeView β real SwiftUI
β² β β tap / edit / toggle
β State.Value = β¦ βββ SwiftApp.OnEvent(id,value) βββ [UnmanagedCallersOnly] ββββ @convention(c)
βββ re-render ββββββββββββββββββββ (node id + value payload)
swiftdotnet_render(json)β C# pushes a patch (replace/updateProps/setChildren); Swift applies it to the observedVNodetree, so unchanged subtrees never rebuild.swiftdotnet_set_event_callback(fn)β Swift calls it on events with a node id + optional value (TextField text, Toggle"true"/"false", null for a Button).swiftdotnet_make_host_controller()β returns aUIHostingControllerC# hosts as the root VC.
Node ids are structural paths ("0.2.1"), stable across renders, so the differ targets nodes by id:
a prop change emits updateProps for just that node; a changed child list emits setChildren on the
parent; identical renders emit nothing. Two-way-bound controls (TextField, Toggle) are Swift
"controlled components" whose local @State syncs both directions via onChange.
| Path | TFM | Role |
|---|---|---|
src/SwiftDotNet |
multi-target | One library. Core/ (platform-neutral DSL, State<T>, Node/JSON, diff engine, IBridge, SwiftApp) compiles for every TFM; Platforms/{iOS,macOS,tvOS,Android,Windows}/ (the bridges + SwiftDotNetHost) are opted in per TFM. TFMs: net10.0;net10.0-android always, net10.0-ios;-macos;-tvos on a Mac, net10.0-windows10.x on Windows. iOS/macOS/tvOS pull the Swift xcframework (SwiftDotNetBridge.targets); Android binds the Compose .aar + Xamarin.AndroidX.Compose.*; Windows pulls WinUI 3. |
src/SwiftDotNet.Gtk |
net10.0 |
Separate (Linux/GTK shares the net10.0 TFM with Core, so folding it in would force every neutral consumer to take the GTK dependency). Pure-C# GTK4 backend over Gir.Core; references the combined SwiftDotNet. |
src/SwiftDotNet.Web |
net10.0 (Razor lib) |
Separate (Blazor has no distinct TFM either). Pure-C# Blazor WebAssembly backend β SwiftDotNetView renders the node tree to HTML/CSS via RenderTreeBuilder; DOM events call back into C#. |
src/SwiftDotNet.Skia |
net10.0 |
Separate (self-drawing engine; SkiaSharp on every neutral consumer). Pure-C# SkiaSharp backend β SkiaBridge keeps a retained scene tree and paints/measures/hit-tests it directly on an SKCanvas. Layout, HarfBuzz text, scrolling, overlays, input/focus, animation clock, SkiaRenderers registry. Host-agnostic via ISkiaHost. |
src/SwiftDotNet.Wayland |
net10.0 |
Separate (drags in Wayland.Platform and its libwayland/libxkbcommon P/Invoke, which only Linux can load). Hosts the Skia backend on a native Wayland surface β no GTK, no GLFW. Supplies only a canvas, title text, input translation and the animation tick; the protocol, windowing, decorations and buffers come from the shared maui-wayland repo. |
src/SwiftDotNet.Tui |
net10.0 |
Separate (same reasoning as GTK β a terminal toolkit on every neutral consumer). Pure-C# XenoAtom.Terminal.UI backend β TuiBridge keeps a retained Visual tree and applies the same patches to it. Includes a hand-rolled PNG decoder and the imageβcharacter-art renderer, so it takes no image dependency. |
src/SwiftDotNet.Tui.Graphics |
net10.0 |
Optional add-on: real Sixel/Kitty/iTerm2 images plus JPEG/WebP/GIF decode. Separate because it drags in SkiaSharp + native assets for three RIDs, which a terminal app shouldn't pay for unless it asks. |
native/SwiftDotNetBridge |
Swift | Bridge.swift + build script β build/SwiftDotNetBridge.xcframework (SwiftUI interpreter; 5 slices β iOS device/sim, tvOS device/sim, macOS) |
native/SwiftDotNetComposeBridge |
Kotlin | Bridge.kt + Gradle β build/SwiftDotNetComposeBridge.aar (Jetpack Compose interpreter) |
sample/SharedUI |
net10.0 |
The demo ContentView (MAUI-style flyout menu) + composite Rating control β one file, shared by all apps |
sample/SampleApp |
multi-target | One sample app, multi-targeted like the library: net10.0-android always, +ios;-macos;-tvos on a Mac, +windows on Windows. Platforms/{iOS,macOS,tvOS,Android,Windows}/ hold the thin per-OS entry points; services and the root view are registered once in SharedUI/SwiftProgram.cs. |
sample/SampleApp.Gtk |
net10.0 |
Thin GTK app: references SwiftDotNet.Gtk (separate β no distinct TFM) |
sample/SampleApp.Web |
net10.0 (Blazor WASM) |
Thin web app: hosts <SwiftDotNetView Root="new ContentView()"> (separate β no distinct TFM) |
sample/SampleApp.Skia |
net10.0 |
Headless harness: renders ContentView to PNGs, drives taps/scroll/typing/overlays/animation (the Skia analog of the SDN_TEST harness). |
sample/SampleApp.Skia.Mac |
net10.0-macos |
Interactive AppKit window: an NSView blits the Skia scene and feeds mouse/scroll/keyboard into the bridge; a timer drives the animation clock. |
src/SwiftDotNet.Skia.Maui |
net10.0-maccatalyst (+more) |
MAUI adapter: SwiftDotNetSkiaView : SKCanvasView hosts the engine on iOS/Android/Mac Catalyst/Windows. Composes with Shiny via MAUI hosting (.UseShiny()) β the Skia UI and Shiny plugins share one DI container. |
src/SwiftDotNet.Maui |
net10.0-ios;-android;-maccatalyst (+-windows) |
MAUI interop: the MauiView node (a real Microsoft.Maui.Controls.View inside a SwiftDotNet tree), the layer that places those controls over the Skia canvas, and MauiEmbedding for the reverse direction. There is deliberately no MAUI backend β see MAUI Interop. |
sample/SampleApp.Skia.Maui |
net10.0-maccatalyst |
MAUI + Shiny demo: MauiProgram calls .UseSkiaSharp().UseShiny() + AddBluetoothLE(); the page resolves IBleManager from the same container. (-p:NoShiny=true builds without Shiny.) |
sample/SampleApp.Tui |
net10.0 |
Thin terminal app: references SwiftDotNet.Tui and registers a TuiRenderers custom renderer. SDN_TUI_GRAPHICS=1 opts into real pixel images. |
sample/SampleApp.Skia.Silk |
net10.0 |
Dependency-free desktop (Windows/macOS/Linux): a Silk.NET (GLFW) window + GL context; SkiaSharp draws onto a GL-backed surface. Base for embedded/framebuffer Linux. |
src/SwiftDotNet.Wpf |
net10.0-windows |
WPF backend β the node tree as real System.Windows.Controls. Separate project (Windows-only TFM); EnableWindowsTargeting so it compiles off Windows too. |
src/SwiftDotNet.Skia.Wpf |
net10.0-windows |
Skia host for WPF: SwiftDotNetSkiaElement paints into a WriteableBitmap back buffer. No SkiaSharp.Views.WPF dependency (that package is .NET-Framework-only). |
src/SwiftDotNet.Skia.WindowsForms |
net10.0-windows |
Skia host for Windows Forms: SwiftDotNetSkiaControl paints into a locked GDI+ bitmap. The only WinForms backend β see WinForms. |
sample/SampleApp.Wpf Β· .Skia.Wpf Β· .Skia.WinForms |
net10.0-windows |
The three Windows-desktop heads. All build on macOS/Linux; CI compiles them on windows-latest. |
All projects are wired into SwiftDotNet.slnx at the repo root.
The per-OS bootstrap lives in the library as reusable abstract hosts, so an app's platform entry point is a one-liner that just names its root view:
Base host (in SwiftDotNet) |
Platform | Subclass in the app |
|---|---|---|
SwiftDotNetAppDelegate : UIApplicationDelegate |
iOS / tvOS | [Register("AppDelegate")] class AppDelegate : SwiftDotNetAppDelegate |
SwiftDotNetAppDelegate : NSApplicationDelegate |
macOS | same (creates + sizes the NSWindow for you) |
SwiftDotNetActivity : ComponentActivity |
Android | [Activity(MainLauncher=true)] class MainActivity : SwiftDotNetActivity |
SwiftDotNetApplication : Application |
Windows | class App : SwiftDotNetApplication |
SwiftDotNetWpfApplication : Application |
Windows (WPF) | class App : SwiftDotNetWpfApplication |
SwiftDotNetSkiaWindow / SwiftDotNetSkiaForm |
Windows (Skia on WPF / WinForms) | app.Run(new SwiftDotNetSkiaWindow(swiftApp)) |
Each override is just protected override SwiftDotNetApp CreateSwiftApp() => SwiftProgram.CreateSwiftApp();
β deliberately the same shape as .NET MAUI's MauiProgram.cs. SwiftProgram is the single place the app
registers its services, logging and root view. So the window/host/activation wiring is written once in the
framework, and the sample declares its UI and its dependencies in exactly one place. (The bases are non-generic abstract
classes β a generic NSObject/Java.Lang.Object subclass can't be registered with the ObjC/Android runtimes.)
The same SharedUI.ContentView renders as SwiftUI on iOS, SwiftUI (AppKit-hosted) on macOS,
SwiftUI on tvOS, Jetpack Compose on Android, GTK4 on Linux, and HTML/DOM on the Web β verified
on device/emulator/desktop/Apple TV sim/browser.
The Apple platforms share one Swift interpreter with a few #if conditionals: macOS swaps UIKitβAppKit hosting;
tvOS (focus-driven, no pointer) falls back for the controls Apple omits there β Slider/DatePicker/ColorPicker
show a value/swatch, Stepperβfocusable β/+ buttons, DisclosureGroupβa header button, GaugeβProgressView.
Linux/GTK is different: GTK is a C/GObject library (not a compiler-plugin framework), so its backend is
pure C# with no native shim β a retained-mode interpreter mapping the node tree to real Gtk.Widgets
via Gir.Core, applying the same diff patches. It is at full vocabulary parity: the same ContentView
builds 325 GTK4 widgets (Gtk.Entry, Gtk.Switch, Gtk.Scale, Gtk.SpinButton, Gtk.DropDown,
Gtk.Calendar, Gtk.ColorDialogButton, Gtk.ListBox, Gtk.Notebook, Gtk.Expander, Gtk.Grid,
Gtk.Overlay, β¦), with modifiers via a GTK CSS provider (border/shadow/corner-radius/background/padding)
and alignment via halign/valign. Run needs GTK4 (brew install gtk4 / apt install libgtk-4-1); on
non-Linux set DYLD_FALLBACK_LIBRARY_PATH/LD_LIBRARY_PATH to the GTK libs.
Windows/WinUI is the same pure-C# "translate to controls" route: SwiftDotNet.Windows maps the node
tree to real WinUI 3 controls (TextBox, ToggleSwitch, Slider, NumberBox, ComboBox, TabView,
Expander, CalendarDatePicker, ColorPicker, ContentDialog, HyperlinkButton, shapes, β¦). WinUI is
fully C#-bindable, so no native shim is needed β unlike SwiftUI/Compose. This backend is scaffolded but
not yet compiled (WinUI 3 / Windows App SDK require Windows to build); expect minor WinUI API fixes on the
first Windows build. Notably, microsoft-ui-reactor is a WinUI-only project with this same architecture β
a sibling, not a dependency; this backend keeps SwiftDotNet's own reconciler.
Windows/WPF takes the same route and is the one that actually builds today: SwiftDotNet.Wpf maps the
node tree to real WPF controls (TextBox, CheckBox, Slider, ComboBox, TabControl, Expander,
DatePicker, Canvas, shapes, β¦). Where WinUI has a control WPF lacks it is rebuilt from parts rather than
dropped β Stepper becomes a RepeatButton pair, ColorPicker a swatch + palette popup, and dialogs an
overlay layer (a modal ShowDialog() would block the render loop mid-patch). Two modifiers land better
than on WinUI: .Shadow is a real DropShadowEffect that follows the content's alpha, and .Disabled is
plain IsEnabled. It compiles on macOS and Linux via EnableWindowsTargeting and on a windows-latest CI
runner β but has never been run. See WPF.
Windows Forms deliberately gets no native-control backend. GDI controls have no transforms, no
per-element opacity, no rounded clipping, no vector shapes and no animation system, so roughly half the
modifier vocabulary would have had to become a silent no-op. It hosts the Skia canvas in one Control
instead, which gives it the complete feature set. See WinForms.
The sample app is unpackaged + self-contained (WindowsPackageType=None, SelfContained=true,
WindowsAppSDKSelfContained=true), so on a Windows machine it runs with no prerequisites beyond the .NET SDK:
dotnet run --project sample/SampleApp -f net10.0-windows10.0.19041.0Web/Blazor is the third pure-C# "translate to controls" backend, where the "control" is an HTML element.
SwiftDotNet.Web is a separate Razor class library: SwiftDotNetView : ComponentBase walks the node tree in
BuildRenderTree, emitting HTML/CSS through Blazor's RenderTreeBuilder β so Blazor's own render-treeβDOM
diff is the write layer (no manual DOM manipulation). VStack/HStackβflex div, TextFieldβ<input>,
Toggleβ<input type=checkbox>, Sliderβrange input, Pickerβ<select>, DatePicker/ColorPickerβnative
inputs, shapesβstyled divs, modifiersβinline CSS; DOM events (onclick, onchange) call back into C# via
EventCallback. It runs in Blazor WebAssembly β the whole framework and your C# UI execute in the browser:
dotnet run --project sample/SampleApp.Web # β http://localhost:5000One Core, three interpreter families. The DSL, State<T>, Node, TreeDiffer, patch protocol, and
SwiftApp are shared verbatim across every backend. Only the leaf renderer differs: a native shim for the
compiler-plugin frameworks (SwiftUI via @_cdecl/P-Invoke, Compose via @JvmStatic/JNI β @Observable VNode β
mutableStateOf VNode β bound EventCallback), or a pure-C# interpreter for the bindable ones (GTK
widgets, WinUI controls, Blazor DOM), all applying the identical diff patches.
Consuming the library: reference the combined
SwiftDotNetpackage. For the Apple targets, also add<Import Project="β¦/SwiftDotNet/SwiftDotNetBridge.targets" />to your app's.csprojβ required becauseNativeReferenceitems don't flow transitively into the app's native link. GTK and Web are plain project references (SwiftDotNet.Gtk/SwiftDotNet.Web); no import needed.
The closest prior art is .NET Comet β James Clancey's SwiftUI-inspired
C# UI toolkit. The authoring surface looks similar (both give you Text(...).Font(...), VStack,
State<T>, a recomputed body), but the substrate is fundamentally different:
Comet renders through .NET MAUI's handler abstraction. SwiftDotNet bypasses MAUI and renders to each platform's own toolkit directly β including the modern declarative ones (SwiftUI, Jetpack Compose) that MAUI predates and doesn't use.
| .NET Comet | SwiftDotNet | |
|---|---|---|
| Rendering substrate | .NET MAUI handlers (implements Microsoft.Maui.IButton etc.); MAUI maps to native |
The platform's own toolkit, directly |
| iOS output | UIKit via MAUI handler | Real SwiftUI |
| Android output | Android Views via MAUI handler | Real Jetpack Compose |
| macOS | Mac Catalyst (iOS-on-Mac) | Native AppKit-hosted SwiftUI |
| Update mechanism | MVU over MAUI's in-process object graph | Structural-path diff engine β JSON patch β native @Observable/mutableStateOf VNode tree across a C-ABI/JNI bridge |
| Dependencies | The entire MAUI stack | Core is dependency-free platform-neutral C#; each backend pulls only its toolkit (GTK/WinUI/Web are pure C#, no shim) |
| Platform reach | Wherever MAUI runs: Win, Android, iOS, macOS (Catalyst), Blazor | iOS, tvOS, native macOS/AppKit, Android, Linux/GTK, Windows/WinUI, Web/DOM |
| Status | Archived July 11, 2025 β "a proof of conceptβ¦ no official support", read-only | Active, early-stage |
Why the substrate choice matters. Comet's bet was to lean on MAUI's abstraction and inherit its platforms for free β the cost being MAUI's control model and its lowest-common-denominator handler layer, and no access to the platforms' modern declarative frameworks (MAUI itself doesn't render through them). SwiftDotNet takes the opposite bet: render as the real native declarative toolkit on each platform, so on iOS you get Apple's own SwiftUI layout/animation/accessibility rather than a UIKit approximation. The price is that SwiftUI and Compose are compiler-plugin-locked, which is exactly why those two backends need a thin Swift/Kotlin shim plus the diff-over-a-bridge machinery β the part Comet never needs because it stays inside MAUI's .NET process. It also shows up in the architecture: SwiftDotNet has two backend routes (native-shim hosts for the compiler-locked toolkits, pure-C# interpreters for the bindable ones), where Comet has one route β MAUI handlers β for everything.
(Both are experimental. The distinction is that Comet is archived; SwiftDotNet is still a live design space β
which is why the DI, native-view-access, and per-view-reconciliation questions in plans/ are open.)
iOS (SwiftUI):
# 1. Build the Swift bridge (iOS/tvOS/macOS slices, min iOS 17)
native/SwiftDotNetBridge/build-xcframework.sh
# 2. Build the sample app for the simulator
dotnet build sample/SampleApp/SampleApp.csproj -f net10.0-ios -r iossimulator-arm64
# 3. Install + launch
xcrun simctl install booted sample/SampleApp/bin/Debug/net10.0-ios/iossimulator-arm64/SampleApp.app
xcrun simctl launch booted com.swiftdotnet.sampleOther platforms β the same sample/SampleApp project, selected by -f:
# macOS / tvOS β reuse the same xcframework from step 1
dotnet build sample/SampleApp -f net10.0-macos
dotnet build sample/SampleApp -f net10.0-tvos
# Android (Compose) β build the .aar first, then the app
native/SwiftDotNetComposeBridge/gradlew -p native/SwiftDotNetComposeBridge assembleRelease
dotnet build sample/SampleApp -f net10.0-android
# Windows (WinUI 3) β on a Windows machine
dotnet run --project sample/SampleApp -f net10.0-windows10.0.19041.0
# Linux/GTK β separate project; needs GTK4 (brew install gtk4 / apt install libgtk-4-1)
dotnet run --project sample/SampleApp.Gtk
# Linux/Wayland β self-drawn Skia on a raw Wayland surface, no GTK.
# Needs a Wayland session + libwayland-client + libxkbcommon. Clone maui-wayland beside this repo.
dotnet run --project sample/SampleApp.Wayland
# Web (Blazor WASM) β separate project; runs the whole framework in the browser
dotnet run --project sample/SampleApp.Web # β http://localhost:5000The same 5-tab ContentView (one file in sample/SharedUI) has been verified rendering natively on six
platforms; Windows is scaffolded pending a Windows build host:
| Platform | Verified on | Notes |
|---|---|---|
| iOS | iPhone Air / iOS 26.5 (simulator) | Real SwiftUI |
| macOS | Desktop (NSWindow + NSHostingController) |
SwiftUI via AppKit hosting |
| tvOS | Apple TV 4K (simulator) | Focus-driven; #if os(tvOS) fallbacks for controls Apple omits |
| Android | Emulator | Real Jetpack Compose |
| Linux | GTK4 desktop | 325 real Gtk.Widgets, pure C# |
| Web | Chrome (Blazor WASM) | Real HTML/DOM, pure C# |
| Windows | β | WinUI 3 backend scaffolded, not yet compiled |
| Linux (Wayland) | β | Self-drawn Wayland backend builds clean; never run against a compositor |
The demo is organized MAUI-Shell-style β a flyout menu (a grouped Form inside a NavigationStack)
whose rows push detail pages β and exercises the whole vocabulary across seven sections:
- Controls: Text & Input (TextField, SecureField, Toggle), Values & Steppers (Slider, Stepper, Picker,
DatePicker, ColorPicker), Rating (composite
Rating+.ScaleEffect). - Interaction: Gestures (double-tap / long-press / swipe), Animation (spring height + opacity).
- Layout: Shapes & Grid, Stacks & Alignment (SF Symbol Images/Label), Cards & Borders.
- Media: Carousel (paged
TabViewwith page dots), Indicators (ProgressView, Gauge), WebView, Maps. - Data: Lists & Selection (
List+ selection), Disclosure & Menus (DisclosureGroup + Menu). - Styling: Global Styles (environment cascade, reusable bundles, ambient control style, Theme).
- Navigation: Sheets & Alerts β NavigationLink (push verified), Link, Sheet, Alert (multi-button), ActionSheet.
- Interactions confirmed live on each backend: flyout navigation push/pop, Menu action, and
Button/TextField/Toggle/Slider bindings β including the full event β C#
Stateβ re-render round-trip (e.g. tapping a star updates the compositeRatingto "5/5" in the browser). - Diff engine + bool/text/value bindings verified deterministically via a Core test harness.
- P/Invoke resolves the bridge via
DllImport("__Internal")β the framework is a load-time dependency, so its@_cdeclsymbols are in the global namespace (a leaf-namedlopenignores@rpath). @Observablerequires iOS 17+.- JSON is hand-rolled (
NodeJson) β zero reflection, trim/AOT-safe (no IL2026).
- Compile + verify the Windows/WinUI 3 backend on a Windows host (expect minor WinUI API fixes).
- Per-view local state ownership (child composite views keep local state across renders β view-instance reconciliation).
- Binary bridge protocol (replace JSON on the hot path); physical-device runs on iOS/Android.
- Keyed
ForEachfor animated list insert/remove/move. - Publish the combined
SwiftDotNet+SwiftDotNet.Gtk+SwiftDotNet.Webas NuGet packages.