Swift Property Wrappers Explained
Published on: June 8, 2020Property 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.
Property wrappers look a lot like macros, but they're not the same thing. If you'd like to learn more about their differences, take a look at my comparison post here.
Why do we need property wrappers?
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.