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
andset
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]:
- It must be defined with the attribute
@propertyWrapper
. - 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:
- The compiler implicitly uses
init(wrappedValue:)
to initializex
with0
. - 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 ofprojectedValue
.
@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:
- Swift code is parsed into the expressions tree by lib/Parse.
- 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. - After ASTContext has been evaluated, it is now able to return property wrapper type info via
getOriginalWrappedProperty()
andgetPropertyWrapperBackingPropertyInfo()
, which are defined in Decl.cpp. - 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
, orunowned
. - A property with a wrapper cannot have custom
set
orget
method. wrappedValue
,init(wrappedValue:)
andprojectedValue
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.
Thanks for reading!
If you enjoyed this post, be sure to follow me on Twitter to keep up with the new content. There I write daily on iOS development, programming, and Swift.