pub struct BoundState<T> { /* private fields */ }Expand description
Rust-side state a SwiftUI control can both read and write.
This is not @State. SwiftUI’s own property wrappers need the framework to
reach into a Swift type’s storage through reflection metadata, which a Rust
type does not have. What this gives instead is the half that matters from
Rust: a real Binding<Value> backed by Rust closures, so a control renders
from Rust state and writes edits straight back into it.
Redisplay is driven explicitly, because SwiftUI cannot observe a Rust value:
a Binding’s getter is read only when something else causes the body to be
evaluated. Register on_write to be told when a control writes, and
answer it by bumping a @State (crate::state::set_state) or by handing
the hosting controller a new tree (crate::HostingView::set_root).
Without that, an edit moves the control and updates the Rust value while a
Text showing that value stays as it was.
Implementations§
Source§impl<T: Clone> BoundState<T>
impl<T: Clone> BoundState<T>
Sourcepub fn new(initial: T) -> Self
pub fn new(initial: T) -> Self
Examples found in repository?
More examples
83fn slider_state() -> &'static views::BoundState<f64> {
84 static S: std::sync::OnceLock<views::BoundState<f64>> = std::sync::OnceLock::new();
85 S.get_or_init(|| views::BoundState::new(0.65))
86}
87
88/// The stepper's value, owned by Rust.
89fn stepper_state() -> &'static views::BoundState<f64> {
90 static S: std::sync::OnceLock<views::BoundState<f64>> = std::sync::OnceLock::new();
91 S.get_or_init(|| views::BoundState::new(3.0))
92}
93
94/// The toggle's state, owned by Rust and read back after every tap.
95fn switch_state() -> &'static views::BoundState<bool> {
96 static S: std::sync::OnceLock<views::BoundState<bool>> = std::sync::OnceLock::new();
97 S.get_or_init(|| views::BoundState::new(false))
98}436fn flag_slot(name: &str, initial: bool) -> views::BoundState<bool> {
437 slots()
438 .lock()
439 .expect("slots")
440 .flags
441 .entry(name.to_string())
442 .or_insert_with(|| views::BoundState::new(initial))
443 .clone()
444}
445
446fn number_slot(name: &str, initial: f64) -> views::BoundState<f64> {
447 slots()
448 .lock()
449 .expect("slots")
450 .numbers
451 .entry(name.to_string())
452 .or_insert_with(|| views::BoundState::new(initial))
453 .clone()
454}
455
456fn string_slot(name: &str, initial: &str) -> views::BoundState<String> {
457 slots()
458 .lock()
459 .expect("slots")
460 .strings
461 .entry(name.to_string())
462 .or_insert_with(|| views::BoundState::new(initial.to_string()))
463 .clone()
464}Sourcepub fn get(&self) -> T
pub fn get(&self) -> T
Examples found in repository?
38fn on_tap() {
39 TAPS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
40 let s = switch_state();
41 s.set(!s.get());
42 refresh();
43}
44
45/// The one piece of SwiftUI-owned state in the application.
46///
47/// Everything the interface shows lives in Rust. SwiftUI cannot observe a Rust
48/// value, so redisplay needs something it *does* track: a real `@State` on a
49/// type synthesised at run time. Bumping it re-runs the body, which rebuilds
50/// the whole tree from whatever Rust now holds.
51///
52/// The view itself is main-thread only — it holds raw metadata pointers — but
53/// its `id` is just a number, and that is all [`refresh`] needs.
54fn root_view() -> views::ViewHandle {
55 thread_local! {
56 static ROOT: std::cell::OnceCell<swiftui_native::state::StatefulView> =
57 const { std::cell::OnceCell::new() };
58 }
59 ROOT.with(|cell| {
60 let sv = cell.get_or_init(|| {
61 let sv = swiftui_native::state::StatefulView::new(0, |_generation| ui());
62 ROOT_ID.store(sv.id, std::sync::atomic::Ordering::Release);
63 sv
64 });
65 sv.as_view()
66 })
67}
68
69static ROOT_ID: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(-1);
70
71/// Tell SwiftUI that Rust state changed, so it re-runs the body.
72fn refresh() {
73 use std::sync::atomic::Ordering;
74 static GENERATION: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(0);
75 let id = ROOT_ID.load(Ordering::Acquire);
76 if id >= 0 {
77 let n = GENERATION.fetch_add(1, Ordering::Relaxed) + 1;
78 swiftui_native::state::set_state(id, n);
79 }
80}
81
82/// The slider's value, owned by Rust and written on every drag.
83fn slider_state() -> &'static views::BoundState<f64> {
84 static S: std::sync::OnceLock<views::BoundState<f64>> = std::sync::OnceLock::new();
85 S.get_or_init(|| views::BoundState::new(0.65))
86}
87
88/// The stepper's value, owned by Rust.
89fn stepper_state() -> &'static views::BoundState<f64> {
90 static S: std::sync::OnceLock<views::BoundState<f64>> = std::sync::OnceLock::new();
91 S.get_or_init(|| views::BoundState::new(3.0))
92}
93
94/// The toggle's state, owned by Rust and read back after every tap.
95fn switch_state() -> &'static views::BoundState<bool> {
96 static S: std::sync::OnceLock<views::BoundState<bool>> = std::sync::OnceLock::new();
97 S.get_or_init(|| views::BoundState::new(false))
98}
99
100fn ui() -> views::ViewHandle {
101 views::zstack(&[
102 views::background_linear_gradient(
103 &views::color(0.0, 0.0, 0.0, 0.0),
104 &[Color::new(0.06, 0.07, 0.16, 1.0), Color::new(0.24, 0.07, 0.33, 1.0)],
105 (0.0, 0.0),
106 (1.0, 1.0),
107 ),
108 views::scroll(&views::padding(
109 &views::vstack_spacing(
110 &[
111 header(),
112 section("Typography", typography()),
113 section("Shapes", shapes()),
114 section("Controls", controls()),
115 section("Symbols", symbols()),
116 section("Effects", effects()),
117 section("Layout", layout()),
118 ],
119 18.0,
120 ),
121 16.0,
122 )),
123 ])
124}
125
126/// White text at a given size and weight — the showcase's workhorse.
127fn label_text(s: &str, size: f64, weight: Option<views::Weight>, alpha: f64) -> views::ViewHandle {
128 views::foreground_style_color(
129 &views::font_system(&views::text(s), size, weight, None),
130 1.0,
131 1.0,
132 1.0,
133 alpha,
134 )
135}
136
137fn header() -> views::ViewHandle {
138 views::vstack_spacing(
139 &[
140 label_text("SwiftUI from Rust", 30.0, Some(views::Weight::Bold), 1.0),
141 label_text(entry(), 13.0, None, 0.75),
142 label_text(
143 &format!("taps: {}", TAPS.load(std::sync::atomic::Ordering::Relaxed)),
144 13.0,
145 Some(views::Weight::Semibold),
146 0.9,
147 ),
148 ],
149 4.0,
150 )
151}
152
153/// A titled group: a caption, then the content, over a rounded panel.
154fn section(title: &str, content: views::ViewHandle) -> views::ViewHandle {
155 views::vstack_spacing(
156 &[
157 label_text(title, 12.0, Some(views::Weight::Semibold), 0.55),
158 views::padding(
159 &views::corner_radius(
160 &views::background(&views::padding(&content, 12.0), 1.0, 1.0, 1.0, 0.07),
161 14.0,
162 ),
163 0.0,
164 ),
165 ],
166 6.0,
167 )
168}
169
170fn typography() -> views::ViewHandle {
171 views::vstack_spacing(
172 &[
173 views::hstack_spacing(
174 &[
175 views::font_text_style(&views::text("Title"), views::TextStyle::Title3),
176 views::italic(&views::text("Italic")),
177 views::monospaced(&views::text("mono")),
178 ],
179 14.0,
180 ),
181 views::kerning(&label_text("K E R N E D", 15.0, None, 0.9), 2.0),
182 views::foreground_style_linear_gradient(
183 &views::font_system(&views::text("Gradient fill"), 22.0, Some(views::Weight::Bold), None),
184 &[
185 views::color_value_of(Color::new(1.0, 0.55, 0.25, 1.0)),
186 views::color_value_of(Color::new(1.0, 0.25, 0.6, 1.0)),
187 ],
188 (0.0, 0.0),
189 (1.0, 0.0),
190 ),
191 ],
192 10.0,
193 )
194}
195
196fn shapes() -> views::ViewHandle {
197 let swatch = |shape: views::Shape, c: Color| {
198 views::frame(&shape.fill(views::color_value_of(c)), 58.0, 44.0)
199 };
200 views::hstack_spacing(
201 &[
202 swatch(views::Shape::circle(), Color::new(0.95, 0.35, 0.30, 1.0)),
203 swatch(views::Shape::capsule(), Color::new(0.98, 0.70, 0.20, 1.0)),
204 swatch(views::Shape::rounded_rectangle(10.0, true), Color::new(0.30, 0.65, 1.0, 1.0)),
205 swatch(views::Shape::ellipse(), Color::new(0.35, 0.85, 0.55, 1.0)),
206 ],
207 10.0,
208 )
209}
210
211fn controls() -> views::ViewHandle {
212 views::vstack_spacing(
213 &[
214 // Two toggles, both starting `false`: one bound to Rust-owned
215 // state, one a constant. If they ever disagree on screen, the
216 // binding's getter is at fault rather than the control.
217 views::toggle_bound("Bound to Rust state", switch_state()),
218 views::toggle("Constant false", false),
219 views::hstack_spacing(
220 &[
221 label_text(&format!("{:.2}", slider_state().get()), 14.0, None, 0.9),
222 views::slider_bound(slider_state(), 0.0, 1.0),
223 ],
224 10.0,
225 ),
226 views::stepper_bound(
227 &format!("Stepper: {:.0}", stepper_state().get()),
228 stepper_state(),
229 0.0,
230 10.0,
231 1.0,
232 ),
233 // Reads the stepper's Rust value. A `ProgressView` takes a plain
234 // number rather than a binding, so it can only ever show what it
235 // was built with — which is fine now that every write rebuilds the
236 // body from current state.
237 views::progress(stepper_state().get(), 10.0),
238 views::hstack_spacing(
239 &[
240 views::button_style(
241 &views::button("Bordered", on_tap),
242 views::ButtonStyle::Bordered,
243 ),
244 views::button_style(
245 &views::button("Glass", on_tap),
246 views::ButtonStyle::Glass,
247 ),
248 ],
249 12.0,
250 ),
251 views::textfield("A text field", ""),
252 ],
253 12.0,
254 )
255}More examples
387pub fn slot_value(name: &str) -> Option<String> {
388 let g = slots().lock().expect("slots");
389 g.flags
390 .get(name)
391 .map(|v| v.get().to_string())
392 .or_else(|| g.numbers.get(name).map(|v| format!("{:.2}", v.get())))
393 .or_else(|| g.strings.get(name).map(|v| format!("{:?}", v.get())))
394}
395
396/// What every slot currently holds, for a status line or a test.
397pub fn slot_summary() -> Vec<(String, String)> {
398 let g = slots().lock().expect("slots");
399 let mut out: Vec<(String, String)> = g
400 .flags
401 .iter()
402 .map(|(k, v)| (k.clone(), v.get().to_string()))
403 .chain(g.numbers.iter().map(|(k, v)| (k.clone(), format!("{:.2}", v.get()))))
404 .chain(g.strings.iter().map(|(k, v)| (k.clone(), format!("{:?}", v.get()))))
405 .collect();
406 out.sort();
407 out
408}45fn matching() -> Vec<usize> {
46 let q = inbox().query.get().to_lowercase();
47 MESSAGES
48 .iter()
49 .enumerate()
50 .filter(|(_, m)| {
51 q.is_empty()
52 || m.from.to_lowercase().contains(&q)
53 || m.subject.to_lowercase().contains(&q)
54 })
55 .map(|(i, _)| i)
56 .collect()
57}48fn main_window() -> ViewHandle {
49 vstack(&[
50 text("A SwiftUI app, no Swift source")
51 .font_size(20.0)
52 .padding(8.0),
53 divider(),
54 text(&format!("count: {}", counter().get()))
55 .font_size(34.0)
56 .padding(12.0),
57 hstack(&[
58 button("−", || counter().set(counter().get() - 1)),
59 button("+", || counter().set(counter().get() + 1)),
60 ]),
61 spacer(),
62 text("⌘, for Settings · the ★ in the menu bar has a menu")
63 .font_size(11.0)
64 .opacity(0.6),
65 ])
66 .padding(24.0)
67 .frame(420.0, 300.0)
68 .focused_scene_value(counter_focus_key(), counter().get())
69 .into_handle()
70}
71
72/// The focused-value key the window publishes its count under.
73///
74/// `.focusedSceneValue(\.key, value)` is how a window tells the commands above
75/// it what is selected. Both halves are synthesised at run time: the
76/// `FocusedValueKey` conformance *and* the `WritableKeyPath<FocusedValues, Int?>`
77/// — `FocusedValues` exports no properties, so unlike `EnvironmentValues` there
78/// is no SwiftUI modifier holding a key path to borrow.
79///
80/// The menu cannot *read* it yet: that needs `@FocusedValue`, a `DynamicProperty`
81/// SwiftUI instantiates itself, which is a different mechanism from anything
82/// here. What this demonstrates is the publishing half, in a real app.
83fn counter_focus_key() -> &'static views::FocusKey {
84 static K: std::sync::OnceLock<views::FocusKey> = std::sync::OnceLock::new();
85 K.get_or_init(|| views::focus_key("sceneCounter"))
86}
87
88fn settings_pane() -> ViewHandle {
89 vstack(&[
90 text("Preferences").font_size(18.0),
91 divider(),
92 text("This pane is a `Settings` scene — the one ⌘, opens."),
93 text(&format!("It sees the same state: {}", counter().get()))
94 .opacity(0.7),
95 ])
96 .padding(24.0)
97 .frame(360.0, 200.0)
98 .into_handle()
99}
100
101fn utility_window() -> ViewHandle {
102 vstack(&[
103 text("Utility").font_size(16.0),
104 text("A `Window`, not a `WindowGroup`: one of it, addressed by id.")
105 .opacity(0.7),
106 ])
107 .padding(20.0)
108 .frame(320.0, 140.0)
109 .into_handle()
110}
111
112fn menu_bar_item() -> ViewHandle {
113 vstack(&[
114 text(&format!("count: {}", counter().get())),
115 button("Reset", || counter().set(0)),
116 ])
117 .padding(10.0)
118 .into_handle()
119}
120
121/// The whole app, as one scene.
122///
123/// Built afresh every time SwiftUI reads `App.body`, which is what makes the
124/// numbers in it live rather than a snapshot taken at launch.
125///
126/// macOS only, and not by choice: `Settings`, `Window`, `MenuBarExtra` and the
127/// whole of `Commands` are `@available(iOS, unavailable)`. iOS gets
128/// [`ios_body`], which is the part of this that exists there.
129#[cfg(target_os = "macos")]
130fn body() -> SceneHandle {
131 scenes! {
132 window_group { main_window() }
133 settings { settings_pane() }
134 window("Utility", "utility") { utility_window() }
135 menu_bar_extra("Count", "star.fill") { menu_bar_item() }
136 commands {
137 // A menu of its own, beside File and Edit.
138 menu("Counter") {
139 vstack(&[
140 button("Increment", || counter().set(counter().get() + 1)),
141 button("Decrement", || counter().set(counter().get() - 1)),
142 divider(),
143 button("Reset", || counter().set(0)),
144 ])
145 }
146 // In place of the standard File ▸ New.
147 replacing(NewItem) { button("New Count", || counter().set(0)) }
148 // Added after the standard Help entry.
149 after(Help) {
150 button("About this Rust app", || {
151 eprintln!("built with no Swift compiler in sight");
152 })
153 }
154 }
155 }
156}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}Sourcepub fn set(&self, v: T)
pub fn set(&self, v: T)
Examples found in repository?
More examples
48fn main_window() -> ViewHandle {
49 vstack(&[
50 text("A SwiftUI app, no Swift source")
51 .font_size(20.0)
52 .padding(8.0),
53 divider(),
54 text(&format!("count: {}", counter().get()))
55 .font_size(34.0)
56 .padding(12.0),
57 hstack(&[
58 button("−", || counter().set(counter().get() - 1)),
59 button("+", || counter().set(counter().get() + 1)),
60 ]),
61 spacer(),
62 text("⌘, for Settings · the ★ in the menu bar has a menu")
63 .font_size(11.0)
64 .opacity(0.6),
65 ])
66 .padding(24.0)
67 .frame(420.0, 300.0)
68 .focused_scene_value(counter_focus_key(), counter().get())
69 .into_handle()
70}
71
72/// The focused-value key the window publishes its count under.
73///
74/// `.focusedSceneValue(\.key, value)` is how a window tells the commands above
75/// it what is selected. Both halves are synthesised at run time: the
76/// `FocusedValueKey` conformance *and* the `WritableKeyPath<FocusedValues, Int?>`
77/// — `FocusedValues` exports no properties, so unlike `EnvironmentValues` there
78/// is no SwiftUI modifier holding a key path to borrow.
79///
80/// The menu cannot *read* it yet: that needs `@FocusedValue`, a `DynamicProperty`
81/// SwiftUI instantiates itself, which is a different mechanism from anything
82/// here. What this demonstrates is the publishing half, in a real app.
83fn counter_focus_key() -> &'static views::FocusKey {
84 static K: std::sync::OnceLock<views::FocusKey> = std::sync::OnceLock::new();
85 K.get_or_init(|| views::focus_key("sceneCounter"))
86}
87
88fn settings_pane() -> ViewHandle {
89 vstack(&[
90 text("Preferences").font_size(18.0),
91 divider(),
92 text("This pane is a `Settings` scene — the one ⌘, opens."),
93 text(&format!("It sees the same state: {}", counter().get()))
94 .opacity(0.7),
95 ])
96 .padding(24.0)
97 .frame(360.0, 200.0)
98 .into_handle()
99}
100
101fn utility_window() -> ViewHandle {
102 vstack(&[
103 text("Utility").font_size(16.0),
104 text("A `Window`, not a `WindowGroup`: one of it, addressed by id.")
105 .opacity(0.7),
106 ])
107 .padding(20.0)
108 .frame(320.0, 140.0)
109 .into_handle()
110}
111
112fn menu_bar_item() -> ViewHandle {
113 vstack(&[
114 text(&format!("count: {}", counter().get())),
115 button("Reset", || counter().set(0)),
116 ])
117 .padding(10.0)
118 .into_handle()
119}
120
121/// The whole app, as one scene.
122///
123/// Built afresh every time SwiftUI reads `App.body`, which is what makes the
124/// numbers in it live rather than a snapshot taken at launch.
125///
126/// macOS only, and not by choice: `Settings`, `Window`, `MenuBarExtra` and the
127/// whole of `Commands` are `@available(iOS, unavailable)`. iOS gets
128/// [`ios_body`], which is the part of this that exists there.
129#[cfg(target_os = "macos")]
130fn body() -> SceneHandle {
131 scenes! {
132 window_group { main_window() }
133 settings { settings_pane() }
134 window("Utility", "utility") { utility_window() }
135 menu_bar_extra("Count", "star.fill") { menu_bar_item() }
136 commands {
137 // A menu of its own, beside File and Edit.
138 menu("Counter") {
139 vstack(&[
140 button("Increment", || counter().set(counter().get() + 1)),
141 button("Decrement", || counter().set(counter().get() - 1)),
142 divider(),
143 button("Reset", || counter().set(0)),
144 ])
145 }
146 // In place of the standard File ▸ New.
147 replacing(NewItem) { button("New Count", || counter().set(0)) }
148 // Added after the standard Help entry.
149 after(Help) {
150 button("About this Rust app", || {
151 eprintln!("built with no Swift compiler in sight");
152 })
153 }
154 }
155 }
156}Sourcepub fn is_persistent(&self) -> bool
pub fn is_persistent(&self) -> bool
Is this state written through to UserDefaults?
Source§impl<T: Persisted> BoundState<T>
impl<T: Persisted> BoundState<T>
Sourcepub fn persistent(key: &str, default: T) -> Self
pub fn persistent(key: &str, default: T) -> Self
State backed by UserDefaults, which is what @AppStorage is for.
The stored value wins over default when there is one — including a
stored false or 0, which is why defaults::has
exists rather than leaning on the typed readers.
Writes go through on set, so a control bound to this
persists as the user changes it and comes back to the same value in the
next process. SwiftUI is not involved in the storage at all: the value
lives in Rust and is mirrored to the same UserDefaults that
@AppStorage would have used.