Let’s talk about Swift’s result builder
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 likefor..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.