UIAlertController with Function Builders
I always found the UIAlertController
API too verbose. You first have to create an instance of UIAlertController
, then create multiple instances of UIAlertAction
and finally add the actions to the controller.
In this post, I will show you how we can leverage the new Swift 5.1 feature of Function Builders to create a simplified and highly readable API.
Our goal
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:
Function Builders
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 proposal focuses on a DSL to represent HTML tree but it is also heavily used in SwiftUI to create view hierarchies with the @ViewBuilder
attribute.
The documentation is not official, but after some digging, here are the methods we can implement:
buildBlock(_ components: Component...) -> Component
requiredbuildIf(_ component: Component?) -> Component
optional, previously namedbuildOptional
.buildEither(first: Component) -> Component
andbuildEither(second: Component) -> Component
, optionals
The buildExpression
, buildDo
and buildFunction
have no effect for the moment.
In practice
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 theComponent
internal currency type. It is only necessary if the DSL wants to either (1) distinguishExpression
types fromComponent
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 !
Conditions
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:
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.
Loops
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:
Conclusion
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: