Greetings, travelere!
Swift’s @resultBuilder
attribute is a powerful feature that simplifies the creation of domain-specific languages (DSLs) within Swift, enabling expressive and concise declarative code. While it is most prominently used in SwiftUI via @ViewBuilder
, its applications extend to various other domains. This article explores the mechanics of resultBuilder, its role in SwiftUI through @ViewBuilder
, and practical examples of its use outside SwiftUI, including robust implementations for SQL query building and validation rule configuration.
What is resultBuilder?
The @resultBuilder
attribute (formerly @functionBuilder
) allows developers to transform a sequence of expressions into a single value using a declarative syntax. It achieves this through a set of static methods defined in a resultBuilder-conforming type, which specify how expressions are collected, combined, and transformed into a final output. This feature enables Swift to interpret blocks of code as structured results, making it ideal for creating intuitive APIs that resemble DSLs.
resultBuildre in SwiftUI: The Role of ViewBuilder
In SwiftUI, @resultBuilder
is primarily encountered through @ViewBuilder
, a specialized result builder designed to construct view hierarchies. SwiftUI leverages @ViewBuilder
to enable developers to write declarative UI code that is readable and maintainable.
How ViewBuilder Works
In SwiftUI, views are often defined using closures that describe their content. For example:
VStack {
Text("Hello")
Text("World")
}
The VStack
initializer accepts a closure annotated with @ViewBuilder
, which processes the sequence of views (Text("Hello")
and Text("World")
) and combines them into a single view hierarchy. ViewBuilder
implements resultBuilder
methods to handle view composition, aggregating multiple views into a cohesive structure, such as a tuple or array-like representation.
ViewBuilder vs. Methods and Properties: Returning some View
Methods or properties annotated with @ViewBuilder
differ from those returning some View
. A method or property returning some View
produces a single, opaque view type. For example:
var content: some View {
Text("Single View")
}
This returns a single Text
view. In contrast, a @ViewBuilder
-annotated method or property can handle a closure containing multiple views and combine them into a single view hierarchy:
@ViewBuilder
var content: some View {
Text("First")
Text("Second")
}
Here, @ViewBuilder
enables a declarative syntax where multiple views are written sequentially without explicit composition, producing a single view hierarchy. This contrasts with a plain some View
return type, which requires a single view expression. The @ViewBuilder
approach is what makes SwiftUI’s DSL-like syntax possible, allowing developers to focus on describing the UI rather than managing view composition.
Practical Examples of resultBuilder Outside SwiftUI
While @ViewBuilder
is a key use case, @resultBuilder
is a general-purpose feature applicable to various domains. Below are two practical examples demonstrating its use outside SwiftUI, with a robust validation example that avoids previous type mismatch errors.
Example 1: Building SQL Queries
A common use case for resultBuilder
is constructing SQL queries in a type-safe, declarative manner:
@resultBuilder
struct SQLQueryBuilder {
static func buildBlock(_ components: String...) -> String {
components
.joined(separator: " ")
.trimmingCharacters(in: .whitespaces)
}
static func buildExpression(_ expression: String) -> String {
expression
}
static func buildOptional(_ component: String?) -> String {
component ?? ""
}
static func buildEither(first: String) -> String {
first
}
static func buildEither(second: String) -> String {
second
}
}
@SQLQueryBuilder
func buildQuery(_ content: () -> String) -> String {
content()
}
let query = buildQuery {
"SELECT *"
"FROM users"
"WHERE age > 18"
}
print(query) // Output: SELECT * FROM users WHERE age > 18
This implementation uses SQLQueryBuilder
to combine multiple string expressions into a single SQL query string. The buildBlock
method joins components with spaces and trims extra whitespace, while methods like buildOptional
and buildEither
ensure the builder handles optional expressions and conditional logic (e.g., if statements) robustly. The @SQLQueryBuilder
attribute allows the closure to be written in a clean, declarative style, making query construction intuitive and reducing boilerplate.
Example 2: Configuring Validation Rules
Another practical application is building a validation system where rules are defined declaratively. The following example uses a Validator
struct to compose validation rules for user input, such as email and name validation, using a resultBuilder to ensure type-safe and expressive rule composition:
enum ValidationError: Error, LocalizedError {
case emptyField
case invalidEmail
case shortPassword
case numbersNotAllowed
case custom(String)
var errorDescription: String? {
switch self {
case .emptyField: "Field cannot be empty"
case .invalidEmail: "Invalid email format"
case .shortPassword: "Password must be at least 8 characters"
case .numbersNotAllowed: "Numbers are not allowed"
case .custom(let message): message
}
}
}
protocol ValidationRule {
func validate(_ input: String) throws
}
struct NotEmptyRule: ValidationRule {
func validate(_ input: String) throws {
if input.isEmpty {
throw ValidationError.emptyField
}
}
}
struct EmailRule: ValidationRule {
func validate(_ input: String) throws {
let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
let predicate = NSPredicate(format: "SELF MATCHES %@", emailRegex)
if !predicate.evaluate(with: input) {
throw ValidationError.invalidEmail
}
}
}
struct MinLengthRule: ValidationRule {
let minLength: Int
func validate(_ input: String) throws {
if input.count < minLength {
throw ValidationError.shortPassword
}
}
}
struct NoNumbersRule: ValidationRule {
func validate(_ input: String) throws {
if input.rangeOfCharacter(from: .decimalDigits) != nil {
throw ValidationError.numbersNotAllowed
}
}
}
@resultBuilder
struct ValidationBuilder {
static func buildBlock(_ components: ValidationRule...) -> [ValidationRule] {
components
}
static func buildOptional(_ component: [ValidationRule]?) -> [ValidationRule] {
component ?? []
}
static func buildEither(first component: [ValidationRule]) -> [ValidationRule] {
component
}
static func buildEither(second component: [ValidationRule]) -> [ValidationRule] {
component
}
}
struct Validator {
private let rules: [ValidationRule]
init(@ValidationBuilder rules: () -> [ValidationRule]) {
self.rules = rules()
}
func validate(_ input: String) throws {
for rule in rules {
try rule.validate(input)
}
}
}
func validateUserInput() {
let emailValidator = Validator {
NotEmptyRule()
EmailRule()
}
let nameValidator = Validator {
NotEmptyRule()
NoNumbersRule()
MinLengthRule(minLength: 2)
}
do {
try emailValidator.validate("test@example.com") // Success
try emailValidator.validate("invalid.email") // Error: Invalid email format
try nameValidator.validate("John") // Success
try nameValidator.validate("J0hn") // Error: Numbers are not allowed
try nameValidator.validate("") // Error: Field cannot be empty
} catch {
print("Validation error: \(error.localizedDescription)")
}
}
This example defines a Validator
struct that uses @ValidationBuilder
to compose validation rules for user input, such as email and name fields. The ValidationRule
protocol and its implementations (NotEmptyRule
, EmailRule
, etc.) define specific validation logic, throwing descriptive errors if the input is invalid. The ValidationBuilder
combines multiple rules into an array, supporting optional and conditional rules via buildOptional
and buildEither
. The Validator
struct applies these rules to input strings, making it ideal for form validation in real-world applications, such as ensuring valid email addresses or names without numbers.
Why Use resultBuilder?
The @resultBuilder
attribute offers several benefits:
1. Readability: It enables a declarative syntax that is concise and natural, reducing boilerplate code.
2. Flexibility: It can be applied to diverse domains, from UI construction to query building and validation.
3. Type Safety: By leveraging Swift’s type system, resultBuilder ensures that composed components are valid and correctly typed.
However, developers must carefully design the builder’s static methods to handle edge cases, such as optional components or conditional logic. Performance considerations are also important when combining large numbers of components, and concurrency rules (e.g., actor isolation) must be respected in SwiftUI or other main-actor-bound contexts.
Conclusion
The @resultBuilder
attribute is a versatile feature in Swift that powers SwiftUI’s declarative syntax through @ViewBuilder
and enables custom DSLs in other domains. By understanding how @ViewBuilder
simplifies view composition in SwiftUI and applying @resultBuilder
to practical scenarios like SQL query construction and validation rule aggregation, developers can create cleaner, more expressive code.
If you enjoyed this article, please feel free to follow me on my social media: