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.
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
Protocol-Oriented Programming Tutorial in Swift 5.1: Getting Started
25 mins
- Getting Started
- Why Protocol-Oriented Programming?
- Hatching the Egg
- Defining Protocol-Conforming Types
- Extending Protocols With Default Implementations
- Enums Can Play, Too
- Overriding Default Behavior
- Extending Protocols
- Effects on the Swift Standard Library
- Off to the Races
- Bringing It All Together
- Top Speed
- Making It More Generic
- Making It More Swifty
- Protocol Comparators
- Mutating Functions
- Where to Go From Here?
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:
- First, define the protocol
Racer
. This protocol defines anything that can be raced in your game. - Then, conform everything to
Racer
so that all our existing types can be raced. Some types, such asMotorcycle
, conform trivially. Others, such asUnladenSwallow
, need a bit more logic. Either way, when you’re done you have a bunch of conformingRacer
types. - 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
.