Chapters

Hide chapters

Swift Apprentice: Beyond the Basics

Second Edition · iOS 18 · Swift 6 · Xcode 16.2

Section I: Beyond the Basics

Section 1: 13 chapters
Show chapters Hide chapters

3. Result Builders
Written by Eli Ganim

Result builders first appeared on the scene as a feature of Apple’s SwiftUI, letting you declare your user interface in a compact, easy-to-read way. It was since expanded as a general language feature that lets you build values by combining a sequence of expressions. Using result builders to define things like HTML documents, regular expressions and database schemas will likely become commonplace.

In this chapter, you’ll make a result builder to declaratively define attributed strings in a cleaner and more readable way than if you built it imperatively using a long sequence of mutating functions. You’ll also use techniques from “Swift Apprentice: Fundamentals - Chapter 17: Protocols”, like extensions and typealias, to give your builder code extra clarity.

Meet NSAttributedString

To demonstrate how result builders work, you’ll build a small project that uses NSAttributedString to show a fancy greet message. By the end of this chapter, you’ll create a string that looks like this:

NSAttributedString is a special object that holds a string and lets you add attributes, like color and font, to the whole string or only to part of it.

First, you’ll write some simple “regular” imperative code to generate the greeting. Later, you’ll convert that code to use a result builder.

Open Xcode, go to File ▸ New ▸ Playground…, choose Blank and name it ResultBuilders.

Enter this function into the playground:

func greet(name: String) -> NSAttributedString {
  let message = NSAttributedString(string: "Hello " + name)
  return message
}

Now, call the function by adding greet(name: "Daenerys") below it. Finally, run the playground and observe the result by clicking the Show Result button to the right:

Adding Color With an Attribute

Right now, you aren’t using any of the capabilities of NSAttributedString. You’ll change that by adding color to the greeting message using an attribute.

Replace greet with this:

func greet(name: String) -> NSAttributedString {
  let attributes = [NSAttributedString.Key.foregroundColor : UIColor.red]
  let message = NSAttributedString(string: "Hello " + name, attributes: attributes)
  return message
}

Run the playground now. You’ll see the string appear in red.

Note that you’re using a different initializer that takes a dictionary of attributes as an argument. NSAttributedString supports many types of attributes, which you can examine by pressing Command-Control and left-clicking on foregroundColor.

Adding Color to a Specific String

What if you wanted to change only the text color of the name of the person you’re greeting and not the word “Hello”? There are two ways to do that: using Range or combining two separate attributed strings. Here, you’ll use the second approach because it’s easier to understand.

For this, you’ll use an Objective-C type NSMutableAttributedString, which lets you append attributed strings to it. Recall that mutable types let you make modifications. In Swift, control of mutability is supported at the instance level with the introducers var and let. On the other hand, Objective-C supports mutability at the class level and requires you to use a different type, NSMutableAttributedString, to make changes.

Replace the current NSAttributedString initialization — the second line in your function — with this:

let message = NSMutableAttributedString()
message.append(NSAttributedString(string: "Hello "))
message.append(NSAttributedString(string: name, attributes: attributes))

Here, you create a mutable attributed string that contains only the word “Hello” without any attributes. You then append another string with the name argument passed in and the attribute of the red color.

Observe the results:

Adding Another Attributed String

If you want to add another string to the mix — for example, one with a different font size — you need yet another attributed string. Add the following code before the return statement:

let attributes2 = [
  NSAttributedString.Key.font : UIFont.systemFont(ofSize: 20),
  NSAttributedString.Key.foregroundColor : UIColor.blue
]
message.append(NSAttributedString(string: ", Mother of Dragons", attributes: attributes2))

You’ll get a result with two different colors and a bigger font size for the last part of the string:

You can see how this kind of string building can get messy quickly. For cases like this, result builders make constructing the attributed string simpler and easier to read.

Result builders enable you to write cleaner code that looks like this:

func greet(name: String) -> NSAttributedString {
  NSAttributedString(string: "Hello ")
  NSAttributedString(string: name, attributes: ...)
  NSAttributedString(string: ", Mother of Dragons", attributes: ...)
}

This code doesn’t have a return statement and doesn’t need to append strings. A result builder gathers the expressions and combines them into a single attributed string. You’ll next see how to implement this.

Creating a Result Builder

Start by creating a new enum called AttributedStringBuilder. To make it an actual result builder, you use the @resultBuilder annotation, which goes above the enum definition.

Add this at the bottom of your playground:

@resultBuilder
enum AttributedStringBuilder {
}

As soon as you add this piece of code, you’re greeted with an error message:

Add the missing required method inside the enum definition:

static func buildBlock(_ components: NSAttributedString...) -> NSAttributedString {
}

buildBlock(_:) is the main entry point; it performs the magic of combining the components. As you can see, it can take multiple components of type NSAttributedString and combine them into a single NSAttributedString.

Two things to note here:

  1. The method uses a variadic parameter (NSAttributedString...), which means the result builder can support any number of components.
  2. For now, the variadic parameter and the return value must be the same type. There’s a way to work around this; you’ll read about it later.

Now, to implement the builder. Insert this code in the buildBlock(_:) method body:

let attributedString = NSMutableAttributedString()
for component in components {
  attributedString.append(component)
}
return attributedString

Here, you go over each component in the components parameter and append each one to an attributed string. Eventually, you return the result of appending all the strings.

Simple, right? These few lines are enough to create the result builder and get you where you want to go.

Building the Greeting String With the Result Builder

Now, you’ll use the result builder to construct the same greeting string you created earlier by creating a new method.

Add this to the bottom of your playground:

@AttributedStringBuilder
func greetBuilder(name: String) -> NSAttributedString {
}

The new annotation, @AttributedStringBuilder, exists thanks to the result builder you defined earlier. You can use this annotation on methods, getters and closures.

To examine how result builders work, you’ll add a few NSMutableAttributedStrings, then call greetBuilder, like so:

@AttributedStringBuilder
func greetBuilder(name: String) -> NSAttributedString {
  NSMutableAttributedString(string: "Hello ")
  NSMutableAttributedString(string: name)
  NSMutableAttributedString(string: ", Mother of Dragons")
}

greetBuilder(name: "Daenerys")

Here, you use NSMutableAttributedString because you want to have the ability to change the attributes later on.

The method will return the string: “Hello Daenerys, Mother of Dragons”. Here, you send the NSMutableAttributedStrings to buildBlock(_:), which you implemented earlier. They’re appended to a single attributed string, then are eventually returned by the result builder.

Improving Readability by Using Extensions and Type Aliases

Earlier in the chapter, you applied attributes like color and font size by creating a dictionary of attributes and then using that dictionary in the attributed string initializer. Now, you’ll use a fancier approach that makes the code more readable.

Add the following code to the bottom of your playground:

extension NSMutableAttributedString {
  public func color(_ color : UIColor) -> NSMutableAttributedString {
    self.addAttribute(NSAttributedString.Key.foregroundColor,
                      value: color,
                      range: NSRange(location: 0, length: self.length))
    return self
  }

  public func font(_ font : UIFont) -> NSMutableAttributedString {
    self.addAttribute(NSAttributedString.Key.font,
                      value: font,
                      range: NSRange(location: 0, length: self.length))
    return self
  }
}

This code uses an extension to add two new methods to the API of NSMutableAttributedString. These methods apply a new attribute to the string, then return it.

Now, you could write something like this (don’t actually add this to the playground):

let name = NSMutableAttributedString(string: "Daenerys").color(.blue)

Nice, isn’t it? This technique isn’t directly related to result builders, but it serves the same purpose: making the code cleaner and easier to read.

Adding Fonts and Color

Now, go back to greetBuilder, which you created earlier, and use some fonts and color! Replace it with this:

@AttributedStringBuilder
func greetBuilder(name: String, title: String) -> NSAttributedString {
  NSMutableAttributedString(string: "Hello ")
  NSMutableAttributedString(string: name)
    .color(.red)
  NSMutableAttributedString(string: ", ")
  NSMutableAttributedString(string: title)
    .font(.systemFont(ofSize: 20))
    .color(.blue)
}

In addition to the fonts and color, the method accepts the title as an argument.

Go ahead and update your call greetBuilder to take the new argument:

greetBuilder(name: "Daenerys", title: "Mother of Dragons")

With this change, you can specify the title of your choice at the call site.

Using typealias

While the result builder code is pretty straightforward, too many NSMutableAttributedString are floating around. Fortunately, you can use typealias to make this code even shorter and more specific to your needs.

Add this line to your playground:

typealias Text = NSMutableAttributedString

Here, you tell the compiler to treat Text as an alias of NSMutableAttributedString. You can now replace all occurrences of NSMutableAttributedString with Text:

@AttributedStringBuilder
func greetBuilder(name: String, title: String) -> NSAttributedString {
  Text(string: "Hello ")
  Text(string: name)
    .color(.red)
  Text(string: ", ")
  Text(string: title)
    .font(.systemFont(ofSize: 20))
    .color(.blue)
}

Finally, you can remove the need to specify the argument label string every time. It’s already clear that you’re passing in strings, so it feels redundant.

To do this, add another initializer that omits the argument label to NSMutableAttributedString. Add this to the extension:

convenience init(_ string: String) {
  self.init(string: string)
}

This new initializer calls the old one with the string you pass in. Now, replace all occurrences of Text(string:) with Text():

@AttributedStringBuilder
func greetBuilder(name: String, title: String) -> NSAttributedString {
  Text("Hello ")
  Text(name)
    .color(.red)
  Text(", ")
  Text(title)
    .font(.systemFont(ofSize: 20))
    .color(.blue)
}

Compare this to how the code looked before you implemented the result builder:

// For comparison purposes only.
func greet(name: String) -> NSAttributedString {
  let attributes = [NSAttributedString.Key.foregroundColor : UIColor.red]
  let message = NSMutableAttributedString()
  message.append(NSAttributedString(string: "Hello "))
  message.append(NSAttributedString(string: name, attributes: attributes))

  let attributes2 = [
    NSAttributedString.Key.font : UIFont.systemFont(ofSize: 20),
    NSAttributedString.Key.foregroundColor : UIColor.blue
  ]
  message.append(NSAttributedString(string: ", Mother of Dragons", 
                                    attributes: attributes2))
  return message
}

Note: Don’t add this last block of code! It is just for comparison.

greetBuilder looks so much better! While the new code isn’t much shorter, it’s much easier to understand. It’s also easier to see what you’re building and which attributes you’ve applied to each string.

Using Conditional Logic

If you pass in an empty title, you’ll get a weird result that looks like this:

greetBuilder(name: "Daenerys", title: "")
// Hello Daenerys, 

See that extra comma at the end? That doesn’t look right. You need to check whether the title is empty, and if it is, don’t add the comma. That should be simple to do by adding an if statement.

Wrap the last two Text elements in an if statement:

if !title.isEmpty {
  Text(", ")
  Text(title)
    .font(.systemFont(ofSize: 20))
    .color(.blue)
}

Oops! Once you add this code, you get an error: Closure containing control flow statement cannot be used with result builder 'AttributedStringBuilder'. What’s going on?

For a result builder to support conditional logic, you must add a new method to its definition. Add this to enum AttributedStringBuilder:

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

Under the hood, this method uses buildBlock(_:) to combine all the components in the if statement’s body. It then returns it if the condition is met. If the condition isn’t met, it returns an empty NSAttributedString. The code now compiles just fine. Give the new logic a try by passing an empty string to the title parameter of greetBuilder. Once you’ve checked the result, set the title back to “Mother of Dragons”.

Using Complex Conditional Logic

Next, you’ll add one final touch: If the title is empty, you’ll make the greet building method append “No title” to the final result. Start by adding an else clause to the existing if statement:

if !title.isEmpty {
  ...
} else {
  Text(", No title")
}

Ugh, another error! Wait, it’s the same error you just fixed by implementing buildOptional. The problem is that buildOptional only works for plain if statements that don’t have an else clause. This limitation is also true for switch statements. You’ll need to implement two new methods for these cases: buildEither(first:) and buildEither(second:).

You add two methods for the if-else case because you might want to distinguish the cases where the if condition was met from cases where it wasn’t.

Add these two methods to AttributedStringBuilder:

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

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

Similar to buildOptional(_:), these methods use buildBlock(_:) to process the expressions, then send the results as the component parameter. You can decide what to do with this value. In this implementation, all you do is return the result for the if and the else clauses.

Now, the error will go away. Call greetBuilder like this:

greetBuilder(name: "Daenerys", title: "")

This call now returns “Hello Daenerys, No title”.

Using Loops with Result Builders

If you’re familiar with Daenerys from the television show “Game of Thrones”, you know she has many titles: Mother of Dragons, Khaleesi, First of Her Name, Breaker of Chains and more. She insists on having all her titles next to her name, so you need support for multiple titles.

To support this, update the declaration of greetBuilder to the following:

@AttributedStringBuilder
func greetBuilder(name: String, titles: [String]) -> NSAttributedString {
  Text("Hello ")
  Text(name)
    .color(.red)
  if !titles.isEmpty {
    for title in titles {
      Text(", ")
      Text(title)
        .font(.systemFont(ofSize: 20))
        .color(.blue)
    }
  } else {
    Text(", No title")
  }
}

Now, add all of Daenerys’ titles:

let titles = ["Khaleesi",
              "Mhysa",
              "First of Her Name",
              "Silver Lady",
              "The Mother of Dragons"]
greetBuilder(name: "Daenerys", titles: titles)

In this new greetBuilder, you iterate over each title and create an attributed string out of it. The result builder should append these to the final result. However, the compiler can’t infer that it needs to do this. You’ll see the familiar error: Closure containing control flow statement cannot be used with result builder 'AttributedStringBuilder'.

You already know the builder throws this error when it’s missing something. In this case, it’s missing a clear definition of how to handle for-in loops. To handle this, you must implement buildArray(_:).

Add the following to your result builder:

static func buildArray(_ components: [NSAttributedString]) -> NSAttributedString {
  let attributedString = NSMutableAttributedString()
  for component in components {
    attributedString.append(component)
  }
  return attributedString
}

This code might seem familiar because it’s identical to how you implemented buildBlock(_:).

Now that you’ve resolved the error, you’ll see the following result:

You might need to enlarge the resulting pop-up to view the entire string.

Supporting Multiple Data Types

The greeting string is getting long, so you’d like to be able to break each title into a new line. This feature should be simple. Add Text("\n") line right after Text(", ") so the function looks like this:

@AttributedStringBuilder
func greetBuilder(name: String, titles: [String]) -> NSAttributedString {
  Text("Hello ")
  Text(name)
    .color(.red)
  if !titles.isEmpty {
    for title in titles {
      Text(", ")
      Text("\n")
      Text(title)
        .font(.systemFont(ofSize: 20))
        .color(.blue)
    }
  } else {
    Text(", No title")
  }
}

\n is a special combination of characters that results in a new line in a string. Rerun the playground and observe the result.

Now, your code has a few Text elements that only have a comma or a line break. Wouldn’t it be nice to replace these with a value that clearly denotes what these strings are?

Add a new enum for the special characters:

enum SpecialCharacters {
  case lineBreak
  case comma
}

Next, use the new enum inside greetBuilder.

Replace Text(", ") with SpecialCharacters.comma and Text("\n") with SpecialCharacters.lineBreak. You’ll immediately get an error: Cannot convert value of type 'SpecialCharacters' to expected argument type 'NSAttributedString'. This error makes sense because the builder expects only NSAttributedStrings.

Fortunately, there’s a solution for that. Result builders let you define how to handle expressions that aren’t the same type as the result builder return type. You’ll do this by implementing buildExpression(_:), which takes the type you want to support as an argument and returns the result builder type.

Add this to the result builder definition:

static func buildExpression(_ expression: SpecialCharacters) -> NSAttributedString {
  switch expression {
  case .lineBreak:
    return Text("\n")
  case .comma:
    return Text(",")
  }
}

Whenever the result builder sees an expression of type SpecialCharacters, it will use the method above to process it before sending it to buildBlock(_:). The contents of this method are pretty straightforward: If the expression is SpecialCharacters.lineBreak, it will return a Text with the special line break combination of characters. If the expression is SpecialCharacters.comma, it will return a Text with a comma.

One important thing to know about buildExpression(_:) is that once it’s implemented, all expressions will be sent to it for processing before being passed to buildBlock(_:). That’s also true for expressions of type NSMutableAttributedString. That’s why you now see the error Cannot convert the value of type 'NSMutableAttributedString' to expected argument type 'SpecialCharacters'.

To fix this, you need to add another buildExpression(_:), this time for expressions of type NSAttributedString:

static func buildExpression(_ expression: NSAttributedString) -> NSAttributedString {
  expression
}

And now, all the errors go away. You can use buildExpression(_:) to add support for more types if you’d like. However, they all have to eventually return an NSAttributedString because that’s the return value of the result builder.

Key Points

Result builders have use beyond Apple’s SwiftUI. Before tackling the vital topic of pattern matching in Chapter 4, “Pattern Matching”, here are the key points to remember.

  • Result builders let you define your own domain-specific language for declaring and configuring values of a specific type.
  • You can use result builders on functions, getters and closures.
  • buildBlock(_:) goes over all expressions in the result builder code and decides what to do with them. Eventually, it returns one expression of the result builder’s type.
  • You must use buildOptional(_:) to support if statements.
  • You must implement buildEither(first:) and buildEither(second:) to support if-else and switch statements.
  • To support for-in loops, you need buildArray(_:).
  • To support expressions other than the result builder return type, you must implement buildExpression(_:).
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.