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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protocol Drawable {
func draw() -> String
}
struct Line: Drawable {
var elements: [Drawable]
func draw() -> String {
return elements.map { $0.draw() }.joined(separator: "")
}
}
struct Text: Drawable {
var content: String
init(_ content: String) { self.content = content }
func draw() -> String { return content }
}
struct Space: Drawable {
func draw() -> String { return " " }
}
struct Stars: Drawable {
var length: Int
func draw() -> String { return String(repeating: "*", count: length) }
}

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
2
3
4
5
6
7
8
9
let line = Line(elements: [
Text("--"),
Space(),
Stars(length: 3),
Space(),
Text("--")
])

print(line.draw()) //-- *** --

ResultBuilder provides a very clean way to do this. You can implement the above features through the following code:

1
2
3
4
5
6
7
8
9
@DrawingBuilder
func drawing() -> Drawable {
Text("--")
Space()
Stars(length: 3)
Space()
Text("--")
}
print(drawing().draw())

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
2
3
4
5
6
@resultBuilder
struct DrawingBuilder {
static func buildBlock(_ components: Drawable...) -> Drawable {
Line(elements: components)
}
}

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
2
3
4
5
6
static func buildExpression(_ expression: Drawable) -> Drawable {
expression
}
static func buildFinalResult(_ component: Drawable) -> Drawable {
component
}

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
2
3
4
5
6
7
8
9
static func buildOptional(_ component: Drawable?) -> Drawable {
component ?? Text("")
}
static func buildEither(first component: Drawable) -> Drawable {
component
}
static func buildEither(second component: Drawable) -> Drawable {
component
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
@DrawingBuilder
func drawing() -> Drawable {
let condition = 2
if condition % 2 == 0 {
Stars(length: 10)
}
if condition % 2 != 0 {

} else {

}
Space()
Stars(length: 3)
}

You can not use break, continue, defer, guard, return, while, or do-catch statements in @resultBuilder

LOOP

1
2
3
4
5
6
7
8
9
10
11
static func buildArray(_ components: [Drawable]) -> Drawable {
Line(elements: components)
}

@DrawingBuilder
func drawing() -> Drawable {
for i in 0...3 {
Stars(length: i)
Stars(length: i * 10)
}
}

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
2
3
4
5
6
7
8
9
@DrawingBuilder
func drawing(count: Int) -> Drawable {
Stars(length: 10)
for i in 0...count {
Stars(length: i)
Stars(length: i * 10)
}
}
print(drawing(count: 10).draw())

Using the result builder syntax in the method’s body

Function Block Parameter

1
2
3
4
5
6
7
8
9
10
11
func draw(@DrawingBuilder componentsBuilder: () -> Drawable) -> String {
componentsBuilder().draw()
}

let content = draw {
Stars(length: 2)
Space()
Text("12345")
}

print(content)

Using the result builder syntax in the method’s parameter when you invoke it.

Property

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct Canvas {
@DrawingBuilder
var someStars: Drawable // Storage Property

@DrawingBuilder
var dog: Drawable {
// Computed Property
Stars(length: 2)
Space()
Text("dog")
Space()
Stars(length: 2)
}

mutating func updateStars() {
someStars = Stars(length: 2)
}
}

var canvas = Canvas { // Can directly use the ResultBuilder to initialize.
Stars(length: 2)
Space()
Stars(length: 2)
}

print(canvas.someStars.draw())
print(canvas.dog.draw())

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.