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

Defining a Cipher Rule

First, you need to define what a cipher rule is. Create a file called CipherRule.swift and add:

protocol CipherRule {
  func encipher(_ value: String) -> String
  func decipher(_ value: String) -> String
}

There will be multiple rule types, so you’ve wisely opted for a protocol. Both encipher(_:) and decipher(_:) take a String and output a String. When enciphering a message, the plain text passes through each rule’s encipher(_:) function to produce the cipher text; when deciphering, the cipher text passes through each rule’s decipher(_:) function to produce the plain text.

Open CipherBuilder.swift. Update buildBlock(_:) to use CipherRule as its type.

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

Because your agent training has raised your powers of observation well above average, you’ll have noticed a problem: How can a varying number of CipherRule arguments be output as a single CipherRule? Can an array of CipherRule elements also be a CipherRule, you ask? Excellent idea; make it so!

Add the following extension below the CipherRule protocol:

// 1
extension Array: CipherRule where Element == CipherRule {
  // 2
  func encipher(_ value: String) -> String {
    // 3
    reduce(value) { encipheredMessage, secret in
      secret.encipher(encipheredMessage)
    }
  }

  func decipher(_ value: String) -> String {
  // 4
    reversed().reduce(value) { decipheredMessage, secret in
      secret.decipher(decipheredMessage)
    }
  }
}
  1. You extend Array by implementing CipherRule when the Element is also a CipherRule.
  2. You fulfill the CipherRule definition by implementing encipher(_:) and decipher(_:).
  3. You use reduce(_:_:) to pass the cumulative value through each element, returning the result of encipher(_:).
  4. You reverse the order and use reduce(_:_:) again, this time calling decipher(_:).

This code is the core of any cipher in Decoder Ring and implements the plan in the previous diagram.

Do not worry about the compiler error, you will resolve it in the Building a Cipher section.

Writing the Rules

It’s time to write your first rule: The LetterSubstitution rule. This rule will take a string and substitute each letter with another letter based on an offset value. For example, if the offset was three, then the letter “a” is replaced by “d”, “b” is replaced by “e”, “c” with “f” and so on…

Create a file called LetterSubstitution.swift and add:

struct LetterSubstitution: CipherRule {
  let letters: [String]
  let offset: Int

  // 1
  init(offset: Int) {
    self.letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".map(String.init)
    self.offset = max(1, min(offset, 25))
  }
  
  // 2
  func swapLetters(_ value: String, offset: Int) -> String {
    // 3
    let plainText = value.map(String.init)
    // 4
    return plainText.reduce("") { message, letter in
      if let index = letters.firstIndex(of: letter.uppercased()) {
        let cipherOffset = (index + offset) % 26
        let cipherIndex = cipherOffset < 0 ? 26 
          + cipherOffset : cipherOffset
        let cipherLetter = letters[cipherIndex]
        return message + cipherLetter
      } else {
        return message + letter
      }
    }
  }
}
  1. Your initializer creates an array of all the upper-case letters and checks that the offset is between 1 and 25.
  2. You implement the core logic of the rule in swapLetters(_:offset:).
  3. You create an array of all the letters in the message and assign it to the plainText variable.
  4. You loop through each letter in plainText and build a result using the appropriate substitute letter determined by the offset. Of course, you're careful to check that the offset of the substitute is valid.

Next, you must add the CipherRule functions needed to fulfill the protocol. Add the following above swapLetters(_:offset:):

func encipher(_ value: String) -> String {
  swapLetters(value, offset: offset)
}

func decipher(_ value: String) -> String {
  swapLetters(value, offset: -offset)
}

Both required functions call swapLetters(_:offset:). Notice that decipher(_:) passes in the negative offset to reverse the enciphered letters.

That's your first rule. Well done, Agent.

Building a Cipher

Now, it's time to put your CipherBuilder to the test. The eggheads at HQ have an idea for something they call the Super-secret-non-egg-related-so-really-uncrackable Cipher. That's quite the mouthful, so how about just creating a file called SuperSecretCipher.swift and adding the following:

struct SuperSecretCipher {
  let offset: Int

  @CipherBuilder
  var cipherRule: CipherRule {
    LetterSubstitution(offset: offset)
  }
}

SuperSecretCipher has an Int property for the letter offset plus a special property: cipherRule. cipherRule is special because you've added the @CipherBuilder annotation, just like you did for buildEggCipherMessage(). This means cipherRule is now a result builder. Inside the body of the result builder, you use your new LetterSubstitution rule and the offset value.

Open ContentView.swift. Remove onAppear(perform:) and buildEggCipherMessage().

Replace the body of processMessage(_:) with the following:

let cipher = SuperSecretCipher(offset: 7)
switch secretMode {
case .encode:
  return cipher.cipherRule.encipher(value)
case .decode:
  return cipher.cipherRule.decipher(value)
}

processMessage(_:) is called whenever the message text changes or the switch is toggled. SuperSecretCipher has an offset of 7, but that's configurable and ultimately up to the eggheads. If the mode is .encipher, it calls encipher(_:) on cipherRule. Otherwise, it calls decipher(_:).

Build and run to see the result of all your hard work.

The app running your first cipher

Remember to try the decipher mode.

The app deciphering a secret code

Expanding Syntax Support

Those eggheads from HQ have reviewed your work and requested changes (of course, they have). They've requested you allow them to specify how many times to perform the substitution, so it's "doubly, no Triply, no QUADRUPLY uncrackable". Maybe they've cracked under the strain! :]

Hop to it, Agent. You might be wondering, given your thoughtful implementation...is it even that hard?

Open SuperSecretCipher.swift. Add the following property to SuperSecretCipher:

let cycles: Int

Replace `cipherRule` with the following:

Now, this is where things start to get even more interesting. Update the body of cipherBuilder like so:

for _ in 1...cycles {
  LetterSubstitution(offset: offset)
}

Open ContentView.swift. In ContentView, update processMessage(_:) with the new argument. Replace:

let cipher = SuperSecretCipher(offset: 7)

With:

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

If you build, you see a new error:

Xcode build error with a fix button

Not a problem. Open CipherBuilder.swift.

If you're feeling lucky, try that Fix button. Otherwise, add the following method to CipherBuilder:

static func buildArray(_ components: [CipherRule]) -> CipherRule {
  components
}

This is another one of those special static functions you can add to any result builder. Because you've planned and ensured that any array of CipherRules is also a CipherRule, your implementation of this method is to simply return components. Well done, you!

Build and run. Your app should triple-encipher the message:

The app triple-enciphering

Brilliant!