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
You are currently viewing page 3 of 3 of this article. Click here to view the first page.

Understanding Result Builder Loops

How does that loop work? Add a breakpoint inside both result builder functions (by clicking the line numbers). Build and run.

Adding Xcode breakpoints in your result builder

When you type a letter, you can see each step. Each time execution stops, click the continue button to jump to the next breakpoint until it's finished.

Debugging your result builder

You'll find that the compiler hits the buildBlock three times, the buildArray once, and then the buildBlock one last time. You can imagine the compiler creating something like this:

// 1
let rule1: CipherRule = CipherBuilder.buildBlock(
  LetterSubstitution(offset: 7)
)
let rule2: CipherRule = CipherBuilder.buildBlock(
  LetterSubstitution(offset: 7)
)
let rule3: CipherRule = CipherBuilder.buildBlock(
  LetterSubstitution(offset: 7)
)
// 2
let rule4: CipherRule = CipherBuilder.buildArray(
  [rule1, rule2, rule3]
)
  1. This is where you loop three times. The result builder calls buildBlock(_:) each time to output a single rule. In this case, the rule is an instance of LetterSubstitution.
  2. The result builder assembles these three rules into a single array and calls buildArray(_:). Once again, the result is output as a single rule.
  3. Finally, the result builder calls buildBlock(_:) again to return that rule as the result.

You'll never see this code anywhere, but imagining what's happening internally when you plan a result builder is helpful. It's all in the planning and your use of CipherRule as the primary type that's paid off handsomely. Nice work, Agent.

Adding Support for Optional Values

Okay...so now those eggheads are scrambling to produce an even stronger cipher. They feel it's unwise to allow official terminology to be output in the cipher text. So they would like to optionally supply a dictionary of official terms and an obfuscated replacement. Like swapping "brains" for "Swiss cheese", you muse.

It's time for another CipherRule!

Create a file called ReplaceVocabulary.swift and add:

struct ReplaceVocabulary: CipherRule {
  // 1
  let terms: [(original: String, replacement: String)]

  func encipher(_ value: String) -> String {
    // 2
    terms.reduce(value) { encipheredMessage, term in
      encipheredMessage.replacingOccurrences(
        of: term.original, 
        with: term.replacement, 
        options: .caseInsensitive
      )
    }
  }

  func decipher(_ value: String) -> String {
    // 3
    terms.reduce(value) { decipheredMessage, term in
      decipheredMessage.replacingOccurrences(
        of: term.replacement, 
        with: term.original, 
        options: .caseInsensitive
      )
    }
  }
}
  1. terms is an array of tuples with two Strings each, matching the original term with its replacement.
  2. In encipher(_:), you loop through the array and perform the replacements in a case-insensitive manner.
  3. decipher(_:) does the same but swaps all the replacements with originals.

Open SuperSecretCipher.swift. Add this property to let the eggheads control the optionality:

let useVocabularyReplacement: Bool

It's a simple Bool that you now need to use in cipherRule. Add the following before the cycles loop:

if useVocabularyReplacement {
  ReplaceVocabulary(terms: [
    ("SECRET", "CHOCOLATE"),
    ("MESSAGE", "MESS"),
    ("PROTOCOL", "LEMON GELATO"),
    ("DOOMSDAY", "BLUEBERRY PIE")
  ])
}

The idea is that, for a message such as "the doomsday protocol is initiated", your cipher will first replace it with "the BLUEBERRY PIE LEMON GELATO is initiated" before the letter substitution occurs. This will surely confound enemy spies!

If you build and run the app, you see a familiar build error:

Another Xcode build error with a fix button

This time, open CipherBuilder.swift. Add the following method to CipherBuilder:

static func buildOptional(_ component: CipherRule?) -> CipherRule {
  component ?? []
}

This is how result builders handle optionality, such as an if statement. This one calls buildOptional(_:) with a CipherRule or nil, depending on the condition.

How can the fallback value for CipherRule be []? This is where you take advantage of the Swift type system. Because you extended Array to be a CipherRule when the element type is CipherRule, you can return an empty array when component is nil. You could expand that function body to express these types explicitly:

let fallback: [CipherRule] = .init(arrayLiteral: [])
return component ?? fallback

But you're in the business of allowing the compiler to just do its thing. :]

In your result builder's design, that empty array will not affect the result, which is precisely what you're looking for in the if useVocabularyReplacement expression. Pretty smart, Agent. That's the sort of on-your-feet thinking that'll get HQ's attention...and maybe that promotion?

Open ContentView.swift. Update cipher inside processMessage(_:) to take in the new useVocabularyReplacement parameter:

let cipher = SuperSecretCipher(
  offset: 7,
  cycles: 3,
  useVocabularyReplacement: true
)

Build and run to see how your SuperSecretCipher performs.

Your final rule is working in the app

Perfect! The eggheads are finally satisfied, and your presence is required at HQ. On to the next mission, Agent, and remember that result builders are at your disposal.

Where to Go From Here?

You've only begun to explore the possibilities of result builders. You can find information about additional capabilities in the documentation:

For inspiration, you might want to check out Awesome result builders, a collection of result builders you can find on GitHub.

If you're looking for an extra challenge, try implementing support for if { ... } else { ... } statements and other result builder logic. Or check out this list of historical ciphers at Practical Cryptography and pick one to form a new CipherRule. You'll find a couple of familiar entries in that list. :]

I hope you enjoyed this tutorial on result builders. If you have any questions or comments, please join the forum discussion below.