swiftui-native 0.0.1

SwiftUI from pure Rust — no Swift bridge, no shim, only swift-runtime calls
Documentation
//! `UserDefaults`, through the Objective-C runtime.
//!
//! The store behind `@AppStorage`, reached directly. SwiftUI's own property
//! wrapper cannot be driven from here — its `wrappedValue` accessor resolves a
//! *canonical location* against the update graph it was installed in, so
//! calling it outside a view body faults inside
//! `UserDefaultLocation.canonicalLocation` no matter how the call is spelled
//! (measured against four conventions; the construction works, the accessor
//! does not). Installing one properly means giving the run-time-synthesised
//! view type an `AppStorage` field so SwiftUI calls `_makeProperty` on it,
//! which is how `@State` is already done here.
//!
//! What that wrapper *is*, though, is a value in `UserDefaults` plus a
//! re-render when it changes — and both halves are already available: this
//! module for the store, and [`BoundState`](crate::views::BoundState) for the
//! binding SwiftUI reads. So persistence is spelled
//! `BoundState::persistent("key", default)`, and every control that takes a
//! binding persists with no further work.

use crate::objc;
use core::ffi::c_void;

/// `UserDefaults.standard`.
fn standard() -> *mut c_void {
    unsafe {
        objc::send(
            objc::class(c"OBJC_CLASS_$_NSUserDefaults"),
            objc::sel(c"standardUserDefaults"),
        )
    }
}

/// Is anything stored under `key`?
///
/// The typed readers cannot answer this: `boolForKey:` returns `false` for a
/// key that was never set, which is indistinguishable from one set to `false`.
/// A stored default has to win over the caller's fallback, and only this says
/// which case it is.
pub fn has(key: &str) -> bool {
    unsafe {
        let v = objc::send_id(standard(), objc::sel(c"objectForKey:"), objc::nsstring(key));
        !v.is_null()
    }
}

/// Forget `key`, so the next read falls back.
pub fn remove(key: &str) {
    unsafe {
        objc::send_id(
            standard(),
            objc::sel(c"removeObjectForKey:"),
            objc::nsstring(key),
        );
    }
}

/// A type `UserDefaults` can hold, and that a binding can therefore persist.
///
/// The four `AppStorage` supports without a `RawRepresentable` detour.
pub trait Persisted: Clone + Send + Sync + 'static {
    /// The stored value, or `fallback` when nothing is stored.
    fn load(key: &str, fallback: Self) -> Self;
    /// Write, replacing whatever was there.
    fn store(key: &str, value: &Self);
}

impl Persisted for bool {
    fn load(key: &str, fallback: Self) -> Self {
        if !has(key) {
            return fallback;
        }
        unsafe {
            objc::send_id_ret_bool(standard(), objc::sel(c"boolForKey:"), objc::nsstring(key))
        }
    }
    fn store(key: &str, value: &Self) {
        unsafe {
            objc::send_bool_id(
                standard(),
                objc::sel(c"setBool:forKey:"),
                *value,
                objc::nsstring(key),
            );
        }
    }
}

impl Persisted for i64 {
    fn load(key: &str, fallback: Self) -> Self {
        if !has(key) {
            return fallback;
        }
        unsafe {
            objc::send_id_ret_i64(standard(), objc::sel(c"integerForKey:"), objc::nsstring(key))
        }
    }
    fn store(key: &str, value: &Self) {
        unsafe {
            objc::send_i64_id(
                standard(),
                objc::sel(c"setInteger:forKey:"),
                *value,
                objc::nsstring(key),
            );
        }
    }
}

impl Persisted for f64 {
    fn load(key: &str, fallback: Self) -> Self {
        if !has(key) {
            return fallback;
        }
        unsafe {
            objc::send_id_ret_f64(standard(), objc::sel(c"doubleForKey:"), objc::nsstring(key))
        }
    }
    fn store(key: &str, value: &Self) {
        unsafe {
            objc::send_f64_id(
                standard(),
                objc::sel(c"setDouble:forKey:"),
                *value,
                objc::nsstring(key),
            );
        }
    }
}

impl Persisted for String {
    fn load(key: &str, fallback: Self) -> Self {
        unsafe {
            let s = objc::send_id(standard(), objc::sel(c"stringForKey:"), objc::nsstring(key));
            if s.is_null() {
                return fallback;
            }
            let p = objc::send_ret_ptr(s, objc::sel(c"UTF8String"));
            if p.is_null() {
                return fallback;
            }
            core::ffi::CStr::from_ptr(p as *const i8)
                .to_string_lossy()
                .into_owned()
        }
    }
    fn store(key: &str, value: &Self) {
        unsafe {
            objc::send_id_id(
                standard(),
                objc::sel(c"setObject:forKey:"),
                objc::nsstring(value),
                objc::nsstring(key),
            );
        }
    }
}