Arjuna Sky Kok I'm a software engineer, a Youtuber, and a writer. I build PredictSalary, SailorCoin, and Pembangun.

Getting started with result builders in Swift

6 min read 1788

Getting Started With Result Builders In Swift

The iOS software engineers who use SwiftUI should be familiar with this syntax:

VStack(alignment: .leading) {
    Image(hotel.picture)
    Text(hotel.name)
    Text(hotel.price)
}

Inside the block, there are three statements, which are part of VStack. These statements are only separated by newlines.

Essentially, this code is telling you that you have a vertical stack (VStack) that is composed of three stacked components, consisting of an image (Image), a text (Text), and another text (Text).

Did you know that you can also use this syntax in regular Swift code, not just in SwiftUI?

This syntax is called result builders, previously known as function builders. Result builders have been a part of Swift since v5.4, which means that you can use this feature in Linux. In this article, we will explore how to write result builders with the buildBlock function, as well as more advanced functions, like buildOptional, buildArray, and buildExpression.

We’ll cover:

The importance of result builders

To understand the importance of result builders, you need to imagine a situation where you don’t have the result builders feature.

For example, instead of using a VStack that accepts components that will be stacked vertically, you can try to create a simple function that accepts string components that will be joined together. Say you feed the function: "Hello World!", "Hello LogRocket!", "Hello Swift!". It will give you this result: "Hello World!, Hello LogRocket!, Hello Swift!".

How are you going to do this? A simple way would be to create a function that accepts components:

func StringStack(_ a: String, _ b: String, _ c: String) -> String {
    a + ", " + b + ", " + c
}

Then, you could invoke this function:

StringStack("Hello World!", "Hello LogRocket!", "Hello Swift!")

It works well, but it doesn’t look very clean. Even if you split the arguments by line, it still looks ugly:

StringStack(
  "Hello World!,
  "Hello LogRocket!",
  "Hello Swift!"
)

The comma ruins everything! But it’s not just the annoying comma that we want to get rid of. There’s also another factor.

Say you combine these three strings with a comma but you want to have the option to change it to something else. You can modify the function:

func StringStack(_ separator: String, _ a: String, _ b: String, _ c: String) -> String {
    a + separator + " " + b + separator + " " + c
}

But when you execute this, the problem becomes obvious:

StringStack(
  "-",
  "Hello World!,
  "Hello LogRocket!",
  "Hello Swift!"
)

In the code block above, it’s hard to distinguish whether you want to stack four strings together or you want to stack three strings together, separated by a dash.

If you were using result builders, you could do this:

StringStack(separator: "-") {
    "Hello World!"
    "Hello LogRocket!"
    "Hello Swift!"
}

The separation of goals between the separator and the string components becomes crystal clear. The separator argument is not lumped together with the strings that you want to join.

Setting up the Playground on XCode

Open XCode. Then create a Swift Playground app. After doing that, navigate to File in the menu and click on New > Playground…. Give it the name ResultBuildersPlayground. You’ll be greeted with the default code that imports UIKit and declares the variable greeting. Then, empty the code.

However, because the result builders feature on Swift doesn’t depend on iOS or Mac, you don’t have to use XCode. You can use Swift on Linux or even an online Swift compiler, like SwiftFiddle.

Building a result builder with buildBlock

Continuing from our last example, say you want to convert the StringStack method into the declarative code. Add the following code to the Playground:

@resultBuilder class ConvertFunctionToResultBuilder {
    static func buildBlock(_ a: String, _ b: String, _ c: String) -> String {
        a + ", " + b + ", " + c
    }
}

Just now, you created a class containing the buildBlock static method and annotated it with @resultBuilder. Inside buildBlock, you added the function to combine the strings. ConvertFunctionToResultBuilder didn’t have to be a class — it could be an enum or a struct. But inside the class/enum/struct, there must be a static method called buildBlock.

Next, you can create the declarative code as you would in SwiftUI:

@ConvertFunctionToResultBuilder func MyFirstDeclarativeCode() -> String {
    "Hello World!"
    "Hello LogRocket!"
    "Hello Swift!"
}

Now, that’s more like it. One last step before you can see the result in its full glory!



Let’s execute the new function:

print(MyFirstDeclarativeCode())

Finally, you can compile the code and run it. This is the result:

Hello World!, Hello LogRocket!, Hello Swift!

All is well! Except, if you look at the implementation of the combining string operation, you can only accept three components. We want to be more flexible.

You can use variadic arguments to achieve this. Change the buildBlock method to something like this:

    static func buildBlock(_ strings: String...) -> String {
        strings.joined(separator: ", ")
    }

Then add another string to MyFirstDeclarativeCode:

@ConvertFunctionToResultBuilder func MyFirstDeclarativeCode() -> String {
    "Hello World!"
    "Hello LogRocket!"
    "Hello Swift!"
    "Hello Result Builders!"
}

Recompile the code and run it to get this result:

Hello World!, Hello LogRocket!, Hello Swift!, Hello Result Bulders!

Building an optional condition using buildOptional

The contents inside the block don’t have to be simple like a string. They can be an if condition, meaning the element can show up depending on a variable or a condition.

Suppose you want to create a breakfast for a guest in your hotel. You can create a result builder for this purpose:

@resultBuilder
struct BreakfastBuilder {
    static func buildBlock(_ food: String...) -> String {
        food.joined(separator: ", ")
    }

    static func buildOptional(_ drink: String?) -> String {
        return drink ?? ""
    }
}

Notice that there is another static function called buildOptional beside the required buildBlock.

You can create another function that will display the real breakfast:

@BreakfastBuilder func makeBreakfast(_ drink: Bool) -> String {
    "Egg"
    "Bread"
    if drink {
        "Milk"
    }
}

Here, you see that there is a conditional statement beside the usual strings. This conditional block will be executed and the result will be sent to buildOptional. The result of buildOptional will then be sent to buildBlock along with other strings.

You can execute it with different conditions:

print(makeBreakfast(false))
print(makeBreakfast(true))

The breakfast will be different depending on whether you want to include a drink or not:

Egg, Bread, 
Egg, Bread, Milk

Using buildEither to build the if condition

With buildOptional, you have an if condition without an alternative branch or the else condition. But with buildEither, you can have that.

You can use the same example but this time with buildEither:

@resultBuilder
struct BreakfastBuilder2 {
    static func buildBlock(_ food: String...) -> String {
        food.joined(separator: ", ")
    }

    static func buildEither(first drink: String) -> String {
        return drink
    }

    static func buildEither(second drink: String) -> String {
        return drink + " with sugar"
    }
}

So instead of buildOptional, you added buildEither but you needed two of these functions, not just one. Remember you will have an if condition and an else condition in a block in a result builder function.

Then you can annotate the function with this result builder:

@BreakfastBuilder2 func makeBreakfast2(_ drinkCoffee: Bool) -> String {
    "Egg"
    "Bread"
    if drinkCoffee {
        "Coffee"
    } else {
        "Tea"
    }
}

Coffee will be sent to the first buildEither, while Tea will be sent to the second buildEither. You can execute the function to confirm this:

print(makeBreakfast2(false))
print(makeBreakfast2(true))

This is the result:

Egg, Bread, Tea with sugar
Egg, Bread, Coffee

Remember that the second buildEither appends the argument with with sugar.


More great articles from LogRocket:


Using buildArray to use arrays

You use strings in the block of a function that has been annotated with a result builder. But wouldn’t it be nice if you could use an array of strings as well? For example, you might want to group food and drinks together.

This is where buildArray comes in handy.

First, create a result builder as follows:

@resultBuilder
struct BreakfastBuilder3 {
    static func buildBlock(_ food: String...) -> String {
        food.joined(separator: ", ")
    }

    static func buildArray(_ drinks: [String]) -> String {
        "Drinks: " + drinks.joined(separator: ", ")
    }
}

This time, add buildArray, which accepts an array and returns a String, the same type as the argument that buildBlock accepts. The returned result will be sent to buildBlock later.

Now, add the function that will be annotated with this result builder:

@BreakfastBuilder3 func makeBreakfast3() -> String {
    "Egg"
    "Bread"
    for d in ["Coffee", "Tea"] {
        "\(d)"
    }
}

To pass an array argument in the block, you don’t pass a literal array, but you create a for-loop statement. In each iteration, add an element of an array. In this case, it’s a string. Later, all of these strings will be put in an array and sent to buildArray.

Then you need to execute this function:

print(makeBreakfast3())

The result shows the concatenated string from buildArray:

Egg, Bread, Drinks: Coffee, Tea

Using buildExpression to accept different elements in a block

In all of our previous examples, we used String-typed elements in a block. But what if you want to use another type of element along with the string? You can handle different kinds of elements by using buildExpression in a block.

As usual, create a result builder:

@resultBuilder
struct BreakfastBuilder4 {
    static func buildBlock(_ food: String...) -> String {
        food.joined(separator: ", ")
    }

    static func buildExpression(_ food: String) -> String {
        food
    }

    static func buildExpression(_ tissueAmount: Int) -> String {
        "\(tissueAmount) pieces of tissue"
    }
}

You added two buildExpressions. One is for String, and it just returns the argument as it is. But the second buildExpression is more interesting because it accepts an integer. Then it interpolates the integer argument inside a string.

Now, it’s time to create a function that accepts strings and an integer:

@BreakfastBuilder4 func makeBreakfast4() -> String {
    "Egg"
    "Bread"
    5
}

Egg and Bread will be sent to the first buildExpression. But 5 will be sent to the second buildExpression. Then the results from these two buildExpressions will be sent to buildBlock.

Finally, execute this function:

print(makeBreakfast4())

The result is as expected:

Egg, Bread, 5 pieces of tissue

Conclusion

In this article, we used the result builders feature that can make your code declarative like the code in SwiftUI. We explored the reason why we use result builders, which is to make the code look cleaner. Then, we used @resultBuilder to create a result builder with the buildBlock static method to define the implementation of a block. Finally, we learned other block builder methods like buildOptional, buildEither, buildArray, and buildExpressions to create more advanced blocks.

This article only scratched the surface of result builders. There’s so much more you can do, like learn how write DSL with result builders. The code for this article is available on this GitHub repository.

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

.
Arjuna Sky Kok I'm a software engineer, a Youtuber, and a writer. I build PredictSalary, SailorCoin, and Pembangun.

Leave a Reply