Skip to main content

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}