Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save vincent-pradeilles/875c9dd165542912f3803f8e01b3e15e to your computer and use it in GitHub Desktop.
Save vincent-pradeilles/875c9dd165542912f3803f8e01b3e15e to your computer and use it in GitHub Desktop.

How can Property Wrappers and Function Builders be leveraged?

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.

Property Wrappers

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 @State or @EnvironmentObject.

Those attributes are what Swift 5.1 calls Property Wrappers. The idea behind them is pretty simple: a Property Wrapper encapsulates a behaviour, 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 @Expirable.

First, we're going to declare our wrapper as a generic struct, and we'll decorate it with the attribute @propertyWrapper:

@propertyWrapper
struct Expirable <Value> {
    
}

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 computer whether the value has expired:

@propertyWrapper
struct Expirable <Value> {
    
    private let lifetime: TimeInterval
    private var expirationDate: Date = Date()
    
    init(lifetime: TimeInterval) {
        self.lifetime = lifetime
    }
    
    private func hasExpired() -> Bool {
        return expirationDate < Date()
    }
}

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:

@propertyWrapper
struct Expirable <Value: ExpressibleByNilLiteral> {
    
    private let lifetime: TimeInterval
    private var expirationDate: Date = Date()
    private var innerValue: Value = nil
    
    var value: Value {
        get { return hasExpired() ? nil : innerValue }
        set {
            self.expirationDate = Date().addingTimeInterval(lifetime)
            self.innerValue = newValue
        }
    }
    
    init(lifetime: TimeInterval) {
        self.lifetime = lifetime
    }
    
    private func hasExpired() -> Bool {
        return expirationDate < 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 @Expirable 🎉

struct Tokens {
    /// authent will expire after 3 seconds
    @Expirable(lifetime: 3) static var authent: String?
}

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.

Tokens.authent = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"

sleep(2)
Tokens.authent // Token is still available
sleep(2)
Tokens.authent // Token has expired

Function Builders

Once again, if you've looked at SwiftUI, there's a good chance that you've stumbled across code such as this:

var body: some View {
	HStack {
		Text("SwiftUI")
		Text("rocks")
	}
}

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("SwiftUI") and 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:

let myData = [1, 2, 3, 4]
        
assert(on: myData) {
	\.isEmpty == false
	\.first == 1
	\.last == 4
}

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:

struct Assert<Type> {
   let assertor: (_ instance: Type) -> ()
    
    init(assertor: @escaping (_ instance: Type) -> ()) {
        self.assertor = assertor
    }
}

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.

func combined(with other: Assert<Type>) -> Assert<Type> {
    return Assert { instance in
        self.assertor(instance)
        other.assertor(instance)
    }
}

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

static var empty: Assert<Type> { return Assert(assertor: { _ in }) }

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:

func == <Type, Value: Equatable>(property: KeyPath<Type, Value>, constant: Value) -> Assert<Type> {
    return Assert(assertor: { instance in
        XCTAssertEqual(instance[keyPath: property], constant)
    })
}

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 @_functionBuilder:

@_functionBuilder
public class AssertBuilder {
    
}

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:

static func buildBlock<T>(_ children: Assert<T>...) -> Assert<T> {
    return children.reduce(.empty, { $0.combined(with: $1) })
}

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:

func assert<Type>(on instance: Type, @AssertBuilder assertions: () -> Assert<Type>) {
    assertions().assertor(instance)
}

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 it to 🎉

let myData = [1, 2, 3, 4]
        
assert(on: myData) {
	\.isEmpty == false
	\.first == 1
	\.last == 4
}

To conclude

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment