swiftui-native 0.0.1

SwiftUI from pure Rust — no Swift bridge, no shim, only swift-runtime calls
Documentation
//! A SwiftUI application on iOS, driven entirely from Rust.
//!
//! Two entry points, both pure runtime work and neither involving a line of
//! Swift or a hand-written Objective-C class:
//!
//! * `app::run` builds a SwiftUI `App` conformance at runtime and calls
//!   `App.main()`, which is what a `@main struct App: App` compiles down to.
//! * `show_window` synthesises a `UIApplicationDelegate` class through the
//!   Objective-C runtime and hands its name to `UIApplicationMain`.
//!
//! Pass `window` as an argument to take the second path.
//!
//! ```sh
//! cargo build -p swiftui-native --example ios_app --target aarch64-apple-ios-sim
//! cargo run   -p swiftui-native --example ios_app          # App.main()
//! cargo run   -p swiftui-native --example ios_app -- window # UIApplicationMain
//! ```
//!
//! On a Mac this builds the `.app`, installs it in a booted simulator, launches
//! it and photographs the result.

// This example compiles for both the Mac it is driven from and the iOS target
// it runs on, and each half is dead on the other: the device entry points have
// no caller on macOS, and the Swift-reference plumbing has no `swiftc` on iOS.
// Deleting either to quiet the host build would break the other target.
#![allow(dead_code, unused_imports)]

use swiftui_native::{app, views, window, Color};

/// Counts taps, so the button has an observable effect.
static TAPS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);

/// What the button does: change Rust-owned state, then redraw from it.
///
/// This is the whole loop in one place. The tap arrives from SwiftUI, the value
/// it changes lives in Rust, and the redraw is Rust handing SwiftUI a freshly
/// built tree — SwiftUI cannot observe a Rust value by itself.
fn on_tap() {
    TAPS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
    let s = switch_state();
    s.set(!s.get());
    refresh();
}

/// The one piece of SwiftUI-owned state in the application.
///
/// Everything the interface shows lives in Rust. SwiftUI cannot observe a Rust
/// value, so redisplay needs something it *does* track: a real `@State` on a
/// type synthesised at run time. Bumping it re-runs the body, which rebuilds
/// the whole tree from whatever Rust now holds.
///
/// The view itself is main-thread only — it holds raw metadata pointers — but
/// its `id` is just a number, and that is all [`refresh`] needs.
fn root_view() -> views::ViewHandle {
    thread_local! {
        static ROOT: std::cell::OnceCell<swiftui_native::state::StatefulView> =
            const { std::cell::OnceCell::new() };
    }
    ROOT.with(|cell| {
        let sv = cell.get_or_init(|| {
            let sv = swiftui_native::state::StatefulView::new(0, |_generation| ui());
            ROOT_ID.store(sv.id, std::sync::atomic::Ordering::Release);
            sv
        });
        sv.as_view()
    })
}

static ROOT_ID: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(-1);

/// Tell SwiftUI that Rust state changed, so it re-runs the body.
fn refresh() {
    use std::sync::atomic::Ordering;
    static GENERATION: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(0);
    let id = ROOT_ID.load(Ordering::Acquire);
    if id >= 0 {
        let n = GENERATION.fetch_add(1, Ordering::Relaxed) + 1;
        swiftui_native::state::set_state(id, n);
    }
}

/// The slider's value, owned by Rust and written on every drag.
fn slider_state() -> &'static views::BoundState<f64> {
    static S: std::sync::OnceLock<views::BoundState<f64>> = std::sync::OnceLock::new();
    S.get_or_init(|| views::BoundState::new(0.65))
}

/// The stepper's value, owned by Rust.
fn stepper_state() -> &'static views::BoundState<f64> {
    static S: std::sync::OnceLock<views::BoundState<f64>> = std::sync::OnceLock::new();
    S.get_or_init(|| views::BoundState::new(3.0))
}

/// The toggle's state, owned by Rust and read back after every tap.
fn switch_state() -> &'static views::BoundState<bool> {
    static S: std::sync::OnceLock<views::BoundState<bool>> = std::sync::OnceLock::new();
    S.get_or_init(|| views::BoundState::new(false))
}

fn ui() -> views::ViewHandle {
    views::zstack(&[
        views::background_linear_gradient(
            &views::color(0.0, 0.0, 0.0, 0.0),
            &[Color::new(0.06, 0.07, 0.16, 1.0), Color::new(0.24, 0.07, 0.33, 1.0)],
            (0.0, 0.0),
            (1.0, 1.0),
        ),
        views::scroll(&views::padding(
            &views::vstack_spacing(
                &[
                    header(),
                    section("Typography", typography()),
                    section("Shapes", shapes()),
                    section("Controls", controls()),
                    section("Symbols", symbols()),
                    section("Effects", effects()),
                    section("Layout", layout()),
                ],
                18.0,
            ),
            16.0,
        )),
    ])
}

/// White text at a given size and weight — the showcase's workhorse.
fn label_text(s: &str, size: f64, weight: Option<views::Weight>, alpha: f64) -> views::ViewHandle {
    views::foreground_style_color(
        &views::font_system(&views::text(s), size, weight, None),
        1.0,
        1.0,
        1.0,
        alpha,
    )
}

fn header() -> views::ViewHandle {
    views::vstack_spacing(
        &[
            label_text("SwiftUI from Rust", 30.0, Some(views::Weight::Bold), 1.0),
            label_text(entry(), 13.0, None, 0.75),
            label_text(
                &format!("taps: {}", TAPS.load(std::sync::atomic::Ordering::Relaxed)),
                13.0,
                Some(views::Weight::Semibold),
                0.9,
            ),
        ],
        4.0,
    )
}

/// A titled group: a caption, then the content, over a rounded panel.
fn section(title: &str, content: views::ViewHandle) -> views::ViewHandle {
    views::vstack_spacing(
        &[
            label_text(title, 12.0, Some(views::Weight::Semibold), 0.55),
            views::padding(
                &views::corner_radius(
                    &views::background(&views::padding(&content, 12.0), 1.0, 1.0, 1.0, 0.07),
                    14.0,
                ),
                0.0,
            ),
        ],
        6.0,
    )
}

fn typography() -> views::ViewHandle {
    views::vstack_spacing(
        &[
            views::hstack_spacing(
                &[
                    views::font_text_style(&views::text("Title"), views::TextStyle::Title3),
                    views::italic(&views::text("Italic")),
                    views::monospaced(&views::text("mono")),
                ],
                14.0,
            ),
            views::kerning(&label_text("K E R N E D", 15.0, None, 0.9), 2.0),
            views::foreground_style_linear_gradient(
                &views::font_system(&views::text("Gradient fill"), 22.0, Some(views::Weight::Bold), None),
                &[
                    views::color_value_of(Color::new(1.0, 0.55, 0.25, 1.0)),
                    views::color_value_of(Color::new(1.0, 0.25, 0.6, 1.0)),
                ],
                (0.0, 0.0),
                (1.0, 0.0),
            ),
        ],
        10.0,
    )
}

fn shapes() -> views::ViewHandle {
    let swatch = |shape: views::Shape, c: Color| {
        views::frame(&shape.fill(views::color_value_of(c)), 58.0, 44.0)
    };
    views::hstack_spacing(
        &[
            swatch(views::Shape::circle(), Color::new(0.95, 0.35, 0.30, 1.0)),
            swatch(views::Shape::capsule(), Color::new(0.98, 0.70, 0.20, 1.0)),
            swatch(views::Shape::rounded_rectangle(10.0, true), Color::new(0.30, 0.65, 1.0, 1.0)),
            swatch(views::Shape::ellipse(), Color::new(0.35, 0.85, 0.55, 1.0)),
        ],
        10.0,
    )
}

fn controls() -> views::ViewHandle {
    views::vstack_spacing(
        &[
            // Two toggles, both starting `false`: one bound to Rust-owned
            // state, one a constant. If they ever disagree on screen, the
            // binding's getter is at fault rather than the control.
            views::toggle_bound("Bound to Rust state", switch_state()),
            views::toggle("Constant false", false),
            views::hstack_spacing(
                &[
                    label_text(&format!("{:.2}", slider_state().get()), 14.0, None, 0.9),
                    views::slider_bound(slider_state(), 0.0, 1.0),
                ],
                10.0,
            ),
            views::stepper_bound(
                &format!("Stepper: {:.0}", stepper_state().get()),
                stepper_state(),
                0.0,
                10.0,
                1.0,
            ),
            // Reads the stepper's Rust value. A `ProgressView` takes a plain
            // number rather than a binding, so it can only ever show what it
            // was built with — which is fine now that every write rebuilds the
            // body from current state.
            views::progress(stepper_state().get(), 10.0),
            views::hstack_spacing(
                &[
                    views::button_style(
                        &views::button("Bordered", on_tap),
                        views::ButtonStyle::Bordered,
                    ),
                    views::button_style(
                        &views::button("Glass", on_tap),
                        views::ButtonStyle::Glass,
                    ),
                ],
                12.0,
            ),
            views::textfield("A text field", ""),
        ],
        12.0,
    )
}

fn symbols() -> views::ViewHandle {
    let icon = |name: &str, c: Color| {
        views::foreground_style_color(
            &views::font_system(&views::system_image(name), 24.0, None, None),
            c.r, c.g, c.b, c.a,
        )
    };
    views::hstack_spacing(
        &[
            icon("star.fill", Color::new(1.0, 0.8, 0.2, 1.0)),
            icon("heart.fill", Color::new(1.0, 0.35, 0.45, 1.0)),
            icon("bolt.fill", Color::new(0.55, 0.8, 1.0, 1.0)),
            icon("cloud.sun.fill", Color::new(0.8, 0.85, 1.0, 1.0)),
            icon("checkmark.seal.fill", Color::new(0.4, 0.9, 0.6, 1.0)),
        ],
        16.0,
    )
}

fn effects() -> views::ViewHandle {
    let tile = |c: Color| views::frame(&views::color(c.r, c.g, c.b, c.a), 48.0, 40.0);
    views::hstack_spacing(
        &[
            views::blur(&tile(Color::new(0.95, 0.4, 0.3, 1.0)), 4.0),
            views::shadow(
                &tile(Color::new(0.3, 0.7, 1.0, 1.0)),
                Color::new(0.0, 0.0, 0.0, 0.8),
                8.0,
                0.0,
                4.0,
            ),
            views::opacity(&tile(Color::new(0.5, 0.9, 0.5, 1.0)), 0.35),
            views::corner_radius(&tile(Color::new(0.9, 0.7, 0.2, 1.0)), 12.0),
            views::border(&tile(Color::new(0.0, 0.0, 0.0, 0.0)), 1.0, 1.0, 1.0, 2.0),
        ],
        10.0,
    )
}

fn layout() -> views::ViewHandle {
    let chip = |s: &str, c: Color| {
        views::corner_radius(
            &views::background(&views::padding(&label_text(s, 13.0, None, 1.0), 8.0), c.r, c.g, c.b, c.a),
            8.0,
        )
    };
    views::vstack_spacing(
        &[
            views::hstack_spacing(
                &[
                    chip("HStack", Color::new(0.35, 0.35, 0.8, 0.8)),
                    views::spacer(),
                    chip("Spacer", Color::new(0.8, 0.35, 0.5, 0.8)),
                ],
                8.0,
            ),
            views::divider(),
            views::zstack(&[
                views::frame(
                    &views::Shape::rounded_rectangle(8.0, true)
                        .fill(views::color_value_of(Color::new(0.2, 0.5, 0.9, 0.55))),
                    120.0,
                    34.0,
                ),
                label_text("ZStack", 13.0, Some(views::Weight::Semibold), 1.0),
            ]),
        ],
        10.0,
    )
}

fn entry() -> &'static str {
    if uses_window() {
        "UIApplicationMain, synthesised delegate"
    } else {
        "App.main(), synthesised conformance"
    }
}

fn uses_window() -> bool {
    std::env::args().any(|a| a == "window")
}

// ═══════════════════════════════════════════════════════════════════════════
// The application — runs on the device
// ═══════════════════════════════════════════════════════════════════════════

#[cfg(not(target_os = "macos"))]
fn main() {
    // Every bound control's write asks Rust to redraw from the new value.
    views::on_write(refresh);
    // `app::run` is the real entry point, and the one `@main struct App: App`
    // compiles to. The IR for that is three lines:
    //
    //   define i32 @main(...)           { call swiftcc @"...$mainyyFZ"() }
    //   define swiftcc @"...$mainyyFZ"  { call @"$s7SwiftUI3AppPAAE4mainyyFZ"(
    //                                        A.Type, A: App witness, swiftself A.Type) }
    //
    // — no `UIApplicationMain`, no delegate, no `UIWindow`. `App.main()` builds
    // the scene, the window and the responder chain itself, which is why this
    // path gets an active application with a scene-attached window while the
    // hand-rolled one had to assemble that by hand.
    #[cfg(feature = "uikit-entry")]
    if uses_window() {
        // The same root as the `App.main()` path, so a comparison between the
        // two measures the entry point and not the tree underneath it.
        window::show_window(&root_view(), "RustUI", 0.0, 0.0);
    }
    app::run(root_view)
}

// ═══════════════════════════════════════════════════════════════════════════
// The driver — runs on the Mac
// ═══════════════════════════════════════════════════════════════════════════
//
// An iOS application is a directory with a binary and an `Info.plist` in it.
// Nothing here needs Xcode: the bundle is written out, handed to `simctl`, and
// the result photographed.

/// Laying the bundle out and putting it on a simulator or a device.
#[cfg(target_os = "macos")]
#[path = "common/simulator.rs"]
mod simulator;

#[cfg(target_os = "macos")]
fn main() {
    simulator::main(&simulator::Bundle {
        example: "ios_app",
        bundle_id: "dev.rswift.RustUI",
        executable: "RustUI",
        display_name: "SwiftUI from Rust",
        launch_args: if uses_window() { &["window"] } else { &[] },
    });
}