NEW: Learn to build the incredible iOS 15 Weather app today! >>

How to create and use protocols

Paul Hudson    @twostraws   

Updated for Xcode 13.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:

  • To create a new protocol we write protocol followed by the protocol name. This is a new type, so we need to use camel case starting with an uppercase letter.
  • Inside the protocol we list all the methods we require in order for this protocol to work the way we expect.
  • These methods do not contain any code inside – there are no function bodies provided here. Instead, we’re specifying the method names, parameters, and return types. You can also mark methods as being throwing or mutating if needed.

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:

  1. We tell Swift that Car conforms to Vehicle by using a colon after the name Car, just like how we mark subclasses.
  2. All the methods we listed in 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.
  3. The methods in 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.
  4. The 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:

  1. A string called name, which must be readable. That might mean it’s a constant, but it might also be a computed property with a getter.
  2. An integer called 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.

Hacking with Swift is sponsored by Essential Developer

SPONSORED Learn the most up-to-date techniques and strategies for testing new and legacy Swift code in this free practical course for iOS devs who want to become complete Senior iOS Developers.

Learn more

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift (Vapor Edition) Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Server-Side Swift (Kitura Edition) Buy Beyond Code

Was this page useful? Let us know!

Average rating: 5.0/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.