Property wrappers to remove boilerplate code in Swift

Property wrappers were first introduced during WWDC 2019 and come with Xcode 11 in Swift 5. It’s a neat addition to the Swift library and allows us to remove a lot of boilerplate code which we probably all have written in our projects.

A background story on property wrappers can be found on the Swift forums for SE-0258.  While the motivation is mainly talking about property wrappers being a solution for @NSCopying properties, there’s a common pattern it solves you might all recognize.

Property wrappers and UserDefaults

The following code shows a pattern you all might recognize. It creates a wrapper around the UserDefaults object to make properties accessible without having to paste the string keys everywhere throughout your project.

extension UserDefaults {

    public enum Keys {
        static let hasSeenAppIntroduction = "has_seen_app_introduction"
    }

    /// Indicates whether or not the user has seen the onboarding.
    var hasSeenAppIntroduction: Bool {
        set {
            set(newValue, forKey: Keys.hasSeenAppIntroduction)
        }
        get {
            return bool(forKey: Keys.hasSeenAppIntroduction)
        }
    }
}

It allows you to set and get values from the user defaults as followed:

UserDefaults.standard.hasSeenAppIntroduction = true

guard !UserDefaults.standard.hasSeenAppIntroduction else { return }
showAppIntroduction()

Now as this seems to be a nice solution, it could easily end up being a large file with a lot of defined keys and properties. The code contains a lot of repetitive parts and asks for a way to make this easier. The @propertyWrapper keyword introduced with the property wrappers implementation solves this.

Using property wrappers to remove boilerplate code

Taking the above example we can easily rewrite this code and remove a lot of overhead. For that, we have to create a new property wrapper which we will call UserDefault. This will eventually allow us to define a property as being a user default property.

@propertyWrapper
struct UserDefault<T> {
    let key: String
    let defaultValue: T

    init(_ key: String, defaultValue: T) {
        self.key = key
        self.defaultValue = defaultValue
    }

    var value: T {
        get {
            return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
        }
        set {
            UserDefaults.standard.set(newValue, forKey: key)
        }
    }
}

The wrapper allows passing in a default value for if there’s no registered value yet. We can pass in any type of value as the wrapper is defined with a generic value T.

Unfortunately, we can’t use the property wrapper in an extension on UserDefaults. Instead, we have to create a new wrapper object which we call UserDefaultsConfig in this example. Reason for this is that class stored properties are not supported in class extensions.

The new wrapper can be defined as followed:

struct UserDefaultsConfig {
    @UserDefault("has_seen_app_introduction", defaultValue: false)
    static var hasSeenAppIntroduction: Bool
}

As you can see we can use the initializer from the defined property wrapper. We pass in the same key as we used before and we set the default value to false. Using this new property is simple:

UserDefaultsConfig.hasSeenAppIntroduction = false
print(UserDefaultsConfig.hasSeenAppIntroduction) // Prints: false
UserDefaultsConfig.hasSeenAppIntroduction = true
print(UserDefaultsConfig.hasSeenAppIntroduction) // Prints: true

Adding more properties using the same wrapper

Unlike the earlier solution, it’s very easy to add more properties when using a property wrapper. We can simply reuse the defined wrapper and instantiate as many properties as we need.

struct UserDefaultsConfig {
    @UserDefault("has_seen_app_introduction", defaultValue: false)
    static var hasSeenAppIntroduction: Bool

    @UserDefault("username", defaultValue: "Antoine van der Lee")
    static var username: String

    @UserDefault("year_of_birth", defaultValue: 1990)
    static var yearOfBirth: Int
}

As you can see, the wrapper works with any type you define as long as the type is supported to be saved in the user defaults as well.

Other usage examples

Property wrappers are used throughout the default Swift APIs as well. The new @State and @Binding keys are an example of this. Another idea could be to create a wrapper for thread-specific writing and reading:

@ThreadSpecific var localPool: MemoryPool

Or, as some might find handy, a way to define command line actions:

@Option(shorthand: "m", documentation: "Minimum value", defaultValue: 0)
var minimum: Int

Conclusion

Property wrappers are a great way to remove some boilerplate in your code. The above example is only one of many scenarios in which it can be useful. Try it out yourself and remove some code in your projects using Xcode 11.