UserDefaults Access via Property Wrappers Is Worse Than You Might Think

I really want to like Swift property wrappers for UserDefaults. But I have a hard time making them 100% useful.

Take the post by Andy Ibanez on the topic for example. It’s a well-written post with great code examples. If you want copy-able code for your app, read it. It’s good.

I want to focus on a code sample about the usage of property wrappers to illustrate a problem I have with the idea, not with Andy’s implementation:

class ImagesViewController: UIViewController {
    @UserDefault(key: .showImages, defaultValue: false)
    var showImages: Bool
}

With this approach, you have a strongly typed enum UserDefault.Key instead of a string-based key. That’s good to avoid typos and stuff. And the defaultValue is super useful to initialize showImages with something if the settings haven’t changed.

The thing is: UserDefaults comes with a mechanism for this already, registerDefaults. Imagine the UserDefaults value getters to be composed of an onion-like structure. At the outermost layer is a stored value in the user’s defaults bundle; beneath that is the transient registerDefaults dictionary, which is explicitly not persisted; then come potential system fall-back values for some keys.

The hip and cool property-wrapper implementations on the web so far ignore the registerDefaults layer completely and provide a local fallback if no value for the key is persisted for the current user.

The problem I see here is that you don’t have a shared default value anymore. Knowledge of the default value is local to the property declaration in a concrete type. You could have this:

class ImagesViewController: UIViewController {
    @UserDefault(key: .showImages, defaultValue: false)
    var showImages: Bool
}

class AvatarViewController: UIViewController {
    @UserDefault(key: .showImages, defaultValue: true)
    var showImages: Bool
}

One has a default value of true, one has a default value of false. Is that by design? If so, why do both use the same underlying settings key if they start out differently? Shouldn’t the settings be finer granulated and 2 keys be used if the default values differ?

I think the answer is yes. That makes the defaultValue parameter less useful and more dangerous. Typos, forgetting to make them consistent during changes, all that can result in potentially unwanted behavior in your app.

That’s not good.

What I would want instead is a collection of known defaults with typed keys. SwiftyUserDefaults does this well since your definition of the known, strongly typed keys comes with an optional initialization step of a default value:

// From the README
extension DefaultsKeys {
    var username: DefaultsKey<String?> { 
        return DefaultsKey("username") 
    }

    var launchCount: DefaultsKey<Int> { 
        return DefaultsKey("launchCount", defaultValue: 0)
    }
}

The default value here, much to my chagrin, currently omits the registerDefaults step as well. But this is better than the property wrapper approach already since the default value is defines in exactly one place. And I understand why folks like this: because you do not have to call registerDefaults during app launch, at all. As a downside, mixed Objective-C/Swift code will not expose the default value to the Objective-C Foundation calls. Neither will you be able to use RxSwift or another reactive extension to observe for changes to UserDefaults keys in the same way that you would if you used registerDefaults. (You can work around this, but I don’t know why you should have to.)

I am using a fork of the framework in my app that reads the defaultValue of an array of DefaultsKeys and assembles a dictionary to send to registerDefaults. I think I should finally submit an updated PR for this to support v5 of the framework.