Wrapping your head around Property Wrappers

Property Wrappers, which were introduced in Swift 5.1, are a layer around instance properties to provide custom logic when accessing them. Before their availability, if we wanted to execute any additional code at the time the properties were accessed, we would use custom getters/setters or property accessors such as willSet/didSet. This approach works well enough but falls flat on its face when we want to reuse the logic elsewhere.

This is where property wrappers shine. Consider the basic yet contrived case of wanting to log something to the console whenever a property is read from or written to. The code would be simple with a getter/setter.

struct Person {
  private let _name: String
  var name: String {
    get {
      print("Read name")
      return _name
    }
    set {
      print("Write to name")
      _name = newValue
    }
  }
}

As you can see in the above example, we resorted to using a computed property with a backing property. When we use a property wrapper, we would use a somewhat similar pattern. 

Furthermore, a property wrapper is itself a class or struct. This makes them a lot more powerful and flexible than a simple getter/setter. In this article, we will look at some basic code examples about how to create a property wrapper, how to use a property wrapper for an instance property, and the behind the scenes compiler magic that brings the feature together.

Syntax

Let’s continue with our silly example of printing to the console when a property is accessed. This will allow us to focus on the syntax. With a property wrapper, the Person class looks like this:

struct Person {
  @PersonWrapper var name: String 
}

We annotate a regular property – name in this case – with an attribute called @PersonWrapper. The @ is required and it is immediately followed by the type which acts as the wrapper. Note that name cannot be a let constant. Because it is now essentially a computed property with no storage for the value it represents.

What remains is to create the wrapper type itself. It can either be a struct or a class.

@propertyWrapper // 1
struct PersonWrapper {
  let wrappedValue: String // 2
}

That right there is a property wrapper stripped to the bare minimum. We introduce two things:

  1. The @propertyWrapper attribute, which marks this as a property wrapper.
  2. The wrappedValue property, which is externally accessible and has the same type as the value it wraps –String in our case.

As you may have guessed, the wrappedValue is what that actually provides the storage for the name property. 

Compiler Magic

During compilation, the person class is transformed to something like below:

struct Person {
  private var _name: PersonWrapper
  
  var name: String {
    return _name.wrappedValue
  }
}

A new private property_name is created with the property wrapper type. It is always private and it always has an underscore prefixed to the name of the wrapped property. You can, in fact, refer to this private property from code within the Person class.

Next, the wrapped property is converted to a computed property, just forwarding to the earlier wrappedValue. In this particular case, wrappedValue happened to be a let constant, hence we only gain a getter. If it were a var, a setter would also have been created for us.

Let’s make the property wrapper slightly more useful, by printing messages to the console when accessed. Rewrite the property wrapper.

@propertyWrapper
struct PersonWrapper {
  private var storage: String // property can be called anything
  
  var wrappedValue: String {
    get {
      print("Property read")
      return storage
    }
    set {
      print("Property written to")
      storage = newValue
    }
  }
}

All we did is move our earlier getter/setter code to the property wrapper. Now, this totally useless piece of code can be reused.

Default values and initializers

Can we give default values to our wrapped properties? Yes, we can!

@PersonWrapper var name: String = "Mayur"

There is one requirement, though. The property wrapper should have an initializer – either explicit or synthesized – of the form init(wrappedValue:). This initializer is used to initialize the_name property when a default value is provided to the name property like shown above.

This initializer is also going to be used from the within the Person struct’s default memberwise initializer.

init(name: String) {
  self._name = PersonWrapper(wrappedValue: name)
}

In the absence of init(wrappedValue:), the synthesized initializer expects a PersonWrapper, not a String.

init(name: PersonWrapper) {
  self._name = name
}

This should be enough to get you comfortable with the basic syntax, and why it has been designed that way.

A more useful example

Let’s build a more useful property wrapper, one which backs a property wrapper to the user defaults. To start off, our property wrapper supports only Ints.

@propertyWrapper
struct Defaults {
    
  private let key = "defaultsKey"
    
  var wrappedValue: Int {
    get {
      UserDefaults.standard.value(forKey: key) as? Int ?? 0
    }
        
    set {
      UserDefaults.standard.set(newValue, forKey: key)
    }
  }
    
  init(wrappedValue: Int) {
    UserDefaults.standard.set(newValue, forKey: key)
  }
}

struct Person {
  @Defaultsvar age: Int
}

While it is slightly better than our earlier contrived example, it still has some major problems.

  • It only works with Integers
  • The same key defaultsKey is used for all instances of the wrapper. This means values will be overwritten when this wrapper is reused.

Let’s solve the first problem using generics.

@propertyWrapper
struct Defaults<Value> {
  private let key = "defaultsKey"
  private let defaultValue: Value
    
  var wrappedValue: Value {
    get {
        UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue
    }
        
    set {
        UserDefaults.standard.set(newValue, forKey: key)
    }
  }
    
  init(wrappedValue: Value) {
    self.defaultValue = wrappedValue
  }
}

struct Person {
  @Defaults var age: Int
}

wrappedValue is now a generic Value, which is inferred to be Int when used in Person. We introduced a defaultValue, which is going to be used when a value is not found in the defaults. 

Let’s solve the second problem by making the key configurable.

@propertyWrapper
struct Defaults<Value> {
  private let key: String
  private let defaultValue: Value
    
  var wrappedValue: Value {
    get {
        UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue
    }
        
    set {
        UserDefaults.standard.set(newValue, forKey: key)
    }
  }
    
  init(key: String, defaultValue: Value) {
    self.key = key
    self.defaultValue = defaultValue
  }
}

struct Person {
  @Defaults var age: Int
}

Note how we no longer have the init(wrappedValue:) initializer. If this property wrapper has to be valuable, it needs to be initialized with 2 values. Luckily for us, this type of initialization is supported at the usage site, like so:

@Defaults(key: "custom key", defaultValue: 10) var age: Int

Remember how we forced the use of the init(wrappedValue) initializer earlier by assigning with the = sign. It turns out that it is syntactic sugar for @Defaults(wrappedValue: 10).

This completes the implementation of our Defaults wrapper. We can even have multiple properties that use it, with each getting its own instance of the Defaults wrapper.

struct Person {
  @Defaults(key: "key01", defaultValue: 10) var age: Int
  @Defaults(key: "key02", defaultValue: 20) var weight: Int
}

Conclusion

We started with understanding one of the motivating factors of property wrappers. We then saw how a basic property wrapper can be created. We understood why the syntax is designed the way it is and how the compiler works some of its magic. We then capped it off by creating a useful property wrapper with more advanced property wrapper initialization.

In future articles, we will see more use cases for property wrappers.

1 Comment

Leave a Reply