UIAlertController with Function Builders
March 5, 2020, 5 min to read
Let’s take this sample code:
As I said earlier, there is a lot to write and we can do better. We could improve the API with regular Swift patterns, but the goal here is to use function builders to create this SwiftUI like sample code:
This new feature introduced in Swift 5.1 is not fully implemented yet. Instead of the public @functionBuilder annotation, you have to use the private @_functionBuilder one. Though, you can find the details of the proposal here.
To better understand the purpose of this feature, this extract highlights the use cases of function builders:
This proposal does not aim to enable all kinds of embedded DSL in Swift. It is focused on one specific class of problem that can be significantly improved with the use of a DSL: creating lists and trees of (typically) heterogenous data from regular patterns. This is a common need across a variety of different domains, including generating structured data (e.g. XML or JSON), UI view hierarchies (notably including Apple’s new SwiftUI framework, which is obviously a strong motivator for the authors), and similar use cases.
The documentation is not official, but after some digging, here are the methods we can implement:
- buildBlock(_ components: Component...) -> Component required
- buildIf(_ component: Component?) -> Component optional, previously named buildOptional.
- buildEither(first: Component) -> Component and buildEither(second: Component) -> Component, optionals
The buildExpression, buildDo and buildFunction have no effect for the moment.
What we want to build in our case is a list of alert actions. An Action is composed of a title, a style (default, destructive or cancel) and a function, triggered when the user taps the alert button on screen.
Once we have an array of Action we can build an alert controller with this factory method:
So far so good, but we didn’t bring anything new yet.
Let’s see how to retrieve this array of actions and create our function builder. We saw earlier that the only required method is buildBlock, so we will start here. buildBlock combines a list of components to a single one.
Note: If we think about views and SwiftUI, that makes sense. From multiple subviews (the components for the @ViewBuilder), buildBlock will create a superview that contains all the subviews.
In our case, the Component type is an array of actions, so the buildBlock method can be written as follows:
Now that we have our ActionBuilder, let’s use it to retrieve an array of actions and pass it to the makeAlertController function.
Note the use of the @ActionBuilder attribute, that will allow us to use our new DSL in the makeActions closure.
That way we can build the following:
But… how is this supposed to be better ?!
I guess that’s not what you expected…
It’s really weird to see a list of arrays of actions. Why not a list of actions? That would make more sense to write.
The explanation here, as we saw earlier, is that our Component is the type [Action]. And buildBlock takes a list of components Component..., meaning [[Action]]. That’s why we have to write it like this.
An evolution, in future implementations of function builders, would be to use the buildExpression method (not yet available):
buildExpression(_ expression: Expression) -> Component is used to lift the results of expression-statements into the Component internal currency type. It is only necessary if the DSL wants to either (1) distinguish Expression types from Component types or (2) provide contextual type information for statement-expressions.
If buildExpression was working, we could write:
But for now, we are stuck with our list of [Action]. That’s not really an issue though, because we can extract this weird behavior into factories:
And our code now matches our initial goal !
We can now add multiple actions to the same alert. But that’s not very dynamic yet…
What if we want to add actions conditionally ? What if we want to add multiple actions at the same time ?
Let’s try to add a condition.
We hit a compiler error: closure containing control flow statement cannot be used with function builder
That’s because we only have implemented the buildBlock method and we need to add buildIf. The implementation is simple, either we have a list of actions or else we return an empty list.
With the buildIf implemented, everything compiles and runs, the if statement is taken into account.
But that’s not enough yet, because if we try to have a else condition in our code, we hit the same compiler error again…
To overcome this, we need to add two more functions: buildEither(first:) and buildEither(second:) used when there is a decision tree with optional sub blocks.
And that makes our code compiling again with if / else conditions.
At the moment we have no way to loop an array of strings and create actions out of them.
If we try, we always hit the same compiler error.
One way to solve this problem is to create an helper function that generates a list of actions for each element of a sequence, and then aggregates all those lists of actions into a single one.
And finally, we can use it like so:
We saw how we could improve the UIAlertController API with very little code. The difficulty with function builders is to find documentation and to understand the cryptic error messages. The feature is really limited at the moment and maybe it’s for the best, to avoid overly complicated DSLs that would not be understandable. But be patient, Swift folks are working on it.
You can find a gist with all the code here.
If you are interested in the subject, here is a list of resources you can use to create your own function builders: