ObservableDefaults Documentation

repository·main·Indexed 19 days ago

https://github.com/fatbobman/observabledefaults

A Swift library that connects Swift Observation to UserDefaults and iCloud key-value storage via class macros. It provides @ObservableDefaults for local persistence and @ObservableCloud for iCloud synchronization, offering a granular alternative to @AppStorage for SwiftUI applications. Features include support for Codable and RawRepresentable types, App Group cross-process synchronization, and a development mode for SwiftUI previews.

Tokens
12.2K
Snippets
35
Records
44
Agent score
58%

What's inside ObservableDefaults

  1. Overview of ObservableDefaults features

    main

    ObservableDefaults provides several mechanisms for managing persistent state in SwiftUI applications:

    • @ObservableDefaults: Persists observable properties in UserDefaults.
    • @ObservableCloud: Synchronizes observable properties through NSUbiquitousKeyValueStore (iCloud).
    • External Change Support: Property-level Observation updates are triggered even when changes originate from outside the model (e.g., via iCloud sync).
    • Customization: Supports custom keys, suite names, and prefixes for specific storage layouts.
    • Type Support: Supports Optional, Codable, and RawRepresentable values.
    • Observe-first mode: Allows models to explicitly opt properties into persistence.
    • Development mode: Isolates cloud-backed models for easier testing and SwiftUI previews.
  2. Understand default value behavior and fallback mechanism

    main

    All persistent properties must be declared with a default value. The framework captures these declaration-time defaults as immutable fallback values.

    Behavior:

    1. The framework stores the current value in UserDefaults or iCloud.
    2. If the key is missing from storage (e.g., deleted externally), the property automatically reverts to the declaration-time default.
    3. Even if you change the property value via a custom initializer, the original declaration-time default remains the fallback if storage is cleared.
    @ObservableDefaults(autoInit: false)
    class User {
        var username = "guest"      // Declaration default: "guest"
        var age: Int = 18          // Declaration default: 18
        
        init(username: String, age: Int) {
            self.username = username  // Current value: "alice", default remains: "guest"
            self.age = age           // Current value: 25, default remains: 18
        }
    }
    
    // If keys are deleted from UserDefaults:
    // user.username reverts to "guest"
    // user.age reverts to 18
  3. Use Observe First mode for selective persistence

    main

    By setting observeFirst: true in the @ObservableDefaults or @ObservableCloud macro, you change the default behavior from 'persist everything' to 'observe everything, persist nothing'.

    In this mode, properties are only stored in UserDefaults or iCloud if they are explicitly marked with @DefaultsBacked or @CloudBacked respectively. This is useful for creating classes where most properties are transient/observable but only a few need persistence.

    // UserDefaults Example
    @ObservableDefaults(observeFirst: true)
    public class LocalSettings {
        public var name: String = "fat"        // Observable only
        @DefaultsBacked(userDefaultsKey: "myHeight")
        public var height = 190                // Observable and persisted
    }
    
    // Cloud Example
    @ObservableCloud(observeFirst: true)
    public class CloudSettings {
        public var localSetting: String = "local" // Observable only
        @CloudBacked(keyValueStoreKey: "user_theme")
        public var theme: String = "light"      // Observable and synced to iCloud
    }
  4. Understand storage resolution rules for types

    main

    The library uses a specific priority order to decide how to persist data. When a type matches multiple constraints, it follows this order:

    1. RawRepresentable & PropertyListValue & Codable (Uses rawValue)
    2. RawRepresentable & PropertyListValue (Uses rawValue)
    3. RawRepresentable (where RawValue is PropertyList-compatible)
    4. PropertyListValue & Codable (Uses JSON Data)
    5. PropertyListValue (Directly as PropertyList object)
    6. Codable only (Uses JSON Data)

    Key behaviors:

    • Enums: If an enum is RawRepresentable and its RawValue is a property-list type (like String or Int), it is stored via its rawValue rather than JSON encoding.
    • URLs: URL / NSURL properties are always persisted as JSON-encoded Data using the URL's Codable representation.
    • Optionals: Non-nil values follow the rules above; nil values result in the key being removed from storage.
  5. Understand fallback behavior for UserDefaults and iCloud

    main

    All persistent properties (marked with @DefaultsBacked or @CloudBacked) must declare a default value. The framework captures this value as an immutable model default.

    Fallback Order for @ObservableDefaults (UserDefaults)

    1. Persistent value in the selected UserDefaults domain.
    2. Default values registered via UserDefaults.register(defaults:).
    3. The model default value captured at declaration.

    Fallback Order for @ObservableCloud (iCloud)

    1. Cloud-persisted value.
    2. The model default value captured at declaration.

    Note: For UserDefaults, removeObject(forKey:) does not necessarily fall back to the declaration default if a registered default exists via UserDefaults.register(defaults:).

  6. Understand storage decision rules for types

    main

    When a property type satisfies multiple constraints, the library selects a storage format based on specificity (most specific wins). This applies to both @ObservableDefaults and @ObservableCloud.

    Priority Order

    1. RawRepresentable & PropertyListValue & Codable (Uses rawValue)
    2. RawRepresentable & PropertyListValue (Uses rawValue)
    3. RawRepresentable (where RawValue is a PropertyList type)
    4. PropertyListValue & Codable (Uses JSON Data)
    5. PropertyListValue (Direct PropertyList value)
    6. Codable only (Lowest priority: uses JSON Data)

    Key Storage Formats

    • RawRepresentable: Saves the rawValue. For example, a String enum saves the string directly.
    • PropertyListValue: Saves the value directly as a PropertyList type.
    • Codable: Encodes to JSON Data.
    • URL / NSURL: Encoded as JSON Data via its Codable representation (not stored as a direct PropertyList URL object).
    • Optional Values: If nil, the corresponding key is deleted from storage. If non-nil, it follows the rules above.

    Manual Read/Write Consistency

    If you manually access UserDefaults or iCloud keys, you must match these rules:

    • RawRepresentable: Write the rawValue.
    • PropertyListValue: Write the original value.
    • Codable: Write JSON Data via JSONEncoder.
    • URL: Write JSON Data via JSONEncoder.
    // Example: Manual write for RawRepresentable (UserDefaults)
    defaults.set(theme.rawValue, forKey: "app_theme")
    
    // Example: Manual write for Codable (UserDefaults)
    defaults.set(try JSONEncoder().encode(profile), forKey: "app_profile")
    
    // Example: Manual write for URL (UserDefaults)
    defaults.set(try JSONEncoder().encode(homepageURL), forKey: "app_homepage")
  7. Supported types for @ObservableDefaults and @ObservableCloud

    main

    ObservableDefaults supports types that conform to property list value protocols. This includes:

    • Basic Types: String, Int, Double, Float, Bool, Date, Data, URL, and all integer types (e.g., Int8, UInt).
    • Collections: Array<Element> and Dictionary<String, Value> where the elements/values conform to property list protocols.
    • Optionals: All supported types can be wrapped in Optional<T>. When an optional property is set to nil, it is removed from storage. If the storage key is missing, it returns its default value (usually nil).
    • Enums: Enums with raw values (e.g., enum Theme: String) are automatically supported.
    • Custom Types: Custom types can be used if they implement Codable.
    // Custom type with Codable for UserDefaults
    @ObservableDefaults
    class LocalStore {
        var people: People = .init(name: "fat", age: 10)
    }
    
    struct People: Codable {
        var name: String
        var age: Int
    }
    
    // Custom type with Codable for Cloud Storage
    @ObservableCloud
    class CloudStore {
        var userProfile: UserProfile = .init(name: "fat", preferences: .init())
    }
    
    struct UserProfile: Codable {
        var name: String
        var preferences: UserPreferences
    }
    
    struct UserPreferences: Codable {
        var theme: String = "light"
        var fontSize: Int = 14
    }
  8. Use observation-first mode

    main

    In "observation-first" mode (set via observeFirst: true), properties are observable by default, but only those explicitly marked with @DefaultsBacked or @CloudBacked are persisted to storage.

    UserDefaults Observation-First

    @ObservableDefaults(observeFirst: true)
    public class LocalSettings {
        public var name: String = "fat"        // Observable only
        public var age = 109                   // Observable only
    
        @DefaultsBacked(userDefaultsKey: "myHeight")
        public var height = 190                // Observable and persisted to UserDefaults
    
        @Ignore
        public var weight = 10                 // Neither observable nor persisted
    }

    iCloud Observation-First

    @ObservableCloud(observeFirst: true)
    public class CloudSettings {
        public var localSetting: String = "local"     // Observable only
        public var tempData = "temp"                  // Observable only
    
        @CloudBacked(keyValueStoreKey: "user_theme")
        public var theme: String = "light"            // Observable and synced to iCloud
    
        @Ignore
        public var cache = "cache"                  // Neither observable nor persisted
    }
  9. Use Cloud storage development mode

    main

    To test @ObservableCloud without setting up a CloudKit container, enable developmentMode. This uses in-memory storage instead of NSUbiquitousKeyValueStore.

    Development mode is automatically enabled if:

    1. developmentMode: true is explicitly set in the macro.
    2. The app is running in a SwiftUI Preview (XCODE_RUNNING_FOR_PREVIEWS environment variable).
    3. The OBSERVABLE_DEFAULTS_DEV_MODE environment variable is set to "true".
    @ObservableCloud(developmentMode: true)
    class CloudSettings {
        var setting1: String = "value1"  // Uses in-memory storage
        var setting2: Int = 42           // Uses in-memory storage
    }
  10. Understand default value fallback behavior

    main

    All persistent properties (marked with @DefaultsBacked or @CloudBacked) must have declaration-time default values. These are captured as immutable model defaults.

    Fallback Order

    @ObservableDefaults (UserDefaults):

    1. Persisted value in the selected UserDefaults domain.
    2. Value provided by UserDefaults.register(defaults:).
    3. Declaration-time model default.

    @ObservableCloud (iCloud Key-Value Store):

    1. Persisted cloud value.
    2. Declaration-time model default.

    Important Note on removeObject(forKey:)

    Calling removeObject(forKey:) on UserDefaults does not necessarily revert to the declaration-time default if a value was previously registered via UserDefaults.register(defaults:). The registered default takes precedence over the declaration-time model default.

    @ObservableDefaults(autoInit: false)
    class User {
        var username = "guest"      // Declaration default: "guest"
        var age: Int = 18          // Declaration default: 18
    
        init(username: String, age: Int) {
            self.username = username
            self.age = age
        }
    }
    
    let user = User(username: "alice", age: 25)
    
    // Current state:
    // - username current value: "alice", default: "guest"
    // - age current value: 25, default: 18
    
    let defaults = UserDefaults.standard
    defaults.register(defaults: ["username": "registered-user"])
    defaults.set("bob", forKey: "username")
    defaults.set(25, forKey: "age")
    defaults.removeObject(forKey: "username")
    defaults.removeObject(forKey: "age")
    
    print(user.username)  // "registered-user" (registered default wins)
    print(user.age)       // 18 (no registered default, uses declaration default)
  11. Integrate local UserDefaults with @ObservableDefaults

    main

    Use the @ObservableDefaults macro on a class to automatically associate its properties with UserDefaults. This enables SwiftUI observation and automatic synchronization with local storage. Properties are stored using their variable names as keys by default, and the macro handles listening for external changes and notifying SwiftUI views of updates.

    import ObservableDefaults
    
    @ObservableDefaults
    class Settings {
        var name: String = "Fatbobman"
        var age: Int = 20
        var nickname: String? = nil
    }
  12. Install ObservableDefaults via Swift Package Manager

    main

    Add ObservableDefaults to your project's dependencies using Swift Package Manager. This library provides macros to connect Swift Observation to UserDefaults and iCloud key-value storage.

    dependencies: [
        .package(
            url: "https://github.com/fatbobman/ObservableDefaults.git",
            from: "1.8.8"
        )
    ]