During WWDC 2019, Apple has unveiled an incredible amount of exciting new pieces of technology. Among them was a new release of the Swift language: Swift 5.1.
Don’t be fooled by its minor version number: this increment is actually packed with new features that will enable a whole new range of syntaxes within our codebases.
In this article, I want to show you examples of how two of those new features, Property Wrappers, and Function Builders, can be effectively implemented and leveraged.
If you’ve looked at some code samples from SwiftUI, you might have wondered at the fact that some properties are decorated with attributes such as
Those attributes are what Swift 5.1 calls Property Wrappers. The idea behind them is pretty simple: a Property Wrapper encapsulates a behavior, that will be triggered whenever the getter and setter of the property are called.
To better understand how they work, and how we can build our own Property Wrappers, let’s have look at an example.
Imagine that our code needs to deal with values that have an expiration date: whenever we try to access such values, we first need to check that it hasn’t expired. To do so, we are going to implement a Property Wrapper that we’ll call
First, we’re going to declare our wrapper as a generic
struct, and we'll decorate it with the attribute
@propertyWrapper lets the compiler know that we intend to use this struct as a Property Wrapper.
Then, we’re going to store some data within the struct, beginning with the lifetime and expiration date of the value. We’ll also implement a helper function, to the computer whether the value has expired:
After that, the next step is to satisfy the API required by a Property Wrapper. This API consists of a single property
var value: Value. We'll implement it as a computed property, and within its getter and setter, we'll perform the logic that manages the expiration date:
As you can see, the actual value is stored within a private property
innerValue, and its value is only returned if the associated expiration date has not yet been reached. You can also note that we added the constraint that the generic type
Value must conform to
ExpressibleByNilLiteral: this will ensure that this Property Wrapper can only be used to decorate optional properties.
And now, the time has finally come for us to use our new custom attribute
If we try and test how this
struct behaves, we'll see that, indeed, through the attribute
@Expirable, the expiration date associated with the value is managed in a way that is completely transparent to the programmer.
Once again, if you’ve looked at SwiftUI, there’s a good chance that you’ve stumbled across code such as this:
If you’re a seasoned Swift developer, you’ve definitely been wondering “Wait, how does this HStack thing actually gets build?”.
We can see that its initializer takes a trailing closure, but this closure seems to be returning two values
Text("rocks"), and that's just not how Swift closures work 🤔
As we might expect, there is no black magic at work, but rather the use of another new feature of Swift 5.1: Function Builders.
The idea behind Function Builders can be a little bit hard to grasp at first, but don’t worry: once you see it in action, it will all become clear.
Basically, Function Builders allow you to write functions inside which every top-level expressions are collected and merged together, resulting in a return value that aggregates them all.
To see how it works under the hood, we are going to implement a custom syntax that will allow us to write assertions using KeyPaths, as follows:
To begin, we’ll start by implementing a type that implements the data we want to aggregate, in our example it’s a type that encapsulates an assertion:
Then we’ll add to this type the ability to be combined with another assertion. The logic behind it is pretty trivial: we just no need to execute both assertions one after the other.
Finally, we’ll define a special value, that will represent an empty assertion. It will prove useful when we need to combine together a collection of assertion, as it will be the ideal candidate for an initial value — if you enjoy algebra, you might recognize that the type
Assertion actually implements a Monoid 🤓.
Then we need a way to build an
Assertion through the use of the operator
==. Fortunately, the way Swift deals with operators make it very easy to implement:
Now, we are almost there, the only thing left to do is to actually implement the Function Builder that will collect and combine our assertions. To do so, we first need to declare a new type and mark it with the attribute
Then, inside this type we need to write a static method that will implement how a collection of assertions should be aggregated into a single value:
As you can see, the code is pretty straightforward: we use
reduce(_:_:) along with our
combine(with:) method to merge a collection of assertions into a single value.
Now we are almost done, there is just one more thing left to do: implement the function
assert(), that will make use of the Function Builder we have just created:
As you can see, the attribute
@AssertBuilder is used to decorate the closure our function takes as a parameter. This indicates to the compiler that every top-level expression of type
Assertion within the closure must be collected, and aggregated together using the implementation of the Function Builder.
This is it, everything we needed has been implemented: we can now write assertions using our goal syntax, and it will build and run as we intended 🎉
As we’ve seen, Swift 5.1 delivered us two exciting new features: Property Wrappers and Function Builders. Together, they open the way to a whole new range of syntaxes that used to be impossible to implement.
However, be careful! Custom syntaxes share a common trait with custom operators: while they can deliver powerful features, they also notably increase the complexity of a codebase and make our code less predictable.
In this respect, when pondering whether you should be building your own Property Wrapper or Function Wrapper, you can ask yourself the following question:
Is your new custom syntax going to be used consistently across a large part of your codebase?
If you feel that the answer is no, then chances are you shouldn’t introduce a new syntax, and instead focus on implementing the feature you need through standard Swift syntax. On the other hand, if the answer is yes, then, by all means, go ahead!