Let’s talk about Swift’s result builder

Thien Codev
4 min readFeb 11, 2024

--

What does it means?
When to use it?
Where to use it?
Why use it?
And how to use it?

Let’s take deep into it by answering these questions.

Overview

Introduced in Swift 5.4, result builders are a powerful tool that allows us to create custom DSLs for constructing complex data structures in a more readable and expressive way. By defining a set of rules for combining smaller pieces of data into a large one, we can build custom flexible way for construction with a more intuitive syntax.
Here’s an example of a result builder in SwiftUI that you used to use but never noticed with it:

struct ContentView: View {
var body: some View {
// This is inside a result builder of View
VStack {
// This is inside a result builder of VStack
Image(systemName: "globe")
Text("Hello, world!")
}
.padding()
}
}

This is familiar, right? In this case, VStack tells us that there are Image and Text inside the vertical stack but two elements are only separated by a newline. This is result builder where Image and Text will be combined into a single View. In other words, the View building blocks are built into a View result. Take a look into the definition of SwiftUI View , we can see the body variable being defined using the @ViewBuilder attribute:

/// For more information about composing views and a view hierarchy,
/// see <doc:Declaring-a-Custom-View>.
@ViewBuilder @MainActor var body: Self.Body { get }

This is exactly how you can use your custom result builder as an attribute of function, variable,...

Create custom result builder

Here are some specific uses of result builders based on the provided sources:

  • Function Parameters: Result builders can be used as function parameters to accept a collection of closures or expressions and combine them into a single result.
  • Combining Values: Result builders can be used to combine multiple values into a single result, often represented as an array or another collection type.
  • Handling Optionals and Conditionals: Result builders can handle partial results that may or may not be available in a given execution.
  • Loop Iteration Results: Result builders support the combination of partial results from all iterations of a loop using the buildArray(_:) method, which is useful when working with loops like for..in … ect.

Let’s create a simple one. For example, if you want to create a function that takes responsibility for concatenating strings, the code below works as you expected.

func concat(a: String, b: String, separator: String) -> String {
return a + separator + b
}

concat(a: "Hello", b: "world", separator: " ")
// "Hello world"

concat(a: "Hello", b: "world", separator: ",")
// "Hello,world"

But it doesn’t look very clean. Even if you split the arguments by line, it still looks ugly, and if you want to concatenate more than two strings, you have to add more params to the function.

func concat(_ separator: String, _ a: String, _ b: String) -> String {
return a + separator + b
}

concat(",",
"Hello",
"world")
// "Hello,world"

Let’s try this with result builder. Firstly, we define a StringBuilder with annotation @resultBuilder . Inside buildBlock , we add the function to combine the strings. In this case, I want to join the string with separator a comma. Then, just modify the concat function a little bit to use result builder. It’s like …

@resultBuilder
struct StringBuilder {
static func buildBlock(_ components: String...) -> String {
components.joined(separator: " ")
}
}
func concat(@StringBuilder _ strings: () -> String) -> String {
strings()
}

let message = concat {
"Hello"
"World!"
"I"
"am"
"Thien"
"Codev"
}
// "Hello World I am Thien Codev"

Now, that’s closer to my request. It’s cool, right?
Let’s try another example but more harder. Suppose one day you and your girlfriend date at a restaurant. When you order, you want some drink on your part but she neither (just kidding). Let’s program this situation.

Here is the restaurant’s menu.

enum MenuItem: String {
case bread
case spinach
case beefSteak
case soup
case spaghetti
case cream
case soda
case coke
}
@resultBuilder
struct MenuBuilder {
static func buildBlock(_ components: [MenuItem]...) -> [MenuItem] {
return components.flatMap { $0 }
}

static func buildOptional(_ component: [MenuItem]?) -> [MenuItem] {
return component ?? []
}

static func buildExpression(_ expression: MenuItem) -> [MenuItem] {
return [expression]
}
}
@MenuBuilder
func order(withDrink: Bool) -> [MenuItem] {
MenuItem.bread
MenuItem.spinach
MenuItem.soup
MenuItem.beefSteak
if withDrink {
MenuItem.coke
}
}

let myOrder = order(withDrink: true)
// [bread, spinach, soup, beefSteak, coke]
let myLoveOrder = order(withDrink: false)
// [bread, spinach, soup, beefSteak]

In real world, you will handle output with condition sometimes. The buildOptional method is designed to handle such scenarios. For this case, it is withDrink . You see that there is a conditional statement beside the usual menu items. This conditional block will be executed and the result will be sent to buildOptional. The result of buildOptional will be sent to buildBlock along with other items.

Above are some simple examples we can apply @resultBuilder in our code. I just showed you some use cases with each specific solution. There are so many ways and so many parts of it that you need to research to take full advantage of its usefulness.

If you’d like to see more advanced, real-world examples of result builders in action, you should check out the https://blog.logrocket.com/result-builders-swift/#conclusion

Conclusion

In this article, we used the result builders feature that can make your code declarative. We find out some of reasons why and how we use result builders, which is to make the code look cleaner. We used @resultBuilder to create a result builder with the buildBlock static method to define the implementation of a block and buildOptional buildExpression ,.. to create more advanced blocks.

--

--