Chapters

Hide chapters

Swift Apprentice: Beyond the Basics

First Edition · iOS 16 · Swift 5.8 · Xcode 14.3

Section I: Beyond the Basics

Section 1: 13 chapters
Show chapters Hide chapters

3. Result Builders
Written by Eli Ganim

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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.

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

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.

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

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))

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

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.

@resultBuilder
enum AttributedStringBuilder {
}

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

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.

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

greetBuilder(name: "Daenerys")

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.

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
  }
}
let name = NSMutableAttributedString(string: "Daenerys").color(.blue)

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)
}
greetBuilder(name: "Daenerys", title: "Mother of Dragons")

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.

typealias Text = NSMutableAttributedString
@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)
}
convenience init(_ string: String) {
  self.init(string: string)
}
@AttributedStringBuilder
func greetBuilder(name: String, title: String) -> NSAttributedString {
  Text("Hello ")
  Text(name)
    .color(.red)
  Text(", ")
  Text(title)
    .font(.systemFont(ofSize: 20))
    .color(.blue)
}
// 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
}

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, 
if !title.isEmpty {
  Text(", ")
  Text(title)
    .font(.systemFont(ofSize: 20))
    .color(.blue)
}
static func buildOptional(_ component: NSAttributedString?) -> NSAttributedString {
  component ?? NSAttributedString()
}

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")
}
static func buildEither(first component: NSAttributedString) -> NSAttributedString {
  component
}

static func buildEither(second component: NSAttributedString) -> NSAttributedString {
  component
}
greetBuilder(name: "Daenerys", 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.

@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")
  }
}
let titles = ["Khaleesi",
              "Mhysa",
              "First of Her Name",
              "Silver Lady",
              "The Mother of Dragons"]
greetBuilder(name: "Daenerys", titles: titles)
static func buildArray(_ components: [NSAttributedString]) -> NSAttributedString {
  let attributedString = NSMutableAttributedString()
  for component in components {
    attributedString.append(component)
  }
  return attributedString
}

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")
  }
}

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

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.

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.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now