Skip to main content

settings/
settings.rs

1//! A settings screen, written the way someone would actually write one.
2//!
3//! Not a feature list — a screen with state that has to survive being edited
4//! from both sides. Every control here is a *bound* form, so what the user does
5//! lands in Rust and what Rust does shows on screen.
6//!
7//! ```sh
8//! cargo run -p swiftui-native --example settings           # macOS
9//! cargo run -p swiftui-native --example settings -- --sim  # iOS simulator
10//! ```
11
12use std::sync::OnceLock;
13use swiftui_native::prelude::*;
14use swiftui_native::{state, views};
15
16/// The whole of the app's state, on the Rust side.
17///
18/// `BoundState` is what a SwiftUI `Binding` reads and writes, so these are not
19/// copies kept in step — they are the storage the controls edit.
20struct Settings {
21    name: views::BoundState<String>,
22    notify: views::BoundState<bool>,
23    volume: views::BoundState<f64>,
24    theme: views::BoundState<i64>,
25    refresh: views::BoundState<f64>,
26    about: views::BoundState<bool>,
27}
28
29fn settings() -> &'static Settings {
30    static S: OnceLock<Settings> = OnceLock::new();
31    S.get_or_init(|| Settings {
32        name: views::BoundState::new("Ada".into()),
33        notify: views::BoundState::new(true),
34        volume: views::BoundState::new(0.6),
35        theme: views::BoundState::new(0),
36        refresh: views::BoundState::new(5.0),
37        about: views::BoundState::new(false),
38    })
39}
40
41const THEMES: [&str; 3] = ["System", "Light", "Dark"];
42
43fn body() -> View {
44    let s = settings();
45
46    form![
47        section![header: text("Account");
48            hstack![
49                text("Name"),
50                spacer(),
51                textfield_bound("Your name", &s.name).text_field_style(
52                    views::TextFieldStyle::RoundedBorderTextFieldStyle,
53                ),
54            ],
55            toggle_bound("Notifications", &s.notify),
56        ],
57        section![header: text("Playback");
58            vstack_aligned(&[
59                hstack![
60                    text("Volume"),
61                    spacer(),
62                    text!("{:.0}%", s.volume.get() * 100.0).fg_system(SystemColor::Gray),
63                ],
64                slider_bound(&s.volume, 0.0, 1.0),
65            ], HorizontalAlignment::Leading),
66            stepper_bound("Refresh every", &s.refresh, 1.0, 60.0, 1.0),
67        ],
68        section![header: text("Appearance");
69            picker_bound!(
70                "Theme",
71                &s.theme,
72                text(THEMES[0]).tag_int(0),
73                text(THEMES[1]).tag_int(1),
74                text(THEMES[2]).tag_int(2),
75            )
76            .picker_style(PickerStyle::InlinePickerStyle),
77        ],
78        section![
79            button("About", show_about)
80                .a11y_hint("Opens a panel describing this app"),
81        ],
82    ]
83    .form_style(FormStyle::GroupedFormStyle)
84    .sheet_bound(&s.about, about_panel())
85}
86
87/// The sheet, which the user can dismiss — the flag is Rust's, so closing it
88/// writes `false` back rather than leaving the two sides disagreeing.
89fn about_panel() -> View {
90    let s = settings();
91    vstack_spacing(
92        &[
93            system_image("gearshape.fill")
94                .symbol_rendering_mode(Some(views::SymbolRenderingMode::Hierarchical))
95                .view()
96                .font_size(48.0)
97                .fg_system(SystemColor::Blue),
98            text("Settings").text_style(TextStyle::Title).bold(),
99            text!("Signed in as {}", s.name.get()).fg_system(SystemColor::Gray),
100            spacer(),
101            button("Done", hide_about).button_style(views::ButtonStyle::BorderedProminent),
102        ],
103        16.0,
104    )
105    .padding(24.0)
106    .presentation_detents(&[views::PresentationDetent::Medium])
107    .presentation_drag_indicator(Visibility::Visible)
108}
109
110fn show_about() {
111    settings().about.set(true);
112    state::refresh();
113}
114
115fn hide_about() {
116    settings().about.set(false);
117    state::refresh();
118}
119
120fn content_view() -> swiftui_native::ViewHandle {
121    state::root_view(|_| body().into_handle())
122}
123
124#[cfg(target_os = "macos")]
125#[path = "common/simulator.rs"]
126mod simulator;
127
128#[cfg(target_os = "macos")]
129fn main() {
130    if std::env::args().any(|a| a == "--sim" || a == "device") {
131        return simulator::main(&simulator::Bundle {
132            example: "settings",
133            bundle_id: "dev.rswift.Settings",
134            executable: "Settings",
135            display_name: "Settings",
136            launch_args: &[],
137        });
138    }
139    if std::env::args().any(|a| a == "--render") {
140        let out = concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/settings.png");
141        swiftui_native::render::render(&content_view(), 420, 640).write_png(out);
142        eprintln!("wrote {out}");
143        return;
144    }
145    show_window(&content_view(), "Settings", 420.0, 640.0);
146}
147
148#[cfg(not(target_os = "macos"))]
149fn main() {
150    swiftui_native::app::run(content_view)
151}