Updated for Xcode 14.2
Protocols are a bit like contracts in Swift: they let us define what kinds of functionality we expect a data type to support, and Swift ensures that the rest of our code follows those rules.
Think about how we might write some code to simulate someone commuting from their home to their office. We might create a small Car
struct, then write a function like this:
func commute(distance: Int, using vehicle: Car) {
// lots of code here
}
Of course, they might also commute by train, so we’d also write this:
func commute(distance: Int, using vehicle: Train) {
// lots of code here
}
Or they might travel by bus:
func commute(distance: Int, using vehicle: Bus) {
// lots of code here
}
Or they might use a bike, an e-scooter, a ride share, or any number of other transport options.
The truth is that at this level we don’t actually care how the underlying trip happens. What we care about is much broader: how long might it take for the user to commute using each option, and how to perform the actual act of moving to the new location.
This is where protocols come in: they let us define a series of properties and methods that we want to use. They don’t implement those properties and methods – they don’t actually put any code behind it – they just say that the properties and methods must exist, a bit like a blueprint.
For example, we could define a new Vehicle
protocol like this:
protocol Vehicle {
func estimateTime(for distance: Int) -> Int
func travel(distance: Int)
}
Let’s break that down:
protocol
followed by the protocol name. This is a new type, so we need to use camel case starting with an uppercase letter.So we’ve made a protocol – how has that helped us?
Well, now we can design types that work with that protocol. This means creating new structs, classes, or enums that implement the requirements for that protocol, which is a process we call adopting or conforming to the protocol.
The protocol doesn’t specify the full range of functionality that must exist, only a bare minimum. This means when you create new types that conform to the protocol you can add all sorts of other properties and methods as needed.
For example, we could make a Car
struct that conforms to Vehicle
, like this:
struct Car: Vehicle {
func estimateTime(for distance: Int) -> Int {
distance / 50
}
func travel(distance: Int) {
print("I'm driving \(distance)km.")
}
func openSunroof() {
print("It's a nice day!")
}
}
There are a few things I want to draw particular attention to in that code:
Car
conforms to Vehicle
by using a colon after the name Car
, just like how we mark subclasses.Vehicle
must exist exactly in Car
. If they have slightly different names, accept different parameters, have different return types, etc, then Swift will say we haven’t conformed to the protocol.Car
provide actual implementations of the methods we defined in the protocol. In this case, that means our struct provides a rough estimate for how many minutes it takes to drive a certain distance, and prints a message when travel()
is called.openSunroof()
method doesn’t come from the Vehicle
protocol, and doesn’t really make sense there because many vehicle types don’t have a sunroof. But that’s okay, because the protocol describes only the minimum functionality conforming types must have, and they can add their own as needed.So, now we’ve created a protocol, and made a Car
struct that conforms to the protocol.
To finish off, let’s update the commute()
function from earlier so that it uses the new methods we added to Car
:
func commute(distance: Int, using vehicle: Car) {
if vehicle.estimateTime(for: distance) > 100 {
print("That's too slow! I'll try a different vehicle.")
} else {
vehicle.travel(distance: distance)
}
}
let car = Car()
commute(distance: 100, using: car)
That code all works, but here the protocol isn’t actually adding any value. Yes, it made us implement two very specific methods inside Car
, but we could have done that without adding the protocol, so why bother?
Here comes the clever part: Swift knows that any type conforming to Vehicle
must implement both the estimateTime()
and travel()
methods, and so it actually lets us use Vehicle
as the type of our parameter rather than Car
. We can rewrite the function to this:
func commute(distance: Int, using vehicle: Vehicle) {
Now we’re saying this function can be called with any type of data, as long as that type conforms to the Vehicle
protocol. The body of the function doesn’t need to change, because Swift knows for sure that the estimateTime()
and travel()
methods exist.
If you’re still wondering why this is useful, consider the following struct:
struct Bicycle: Vehicle {
func estimateTime(for distance: Int) -> Int {
distance / 10
}
func travel(distance: Int) {
print("I'm cycling \(distance)km.")
}
}
let bike = Bicycle()
commute(distance: 50, using: bike)
Now we have a second struct that also conforms to Vehicle
, and this is where the power of protocols becomes apparent: we can now either pass a Car
or a Bicycle
into the commute()
function. Internally the function can have all the logic it wants, and when it calls estimateTime()
or travel()
Swift will automatically use the appropriate one – if we pass in a car it will say “I’m driving”, but if we pass in a bike it will say “I’m cycling”.
So, protocols let us talk about the kind of functionality we want to work with, rather than the exact types. Rather than saying “this parameter must be a car”, we can instead say “this parameter can be anything at all, as long as it’s able to estimate travel time and move to a new location.”
As well as methods, you can also write protocols to describe properties that must exist on conforming types. To do this, write var
, then a property name, then list whether it should be readable and/or writeable.
For example, we could specify that all types that conform Vehicle
must specify how many seats they have and how many passengers they currently have, like this:
protocol Vehicle {
var name: String { get }
var currentPassengers: Int { get set }
func estimateTime(for distance: Int) -> Int
func travel(distance: Int)
}
That adds two properties:
name
, which must be readable. That might mean it’s a constant, but it might also be a computed property with a getter.currentPassengers
, which must be read-write. That might mean it’s a variable, but it might also be a computed property with a getter and setter.Type annotation is required for both of them, because we can’t provide a default value in a protocol, just like how protocols can’t provide implementations for methods.
With those two extra requirements in place, Swift will warn us that both Car
and Bicycle
no longer conform to the protocol because they are missing the properties. To fix that, we could add the following properties to Car
:
let name = "Car"
var currentPassengers = 1
And these to Bicycle
:
let name = "Bicycle"
var currentPassengers = 1
Again, though, you could replace those with computed properties as long as you obey the rules – if you use { get set }
then you can’t conform to the protocol using a constant property.
So now our protocol requires two methods and two properties, meaning that all conforming types must implement those four things in order for our code to work. This in turn means Swift knows for sure that functionality is present, so we can write code relying on it.
For example, we could write a method that accepts an array of vehicles and uses it to calculate estimates across a range of options:
func getTravelEstimates(using vehicles: [Vehicle], distance: Int) {
for vehicle in vehicles {
let estimate = vehicle.estimateTime(for: distance)
print("\(vehicle.name): \(estimate) hours to travel \(distance)km")
}
}
I hope that shows you the real power of protocols – we accept a whole array of the Vehicle
protocol, which means we can pass in a Car
, a Bicycle
, or any other struct that conforms to Vehicle
, and it will automatically work:
getTravelEstimates(using: [car, bike], distance: 150)
As well as accepting protocols as parameters, you can also return protocols from a function if needed.
Tip: You can conform to as many protocols as you need, just by listing them one by one separated with a comma. If you ever need to subclass something and conform to a protocol, you should put the parent class name first, then write your protocols afterwards.
SPONSORED Thorough mobile testing hasn’t been efficient testing. With Waldo Sessions, it can be! Test early, test often, test directly in your browser and share the replay with your team.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.