Understanding Result Builders & View Builders in SwiftUI part 1
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