swiftui_native/defaults.rs
1//! `UserDefaults`, through the Objective-C runtime.
2//!
3//! The store behind `@AppStorage`, reached directly. SwiftUI's own property
4//! wrapper cannot be driven from here — its `wrappedValue` accessor resolves a
5//! *canonical location* against the update graph it was installed in, so
6//! calling it outside a view body faults inside
7//! `UserDefaultLocation.canonicalLocation` no matter how the call is spelled
8//! (measured against four conventions; the construction works, the accessor
9//! does not). Installing one properly means giving the run-time-synthesised
10//! view type an `AppStorage` field so SwiftUI calls `_makeProperty` on it,
11//! which is how `@State` is already done here.
12//!
13//! What that wrapper *is*, though, is a value in `UserDefaults` plus a
14//! re-render when it changes — and both halves are already available: this
15//! module for the store, and [`BoundState`](crate::views::BoundState) for the
16//! binding SwiftUI reads. So persistence is spelled
17//! `BoundState::persistent("key", default)`, and every control that takes a
18//! binding persists with no further work.
19
20use crate::objc;
21use core::ffi::c_void;
22
23/// `UserDefaults.standard`.
24fn standard() -> *mut c_void {
25 unsafe {
26 objc::send(
27 objc::class(c"OBJC_CLASS_$_NSUserDefaults"),
28 objc::sel(c"standardUserDefaults"),
29 )
30 }
31}
32
33/// Is anything stored under `key`?
34///
35/// The typed readers cannot answer this: `boolForKey:` returns `false` for a
36/// key that was never set, which is indistinguishable from one set to `false`.
37/// A stored default has to win over the caller's fallback, and only this says
38/// which case it is.
39pub fn has(key: &str) -> bool {
40 unsafe {
41 let v = objc::send_id(standard(), objc::sel(c"objectForKey:"), objc::nsstring(key));
42 !v.is_null()
43 }
44}
45
46/// Forget `key`, so the next read falls back.
47pub fn remove(key: &str) {
48 unsafe {
49 objc::send_id(
50 standard(),
51 objc::sel(c"removeObjectForKey:"),
52 objc::nsstring(key),
53 );
54 }
55}
56
57/// A type `UserDefaults` can hold, and that a binding can therefore persist.
58///
59/// The four `AppStorage` supports without a `RawRepresentable` detour.
60pub trait Persisted: Clone + Send + Sync + 'static {
61 /// The stored value, or `fallback` when nothing is stored.
62 fn load(key: &str, fallback: Self) -> Self;
63 /// Write, replacing whatever was there.
64 fn store(key: &str, value: &Self);
65}
66
67impl Persisted for bool {
68 fn load(key: &str, fallback: Self) -> Self {
69 if !has(key) {
70 return fallback;
71 }
72 unsafe {
73 objc::send_id_ret_bool(standard(), objc::sel(c"boolForKey:"), objc::nsstring(key))
74 }
75 }
76 fn store(key: &str, value: &Self) {
77 unsafe {
78 objc::send_bool_id(
79 standard(),
80 objc::sel(c"setBool:forKey:"),
81 *value,
82 objc::nsstring(key),
83 );
84 }
85 }
86}
87
88impl Persisted for i64 {
89 fn load(key: &str, fallback: Self) -> Self {
90 if !has(key) {
91 return fallback;
92 }
93 unsafe {
94 objc::send_id_ret_i64(standard(), objc::sel(c"integerForKey:"), objc::nsstring(key))
95 }
96 }
97 fn store(key: &str, value: &Self) {
98 unsafe {
99 objc::send_i64_id(
100 standard(),
101 objc::sel(c"setInteger:forKey:"),
102 *value,
103 objc::nsstring(key),
104 );
105 }
106 }
107}
108
109impl Persisted for f64 {
110 fn load(key: &str, fallback: Self) -> Self {
111 if !has(key) {
112 return fallback;
113 }
114 unsafe {
115 objc::send_id_ret_f64(standard(), objc::sel(c"doubleForKey:"), objc::nsstring(key))
116 }
117 }
118 fn store(key: &str, value: &Self) {
119 unsafe {
120 objc::send_f64_id(
121 standard(),
122 objc::sel(c"setDouble:forKey:"),
123 *value,
124 objc::nsstring(key),
125 );
126 }
127 }
128}
129
130impl Persisted for String {
131 fn load(key: &str, fallback: Self) -> Self {
132 unsafe {
133 let s = objc::send_id(standard(), objc::sel(c"stringForKey:"), objc::nsstring(key));
134 if s.is_null() {
135 return fallback;
136 }
137 let p = objc::send_ret_ptr(s, objc::sel(c"UTF8String"));
138 if p.is_null() {
139 return fallback;
140 }
141 core::ffi::CStr::from_ptr(p as *const i8)
142 .to_string_lossy()
143 .into_owned()
144 }
145 }
146 fn store(key: &str, value: &Self) {
147 unsafe {
148 objc::send_id_id(
149 standard(),
150 objc::sel(c"setObject:forKey:"),
151 objc::nsstring(value),
152 objc::nsstring(key),
153 );
154 }
155 }
156}