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.

Adam Waite
Sep 26 · 5 min read
Vincent Pradeilles presents Property Wrappers or How Swift decided to become Java

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 presents dogs (because, dogs):

Dog model

We have an API client interface APIType that we’ll use to fetch dogs from a remote service (perhaps the dog-api):

APIType interface

And we have a view model that will drive a view controller with some dogs to display:

DogsViewModel will drive a view controller (we might handle errors in production, if we feel like it)

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).

DogViewModelTests

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:

DogViewModelTests this time appending invocations in an Array

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.

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:

  1. Be defined with the attribute @propertyWrapper
  2. Have a property named wrappedValue

Let’s implement @Spy as a struct:

A property wrapper called Spy

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:

Adding behaviour in Spy wrappedValue setter

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.

Implementing projectedValue to expose the property wrapper

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:

Using the property wrapper in the tests to track call history

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!


Chip

We’re the team behind Chip, the AI savings app. We’re automating your money stuff, so you can make the things you want in life happen. getchip.uk

Thanks to Jack Tudor

Adam Waite

Written by

Chip

Chip

We’re the team behind Chip, the AI savings app. We’re automating your money stuff, so you can make the things you want in life happen. getchip.uk

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade