Expand description
§SwiftUI Native — pure Rust, zero Swift source files
Constructs SwiftUI views entirely from Rust using:
dlsymto resolve symbols fromSwiftUI.framework- arm64 inline assembly for Swift calling convention
swift-runtime-sysfor retain/release and string creation
┌────────────┐ dlsym ┌──────────────────────┐
│ Rust code │ ──────────► │ SwiftUI.framework │
│ (arm64 │ Swift CC │ (Text, Color, etc.) │
│ asm) │ ──────────► │ │
└────────────┘ └──────────────────────┘§Quick start
use swiftui_native::{prelude::*, swiftui};
run(|| swiftui! {
VStack(spacing: 8.0) {
Text("Hello from pure Rust!").text_style(.large_title)
Text("No Swift source files, no bridging header.")
}
.padding(20.0)
});swiftui! is SwiftUI’s own syntax; the same tree written as calls is
vstack_spacing(&[…], Some(8.0)).padding(20.0), and views underneath that
is the layer where every argument is explicit. All three are the same thing —
pick the one that suits the code around it.
§Platform
arm64 macOS and iOS, device or simulator. Requires the Swift 6.3+ runtime.
Almost none of this is platform-specific: SwiftUI’s mangled symbols and the
Swift calling convention are the same everywhere, so views, abi,
state, view and the rest are shared verbatim. What differs is the
host UI framework SwiftUI draws through, and it differs in three places:
hosting— AppKit hasNSHostingView, a view. UIKit has onlyUIHostingController, so there the view comes from.viewand the SwiftUI side lives on the controller.render—cacheDisplayInRect:has no UIKit counterpart, so iOS rasterises the layer into a CoreGraphics bitmap instead. The window, sheet, popover and compositor captures are AppKit-only.window— anNSWindowtakes a content view and has a title bar; aUIWindowtakes a root view controller and has neither.
§Conforming to SwiftUI’s protocols, from Rust
Several of SwiftUI’s protocols exist to be implemented by the caller:
Layout, ButtonStyle, ViewModifier, Shape, Transition,
CustomAnimation, NSViewRepresentable, and the three keyed bags —
EnvironmentKey, FocusedValueKey, ContainerValueKey. A Swift program writes a struct and
the compiler emits a witness table; there is no compiler here, so the witness
table is built at run time and registered with
swift_registerProtocolConformances — after which SwiftUI
finds it by the same lookup it uses for its own.
What lands in Rust is a closure. views::shape_fn is handed the rect and
draws a path; views::ns_view_fn makes an NSView through the
Objective-C runtime and SwiftUI hosts it; views::animation_fn is handed a time and answers with a
fraction, which is then applied to a value whose type Rust never learns, by
calling VectorArithmetic.scale(by:) through that type’s own witness table.
Two things make this harder than it sounds, and both are written up where
they bite: every such callback uses the witness-method convention, which no
extern "C" signature can express (see views::modifier_fn’s module); and Shape
requires View, whose Body is a type that cannot be built until Shape
exists (see views::shape_fn’s module).
§Observation
@Observable works, and the thing that made it look impossible was wrong.
Observation.Observable is a marker protocol with zero requirements, and
ObservationRegistrar and withObservationTracking are fully exported — so
the only real obstacle was that an @Observable model is a class and this
crate mints structs.
It does not have to be a Swift class. Observable requires AnyObject, an
NSObject subclass is one, and objc::ClassBuilder mints those. The piece
that was genuinely missing was a conformance record whose type reference is a
class rather than a nominal descriptor — IndirectObjCClass, one of the four
kinds the format already has, which conformance::Conformer now emits.
What that buys is in observable: a model owned by Rust that SwiftUI
re-renders on, without a @State write anywhere. state::StatefulView is
still there for view-local state; this is for a model that outlives the view.
§Declaring your own keys and models
An environment key, a container value key, a focused value key and an
@Observable model are all things Swift declares with a keyword and this
crate synthesises at run time. Each is a static — one name is one
conformance and one key path — so there is a macro per kind that writes the
static for you:
environment_keys! {
/// How deeply this row is nested.
depth: i64 = 0,
/// What to greet the reader with.
greeting: String = "hello",
}
observable! {
/// Everything the app's windows share.
pub AppModel {
count: i64 = 0,
name: String = "Ada",
}
}
run(|| {
let model = AppModel::shared();
environment_reader(move |env| {
format!("{}, {} — {} deep", env.value_for(greeting()), model.name.get(), env.value_for(depth()))
})
.environment_value(depth(), 3)
.environment_model(model.as_model())
});Each property of a model has a key path of its own, so a body that read
count is rebuilt when count changes and left alone when name does. See
environment_keys!, container_keys!, focused_keys! and
observable!.
§Known limitations
Closure contexts are leaked, deliberately. A @ViewBuilder body that
SwiftUI stores — a sheet’s content, an alert’s actions, a context menu’s
items — is called long after the binding that created it has returned, so
its context and a copy of its child are leaked rather than freed. Building
such a view once per process costs nothing; rebuilding one every frame
leaks every frame. The non-escaping cases (a stack’s content, which the
initialiser calls before it returns) borrow instead and leak nothing.
examples/parity’s rebuild_is_bounded check measures the non-escaping side:
it builds a stack of eleven children a hundred times, divides the growth in
resident memory by a hundred, and fails above 4096 bytes per rebuild. The
escaping cases are not bounded by anything, which is the point of naming them
here.
ViewHandle::as_ptr mints a new AnyView each call. It hands back a
+1 box for a callee to consume. Two calls on the same handle return two
different boxes, and a caller that takes one without handing it to Swift
has leaked it.
A @ViewBuilder closure’s result type is fixed when the view is built.
GeometryReader and EnvironmentReader take a Rust closure run per layout,
so there is no type to name when the reader is constructed and its content
is erased to AnyView. Swift’s equivalent is fixed by the compiler, so the
two build different — equally valid — types there. Everything else builds
the same type Swift does, which examples/parity checks case by case.
A Tab’s content is boxed, and the whole TabContent chain with it.
A Tab is a TabContent rather than a View, so what holds it is a
TabView<_, Content: TabContent>, and every link — the tab, the
TupleTabContent over several of them, the TabView around those — names
Tab<Never, AnyView, DefaultTabLabel> in its own type. Typing one link and
not the rest describes a value of one layout to code reading another; moving
them together is a TabContent composition of its own. Everything else that
knows its children when it is built keeps their types — Grid, GridRow,
Section in all four forms, both NavigationSplitView shapes, Picker,
Gauge, ViewThatFits, Menu, DisclosureGroup, List, TabView,
NavigationStack, GroupBox, Group, ControlGroup, Form, ForEach,
TimelineView, Toggle and the stacks — which
views::substitute_args composes per content rather than naming a
fixed instantiation.
All of these are verified the same way, against a Swift reference built for
the same platform: examples/parity on macOS, examples/ios_parity in a simulator.
examples/ios_app runs a SwiftUI application on iOS through either entry
point — a runtime-synthesised App conformance, or UIApplicationMain with
a delegate class built through the Objective-C runtime.
Re-exports§
pub use color::Color;pub use hosting::HostingView;pub use render::Rendered;pub use view::View;pub use window::Window;pub use views::ViewHandle;
Modules§
- abi
- Driving the Swift runtime directly: the ABI this crate speaks.
- app
- SwiftUI App lifecycle — pure Rust, no Swift compiler.
- asyncfn
- Swift
asyncclosures, supplied from Rust. - cg
- CoreGraphics — turning a
CGImageinto bytes, and bytes into a PNG. - color
- Color constants and helpers.
- conformance
- Synthesising Swift protocol conformances at run time.
- defaults
UserDefaults, through the Objective-C runtime.- diag
- Diagnostics — human-readable Swift type names for errors and debug output.
- dot
- Leading-dot syntax, resolved by Rust’s type system.
- existential
any Viewexistential container — pure Rust, no Swift code.- hosting
- The bridge that puts a SwiftUI view inside a native view.
- keypath
WritableKeyPathbuilt at run time, without a compiler.- macros
- Declarative macros for building view trees.
- mangle
- Swift type manglings, built rather than counted.
- metal
- Metal shader effects —
colorEffect,layerEffectanddistortionEffect. - objc
- Minimal Objective-C runtime bridge.
- observable
@Observablefrom Rust — a model SwiftUI re-renders on.- prelude
- Everything you need for a native SwiftUI app.
- render
- Deterministic offscreen rendering — the basis of the visual parity tests.
- resolve
- Symbol resolution from SwiftUI.framework — pure dlsym, no Swift code.
- runtime
- The Swift runtime’s own entry points, made safe to call.
- scene
- Scenes and commands — what an app is, above its views.
- state
- Runtime synthesis of a Swift view type with real
@Statestorage. A Swift view type synthesised at runtime, with real@Statestorage. - view
View— chainable modifier API for ergonomic view construction.- views
- Building SwiftUI views from Rust, with no Swift source anywhere.
- window
- Window hosting — pure Rust.
Macros§
- container_
keys - Declare container-value keys of your own — as
environment_keys!, forContainerValues. - control_
group ControlGroup { … }- environment_
keys - Declare environment keys of your own.
- focused_
keys - Declare focused-value keys of your own — as
environment_keys!, forFocusedValues. - foreach
ForEachover a range or a collection.- form
Form { … }over several rows.- grid
- Build a
Gridfrom a list of rows. - grid_
row - Build a
GridRowfrom a list of cells. - group
Group { … }— several views where one is wanted, with no layout of its own.- hstack
- Build an
HStackfrom a list of children — seevstack!. - image
Image(systemName: "…")with interpolation — seetext!.- laid_
out - A Rust layout, given children —
MyLayout { … }in Swift. - lazy_
hstack - Build a
LazyHStackfrom a list of children. - lazy_
vstack - Build a
LazyVStackfrom a list of children. - list
List { … }over several rows.- ltext
swiftui! { … }— SwiftUI’s declarative syntax, lowered to this crate’s DSL.- menu
Menu(_:) { … }over several items.- observable
- Declare an
@Observablemodel — the properties, not the plumbing. - picker
Picker(_:selection:)over rows with a fixed selection — seepicker_bound!for the one the user can change.- picker_
bound Picker(_:selection:)over rows, bound to Rust state.- scenes
- An app’s
body, written the way Swift writes it. - section
Section { … }, optionally with a header:section![header: text("Title"); …].- stateful
swiftui! { … }— SwiftUI’s declarative syntax, lowered to this crate’s DSL.- swiftui
swiftui! { … }— SwiftUI’s declarative syntax, lowered to this crate’s DSL.- tab_
view TabView { … }over several tabs.- tabs
TabView { Tab(…); … }over severaltabs.- text
Text("…")with interpolation, the way Swift writesText("\(name)!").- type_
by_ name - Resolve a Swift type from its mangled name string.
- view_if
- A
ViewBuilder-style conditional that keeps each branch’s identity. - vstack
- Build a
VStackfrom a list of children. - zstack
- Build a
ZStackfrom a list of children, back to front.