If you're keeping up with the new features of Swift, you've probably heard of Property Wrappers. They were introduced a few months ago as part of Swift 5.1 and we've covered their basics in a previous article.
Today, we're going to take things a bit further by exploring an unexpected use case of Property Wrappers!
Now, the name Property Wrapper can be slightly deceiving, because when we read Property, we intuitively think of data.
And this makes sense, because when we write code in Swift our primary use case for properties is to store data. However, we shouldn't forget that, in Swift, functions types are first-class citizen. Meaning that the signature of a function is a type with the same features than any other Swift type. Consequently, it is perfectly legal to store a function inside a property, as follows:
undefined
This fact bears a very interiesting consequence: since functions can be stored inside properties, it means that Property Wrappers could also be made to work with functions 🎉
So let us take a look at the kind of construct this approach could enable us to build.
As an example, we'll be implementing something that is rather straightforward to understand: a caching mechanism.
To begin, we are going to define a struct
called Cached
:
undefined
For the moment, this struct
does nothing more than storing a function. To turn it into a Property Wrapper, we'll decorate it with the attribute @propertyWrapper
:
undefined
The compiler is now going to ask us to implement a wrappedValue
:
undefined
Its getter is straightforward to implement: we just return our stored function. The actual caching logic is going to be implemented inside the setter:
undefined
So what's happening inside this setter? First we declare the cache that will store the results of our computations. To keep things simple, we'll use a Dictionary
, with a Key
and Value
that respectively match the Input
and Output
of the function we want to cache.
Then, we'll assign a value to our cachedFunction
. The logic here is simple: when the cachedFunction
is called, we take its argument and look it up in our cache
. If we find a match, we return the value we had previously stored. Otherwise, we perform the computation, store the result in the cache, and return it.
(You might be wondering why the variable cache
is defined inside the setter and not as a private var
of the struct
. The explanation is simple: the cache
is being mutated inside an escaping closure, which is not allowed for properties of a struct
. By defining it inside the setter, we avoid this issue.)
Now that our wrapper has been implemented, we can try it out:
undefined
And if we run this code, we'll indeed see that our caching mechanism is working as expected 🎉
undefined
We are now able to define custom wrappers that will decorate pieces of our code and bring them additional behavior in a very seamless way! To some extent, we could say that we've built our first Function Wrapper 🚀
In this article, we took the example of implementing a caching mechanism, but we could cover many more use cases: for instance, we could implement wrappers like @Delayed(delay: 0.3)
and @Debounced(delay: 0.3)
, to deal with the timing of code execution. Or we could provide thread safety via a wrapper @ThreadSafe
, that would wrap the execution of a piece of code around a Lock
.
The possibilities are basically endless, and now it's up to you to think of and implement the Function Wrappers that will make sense inside your own codebase 💪