16
Protocols
Written by Ehab Yosry Amer
Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as
text.You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.
In this book, you’ve learned about the three named types: structs, classes and enums. There’s one more named type to learn about: the protocol.
Unlike the other named types, protocols don’t define anything you instantiate directly. Instead, they define an interface or blueprint that actual concrete types conform to. With a protocol, you define a common set of properties and behaviors that concrete types go and implement.
You’ve been using protocol behind the scenes from the beginning of this book. In this chapter, you’ll learn the details about protocols and see why they’re central to Swift.
Introducing protocols
You define a protocol much as you do any other named type. Enter the following into a playground:
protocol Vehicle {
func accelerate()
func stop()
}
The keyword protocol
is followed by the name of the protocol, followed by the curly braces with the members of the protocol inside. The big difference you’ll notice is that the protocol doesn’t contain any implementation.
That means you can’t instantiate a Vehicle
directly:
Instead, you use protocols to enforce methods and properties on other types. What you’ve defined here is something like the idea of a vehicle — it’s something that can accelerate and stop.
Protocol syntax
A protocol can be adopted by a class, struct or enum — and when another type adopts a protocol, it’s required to implement the methods and properties defined in the protocol. Once a type implements all members of a protocol, the type is said to conform to the protocol.
class Unicycle: Vehicle {
var peddling = false
func accelerate() {
peddling = true
}
func stop() {
peddling = false
}
}
Methods in protocols
In the Vehicle
protocol above, you define a pair of methods, accelerate()
and stop()
, that all types conforming to Vehicle
must implement.
enum Direction {
case left
case right
}
protocol DirectionalVehicle {
func accelerate()
func stop()
func turn(_ direction: Direction)
func description() -> String
}
protocol OptionalDirectionVehicle {
// Build error!
func turn(_ direction: Direction = .left)
}
protocol OptionalDirectionVehicle {
func turn()
func turn(_ direction: Direction)
}
Properties in protocols
You can also define properties in a protocol:
protocol VehicleProperties {
var weight: Int { get }
var name: String { get set }
}
Initializers in protocols
While protocols themselves can’t be initialized, they can declare initializers that conforming types should have:
protocol Account {
var value: Double { get set }
init(initialAmount: Double)
init?(transferAccount: Account)
}
class BitcoinAccount: Account {
var value: Double
required init(initialAmount: Double) {
value = initialAmount
}
required init?(transferAccount: Account) {
guard transferAccount.value > 0.0 else {
return nil
}
value = transferAccount.value
}
}
var accountType: Account.Type = BitcoinAccount.self
let account = accountType.init(initialAmount: 30.00)
let transferAccount = accountType.init(transferAccount: account)!
Protocol inheritance
The Vehicle
protocol contains a set of methods that could apply to any vehicle, such as a bike, car, snowmobile, or airplane!
protocol WheeledVehicle: Vehicle {
var numberOfWheels: Int { get }
var wheelSize: Double { get set }
}
Mini-exercises
- Create an
Area
protocol that defines a read-only propertyarea
of typeDouble
. - Implement
Area
with structs representingSquare
,Triangle
andCircle
. - Add a circle, a square and a triangle to an array. Convert the array of shapes to an array of areas using
map
.
Implementing protocols
As you’ve already seen, when you declare your type as conforming to a protocol, you must implement all the requirements declared in the protocol:
class Bike: Vehicle {
var peddling = false
var brakesApplied = false
func accelerate() {
peddling = true
brakesApplied = false
}
func stop() {
peddling = false
brakesApplied = true
}
}
Implementing properties
Recall that properties in protocols come with a get
and possibly a set
requirement and that a conforming type must conform to at least these requirements.
class Bike: WheeledVehicle {
let numberOfWheels = 2
var wheelSize = 16.0
var peddling = false
var brakesApplied = false
func accelerate() {
peddling = true
brakesApplied = false
}
func stop() {
peddling = false
brakesApplied = true
}
}
Associated types in protocols
You can also add an associated type as a protocol member. When using associatedtype
in a protocol, you’re simply stating there is a type used in this protocol, without specifying what type this should be. It’s up to the protocol adopter to decide what the exact type should be.
protocol WeightCalculatable {
associatedtype WeightType
var weight: WeightType { get }
}
class HeavyThing: WeightCalculatable {
// This heavy thing only needs integer accuracy
typealias WeightType = Int
var weight: Int { 100 }
}
class LightThing: WeightCalculatable {
// This light thing needs decimal places
typealias WeightType = Double
var weight: Double { 0.0025 }
}
// Build error!
// protocol 'WeightCalculatable' can only be used as a generic
// constraint because it has Self or associated type requirements.
let weightedThing: WeightCalculatable = LightThing()
Implementing multiple protocols
A class can only inherit from a single class — this is the property of “single inheritance”. By contrast, a class, structure or enumeration can conform to as many protocols as you’d like! Suppose instead of creating a WheeledVehicle
protocol that inherits from Vehicle
that you made Wheeled
a protocol.
protocol Wheeled {
var numberOfWheels: Int { get }
var wheelSize: Double { get set }
}
class Bike: Vehicle, Wheeled {
// Implement both Vehicle and Wheeled
}
Protocol composition
In the previous section, you learned how to implement multiple protocols. Sometimes you need a function to take a data type that must conform to multiple protocols. That is where protocol composition comes in. Imagine you need a function that needs access to the Vehicle
protocol’s stop()
function and the Wheeled
protocol’s numberOfWheels
property. You can do this using the &
composition operator.
func roundAndRound(transportation: Vehicle & Wheeled) {
transportation.stop()
print("The brakes are being applied to
\(transportation.numberOfWheels) wheels.")
}
roundAndRound(transportation: Bike())
// The brakes are being applied to 2 wheels.
Extensions & protocol conformance
You can also adopt protocols using extensions. This language feature lets you add protocol conformance to types you don’t necessarily own. Consider the simple example below, which adds a custom protocol to String
:
protocol Reflective {
var typeName: String { get }
}
extension String: Reflective {
var typeName: String {
"I’m a String"
}
}
let title = "Swift Apprentice!"
title.typeName // I’m a String
class AnotherBike: Wheeled {
var peddling = false
let numberOfWheels = 2
var wheelSize = 16.0
}
extension AnotherBike: Vehicle {
func accelerate() {
peddling = true
}
func stop() {
peddling = false
}
}
Requiring reference semantics
Protocols can be adopted by both value types (structs and enums) and reference types (classes), so you might wonder if protocols have reference or value semantics.
protocol Named {
var name: String { get set }
}
class ClassyName: Named {
var name: String
init(name: String) {
self.name = name
}
}
struct StructyName: Named {
var name: String
}
var named: Named = ClassyName(name: "Classy")
var copy = named
named.name = "Still Classy"
named.name // Still Classy
copy.name // Still Classy
named = StructyName(name: "Structy")
copy = named
named.name = "Still Structy?"
named.name // Still Structy?
copy.name // Structy
protocol Named: AnyObject {
var name: String { get set }
}
Protocols: More than bags of syntax
As you have seen, protocols let you specify many syntax requirements for conforming types. However, they can’t (and never will) let you specify every conceivable requirement for the compiler to check. For example, a protocol may need to specify complexity requirements (O(1) vs. O(n)) for an operation, and it can do this only by stating it in comments. You need to understand all of these requirements that a protocol makes to conform correctly. This reality has lead to the refrain that protocols are more than just bags of syntax that the compiler can check.
Protocols in the Standard Library
The Swift standard library uses protocols extensively in ways that may surprise you. Understanding the roles protocols play in Swift can help you write clean, decoupled “Swifty” code.
Equatable
Some of the simplest code compares two integers with the ==
operator:
let a = 5
let b = 5
a == b // true
let swiftA = "Swift"
let swiftB = "Swift"
swiftA == swiftB // true
class Record {
var wins: Int
var losses: Int
init(wins: Int, losses: Int) {
self.wins = wins
self.losses = losses
}
}
let recordA = Record(wins: 10, losses: 5)
let recordB = Record(wins: 10, losses: 5)
recordA == recordB // Build error!
protocol Equatable {
static func ==(lhs: Self, rhs: Self) -> Bool
}
extension Record: Equatable {
static func ==(lhs: Record, rhs: Record) -> Bool {
lhs.wins == rhs.wins &&
lhs.losses == rhs.losses
}
}
recordA == recordB // true
Comparable
A subprotocol of Equatable
is Comparable
:
protocol Comparable: Equatable {
static func <(lhs: Self, rhs: Self) -> Bool
static func <=(lhs: Self, rhs: Self) -> Bool
static func >=(lhs: Self, rhs: Self) -> Bool
static func >(lhs: Self, rhs: Self) -> Bool
}
extension Record: Comparable {
static func <(lhs: Record, rhs: Record) -> Bool {
if lhs.wins == rhs.wins {
return lhs.losses > rhs.losses
}
return lhs.wins < rhs.wins
}
}
“Free” functions
While ==
and <
are useful in their own right, the Swift library provides you with many “free” functions and methods for types that conform to Equatable
and Comparable
.
let teamA = Record(wins: 14, losses: 11)
let teamB = Record(wins: 23, losses: 8)
let teamC = Record(wins: 23, losses: 9)
var leagueRecords = [teamA, teamB, teamC]
leagueRecords.sort()
// {wins 14, losses 11}
// {wins 23, losses 9}
// {wins 23, losses 8}
leagueRecords.max() // {wins 23, losses 8}
leagueRecords.min() // {wins 14, losses 11}
leagueRecords.starts(with: [teamA, teamC]) // true
leagueRecords.contains(teamA) // true
Other useful protocols
While learning the entire Swift standard library isn’t vital to your success as a Swift developer, there are a few other essential protocols you’ll find helpful in almost any project.
Hashable
class Student {
let email: String
let firstName: String
let lastName: String
init(email: String, firstName: String, lastName: String) {
self.email = email
self.firstName = firstName
self.lastName = lastName
}
}
extension Student: Hashable {
static func ==(lhs: Student, rhs: Student) -> Bool {
lhs.email == rhs.email &&
lhs.firstName == rhs.firstName &&
lhs.lastName == rhs.lastName
}
func hash(into hasher: inout Hasher) {
hasher.combine(email)
hasher.combine(firstName)
hasher.combine(lastName)
}
}
let john = Student(email: "johnny.appleseed@apple.com",
firstName: "Johnny",
lastName: "Appleseed")
let lockerMap = [john: "14B"]
Identifiable
extension Student: Identifiable {
var id: String {
email
}
}
CustomStringConvertible
The convenient CustomStringConvertible
protocol helps you log and debug instances.
print(john)
// Student
protocol CustomStringConvertible {
var description: String { get }
}
extension Student: CustomStringConvertible {
var description: String {
"\(firstName) \(lastName)"
}
}
print(john)
// Johnny Appleseed
Challenge
Before moving on, here is a challenge to test your knowledge of protocols. It is best to try to solve it yourself, but, as always, a solution is available if you get stuck.
Challenge 1: Pet shop tasks
Create a collection of protocols for tasks at a pet shop with dogs, cats, fish and birds.
Key points
- Protocols define a contract that classes, structs and enums can adopt.
- Adopting a protocol requires a type to conform to the protocol by implementing all methods and properties of the protocol.
- A type can adopt any number of protocols, which allows for a quasi-multiple inheritance not permitted through subclassing.
- You can use extensions for protocol adoption and conformance.
- The Swift standard library uses protocols extensively. You can use many of them, such as
Equatable
andHashable
, with your own types.