Protocol-Oriented Programming Tutorial in Swift 5.1: Getting Started

In this protocol-oriented programming tutorial, you’ll learn about extensions, default implementations and other techniques to add abstraction to your code. By Andy Pereira.

4.8 (53) · 3 Reviews

Download materials
Save for later
Share
You are currently viewing page 3 of 3 of this article. Click here to view the first page.

Making It More Generic

Suppose Racers is rather large, and you only want to find the top speed for a subset of the participants. The solution is to alter topSpeed(of:) to take anything that's a Sequence instead of the concrete Array.

Replace your existing implementation of topSpeed(of:) with the following function:

// 1
func topSpeed<RacersType: Sequence>(of racers: RacersType) -> Double
    /*2*/ where RacersType.Iterator.Element == Racer {
  // 3
  racers.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}

This one may look a bit scary, but here's how it breaks down:

  1. RacersType is the generic type for this function. It can be any type that conforms to the Swift standard library's Sequence protocol.
  2. The where clause specifies that the Element type of the Sequence must conform to your Racer protocol to use this function.
  3. The actual function body is the same as before.

Now add the following code to the bottom of your playground:

topSpeed(of: racers[1...3]) // 42

Build the playground and you'll see an answer of 42 as the output. The function now works for any Sequence type including ArraySlice.

Making It More Swifty

Here's a secret: You can do even better. Add this at the end of your playground:

extension Sequence where Iterator.Element == Racer {
  func topSpeed() -> Double {
    self.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
  }
}

racers.topSpeed()        // 5100
racers[1...3].topSpeed() // 42

Borrowing from the Swift standard library playbook, you've now extended Sequence itself to have a topSpeed() function. The function is easily discoverable and only applies when you are dealing with a Sequence of Racer types.

Protocol Comparators

Another feature of Swift protocols is how you denote operator requirements such as equality of objects for == or how to compare objects for > and <. You know the deal — add the following code to the bottom of your playground:

protocol Score {
  var value: Int { get }
}

struct RacingScore: Score {
  let value: Int
}

Having a Score protocol means you can write code that treats all scores the same way. But by having different concrete types, such as RacingScore, you won't mix up these scores with style scores or cuteness scores. Thanks, compiler!

You want scores to be comparable so you can tell who has the highest score. Before Swift 3, developers needed to add global operator functions to conform to these protocols. Today, you can define these static methods as part of the model. Do so by replacing the definition of Score and RacingScore with the following:

protocol Score: Comparable {
  var value: Int { get }
}

struct RacingScore: Score {
  let value: Int
  
  static func <(lhs: RacingScore, rhs: RacingScore) -> Bool {
    lhs.value < rhs.value
  }
}

Nice! You've encapsulated all of the logic for RacingScore in one place. Comparable only requires you to provide an implementation for the less-than operator. The rest of the operators for comparison, like greater-than, have default implementations provided by the Swift standard library based on the less-than operator.

Test your newfound operator skills with the following line of code at the bottom of your playground:

RacingScore(value: 150) >= RacingScore(value: 130) // true

Build the playground and you'll notice that the answer is true as expected. You can now compare scores!

Mutating Functions

So far, every example you've implemented has demonstrated how to add functionality. But what if you want to have a protocol define something that changes an aspect of your object? You can do this by using mutating methods in your protocol.

At the bottom of the playground, add the following new protocol:

protocol Cheat {
  mutating func boost(_ power: Double)
}

This defines a protocol that gives your type the ability to cheat. How? By adding a boost to anything you feel is appropriate.

Next, create an extension on SwiftBird that conforms to Cheat with the following code:

extension SwiftBird: Cheat {
  mutating func boost(_ power: Double) {
    speedFactor += power
  }
}

Here, you implement boost(_:) and make speedFactor increase by the power passed in. You add the mutating keyword to let the struct know one of its values will change within this function.

Add the following code to the playground to see how this works:

var swiftBird = SwiftBird(version: 5.0)
swiftBird.boost(3.0)
swiftBird.airspeedVelocity // 5015
swiftBird.boost(3.0)
swiftBird.airspeedVelocity // 5030

Here, you've created a SwiftBird that is mutable, and you boosted its velocity by three and then by three again. Build the playground and you should notice that the airspeedVelocity of the SwiftBird has increased with each boost.

Where to Go From Here?

Use the Download Materials button at the top or bottom of this tutorial to download the completed playground.

At this point, you've tested the power of protocol-oriented programming by creating simple protocols and expanding them with protocol extensions. With default implementations, you can give existing protocols common and automatic behavior. This is like a base class only better, since they can apply to struct and enum types too.

You've also seen that protocol extensions can extend and provide default behavior to protocols in the Swift standard library, Cocoa, Cocoa Touch or any third-party library.

To continue learning more about protocols, read the official Swift documentation.

You can view an excellent WWDC session on protocol-oriented programming on Apple's developer portal. It provides an in-depth exploration of the theory behind it all.

Read more about the rationale for operator conformance in its Swift evolution proposal. You may also want to learn more about Swift Collection protocols and learn how to build your own.

As with any programming paradigm, it's easy to get overly exuberant and use it for all the things. This interesting blog post by Chris Eidhof reminds readers that they should beware of silver-bullet solutions. Don't use protocols everywhere "just because."

Have any questions? Let us know in the forum discussion below!