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}