Home

Awesome

UserDefaultsObservation

Combining UserDefaults with Observation, giving you the ability to easily create Observable UserDefaults-backed classes.

  1. Why UserDefaultsObservation?

  2. Requirements

  3. Installation

  4. Usage

  5. Change Log

Why UserDefaultsObservation

Most applications use UserDefaults to store some user preferences, application defaults, and other pieces of information. In the world of SwiftUI, the @AppStorage property wrapper was introduced. This provided access to UserDefaults and a way to invalidate a View triggering a redraw.

However, @AppStorage has a few limitations:

  1. The initialization of it requires a string key and default value, making reuse difficult
  2. It is advised against using it in non-SwiftUI code. While UserDefaults APIs are good, it is now a different method for accessing the same information.
  3. Refactoring code can be cumbersome depending on how widespread your usage keys are

Wrapping UserDefaults to provide a centralized location of maintaining keys and default values is one solution. However, it does not provide the view invalidating benefits of AppStorage. There are solutions to that as well, but they are sometimes not the most elegant.

UserDefaultsObservation aims to solve these issues by:

  1. Providing the ability to define any class as both UserDefaults-backed and Observable
  2. Centralizing the definition of UserDefaults keys and their default values
  3. Able to be used in both SwiftUI and non-SwiftUI code

Requirements

This package is built on Observation and Macros that are releasing in iOS 17, macOS 14.

Installation

SwiftPM

File > Add Package Dependencies. Use this URL in the search box: https://github.com/tgeisse/UserDefaultsObservation

Usage

Creating a Class

To create a class that is UserDefaults backed, import Foundation and UserDefaultsObservation. Then mark the class with the @ObservableUserDefaults macro. Define variables as you normally would:

import Foundation
import UserDefaultsObservation

@ObservableUserDefaults
class MySampleClass {
    var firstUse = false
    var username: String? = nil
}

Should you need to ignore a variable, use the @ObservableUserDefaultsIgnored macro. Note: variables with accessors will be ignored as if they have the @ObservableUserDefaultsIgnored macro attribute attached.

@ObservableUserDefaults
class MySampleClass {
    var firstUse = false
    var username: String? = nil
    
    @ObservableUserDefaultsIgnored
    var someIgnoredProperty = "hello world"
}

Defining the UserDefaults Key

A default key is created for you as {ClassName}.{PropertyName}. In the example above, the keys would be the following:

In situations you need to control the key - e.g. refactoring or migrating existing keys - you can mark a property with the @@UserDefaultsProperty attribute and provide the full UserDefaults key as a parameter. As an example:

@ObservableUserDefaults
class MySampleClass {
    var firstUse = false
    var username: String? = nil
    
    @ObservableUserDefaultsIgnored
    var someIgnoredProperty = "hello world"
    
    @UserDefaultsProperty(key: "myPreviousKey")
    var existingUserDefaults: Bool = true
}

Using a custom UserDefaults suite

To use a custom UserDefaults store, you can use the @UserDefaultsStore attribute to denote the UserDefaults store variable.

@ObservableUserDefaults
class MySampleClass {
    var firstUse = false
    var username: String? = nil
    
    @ObservableUserDefaultsIgnored
    var someIgnoredProperty = "hello world"
    
    @UserDefaultsProperty(key: "myPreviousKey")
    var existingUserDefaults: Bool = true
    
    @UserDefaultsStore
    var myStore = UserDefaults(suiteName: "MyStore.WithSuiteName.Example")
}

Should you need to change the store at runtime, one option is to do so with an initializer:

@ObservableUserDefaults
class MySampleClass {
    var firstUse = false
    var username: String? = nil
    
    @UserDefaultsStore
    var myStore: UserDefaults
    
    init(_ store: UserDefaults = .standard) {
        self.myStore = store
    }
}

Compiler Flag Dependent UserDefaults Suite

If you would like to define the store using compiler flags, there are a few ways to accomplish this. The first is with a computed property:

@ObservableUserDefaults
class MySampleClass {
    var firstUse = false
    var username: String? = nil
    
    @ObservableUserDefaultsIgnored
    var someIgnoredProperty = "hello world"
    
    @UserDefaultsProperty(key: "myPreviousKey")
    var existingUserDefaults: Bool = true
    
    @UserDefaultsStore
    var myStore: UserDefaults {
        #if DEBUG
            return UserDefaults(suiteName: "myDebugStore.example")!
        #else
            return UserDefaults(suiteName: "myProductionStore.example")!
        #endif
    }
}

If computing this each time is not desired, then this is another option:

    @UserDefaultsStore
    var myStore: UserDefaults = {
        #if DEBUG
            return UserDefaults(suiteName: "myDebugStore.example")!
        #else
            return UserDefaults(suiteName: "myProductionStore.example")!
        #endif
    }()

One more option is to put the compiler flag code into the initializer

    @UserDefaultsStore
    var myStore: UserDefaults
    
    init(_ store: UserDefaults = .standard) {
        #if DEBUG
            self.myStore = UserDefaults(suiteName: "myDebugStore.example")
        #else
            self.myStore = store
        #endif
    }

Cloud Storage with NSUbiquitousKeyValueStore

A property can be marked for storage in NSUbiquitousKeyValueStore by using the @CloudProperty attribute. This attribute will sync write changes to the cloud and use UserDefaults to cache values locally. The values will be cached in the UserDefaults store of the class containing the CloudProperty.

Let's take a look at syncing a user's favorite color:

@ObservableUserDefaults
class UsersPreferences {
    @CloudProperty(key: "yourCloudKey",
                   userDefaultKey: "differentUserDefaultKey",
                   onStoreServerChange: .cloudValue,
                   onInitialSyncChange: .cloudValue,
                   onAccountChange: .cachedValue)
    var favoriteColor: String = "Green"
}

This displays the parameters the user can or must define. Let's review each:

[!TIP] Omitting the userDefaultKey parameter will use the value of the key parameter for UserDefaults.

Options for Values on External Notifications

For each external notification event reason, the user can select which value to use to update the cloud property. There are currently four options available:

Cloud Storage Quota Violation

[!WARNING] This package does not take any action when the NSUbiquitousKeyValueStoreQuotaViolationChange external notification is received.

Supported Types

All of the following types are supported, including their optional counterparts:

Unsupported Types

Unsupported times should throw an error during compile time. The error will be displayed as if it is in the macro, but it is likely the type that is the issue. Should this variable need to be kept on the class, then it may need to be @ObservableUserDefaultsIgnored.

Change Log

0.5.2

0.5.1

0.5.0

New Features and Code Organization

0.4.4

0.4.3

0.4.2

0.4.1

0.4.0

0.3.3

README updates Included additional examples of how to use the @ObservableUserDefaultsStore property attribute

0.3.2

README updates

0.3.1

README updates

0.3.0

New Features and Code Organization