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

Enums Can Play, Too

Enum types in Swift are much more powerful than enums from C and C++. They adopt many features that only class or struct types traditionally support, meaning they can conform to protocols.

Add the following enum definition to the end of the playground:

enum UnladenSwallow: Bird, Flyable {
  case african
  case european
  case unknown
  
  var name: String {
    switch self {
    case .african:
      return "African"
    case .european:
      return "European"
    case .unknown:
      return "What do you mean? African or European?"
    }
  }
  
  var airspeedVelocity: Double {
    switch self {
    case .african:
      return 10.0
    case .european:
      return 9.9
    case .unknown:
      fatalError("You are thrown from the bridge of death!")
    }
  }
}

By defining the correct properties, UnladenSwallow conforms to the two protocols Bird and Flyable. Because it’s such a conformist, it also enjoys default implementation for canFly.

Did you really think a tutorial involving airspeedVelocity could pass up a Monty Python reference? :]

Overriding Default Behavior

Your UnladenSwallow type automatically received an implementation for canFly by conforming to the Bird protocol. However, you want UnladenSwallow.unknown to return false for canFly.

Can you override the default implementation? You bet. Go back to the end of your playground and add some new code:

extension UnladenSwallow {
  var canFly: Bool {
    self != .unknown
  }
}

Now only .african and .european will return true for canFly. Try it out! Add the following code at the end of your playground:

UnladenSwallow.unknown.canFly         // false
UnladenSwallow.african.canFly         // true
Penguin(name: "King Penguin").canFly  // false

Build the playground and you’ll notice it shows the values as given in the comments above.

In this way, you override properties and methods much like you can with virtual methods in object-oriented programming.

Extending Protocols

You can also conform your own protocols to other protocols from the Swift standard library and define default behaviors. Replace your Bird protocol declaration with the following code:

protocol Bird: CustomStringConvertible {
  var name: String { get }
  var canFly: Bool { get }
}

extension CustomStringConvertible where Self: Bird {
  var description: String {
    canFly ? "I can fly" : "Guess I'll just sit here :["
  }
}

Conforming to CustomStringConvertible means your type needs to have a description property so it be converted to a String automatically when needed. Instead of adding this property to every current and future Bird type, you’ve defined a protocol extension that CustomStringConvertible will only associate with types of Bird.

Try it out by entering the following at the bottom of the playground:

UnladenSwallow.african

Build the playground and you should see “I can fly” appear in the assistant editor. Congratulations! You’ve extended your protocol.

Effects on the Swift Standard Library

Protocol extensions can’t grip a one-pound coconut by the husk, but as you’ve seen, they can provide an efficient method for customizing and extending the capabilities of named types. The Swift team also employed protocols to improve the Swift standard library.

Add this code to the end of your playground:

let numbers = [10, 20, 30, 40, 50, 60]
let slice = numbers[1...3]
let reversedSlice = slice.reversed()

let answer = reversedSlice.map { $0 * 10 }
print(answer)

You may be able to guess the printed answer, but what might surprise you are the types involved.

For example, slice is not an Array<Int> but an ArraySlice<Int>. This special wrapper type acts as a view into the original array, offering a fast and efficient way to perform operations on sections of a larger array. Similarly, reversedSlice is a ReversedCollection<ArraySlice<Int>>, another wrapper type with a view into the original array.

Fortunately, the wizards developing the Swift standard library defined the map function as an extension to the Sequence protocol, which all Collection types conform to. This lets you call map on Array as easily as on ReversedCollection and not notice the difference. You’ll borrow this important design pattern shortly.

Off to the Races

So far, you’ve defined several types that conform to Bird. You’ll now add something completely different at the end of your playground:

class Motorcycle {
  init(name: String) {
    self.name = name
    speed = 200.0
  }

  var name: String
  var speed: Double
}

This class has nothing to do with birds or flying. You just want to race motorcycles against penguins. It’s time to bring these wacky racers to the starting line.

Bringing It All Together

To unify these disparate types, you need a common protocol for racing. You can manage this without even touching the original model definitions thanks to a fancy idea called retroactive modeling. Just add the following to your playground:

// 1
protocol Racer {
  var speed: Double { get }  // speed is the only thing racers care about
}

// 2
extension FlappyBird: Racer {
  var speed: Double {
    airspeedVelocity
  }
}

extension SwiftBird: Racer {
  var speed: Double {
    airspeedVelocity
  }
}

extension Penguin: Racer {
  var speed: Double {
    42  // full waddle speed
  }
}

extension UnladenSwallow: Racer {
  var speed: Double {
    canFly ? airspeedVelocity : 0.0
  }
}

extension Motorcycle: Racer {}

// 3
let racers: [Racer] =
  [UnladenSwallow.african,
   UnladenSwallow.european,
   UnladenSwallow.unknown,
   Penguin(name: "King Penguin"),
   SwiftBird(version: 5.1),
   FlappyBird(name: "Felipe", flappyAmplitude: 3.0, flappyFrequency: 20.0),
   Motorcycle(name: "Giacomo")]

Here’s what this does:

  1. First, define the protocol Racer. This protocol defines anything that can be raced in your game.
  2. Then, conform everything to Racer so that all our existing types can be raced. Some types, such as Motorcycle, conform trivially. Others, such as UnladenSwallow, need a bit more logic. Either way, when you’re done you have a bunch of conforming Racer types.
  3. With all of the types at the starting line, you now create an Array<Racer> which holds an instance of each of the types you’ve created.

Build the playground to check everything compiles.

Top Speed

It’s time to write a function that determines the top speed of the racers. Add the following code to the end of your playground:

func topSpeed(of racers: [Racer]) -> Double {
  racers.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}

topSpeed(of: racers) // 5100

This function uses the Swift standard library function max to find the racer with the highest speed and return it. It returns 0.0 if the user passes in an empty Array in for racers.

Build the playground and you'll see that the max speed of the racers you created earlier is indeed 5100.