Swift5.4 New Feature ResultBuilder
Recently, the Apple team released a new Swift version 5.4. In this update, Swift imports some new syntaxes. The ResultBuilder
is the most important syntax I think in them. Actually, the ResultBuilder
has already can be used in previous Swift versions. But its name is _functionBuilder
. In SwiftUI, Apple often uses it as the constructor parameter of Container Views. The body
property in SwiftUI is also declared with it.
But as you think of, the _functionBuilder
has an underline prefix, which means Apple doesn’t want us using this syntax. Today, the finished version of the _functionBuilder
is coming. It provides more features than the _functionBuilder.
What are the problems resolved by ResultBuilder ?
Let’s use the Apple official demo:
1 | protocol Drawable { |
The above demo’s function is just generating different strings. We can use the struct Line
to merge serval Drawable
instances and then use the function draw()
to generate a string.
Normally we use it like below:
1 | let line = Line(elements: [ |
ResultBuilder
provides a very clean way to do this. You can implement the above features through the following code:
1 |
|
As you can see. In this function, we don’t declare any parameters and don’t use a return keyword in its body. Just like declaring UI trees in SwiftUI. The attribute @DrawingBuilder
is the key to implement the effect.
1 | @resultBuilder |
We declare a struct DrawingBuilder
with the attribute @resultBuilder
. The @resultBuilder
requires the struct to implement a buildBlock
method.
In the body of the method drawing
, we can only instance types conformed Drawable
protocol if they don’t be assigned to a local variable.
If you write a return
keyword, the function will become a normal method. the @DrawingBuilder
will be ignored.
We invoke the method drawing
will invoke the buildBlock
method like this:
1 | DrawingBuilder.buildBlock(Text("--"), Space(), Stars(length: 3), Space(), Text("--")) // = drawing() |
Transforms Components and Results
resultBuilder
declares many methods to implement different functions . Following two methods can help us to transform values during runtime.
1 | static func buildExpression(_ expression: Drawable) -> Drawable { |
When the function buildBlock
is invoked, all the components will invoke the method buildExpression
to transform to a new component. In this, you can modify or replace the component or do nothing. Like the map
function of sequences. this method will be invoked repeatedly. the times of invocations depends on the count of components.
When the function buildBlock
return the result. resultBuilder will invoke the function buildFinalResult
with the param result. In the finally, you have one chance to modify the result value, the result of the function buildFinalResult
will replace the result of the function buildBlock
.
IF / IF ELSE / SWITCH
Sometimes we need some conditionals during generating result. In normal functions, we can write condition expressions to do this. But in resultBuilder
, we can only use if
, if else
and switch
expressions by implementing three functions below.
1 | static func buildOptional(_ component: Drawable?) -> Drawable { |
The function buildOptions
is invoked by single if
expression. The function buildEither(first:)
and the function buildEither(second:)
are invoked by if else
expression. the body of if
invokes the first method, the body of else
invokes the second.
After implementing the two buildEither
methods, you can write switch expressions directly.
1 |
|
You can not use break, continue, defer, guard, return, while, or do-catch statements in
@resultBuilder
LOOP
1 | static func buildArray(_ components: [Drawable]) -> Drawable { |
In the function drawing
, the loop executes four times. so the components of the function buildArray
has four elements. Every element is generated by the function buildBlock
with the loop’s body buildBlock(Stars(length: i), Stars(length: i * 10))
.
buildLimitedAvailability
It’s used in the compiler available expressions (#available
). If we use a available condition expression in a result builder. The result value might contains the type info about the unavailable type. This could cause your program to crash. But it’s a rare scene, I don’t want to deep it. You can read the apple document to get more detail about it.
Where should we use ResultBuilder ?
Function
1 |
|
Using the result builder syntax in the method’s body
Function Block Parameter
1 | func draw( componentsBuilder: () -> Drawable) -> String { |
Using the result builder syntax in the method’s parameter when you invoke it.
Property
1 | struct Canvas { |
There are two usages. The one is used at a computed property (dog). It’s easy to understand, just like using it in a no-parameter function.
As storage properties. When the type is a struct, the default constructor of it will automatically transform the init-param to () -> Drawable
(Be declared like a function having a block property marked with @DrawingBuilder attribute)
You can’t declare a storage property with ResultBuilder in a class. The ResultBuilder
requires properties have a getter. But you can use it at computed properties in classes.