/ SWIFT

The Complete Guide to Property Wrappers in Swift 5

Swift properties often contain extra code in their get and set methods. Say, when you want to observe property changes, make it atomic or persist in user defaults. Some patterns, like lazy and @NSCopying, are already baked into the compiler. However, this approach doesn’t scale well.

Property wrapper is the Swift language feature that allows us to define a custom type, that implements behavior from get and set methods, and reuse it everywhere. In this article let’s study everything about property wrappers:

  • Which problems do they solve?
  • How to implement a property wrapper?
  • How to access a property wrapper, its wrapped value, and projection?
  • How we can utilize property wrappers in our code?
  • How property wrappers are synthesized by the Swift compiler?
  • Which restrictions do property wrappers impose?

Understanding Property Wrappers

To better understand property wrappers, let’s follow an example to see which problems do they solve. Say, we want to add extra logging to our app. Every time a property changes, we print its new value to the Xcode console. It’s useful when chasing a bug or tracing the flow of data. The straightforward way of doing this is by overriding a setter:

struct Bar {
    private var _x = 0
    
    var x: Int {
        get { _x }
        set {
            _x = newValue
            print("New value is \(newValue)") 
        }
    }
}

var bar = Bar()
bar.x = 1 // Prints 'New value is 1'

If we continue logging more properties like this, the code will become a mess soon. Instead of duplicating the same pattern over and over for every new property, let’s declare a new type, which does the logging:

struct ConsoleLogged<Value> {
    private var value: Value
    
    init(wrappedValue: Value) {
        self.value = wrappedValue
    }

    var wrappedValue: Value {
        get { value }
        set { 
            value = newValue
            print("New value is \(newValue)") 
        }
    }
}

Here is how we can rewrite Bar to be using ConsoleLogged:

struct Bar {
    private var _x = ConsoleLogged<Int>(wrappedValue: 0)
    
    var x: Int {
        get { _x.wrappedValue }
        set { _x.wrappedValue = newValue }
    }
}

var bar = Bar()
bar.x = 1 // Prints 'New value is 1'

Swift provides first-class language support for this pattern. All we need to do is add the @propertyWrapper attribute to our ConsoleLogged type:

@propertyWrapper
struct ConsoleLogged<Value> {
    // The rest of the code is unchanged
}

You can think of property wrapper as a regular property, which delegates its get and set to some other type.

At the property declaration site we can specify which wrapper implements it:

struct Bar {
    @ConsoleLogged var x = 0
}

var bar = Bar()
bar.x = 1 // Prints 'New value is 1'

The attribute @ConsoleLogged is a syntactic sugar, which translates into the previous version of our code.

Implementing a Property Wrapper

There are two requirements for a property wrapper type [1]:

  1. It must be defined with the attribute @propertyWrapper.
  2. It must have a wrappedValue property.

Here is how the simplest wrapper looks:

@propertyWrapper
struct Wrapper<T> {
   var wrappedValue: T
}

We can now use the attribute @Wrapper at the property declaration site:

struct HasWrapper {
    @Wrapper var x: Int
}

let a = HasWrapper(x: 0)

We can pass a default value to the wrapper in two ways:

struct HasWrapperWithInitialValue {
    @Wrapper var x = 0 // 1
    @Wrapper(wrappedValue: 0) var y // 2
}

There is a difference between the two declarations:

  1. The compiler implicitly uses init(wrappedValue:) to initialize x with 0.
  2. The initializer is specified explicitly as a part of an attribute.

Accessing a Property Wrapper

It’s often useful to provide extra behavior in a property wrapper:

@propertyWrapper
struct Wrapper<T> {
    var wrappedValue: T

    func foo() { print("Foo") }
}

We can access the wrapper type by adding an underscore to the variable name:

struct HasWrapper {
    @Wrapper var x = 0

    func foo() { _x.foo() }
}

Here _x is an instance of Wrapper<T>, hence we can call foo(). However, calling it from the outside of HasWrapper will generate a compilation error:

let a = HasWrapper()
a._x.foo() // ❌ '_x' is inaccessible due to 'private' protection level

The reason for that is that the synthesized wrapper has a private access control level. We can overcome this by using a projection.

A property wrapper may expose more API by defining a projectedValue property. There any no restrictions on the type of projectedValue.

@propertyWrapper
struct Wrapper<T> {
    var wrappedValue: T

    var projectedValue: Wrapper<T> { return self }

    func foo() { print("Foo") }
}

Dollar sign is the syntactic sugar to access the wrapper’s projection:

let a = HasWrapper()
a.$x.foo() // Prints 'Foo'

In summary, there are three ways to access a wrapper:

struct HasWrapper {
    @Wrapper var x = 0
    
    func foo() {
        print(x) // `wrappedValue`
        print(_x) // wrapper type itself
        print($x) // `projectedValue`
    }
}

Behind the Scenes

Let’s dig one level deeper and find out how property wrappers are synthesized on the Swift compiler level:

  1. Swift code is parsed into the expressions tree by lib/Parse.
  2. ASTWalker traverses the tree and builds ASTContext out of it. Specifically, when the walker finds a property with the @propertyWrapper attribute, it adds this information to the context for later use.
  3. After ASTContext has been evaluated, it is now able to return property wrapper type info via getOriginalWrappedProperty() and getPropertyWrapperBackingPropertyInfo(), which are defined in Decl.cpp.
  4. During the SIL generation phase, the backing storage for a property with an attached wrapper is generated SILGen.cpp.

Here you can learn more about the compilation process: Understanding Xcode Build System.

Usage Restrictions

Property wrappers come not without their price. They impose a number of restrictions [1]:

  • Property wrappers are not yet supported in top-level code (as of Swift 5.1).
  • A property with a wrapper cannot be overridden in a subclass.
  • A property with a wrapper cannot be lazy, @NSCopying, @NSManaged, weak, or unowned.
  • A property with a wrapper cannot have custom set or get method.
  • wrappedValue, init(wrappedValue:) and projectedValue must have the same access control level as the wrapper type itself.
  • A property with a wrapper cannot be declared in a protocol or an extension.

Usage Cases

Property wrappers have a number of usage scenarios, when they really shine. Several of them are built into the SwiftUI framework: @State, @Published, @ObservedObject, @EnvironmentObject and @Environment. The others have been widely used in the Swift community:

Summary

Property wrappers is a powerful Swift 5 feature, that adds a layer of separation between code that manages how a property is stored and the code that defines a property [3].

When deciding to use property wrappers, make sure to take into account their drawbacks:

  • Property wrappers have multiple language constraints, as discussed in Usage Restrictions section.
  • Property wrappers require Swift 5.1, Xcode 11 and iOS 13.
  • Property wrappers add even more syntactic sugar to Swift, which makes it harder to understand and raises entrance barrier for newcomers.