Wrapping your head around Property Wrappers in Swift

Published by donnywals on

Property wrappers are a feature that was introduced in Swift 5.1 and they play a huge role in SwiftUI and Combine which are two frameworks that shipped alongside Swift 5.1 in iOS 13. The community was quick to create some useful examples that were embraced by folks relatively quickly.

As a user of property wrappers, you don't need to be concerned about what they are exactly, or how they work. All that you need to know is how you can use them. However, if you're curious how property wrappers work on the inside, this is just the post for you.

This week I would like to take a deep dive into property wrappers and take you on a journey to see how they work exactly.

The Swift evolution proposal

The proposal for property wrappers in Swift was a controversial proposal, to say the least. This feature was initially coined as "property delegates" but failed to make it through the initial review. It took three revisions of the initial proposal to get property wrappers accepted by the community.

Ultimately, the community settled on the fourth version of the property wrappers proposal.

Way before property wrappers were proposed, Joe Groff proposed a feature called Property Behaviors. This happened in 2016, three years before property wrappers were introduced.

What we can take from this is that property wrappers are a feature that has been a long time coming, and based on the length and thoroughness of the initial proposal I think it's safe to say that the design and ergonomics for property wrappers had some years to crystalize and mature before the final proposal got accepted and implemented for Swift 5.1.

Why do we need property wrappers?

If you look at property wrappers if you haven't read the Swift evolution proposal, you might wonder why we even need them. Property wrappers just make Swift look like Java and that's obviously not desirable (I'm joking, Java is a fine language).

The reason that the Swift team wanted to add property wrappers to the Swift language is to help facilitate common patterns that are applied to properties all the time. If you've ever marked a property as lazy, you've used such a pattern. The Swift compiler knows how to handle lazy, and all the code needed to expand your lazy keyword into code that actually makes your property lazy is hardcoded into the compiler.

Since there are many more of these patterns that can be applied to properties, hardcoding all of them wouldn't make sense. Especially since one of the goals of property wrappers is to allow developers to provide their own patterns in the form of property wrappers.

Let's back up and zoom in on the lazy keyword. I just mentioned that the compiler expands your code into other code that makes your property lazy. Let's take a look at what this would look like if we didn't have the lazy keyword, and we'd have to write a lazy property ourselves.

This example is taken directly from the Swift evolution proposal and slightly modified for readability:

struct MyObject {
  private var _myProperty: Int?

  var myProperty: Int {
    get {
      if let value = _myProperty { return value }
      let initialValue = 1738
      _myProperty = initialValue
      return initialValue
    }
    set {
      _myProperty = newValue
    }
  }
}

Notice that this code is far more verbose than just writing lazy var myProperty: Int?.

Capturing a large number of these patterns in the compiler isn't desirable, and it's also not very extensible. The Swift team wanted to allow themselves, and developers to use keywords to define their own patterns that are similar to lazy to help them clean up their code, and make their code more expressive.

Note that property wrappers do not allow developers to do otherwise impossible things. They merely allow developers to express patterns and intent using a more expressive syntax. Let's move on and look at an example.

Picking apart a property wrapper

A property wrapper that I use a lot is the @Published property wrapper from Combine. This property wrapper converts the property that it's applied to into a publisher that will notify subscribers when you change that property's value. This property wrapper is used like this:

class MyObject {
  @Published var myProperty = 10
}

Fairly straightforward, right?

To access the created publisher I need to use a $ prefix on myProperty. So $myProperty. The myProperty property points to the underlying value which is an Int with a value of 10 by default in this case. There's also a second prefix that can be applied to a property wrapper which is _, so _myProperty. This is a private property so it can only be accessed from within MyObject in this case but it tells us a lot about how property wrappers work. In the case of the MyObject example above, _myProperty is a Published<Int>. $myProperty is a Published.Publisher and myProperty is an Int. So that single line of code results in three different kinds of properties we can access. Let's define a custom property wrapper and find out what each of these three properties is, and what it does.

@propertyWrapper
struct ExampleWrapper<Value> {
  var wrappedValue: Value
}

This property wrapper is very minimal, and not very useful at all. However, it's good enough for us to explore the anatomy of a property wrapper.

First, notice that the ExampleWrapper struct has an annotation on the line before its definition: @propertyWrapper. This annotation means that the struct that's defined after it is a property wrapper. Also note that the ExampleWrapper is generic over Value. This Value is the type of the wrappedValue property.

Property wrappers don't have to be generic. You can hardcode the type of wrappedValue if needed. You could hardcode the wrappedValue if you want your property wrapper to only work for a specific type. Alternatively, you can constrain the type of Value if needed.

The wrappedValue property is required for a property wrapper. All property wrappers must have a non-static property called wrappedValue.

Let's put this ExampleWrapper to work:

class MyObject {
  @ExampleWrapper var myProperty = 10

  func allVariations() {
    print(myProperty)
    //print($myProperty)
    print(_myProperty)
  }
}

let object = MyObject()
object.allVariations()

Notice that I have commented out $myProperty. I will explain why in a moment.

When you run this code, you would see the following printed in Xcode's console:

10
ExampleWrapper<Int>(wrappedValue: 10)

myProperty still prints as 10. Accessing the property that's marked with a property wrapper directly will print the wrappedValue property of the property wrapper. When you print _myProperty, you access the property wrapper object itself. Notice that _myProperty is a member of MyObject. You can type self._myProperty and Swift will know what to do, even though you never explicitly defined _myProperty yourself. I mentioned earlier that _myProperty is private so you can't access it from outside of MyObject but it's there.

The reason is that the Swift compiler will take that @ExampleWrapper var myProperty = 10 line and convert in something else behind the curtain:

private var _myProperty: ExampleWrapper<Int> = ExampleWrapper<Int>(wrappedValue: 10)

var myProperty: Int {
  get { return _myProperty.wrappedValue }
  set { _myProperty.wrappedValue = newValue }
}

There are two things that we can learn from this example. First, you can see that property wrappers really aren't magic. They are actually relatively straightforward. This doesn't make them simple, or easy, but once you know that this conversion from a single definition is exploded into two separate definitions it suddenly becomes a lot easier to reason about.s

The _myProperty isn't some kind of magic value. It's a real member of MyObject that's created by the compiler. And myProperty returns the value of wrappedValue because it's hardcoded that way. Not by us, but by the compiler.

The _myProperty property is called the synthesized storage property. It's where the property wrapper which provides the storage for its wrapped value exists.

So where's $myProperty?

Not all property wrappers come with a $ prefixed flavor. The $ prefixed version of a property wrapped property is called the projected value. The projected value can be useful to provide a special, or different interface for a specific property wrapper like @Published does for example. To add a projected value to a property wrapper you must implement a projectedValue property on the property wrapper definition.

In MyExampleWrapper this would look as follows:

@propertyWrapper
struct ExampleWrapper<Value> {
  var wrappedValue: Value

  var projectedValue: Value {
    get { wrappedValue }
    set { wrappedValue = newValue }
  }
}

This example isn't useful at all, I will show you a more useful example in the next section. For now, I want to show you the anatomy of a property wrapper without any bells and whistles.

If you'd use this property wrapper like before, Swift will generate the following code for you:

private var _myProperty: ExampleWrapper<Int> = ExampleWrapper<Int>(wrappedValue: 10)

var myProperty: Int {
  get { return _myProperty.wrappedValue }
  set { _myProperty.wrappedValue = newValue }
}

var $myProperty: Int {
  get { return _myProperty.projectedValue }
  set { _myProperty.projectedValue = newValue }
}

An extra property is created that uses the private _myProperty's projectedValue for it's get and set implementations.

Since _myProperty is private, your projected value might provide direct access to the property wrapper which is one of the examples shown in the original property wrapper proposals. Alternatively, you could expose a completely different object as your property wrapper's projected value. It's up to you to make this choice. The @Published property wrapper uses its projectedValue to expose a publisher.

Implementing a property wrapper

I have already shown you how to define a simple property wrapper, but let's be honest. That example was boring and kind of bad. In this section, we'll look at implementing a custom property wrapper that mimics the behavior of Combine's @Published property wrapper. If you want to learn about Combine I have several posts about it in the Combine section of this blog.

Let's define the basics first:

@propertyWrapper
struct DWPublished<Value> {
  var wrappedValue: Value
}

This defines a property wrapped that wraps any kind of value. That's good. The goal here is to implement a projected value that exposes some kind of publisher. I will use a CurrentValueSubject for this. Whenever wrappedValue gets a new value, the CurrentValueSubject should emit a new value to its subscribers. A basic implementation might look like this:

@propertyWrapper
class DWPublished<Value> {
  var wrappedValue: Value {
    get { subject.value }
    set { subject.value = newValue }
  }

  private let subject: CurrentValueSubject<Value, Never>

  var projectedValue: CurrentValueSubject<Value, Never> {
    get { subject }
  }

  init(wrappedValue: Value) {
    self.subject = CurrentValueSubject(wrappedValue)
  }
}

Warning:
This implementation is very basic and should not be used as a reference for how @Published is actually implemented. I'm sure there might be bugs with this code. My goal is to help you understand how property wrappers work. Not to show you a perfect custom @Published property wrapper.

This code is vastly different from what you've seen before. The wrappedValue used the private subject to implement its get and set. This means that the wrapped value is always in sync with the subject's current value.

The projectedValue only has it's get specified. We don't want users of this property wrapper to assign anything to projectedValue; it's read-only.

When a property wrapper is initialized in its simplest form, it receives its wrapped value. The wrapped value passed to DWPublished is used to set up the subject with the value we're supposed to wrap as its initial value.

Using this property wrapper would look like this:

class MyObject {
  @DWPublished var myValue = 1
}

let obj = MyObject()
obj.$myValue.sink(receiveValue: { int in
  print("received int: \(int)")
})

obj.myValue = 2

The printed output for this example would be:

received int: 1
received int: 2

Pretty neat, right?

Since the property wrapper's projected value is a CurrentValueSubject, it has a value property that we can assign values to. If I'd do this, the property wrapper's wrappedValue is also updated because the CurrentValueSubject is used to drive the wrappedValue of my property wrapper.

obj.$myValue.value = 3
print(obj.myValue) // 3

This is something that's not possible with the @Published property wrapped because Apple exposes the projectedValue for @Published as a custom type called Published.Publisher instead of a CurrentValueSubject.

A more complicated property wrapper might take some kind of configuration, like a maximum or minimum value. Let's say I want to expand my @DWPublished property wrapper to limit its output by debouncing it. I would like to write the following code in MyObject to configure this:

class MyObject {
  @DWPublished(debounce: 0.3) var myValue = 1
}

This would debounce my published values with 300 milliseconds. We can update the initializer for DWPublished to accept this argument, and refactor the code a little bit:

@propertyWrapper
class DWPublished<Value> {
  var wrappedValue: Value {
    get { subject.value }
    set { subject.value = newValue }
  }

  private let subject: CurrentValueSubject<Value, Never>
  private let publisher: AnyPublisher<Value, Never>

  var projectedValue: AnyPublisher<Value, Never> {
    get { publisher }
  }

  init(wrappedValue: Value, debounce: DispatchQueue.SchedulerTimeType.Stride) {
    self.subject = CurrentValueSubject(wrappedValue)
    self.publisher = self.subject
      .debounce(for: debounce, scheduler: DispatchQueue.global())
      .eraseToAnyPublisher()
  }
}

The initializer for my property wrapper now accepts the debounce interval and uses this interval to create an all-new publisher that debounces my CurrentValueSubject. I erase this publisher to AnyPublisher so I have a nice type for my publisher instead of Publishers.Debounce<CurrentValueSubject<Value, Never>, S> where S : Scheduler which would be the type of my publisher if I didn't erase it.

My property wrapper's wrappedValue still shadows subject.value. The projectedValue now uses the debounced publisher for its get instead of the CurrentValueSubject.

Using this property wrapper now looks as follows:

class MyObject {
  @DWPublished(debounce: 0.3) var myValue = 1
}

var cancellables = Set<AnyCancellable>()

let obj = MyObject()
obj.$myValue
  .sink(receiveValue: { int in
    print("received int: \(int)")
  })
  .store(in: &cancellables)

obj.myValue = 2
obj.myValue = 3
obj.myValue = 4

If you would run this in a playground, only received int: 4 should be printed. That's the debouncer at work and it's exactly what I wanted to happen.

Note that because the property wrapper's projected value is now an AnyPublisher, it's no longer possible to assign new values using $myValue.value like we could before.

In summary

In this week's post, you saw how property wrappers work internally, and what happens when you use a property wrapper. I showed you that the Swift compiler generates code on your behalf and that a property wrapper is far from magic. Swift generates a _ prefixed private property that's an instance of your property wrapper, a $ prefixed property that shadows the private property's projectedValue property, and that the original property shadows the wrappedValue property of the _ prefixed private property.

Once you understand this, you can quickly see how property wrappers work and how they might be implemented. I demonstrated this by implementing my own version of Combine's @Published property wrapper. After that, I showed you how to create a property wrapper that can be configured with extra arguments by expanding your property wrapper's initializer.

I hope that you have a much clearer picture of how property wrappers work now and that using them feels less like magic. For questions or feedback, I would love to hear from you on Twitter.


Practical Combine

Learn everything you need to know about Combine and how you can use it in your projects with my new book Practical Combine. You'll get thirteen chapters, a Playground and a handful of sample projects to help you get up and running with Combine as soon as possible.

The book is available as a digital download for just $24.99!

Get Practical Combine

Receive weekly updates about my posts

Categories: Swift