Property wrappers in Swift with code examples

The translation of the article was prepared as part of the online course “iOS Developer. Professional”… If you are interested in learning more about the course, come to Open Day online.


Property Wrappers in Swift allow you to extract common logic into a separate wrapper object. Since its introduction during WWDC 2019 and coming to Xcode 11 with Swift 5, there have been many examples that have been shared in the community. This is a nifty addition to the Swift library, allowing you to remove a lot of boilerplate code that we probably all wrote in our projects.

A history about property wrappers can be found on the Swift forums for SE-0258… While the desirability of using them mostly suggests that property wrappers are the solution to @NSCopying properties, there is a general pattern that is realized by them, and you will probably soon find out everything.

What is a property wrapper?

A property wrapper can be thought of as an extra layer that defines how the property is stored or computed as it is read. This is especially useful for replacing duplicate code in property getters and setters.

Typical examples are custom default properties, in which custom getters and setters are used to transform the value appropriately. An example implementation might look like this:

extension UserDefaults {

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

Operator @UserDefault makes a call to the wrapper of the property. As you can see, we can give it several parameters that are used to customize the wrapper of the property. There are several ways to interact with a property wrapper, such as using a wrapped and predicted value. You can also customize the wrapper with embedded properties, which we’ll talk about later. Let’s first look at an example of a User Defaults property wrapper.

Property Wrappers and UserDefaults

The following code shows a pattern that is easy to recognize. It creates a wrapper around the object UserDefaultsto make properties available without having to embed string keys everywhere in 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 user defaults from anywhere as follows:

UserDefaults.standard.hasSeenAppIntroduction = true

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

This seems like a great solution, but it can easily turn into a large file with many keys and properties set. The code is repetitive and so it begs a way to make it easier. Custom wrapping properties using keyword @propertyWrapper can help us solve this problem.

Using property wrappers to remove boilerplate code

Taking the above example, we can rewrite the code and remove a lot of unnecessary things. To do this, we need to create a new wrapper property that we will call UserDefault. This will ultimately allow us to define it as the default user property.

If you are using SwiftUI you might be better off using a property wrapper AppStorage… Let’s just consider this 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 you to pass a default value if there is no registered value yet. We can pass any value as the wrapper is defined by the generic Value.

Now we can modify our previous code implementation and create the following extension for the type UserDefaults:

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 wrapper of the property being defined. We pass in the same key as before and set the default to false. Using this new property is very simple:

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

In some cases, you will want to define your own custom default values. For example, in cases where you have an application group defining custom defaults. Our installed wrapper uses the standard custom default options by default, but you can override them 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 additional properties with a single wrapper

Unlike the old solution, it is very easy to add additional properties when using a wrapper. We can simply reuse a specific 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 on any type you define, as long as that type is supported for persistence in user defaults.

Storing Optionals with the Default User Properties Wrapper

A common problem that you can run into when using property wrappers is that a common value allows you to define either all options or all values ​​without the wrapper. There is a common technique in the community to solve this problem that uses a custom protocol AnyOptional:

/// Allows to match for optionals with generics that are defined as non-optional.
public protocol AnyOptional {
    /// Returns `true` if `nil`, otherwise `false`.
    var isNil: Bool { get }
}
extension Optional: AnyOptional {
    public var isNil: Bool { self == nil }
}

We can extend our property wrapper UserDefaultto comply with this protocol:

extension UserDefault where Value: ExpressibleByNilLiteral {
    
    /// Creates a new User Defaults property wrapper for the given key.
    /// - Parameters:
    ///   - key: The key to use with the user defaults store.
    init(key: String, _ container: UserDefaults = .standard) {
        self.init(key: key, defaultValue: nil, container: container)
    }
}

This extension creates an additional initializer that removes the requirement to define a default value and allows for options.

Finally, we need to customize our wrapper value setter to allow objects to be removed from the custom defaults:

@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 {
            // Check whether we're dealing with an optional and remove the object if the new value is nil.
            if let optional = newValue as? AnyOptional, optional.isNil {
                container.removeObject(forKey: key)
            } else {
                container.set(newValue, forKey: key)
            }
        }
    }

    var projectedValue: Bool {
        return true
    }
}

This now allows us to define optionals and take values ​​equal to zero:

extension UserDefaults {

    @UserDefault(key: "year_of_birth")
    static var yearOfBirth: Int?
}

UserDefaults.yearOfBirth = 1990
print(UserDefaults.yearOfBirth) // Prints: 1990
UserDefaults.yearOfBirth = nil
print(UserDefaults.yearOfBirth) // Prints: nil

Excellent! We can now deal with all the scripts with a custom default wrapper. The last thing to add is the predicted value, which will be converted to Combine publisher, just like in the property wrapper @Published

Predicting a value from a property wrapper

Property wrappers have the ability to add one more property besides the wrapped value, which is called the predicted value. In this case, we can predict a different value based on the wrapped value. A typical example is using publisher Combineso that we can watch the changes as they happen.

To do this with a property wrapper user defaults, we have to add publisherthat will be the subject of the pass-through. It’s all about the name: it will simply transmit changes in values. The implementation looks like this:

import Combine
 
 @propertyWrapper
 struct UserDefault<Value> {
     let key: String
     let defaultValue: Value
     var container: UserDefaults = .standard
     private let publisher = PassthroughSubject<Value, Never>()
     
     var wrappedValue: Value {
         get {
             return container.object(forKey: key) as? Value ?? defaultValue
         }
         set {
             // Check whether we're dealing with an optional and remove the object if the new value is nil.
             if let optional = newValue as? AnyOptional, optional.isNil {
                 container.removeObject(forKey: key)
             } else {
                 container.set(newValue, forKey: key)
             }
             publisher.send(newValue)
         }
     }

     var projectedValue: AnyPublisher<Value, Never> {
         return publisher.eraseToAnyPublisher()
     }
 } 
We can now start 

Now we can start observing changes in our object as follows:

let subscription = UserDefaults.$username.sink { username in
     print("New username: (username)")
 }
 UserDefaults.username = "Test"
 // Prints: New username: Test 

It is wonderful! This allows us to react to any changes. Since we previously defined our property statically, this publisher will now work throughout our application. If you want to know more about Combine, be sure to check out my article Getting started with the Combine framework in Swift

Defining Sample Files Using Property Wrapping

The above example focuses heavily on user defaults, but what if you wanted to define a different wrapper? Let’s take a look at another example that will hopefully give you some ideas.

Let’s take the following property wrapper where 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 sample files that we might need for debugging or when running tests:

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

Property projectedValue allows us to read the filename used in the property wrapper:

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

This can be useful when you want to know what initial value (s) were used by the wrapper to compute the final value. Note that here we are using a dollar sign as a prefix to access the predicted value.

Accessing certain private properties

While it is not recommended to work with property wrappers in this way, in some cases it may be useful to read specific wrapper properties. I’ll just demonstrate that this is possible, and you might want to rethink your code implementation if you need to access private properties.

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

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

Be skeptical about this and see if you can solve your problems using other solutions.

Other use cases

Property wrappers are also used in the standard Swift APIs. Especially in SwiftUI you will find property wrappers like @StateObject and @Binding… They all have something in common: easier access to commonly used templates.

Inspired by these built-in examples, you can start thinking about creating your own property wrappers. Another idea might be to create a wrapper for doing things on the command line:

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

Or for views whose layouts are defined in code:

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

This last example I use a lot in my projects for views that use auto-layout and require that translatesAutoresizingMaskIntoConstraints was installed in false… You can read more about this example in my blog post: Automatic Layout in Swift: Writing Constraints Programmatically

Conclusion

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

For more Swift tips, take a look at swift category page… Do not be shy contact with me or write to me in Twitterif you have additional recommendations or feedback. Thank you!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *