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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Swift Result Builders: Getting Started
20 mins
- Getting Started
- Introducing Decoder Ring
- Making Your First Result Builder
- Understanding Result Builders
- Planning Your Cipher Builder
- Defining a Cipher Rule
- Writing the Rules
- Building a Cipher
- Expanding Syntax Support
- Understanding Result Builder Loops
- Adding Support for Optional Values
- Where to Go From Here?
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)
}
}
}
- You extend
Array
by implementingCipherRule
when theElement
is also aCipherRule
. - You fulfill the
CipherRule
definition by implementingencipher(_:)
anddecipher(_:)
. - You use
reduce(_:_:)
to pass the cumulativevalue
through each element, returning the result ofencipher(_:)
. - You reverse the order and use
reduce(_:_:)
again, this time callingdecipher(_:)
.
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
}
}
}
}
- Your initializer creates an array of all the upper-case letters and checks that the
offset
is between 1 and 25. - You implement the core logic of the rule in
swapLetters(_:offset:)
. - You create an array of all the letters in the message and assign it to the
plainText
variable. - You loop through each letter in
plainText
and build a result using the appropriate substitute letter determined by theoffset
. 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.
Remember to try the decipher mode.
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:
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 CipherRule
s 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:
Brilliant!