Swift Result Builders: Getting Started

Adding @resultBuilder in Swift 5.4 was important, but you might have missed it. It’s the secret engine behind the easy syntax you use to describe a view’s layout: @ViewBuilder. If you’ve ever wondered whether you could create custom syntax like that in your projects, the answer is yes! Even better, you’ll be amazed at how […] By Andrew Tetlaw.

4.2 (5) · 1 Review

Download materials
Save for later
Share

Adding @resultBuilder in Swift 5.4 was important, but you might have missed it. It’s the secret engine behind the easy syntax you use to describe a view’s layout: @ViewBuilder. If you’ve ever wondered whether you could create custom syntax like that in your projects, the answer is yes! Even better, you’ll be amazed at how straightforward it is.

In this tutorial, you’ll learn:

  • Swift syntax for creating a result builder
  • Tips for planning your result builder
  • How to use a result builder to create a mini-language
Note: This beginner-level tutorial assumes you’re comfortable building an iOS app using Xcode and Swift, familiar with the Swift type system and have a good understanding of SwiftUI.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of this tutorial. Open the starter project.

Introducing Decoder Ring

Agent: Your mission, should you choose to accept it, is to complete the Decoder Ring app. Although you have top-secret code experts at your disposal to design the best ciphers, they would prefer not to spend much time implementing them in Swift. Can you design a Domain Specific Language that allows them to concentrate on cipher implementation and not be bothered with that Swift intricacies? Of course, you can!

Note: A Domain Specific Language (DSL) is a programming language specifically tailored for a particular purpose (or domain). This stands in contrast to a general-purpose language like Swift, which can be used for various software purposes.

If you build and run Decoder Ring, you will find a simple app with a single screen.

First run of the Decoder Ring app

The top field is a text entry field where an agent can type a message to be enciphered, which is then displayed in the bottom field. By switching the mode from Encode to Decode, the agent can instead paste an enciphered message into the top field to be deciphered in the bottom field. Currently, the app lacks enciphering/deciphering functionality.

It’s time to get cracking!

Making Your First Result Builder

To understand how result builders function, it’s best to dive right in. Create a file named CipherBuilder.swift. Add the following code:

// 1
@resultBuilder
// 2
enum CipherBuilder {
  // 3
  static func buildBlock(_ components: String...) -> String {
    components
      .joined(separator: " ")
      .replacingOccurrences(of: "e", with: "🥚")
  }
}
  1. You start with the @resultBuilder attribute, used to specify that the following definition is a result builder. @resultBuilder can annotate any type that allows a static method.
  2. You’ve used an enum because CipherBuilder doesn’t need to have instances created. Instead, it only contains static methods.
  3. You implement a static buildBlock(_:) function. This is the only requirement for a result builder. Your function takes any number of String arguments and returns a String containing all the arguments joined with a space and all instances of the letter e replaced with the egg emoji: 🥚.

The agency’s eggheads have called this the Egg Cipher. Next, you need to use your new result builder somewhere in the app. Open ContentView.swift and add the following at the end of the file:

// 1
@CipherBuilder
// 2
func buildEggCipherMessage() -> String {
  // 3
  "A secret report within the guild."
  "4 planets have come to our attention"
  "regarding a plot that could jeopardize spice production."
}
  1. Now, you can use CipherBuilder to annotate your code. You specify that buildEggCipherMessage() is a result builder implemented in CipherBuilder.
  2. Your method returns a String, matching the return type of your result builder.
  3. Inside your method, you list several strings matching the expected argument type String... in your result builder.

To show the output in the view body, add a modifier to the end of the ZStack:

.onAppear {
  secret = buildEggCipherMessage()
}

This code calls your result builder and set the output label to the returned value. Build and run to see the result.

The egg cipher results

As expected, the three strings are joined, and each instance of “e” is replaced with an egg.

Understanding Result Builders

It’s worth exploring what’s going on here. You’re simply listing strings in the body of buildEggCipherMessage(). There are no commas, and it’s not an array. So how does it work?

The compiler rewrites the body of your buildEggCipherMessage() according to the rules you’ve defined in CipherBuilder. So when Xcode compiles this code:

{
  "A secret report within the guild."
  "4 planets have come to our attention"
  "regarding a plot that could jeapardize spice production."
}

You can imagine it becomes something like this:

return CipherBuilder.buildBlock(
  "A secret report within the guild.",
  "4 planets have come to our attention",
  "regarding a plot that could jeapardize spice production."
)

As you expand your knowledge of result builders, imagining what the compiler translates your code to will help you understand what’s happening. As you’ll see, all kinds of programming logic can be supported using result builders, including loops and if-else statements. It’s all rewritten auto-magically to call your result builder’s foundational static function.

When was the concept of a result builder first introduced?
Result builders have been in Swift since 5.1 under different guises. With the arrival of SwiftUI, before result builders were officially part of the Swift language, they existed as a proposed feature called @_functionBuilder. This was the first implementation from Apple that powered the @ViewBuilder syntax of SwiftUI. Initially, the expected official name was @functionBuilder. However, after revising the proposal (SE-0289), that name became @resultBuilder. Be aware that you might find references to @functionBuilder or even @_functionBuilder in blogs and other resources.

Planning Your Cipher Builder

Now, the Egg Cipher isn’t exactly uncrackable. Back to the drawing board!

Any effective cipher will have steps, or cipher rules, to perform. Each rule applies an operation on the text and provides a new result. Taking the secret message as plain text, the cipher performs each rule sequentially until it yields the final enciphered text.

A planning diagram showing two rules

For your cipher, each rule will take a String input, modify it in some way and output a String result that’s passed to the following rule. Eventually, the last rule will output the final text. The deciphering process will be the same except in reverse. Your CipherBuilder will need to support any number of rules and, preferably, share rules across cipher definitions so you can test different combinations of ciphers.

As you’ll see, the amount of code you need to implement the result builder is quite small. Most of your time goes toward planning the types you’ll need for your DSL to make sense and be practical.