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:
buildBlock
buildOptional
buildArray
to use arraysbuildExpression
to accept different elements in a blockTo 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.
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.
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!
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
buildEither
to build the if
conditionWith 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
.
buildArray
to use arraysYou 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
buildExpression
to accept different elements in a blockIn 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 buildExpression
s. 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
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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.