Skip to main content

Crate swiftui_native

Crate swiftui_native 

Source
Expand description

§SwiftUI Native — pure Rust, zero Swift source files

Constructs SwiftUI views entirely from Rust using:

  • dlsym to resolve symbols from SwiftUI.framework
  • arm64 inline assembly for Swift calling convention
  • swift-runtime-sys for 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 has NSHostingView, a view. UIKit has only UIHostingController, so there the view comes from .view and 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 — an NSWindow takes a content view and has a title bar; a UIWindow takes 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 async closures, supplied from Rust.
cg
CoreGraphics — turning a CGImage into 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 View existential container — pure Rust, no Swift code.
hosting
The bridge that puts a SwiftUI view inside a native view.
keypath
WritableKeyPath built 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, layerEffect and distortionEffect.
objc
Minimal Objective-C runtime bridge.
observable
@Observable from 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 @State storage. A Swift view type synthesised at runtime, with real @State storage.
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!, for ContainerValues.
control_group
ControlGroup { … }
environment_keys
Declare environment keys of your own.
focused_keys
Declare focused-value keys of your own — as environment_keys!, for FocusedValues.
foreach
ForEach over a range or a collection.
form
Form { … } over several rows.
grid
Build a Grid from a list of rows.
grid_row
Build a GridRow from a list of cells.
group
Group { … } — several views where one is wanted, with no layout of its own.
hstack
Build an HStack from a list of children — see vstack!.
image
Image(systemName: "…") with interpolation — see text!.
laid_out
A Rust layout, given children — MyLayout { … } in Swift.
lazy_hstack
Build a LazyHStack from a list of children.
lazy_vstack
Build a LazyVStack from 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 @Observable model — the properties, not the plumbing.
picker
Picker(_:selection:) over rows with a fixed selection — see picker_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 several tabs.
text
Text("…") with interpolation, the way Swift writes Text("\(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 VStack from a list of children.
zstack
Build a ZStack from a list of children, back to front.