Skip to main content

swift_org_tutorial/
swift_org_tutorial.rs

1//! The SwiftUI tutorial from swift.org, built from Rust.
2//!
3//! <https://www.swift.org/getting-started/swiftui/>
4//!
5//! The Swift it reproduces, in full:
6//!
7//! ```swift
8//! struct ContentView: View {
9//!     var activities = ["Archery", "Baseball", …]
10//!     var colors: [Color] = [.blue, .cyan, …]
11//!     @State private var selected = "Baseball"
12//!     @State private var id = 1
13//!
14//!     var body: some View {
15//!         VStack {
16//!             Text("Why not try…")
17//!                 .font(.largeTitle.bold())
18//!             VStack {
19//!                 Circle()
20//!                     .fill(colors.randomElement() ?? .blue)
21//!                     .padding()
22//!                     .overlay(
23//!                         Image(systemName: "figure.\(selected.lowercased())")
24//!                             .font(.system(size: 144))
25//!                             .foregroundColor(.white)
26//!                     )
27//!                 Text("\(selected)!")
28//!                     .font(.title)
29//!             }
30//!             .transition(.slide)
31//!             .id(id)
32//!             Spacer()
33//!             Button("Try again") {
34//!                 withAnimation(.easeInOut(duration: 1)) {
35//!                     selected = activities.randomElement() ?? "Archery"
36//!                     id += 1
37//!                 }
38//!             }
39//!             .buttonStyle(.borderedProminent)
40//!         }
41//!     }
42//! }
43//! ```
44//!
45//! Every line of that has a counterpart below, and nothing is approximated: the
46//! `@State` is real `@State` on a type synthesised at run time, the transition
47//! is `AnyTransition.slide`, and the button's action runs
48//! `withAnimation(.easeInOut(duration: 1))` through SwiftUI's own entry point.
49//!
50//! ```sh
51//! cargo run -p swiftui-native --example swift_org_tutorial            # macOS
52//! cargo build -p swiftui-native --example swift_org_tutorial \
53//!     --target aarch64-apple-ios-sim                                  # iOS
54//! ```
55
56use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering};
57use swiftui_native::prelude::*;
58use swiftui_native::state;
59use swiftui_native::views::{ButtonStyle, TextStyle};
60
61// ── The model ───────────────────────────────────────────────────────────────
62//
63// `var activities` and `var colors` are stored properties of the Swift struct.
64// Here they are plain Rust data, which is the point: the state lives in Rust
65// and SwiftUI reads it when it evaluates the body.
66
67const ACTIVITIES: &[&str] = &[
68    "Archery", "Baseball", "Basketball", "Bowling", "Boxing", "Cricket", "Curling", "Fencing",
69    "Golf", "Hiking", "Lacrosse", "Rugby", "Squash",
70];
71
72/// `var colors: [Color] = [.blue, .cyan, .gray, .green, .indigo, .mint,
73/// .orange, .pink, .purple, .red]`.
74///
75/// SwiftUI's own colours, asked for by name. They are not fixed triples —
76/// `.blue` differs between macOS and iOS and again in dark appearance — so
77/// naming them is both shorter and more correct than spelling out components.
78const COLORS: &[SystemColor] = &[
79    SystemColor::Blue,
80    SystemColor::Cyan,
81    SystemColor::Gray,
82    SystemColor::Green,
83    SystemColor::Indigo,
84    SystemColor::Mint,
85    SystemColor::Orange,
86    SystemColor::Pink,
87    SystemColor::Purple,
88    SystemColor::Red,
89];
90
91// ── `@State private var selected` and `@State private var id` ───────────────
92//
93// Two pieces of state. `id` is the one SwiftUI tracks — bumping it is what
94// re-runs the body and what `.id()` keys the transition on. `selected` is
95// Rust's, read back every time the body runs.
96
97/// `@State private var selected = "Baseball"` — held as an index into
98/// [`ACTIVITIES`] so it is one word to write.
99static SELECTED: AtomicUsize = AtomicUsize::new(1);
100
101/// `@State private var id = 1`.
102static ID: AtomicI64 = AtomicI64::new(1);
103
104/// Which colour the circle is drawn in. `colors.randomElement()` is evaluated
105/// every time the body runs, so this changes with the rest.
106static COLOR: AtomicUsize = AtomicUsize::new(0);
107
108fn selected() -> &'static str {
109    ACTIVITIES[SELECTED.load(Ordering::Relaxed) % ACTIVITIES.len()]
110}
111
112/// Swift's `randomElement()`. A tiny xorshift rather than a dependency — the
113/// tutorial only needs "a different one than last time".
114fn random(upper: usize) -> usize {
115    static SEED: AtomicUsize = AtomicUsize::new(0x2545_F491_4F6C_DD1D);
116    let mut x = SEED.load(Ordering::Relaxed);
117    x ^= x << 13;
118    x ^= x >> 7;
119    x ^= x << 17;
120    SEED.store(x, Ordering::Relaxed);
121    x % upper
122}
123
124// ── `var body: some View` ───────────────────────────────────────────────────
125//
126// Read this against the Swift at the top of the file. The shape of it is the
127// same: a value, then modifiers on it, nested the way the braces nest.
128
129fn body() -> View {
130    let colour = COLORS[COLOR.load(Ordering::Relaxed) % COLORS.len()];
131
132    vstack![
133        // Text("Why not try…").font(.largeTitle.bold())
134        text("Why not try…").text_style(TextStyle::LargeTitle).bold(),
135        vstack![
136            // Circle().fill(colors.randomElement() ?? .blue).padding().overlay(…)
137            circle_shape().fill_system(colour).padding(16.0).overlay(
138                // Image(systemName: "figure.\(selected.lowercased())")
139                //     .font(.system(size: 144)).foregroundColor(.white)
140                image!("figure.{}", selected().to_lowercase())
141                    .font_size(144.0)
142                    .fg(Color::WHITE),
143            ),
144            // Text("\(selected)!").font(.title)
145            text!("{}!", selected()).text_style(TextStyle::Title),
146        ]
147        .transition(Transition::Slide)
148        .id(ID.load(Ordering::Relaxed)),
149        spacer(),
150        // Button("Try again") { … }.buttonStyle(.borderedProminent)
151        button("Try again", try_again).button_style(ButtonStyle::BorderedProminent),
152    ]
153}
154
155/// The button's action:
156///
157/// ```swift
158/// withAnimation(.easeInOut(duration: 1)) {
159///     selected = activities.randomElement() ?? "Archery"
160///     id += 1
161/// }
162/// ```
163fn try_again() {
164    SELECTED.store(random(ACTIVITIES.len()), Ordering::Relaxed);
165    COLOR.store(random(COLORS.len()), Ordering::Relaxed);
166    ID.fetch_add(1, Ordering::Relaxed);
167    // Writing the `@State` inside `withAnimation` is what makes SwiftUI
168    // interpolate: the body re-runs, `.id()` sees a new identity, and the old
169    // card slides out while the new one slides in.
170    state::refresh_animated(state::ease_in_out(1.0));
171}
172
173/// `ContentView` itself: a Swift type with real `@State`, synthesised at run
174/// time, whose body is the Rust function above.
175fn content_view() -> swiftui_native::ViewHandle {
176    state::root_view(|_generation| body().into_handle())
177}
178
179/// Laying the bundle out and putting it on a simulator or a device.
180#[cfg(target_os = "macos")]
181#[path = "common/simulator.rs"]
182mod simulator;
183
184#[cfg(target_os = "macos")]
185fn main() {
186    // `--sim` and `--device` build for iOS and run it there; see `common/simulator.rs`.
187    if std::env::args().any(|a| a == "--sim" || a == "device" || a == "ipa") {
188        return simulator::main(&simulator::Bundle {
189            example: "swift_org_tutorial",
190            bundle_id: "dev.rswift.WhyNotTry",
191            executable: "WhyNotTry",
192            display_name: "Why not try…",
193            launch_args: &[],
194        });
195    }
196    // `--render` draws one frame to a PNG instead of opening a window, so the
197    // result can be looked at without a display.
198    if std::env::args().any(|a| a == "--render") {
199        let out = concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/tutorial.png");
200        let frame = swiftui_native::render::render(&content_view(), 420, 640);
201        frame.write_png(out);
202        eprintln!("wrote {out} ({} colours)", frame.distinct());
203        return;
204    }
205    // `WindowGroup { ContentView() }` — the same tree the tutorial's `App` hosts.
206    show_window(&content_view(), "Why not try…", 420.0, 640.0);
207}
208
209#[cfg(not(target_os = "macos"))]
210fn main() {
211    swiftui_native::app::run(content_view)
212}