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?
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:
-
RacersType
is the generic type for this function. It can be any type that conforms to the Swift standard library'sSequence
protocol. - The
where
clause specifies that theElement
type of theSequence
must conform to yourRacer
protocol to use this function. - 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!