Swift Property Wrappers, User Defaults and Unique Default Values

How to leverage the power of Property Wrappers to save and retrieve App Settings with a unique Default Value

César Vargas Casaseca
Axel Springer Tech
7 min readJun 21, 2021

--

Photo by Rudy Dong on Unsplash

With the release of property wrappers together with Swift 5 and Xcode 11, many patterns can now be reimplemented in an easier, cleaner and faster approach. For instance, we use them at WELT to add an extra layer of logic to some UIColor instances, so the appropriate color value is provided depending on the user setting selected color mode. In the same way, we implemented a property wrapper that access Firebase Remote Config that provides the value specified by a key parameter. In this fashion we do not need all the repetitive code necessary to replicate this functionality without property wrappers.

Likewise, we wanted to implement a property wrapper that, given a key parameter, saves and retrieves a value from the UserDefaults. Furthermore, we needed to define a unique default value in case there is none stored, and we want to make it extensible to RawRepresentable values so it can be used with enums. In this post we are going to see what the heck are property wrappers, in what cases they can be useful, and how we can extend them to implement our requirements.

What are Property Wrappers?

Photo by Marek Piwnicki on Unsplash

According to the docs:

A property wrapper adds a layer of separation between code that manages how a property is stored and the code that defines a property.

That is, it is an extra layer that defines how a property is stored or computed when reading it. The main purpose of this pattern is to remove all the boilerplate code that you would have to write for each property that needs that logic. Again from the docs:

For example, if you have properties that provide thread-safety checks or store their underlying data in a database, you have to write that code on every property. When you use a property wrapper, you write the management code once when you define the wrapper, and then reuse that management code by applying it to multiple properties.

To illustrate, let’s take a look at this example:

Here we have defined a property wrapper that always provides the absolute of the set value. This way, we use the standard abs() function only once, instead of using it for every access of the property, or every getter and setter of those properties that need this logic. Similarly, the functionality is now encapsulated in its own struct, making the code cleaner and more readable.

Our case: App Settings in User Defaults

A very common pattern on iOS development is a wrapper over UserDefaults, in order to avoid the Read/Write code duplication, hide these details, and ensure testability by dependency injection. This is a perfect case to be updated with a property wrapper:

In this example, we wanted to know if the app onboarding screen was shown, in order to avoid displaying it again the second time the user opened the app. Thusly, in the RootViewModel , we add the wrapper to the property, and when the view did appear, we check that value. Note how easy it is here to access the stored value.

This code works, but it is obviously flawed. We return an optional, making the client responsible of checking whether there is a value, and providing a default value instead on every access.

Could we move this logic into our property wrapper?

Default Values

Photo by engin akyurt on Unsplash

In the same way a property wrapper avoids repetitive boilerplate code, we want to encapsulate the default value logic into the wrapper itself, so it always returns a non optional and sets the client free of implementing that logic:

This is implemented in the property wrapper by adding a defaultValue property and returning it in case nothing can be retrieved from the UserDefaults. In such manner, the property wrapper returns a non optional and the client can just add the required default value in the initializer.

Now it looks much better, but, what if we want to have a unique default value for all the application? In the previous example each client, in this case the RootViewModel , is able to specify the default value they want. This is totally fine for our onboarding use case, but what if we do not want to give the property wrapper client the power to define the default value?

What if we want to have a unique default value for all the application?

For instance, imagine we have an UserDefaults setting that holds whether we want to use our backend dev environment. We need this setting to be consistent along the whole application, that is, always point to the same backend environment to avoid an unconsistent and confusing state. On top of that, the user should be able to change this value anytime, for instance, if the QA Team wants to test a task against any environment.

Again, we do not want the class where the property wrapper is applied, for instance the Network Fetcher, to bear the power of defining that default value. We want this logic to be unique and contained in its own class. For that, we take note of how Apple implements a generic key definition process of the SwiftUI Environment property wrapper with EnvironmentValues. Check these links for more information.

Going back to our example, we want to define a global unique default value for our environment setting. Given that what defines that setting uniquely is a key, we create a protocol that will act as a key to retrieve the value. This key protocol will contain a static property to hold the default value to be returned in case the setting is not yet in the user defaults.

As our pattern can return a generic value, we need an associatedtype within this protocol:

Next, we have to write an intermediate class to encapsulate the logic of retrieving the UserDefaults value given an AppSettingKey:

Note how it will return the key static default value if there is no value stored in the container.

Now that we have this proxy to the UserDefaults, we can rewrite our property wrapper to use it:

In the wrappedValue property it uses AppSettingsValues to retrieve the stored value.

Finally, we have to create a key for our specific setting case. It implements the AppSettingKey protocol and an extension of AppSettingsValues to provide the key as a ReferenceWritableKeyPath :

Once we have everything, we can use it effortlessly:

We just pass the AppSettingValueskeypath created together with the UseDevEnvironmentKey to the property wrapper initializer, and it’s ready to use. That simple!

But I have an enum!

Photo by Sam Moqadam on Unsplash

Again, the example is doing what we want, but we are not quite there yet. As you saw, we use a boolean value to determine which environment we are going to point: if it is true we use the development endpoints, otherwise production. This is suboptimal though.

What if, on top of dev and prod, we also have a test environment? How can improve our code to handle multiple cases? Cases?, yeah, we need an enum But wait, it is not so simple, even if we can change our environment data representation from a boolean to an enum, this type cannot be stored by default in the UserDefaults. We need to make that enum conform to the RawRepresentable protocol, by specifying a string, integer, or floating-point raw type. The Swift compiler automatically adds RawRepresentable conformance when doing it:

public enum Environment: String {
case development
case test
case production
}

Once we have a raw value for each case, we can store that value in UserDefaults, and when reading it, we will create an Environment instance from the raw value. This is implemented by creating its own App Setting Key protocol to enforce that the associatedtype conforms to RawRepresentable and adding a new subscript function in the AppSettingsValues class:

Note how powerful is the where keyword in swift, it allows us to easily filter out values, in this case those that don’t conform to RawRepresentableAppSettingKey

Epilogue: Testability

Without delving too deep in a topic that deserves its own post, we ensure testability by allowing the injection of the needed dependencies. The UserDefaults container is passed in the initializer of the AppSettingsValues, so we can inject it when testing. For instance, we could create our own UserDefaults instance other than .default, by initializing it with a suite name. Once we have that instance, we pass it to create the AppSettingsValues, and set this one to the property of the AppSettings property wrapper:

Wrapping Up

Photo by Matthew Henry on Unsplash

We went through many topics today:

  • What are property wrappers and what are they used for
  • How can we apply property wrappers to read and write values from User Defaults
  • How can we add a default value for each use case of our User Defaults
  • How can specify a unique global default value for a property wrapper key
  • How can we enhance our User Defaults property wrapper to apply it to enums
  • How can we ensure testability by injecting our User Defaults container

I hope you liked this story and can profit from it. If you have any question or suggestion do not hesitate to drop a message below. I would like to thank my colleagues Ivan Lisovyi and Haroon Ur Rasheed for their critical contribution on this topic.
Happy wrapping!!

--

--

César Vargas Casaseca
Axel Springer Tech

Senior iOS Developer at Automattic. Allegedly a clean pragmatic one. Yes, sometimes I do backend. Yes, in Kotlin, Javascript or even Swift.