Property Wrappers in Swift 5.1
An introduction to one of Swift 5.1’s powerful new features: Property Wrappers. We implement a property wrapper to improve a common pattern we’re faced with when writing unit tests.
Chip sent the Apps Team to NSSpain last week. All of the talks were excellent but my personal favourite was “Property Wrappers or How Swift decided to become Java” by Vincent Pradeilles.
In his talk Vincent introduced a new feature in Swift 5.1 that perhaps got overshadowed in the excitement of SwiftUI and Combine: property wrappers.
Property wrappers are comparable to Kotlin’s annotations. Simply put, property wrappers encapsulate read and write access to a property while adding extra behaviour. We might use a property wrapper, for example, to read and persist values to UserDefaults
when accessed.
If you’ve played with SwiftUI, then you might have written something along the lines of:
@State var labelText: String = "Make it happen!"
That @State
bit, that’s a property wrapper. It adds behaviour to var labelText
so that whenever it’s mutated any associated bindings are updated to reflect the change.
Implementing a property wrapper
Let’s discover the power of property wrappers by building our own. Our property wrapper will address a common pattern we’re faced when writing unit tests.
As well behaved iOS developers, we strive to use dependency injection techniques to promote clear separation of concerns, modularity and highly testable code. In a unit test we might inject a dependency in the form of a test double and later verify it was called with the expected arguments. We might also verify the count of calls. This is often called a spy.
Our app: Dog Browser
Our app presents dogs (because, dogs):
We have an API client interface APIType
that we’ll use to fetch dogs from a remote service (perhaps the dog-api):
And we have a view model that will drive a view controller with some dogs to display:
Testing
Let’s test DogsViewModel
. We want our test to make sure that when we invoke getDogs
on our subject, our injected API is instructed to fetch dogs at the correct URL (inferred from breed
).
As previously mentioned, in a unit test we’ll often want to count how many times a dependency is invoked, and with what arguments. Below we’ve added this behaviour by making getCalledWithURL
an Array
, and appending each call:
Having invocations tracked in an array allows us to verify multiple calls to the stub, but that’s going to get tedious when most of the time we’ll just want to verify the latest call.
It’s tedious because we’d have to pull values out of the collection to assert on them (e.g XCTAssert(testAPI.getCalledWithURL.last?,...
). We could split them into two variables: latestGetCalledWithURL: URL
and getCalledWithURLHistory: [URL]
, but doing that for every variable in every test creates duplication.
Enter @Spy, our @propertyWrapper!
We want to add behaviour to getCalledWithURL
to track set
history so that we can assert the count and ordering of mutations. At the same time we don’t want to have to deal with pulling the latest value out of an array type. We’re going to implement a property wrapper called @Spy
to do just this.
To be a property wrapper a type must:
- Be defined with the attribute
@propertyWrapper
- Have a property named
wrappedValue
Let’s implement @Spy
as a struct
:
We can now decorate getCalledWithURL
with @Spy
:
@Spy var getCalledWithURL: URL? = nil
As if by magic the compiler has synthesised that as a private variable holding the property wrapper (prefixed with an underscore), and a property that forwards any getter and setter calls to the wrapper (going through Spy’s implementation).
i.e @Spy var getCalledWithURL: URL? = nil
is synthesised as:
private var _getCalledWithURL = Spy<URL?>(wrappedValue: nil)var getCalledWithURL: URL? {
get { return _getCalledWithURL.wrappedValue }
set { _getCalledWithURL.wrappedValue = newValue }
}
This means that you’ll be able to access _getCalledWithURL
inside the scope of the defining type without doing anything.
Let’s implement a history and set count to Spy
:
By updating the wrappedValue
setter, every time getCalledWithURL
is mutated the spy is updated with a record of the set in the history
variable. The computed property count
will return the number of times that the variable has been mutated.
It took me a long time to figure this one out…but we can expose the property wrapper to the outside world by implementing projectedValue
.
When projectedValue
is present, a new variable prefixed with $
is synthesised exposing the property wrapper:
public var $getCalledWithURL: Spy<URL?> {
get { _getCalledWithURL.projectedValue }
set { _getCalledWithURL.projectedValue = newValue }
}
We’re now ready to use the property wrapper in our tests:
Going forward, any variable annotated with @Spy
will hold a history of any mutations and an associated count. They’re accessible through $getCalledWithURL
.
Since we have defined Spy<Value>
ourselves we can add infinite behaviour. We might for example want to add a log of time stamps, the thread the set was performed on, or even a time delta if we’re implementing throttle behaviour.
Conclusion
Property wrappers are a powerful new feature in Swift 5.1. By implementing @propertyWrapper
we can decorate variables with additional behaviour on read and write. Property wrappers come with great potential for improving safety, reducing complexity and removing boilerplate code.
Enjoy!