In Swift 5.4, a powerful new feature was introduced that revolutionized how we can work with composable pieces of code: Result Builders. If you’ve written any SwiftUI code, you’ve probably already encountered this feature without realizing it. Result Builders underpin much of the magic that makes SwiftUI’s declarative syntax possible. However, their use isn’t limited to SwiftUI. In this blog post, we’re going to delve into Result Builders, what they are, and how you can use them to build more expressive and powerful APIs.

Understanding Result Builders

Result Builders, initially introduced as Function Builders, is a compiler feature that enables creating intuitive and declarative APIs. This feature is heavily used in SwiftUI and DSL (Domain Specific Language) building. The goal is to create an expressive API that reads almost like natural language and less like traditional, often verbose, Swift code.

Consider the SwiftUI View, which uses the ViewBuilder result builder:

VStack {
    Text("Hello, World!")
    Divider()
    Text("Welcome to SwiftUI!")
}

This code is concise, easy to read, and describes the layout visually in its structure. This is made possible by result builders.

Let’s dig deeper to understand this concept and to make our own custom result builder.

The Building Blocks of Result Builders

Result Builders rely on several methods to work their magic. These methods are:

  • buildBlock: This method is the heart of a result builder. It accepts variadic input and returns the constructed output.
  • buildOptional: This method handles optional values within the block.
  • buildEither(first:) and buildEither(second:): These methods handle control flow like if-else statements.
  • buildArray: This method allows the inclusion of an array of components in a block.
  • buildExpression: This method provides a mechanism to interpret the individual expressions that make up the block.
  • buildFinalResult: This method provides an opportunity to modify the final return value of the buildBlock.

Creating a Custom Result Builder

Now let’s create a custom result builder. We’ll build a StringConcatenator that will help us construct a single string from multiple string inputs.

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

Let’s use our StringConcatenator:

func createMessage(@StringConcatenator message: () -> String) {
    print(message())
}

createMessage {
    "Hello, World!"
    "Welcome to Swift Result Builders!"
    "Enjoy coding!"
}

In the console, you would see:

Hello, World!
Welcome to Swift Result Builders!
Enjoy coding!

Handling Optionals

If we want to handle optional strings in our StringConcatenator, we can make use of the buildOptional method. Let’s update our StringConcatenator to handle optional strings:

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

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

Now, we can pass optional strings to our createMessage function:

let optionalString: String? = nil

createMessage {
    "Hello, World!"
    if let optionalString {
        optionalString
    }
    "Enjoy coding!"
}

In the console, you would see:

Hello, World!

Enjoy coding!

Handling Control Flow

If we want to handle control flow, like if-else statements, in our StringConcatenator, we can make use of the buildEither(first:) and buildEither(second:) methods.

Let’s update our StringConcatenator to handle control flow:

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

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

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

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

Now, we can pass if-else statements to our createMessage function:

let shouldWelcome = false

createMessage {
    "Hello, World!"
    if shouldWelcome {
        "Welcome to Swift Result Builders!"
    } else {
        "Have a great day!"
    }
    "Enjoy coding!"
}

In the console, you would see:

Hello, World!
Have a great day!
Enjoy coding!

Handling Arrays

If we want to handle arrays of strings in our StringConcatenator, we can make use of the buildArray method.

Let’s update our StringConcatenator to handle arrays:

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

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

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

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

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

Now, we can pass arrays of strings to our createMessage function:

let names = ["Alice", "Bob", "Charlie"]

createMessage {
    "Hello, World!"
    for name in names {
        "Hello, \(name)!"
    }
    "Enjoy coding!"
}

In the console, you would see:

Hello, World!
Hello, Alice!
Hello, Bob!
Hello, Charlie!
Enjoy coding!

Concluding Thoughts

Swift Result Builders is a powerful feature that allows us to create expressive, easy-to-read APIs. By understanding the concepts behind result builders, you can create custom result builders that will enable you to build cleaner and more manageable Swift code.

While this post provides a broad introduction to result builders and their capabilities, there’s a lot more depth to explore. The possibilities are virtually limitless! By crafting your custom result builders, you can shape Swift to fit your unique needs and build APIs that feel like they’re designed just for your project. Happy coding!