Understanding Result Builders & View Builders in SwiftUI part 1

abdul ahad
4 min readFeb 20, 2024

--

Photo by Roman Synkevych on Unsplash

SwiftUI, introduced by Apple, is often described as a DSL (Domain-Specific Language) for building user interfaces in a declarative manner.

In a DSL, the syntax is tailored to a specific domain or problem, making it more readable and understandable for tasks within that domain. Simlarly to SwiftUI, HTML is also a DSL to describe a document displayed on a web page.

HStack {
Text("Hello abdul!")
Divider()
Text("Welcome!")
}

The above declarative syntax which almost everyone using SwiftUI has encountered before is possible using ViewBuilders

but what is a ViewBuilder?

To understand @ViewBuilder we must first understand ResultBuilder as ViewBuilder is built on top of ResultBuilder.

Understanding Result Builders

Let’s try to understand how Result Builder acts as a DSL by converting a common string interpolation in Swift Result Builder.

Here's an example of how string interpolation normally works in Swift:

struct Person {
var name: String
var age: Int
}

let person = Person(name: "John", age: 30)

let description =
"Name: \(person.name), Age: \(person.age)\n"
+
"Name: \(person.name), Age: \(person.age)\n"
+
"Name: \(person.name), Age: \(person.age)\n"
+
"Name: \(person.name), Age: \(person.age)\n"

print(description)

let's convert into DSL-like syntax using result builders.

there are 2 prerequisites to use a result builder.

  • buildBlock: This method is the heart of a result builder. It accepts variadic input and returns the constructed output.
  • add @resultBuilder attribute to your component

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

//we take in the stringbuilder as parameter
func description(@StringBuilder content: () -> String) {
return print(content())
}

description {
"Name: \(person.name), Age: \(person.age)"
"Name: \(person.name), Age: \(person.age)"
"Name: \(person.name), Age: \(person.age)"
"Name: \(person.name), Age: \(person.age)"
}

you should get the same behavior as the above but the code looks concise and neat. Each expression `Name: \(person.name), Age: \(person.age)` is ‘build block’ that gets combined into a final String.

let’s leverage more methods of the result builder.

buildOptional:

  • Purpose: Handles optional values within the result builder block.
  • Functionality: Allows for the inclusion of optional values in the block.

Example Usage:

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

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

}
func description(@StringBuilder content: () -> String) {
return print(content())
}

//usage

//MARK: Result builder optional
let nilString:String? = nil
description {
"abdul"
if let nilString {
nilString
}
"ahad"
}

buildEither(first:) and buildEither(second:):

  • Purpose: Handles control flow like if-else statements.
  • Functionality: Allows for branching based on conditions.

Example Usage:

@resultBuilder
struct StringBuilder {
static func buildBlock(_ components: String...) -> String {
return components.joined(separator: "\n")
}
static func buildEither(first component: String) -> String {
return component
}

static func buildEither(second component: String) -> String {
return component
}

}
func description(@StringBuilder content: () -> String) {
return print(content())
}

//usage
//MARK: Result builder if/else
let lovesFootball = true
description {
"abdul"
if lovesFootball {
"loves football"
}else{
"hates football"
}
}

buildArray:

  • Purpose: Allows the inclusion of an array of components in a block.
  • Functionality: Facilitates working with arrays of values.

Example

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

static func buildArray(_ components: [String]) -> String {
return components.joined(separator: "\n")
}

}

//usage
//MARK: Result builder array
let names = ["owais", "hashmi", "trump"]
description {
"abdul"
for name in names {
name
}
"sara"
}

buildExpression:

  • Purpose: Provides a mechanism to interpret individual expressions in the block.
  • Functionality: Enables handling and interpretation of expressions.

Example Usage:

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

static func buildExpression(_ expression: String) -> String {
return expression + " ADDED EXPRESSION"
}
}
//usage
//MARK: Result builder individual Expression
description {
"abdul"
"bilal"
}

buildFinalResult:

  • Purpose: Provides an opportunity to modify the final return value of the buildBlock
  • Functionality: Allows for final adjustments before returning the result.

Example Usage:


struct StringBuilder {
static func buildBlock(_ components: String...) -> String {
return components.joined(separator: "\n")
}

static func buildFinalResult(_ result: String) -> String {
// Perform final adjustments on the result before returning
return result.uppercased()
}
}
//usage
//MARK: Result builder Final Expression
description {
"abdul"
"sara"
}

These methods collectively empower result builders to handle various scenarios and provide a flexible and expressive way to construct complex values in a declarative manner.

Understanding result builders, and how @ViewBuilder specifically works, is essential for effectively using it in SwiftUI apps.

in the next part, we will talk about ViewBuilders.

Github Reference: https://github.com/abdahad1996/SwiftUI_Bootcamp/blob/main/SwiftUI_Bootcamp/SwiftUI_Bootcamp/ResultBuilder

--

--

abdul ahad
abdul ahad

Written by abdul ahad

A software developer dreaming to reach the top and also passionate about sports and language learning

No responses yet