Getting started with Property Wrappers in Swift

Property Wrappers in Swift allow you to extract common logic in a distinct wrapper object. Since the introduction during WWDC 2019 and becoming available in Xcode 11 with Swift 5 there have been many examples shared across the community. It’s a neat addition to the Swift library that allows removing 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.

What is a Property Wrapper?

A property wrapper can be seen as an extra layer that defines how a property is stored or computed on reading. It’s especially useful for replacing repetitive code found in getters and setters of properties.

A common example is custom-defined user defaults properties in which a custom getter and setter are used to transform the value accordingly.

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 great solution, it could easily end up being a large file with a lot of defined keys and properties. The code is repetitive and asks for a way to make this easier. A custom property wrapper using the @propertyWrapper keyword can help us solve this problem.

Using property wrappers to remove boilerplate code

Taking the above example we can easily rewrite the 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.

If you’re using SwiftUI, you might want to use the AppStorage property wrapper instead. Take this just as an example of replacing repetitive code.

@propertyWrapper
struct UserDefault<Value> {
    let key: String
    let defaultValue: Value
    var container: UserDefaults = .standard

    var wrappedValue: Value {
        get {
            return container.object(forKey: key) as? Value ?? defaultValue
        }
        set {
            container.set(newValue, forKey: key)
        }
    }
}

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

We can now change our previous code implementation and create the following extension on the UserDefaults type:

extension UserDefaults {

    @UserDefault(key: "has_seen_app_introduction", defaultValue: false)
    static var hasSeenAppIntroduction: Bool
}

As you can see we can use the default generated struct 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:

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

In some cases, you might want to define your own custom user defaults. For example, in cases where you have an app group defined user defaults. Our defined wrapper defaults to the standard user defaults but you can override this to use your own container:

extension UserDefaults {
    static let groupUserDefaults = UserDefaults(suiteName: "group.com.swiftlee.app")!

    @UserDefault(key: "has_seen_app_introduction", defaultValue: false, container: .groupUserDefaults)
    static var hasSeenAppIntroduction: Bool
}

Adding more properties using the same wrapper

Unlike the old 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.

extension UserDefaults {

    @UserDefault(key: "has_seen_app_introduction", defaultValue: false)
    static var hasSeenAppIntroduction: Bool

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

    @UserDefault(key: "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.

Projecting a Value From a Property Wrapper

Property wrappers have the option to add another property besides the wrapped value. This allows us to project another value based on the wrapped value, for example.

Take the following property wrapper in which we define a sample file:

@propertyWrapper
struct SampleFile {

    let fileName: String

    var wrappedValue: URL {
        let file = fileName.split(separator: ".").first!
        let fileExtension = fileName.split(separator: ".").last!
        let url = Bundle.main.url(forResource: String(file), withExtension: String(fileExtension))!
        return url
    }

    var projectedValue: String {
        return fileName
    }
}

We can use this wrapper to define our sample files which we might want to use for debugging or while running tests:

struct SampleFiles {
    @SampleFile(fileName: "sample-image.png")
    static var image: URL
}

The projectedValue property allows us to read out the file name as used in the property wrapper:

print(SampleFiles.image) // Prints: "../resources/sample-image.png"
print(SampleFiles.$image) // Prints: "sample-image.png"

This can be useful in cases you want to know which initial value(s) have been used by the wrapper to compute the final value. Note that we’re using the dollar sign here as a prefix to access the projected value.

Accessing private defined properties

Although it’s not recommended to work with property wrappers this way it can be useful in some cases to read the defined properties of a wrapper. I’m just going to demonstrate this is possible but you might want to rethink your code implementation if you find yourself having the need to access private properties.

In the above example, we could access the filename as well by using an underscore prefix. This allows us to access the private property filename:

extension SampleFiles {
    static func printKey() {
        print(_image.fileName)
    }
}

Take it with a grain of salt and see whether you can solve your needs by using a different instance type instead.

Other usage examples

Property wrappers are used throughout the default Swift APIs as well. Especially in SwiftUI, you’ll find property wrappers like @StateObject and @Binding. They all have something in common: making often used patterns easier to access.

Inspired by these built-in examples you can start thing about creating your own property wrappers. Another idea could be to create a wrapper for command-line actions:

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

Or for views that have their layouts defined in code:

final class MyViewController {
    @UsesAutoLayout
    var label = UILabel()
}

This last example I’m using often in my projects for views that are using auto-layout and require the translatesAutoresizingMaskIntoConstraints to be set to false. You can read more about this example in my blog post: Auto Layout in Swift: Writing Constraints Programmatically.

Conclusion

Property wrappers are a great way to remove boilerplate in your code. The above example is only one of many scenarios in which it can be useful. You can try it out yourself by finding repetitive code and replacing it with a custom wrapper.

If you like to learn more tips on Swift, check out the swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.

Thanks!