Weekly Swift articles, podcasts and tips by John Sundell.

Protocols

Published on 03 Apr 2020

While many languages support the concept of protocols (or “Interfaces” as they’re also often called), Swift treats protocols as a true cornerstone of its overall design — with Apple even going so far as to call Swift a “protocol-oriented programming language”.

Essentially, protocols enable us to define APIs and requirements without tying them to one specific type or implementation. For example, let’s say that we’re working on some form of music player, and that we’ve currently implemented our playback code as two separate methods within a Player class — one for playing songs, and one for playing albums:

class Player {
    private let avPlayer = AVPlayer()

    func play(_ song: Song) {
        let item = AVPlayerItem(url: song.audioURL)
        avPlayer.replaceCurrentItem(with: item)
        avPlayer.play()
    }

    func play(_ album: Album) {
        let item = AVPlayerItem(url: album.audioURL)
        avPlayer.replaceCurrentItem(with: item)
        avPlayer.play()
    }
}

Looking at the above implementation, we definitely have a fair amount of code duplication, since both of our play methods need to do more or less the exact same thing — convert the resource that is being played into an AVPlayerItem and then play it using an AVPlayer instance.

That’s one of the kinds of problems that protocols can help us solve in a much more elegant manner. To get started, let’s define a new protocol called Playable, which will require each type that’s conforming to it to implement an audioURL property:

protocol Playable {
    var audioURL: URL { get }
}

The above get keyword is used to specify that in order to conform to our new protocol, a type only needs to declare a read-only audioURL property — it doesn’t have to be writable.

We can then make different types conform to our new protocol in two ways. One way is to declare the conformance as part of the type declaration itself — for example like this:

struct Song: Playable { 
    var name: String
    var album: Album
    var audioURL: URL
    var isLiked: Bool
}

The other way is to declare conformance through an extension — which can simply be done using an empty extension in case a type already meets all of the protocol’s requirements (which is the case for our Album model below):

struct Album {
    var name: String
    var imageURL: URL
    var audioURL: URL
    var isLiked: Bool
}

extension Album: Playable {} 

With the above changes in place we can now simplify our Player class quite a lot — by merging our two previous play methods into one that, rather than accepting a concrete type (such as Song or Album), now accepts any type that conforms to our new Playable protocol:

class Player {
    private let avPlayer = AVPlayer()

    func play(_ resource: Playable) {
        let item = AVPlayerItem(url: resource.audioURL)
        avPlayer.replaceCurrentItem(with: item)
        avPlayer.play()
    }
}

That’s much nicer! However, there is one tiny issue with our above protocol, and that’s its name. While Playable might’ve initially seemed like an appropriate name, it sort of indicates that the types conforming to it can actually perform playback, which isn’t the case. Instead, since our protocol is all about converting an instance into an audio URL, let’s rename it to AudioURLConvertible — to make things crystal clear:

// Renaming our declaration:
protocol AudioURLConvertible {
    var audioURL: URL { get }
}

// Song's conformance to it:
struct Song: AudioURLConvertible {
    ...
}

// The Album extension:
extension Album: AudioURLConvertible {}

// And finally how we use it within our Player class:
class Player {
    private let avPlayer = AVPlayer()

    func play(_ resource: AudioURLConvertible) {
        ...
    }
}

On the flip side of the coin, let’s now take a look at a protocol that does require an action (or in other words, a method), which makes it a nice fit for the typical “-able” naming suffix. In this case we’ll require a mutating method, since we want to enable any types conforming to our protocol to mutate their own state (that is, change property values) within their implementations:

protocol Likeable {
    mutating func markAsLiked()
}

extension Song: Likeable {
    mutating func markAsLiked() {
        isLiked = true
    }
}

Since most types that will conform to our new Likeable are likely (no pun intended) to implement our markAsLiked method requirement the exact same way as Song does, we might also choose to make that isLiked property our requirement instead (and also require it to be mutable by adding the set keyword).

protocol Likeable {
    var isLiked: Bool { get set }
}

The cool thing is that, if we still want our API to be something.markAsLiked(), then we can easily make that happen using a protocol extension — which enable us to add new methods and computed properties to all types that conforms to a given protocol:

extension Likeable {
    mutating func markAsLiked() {
        isLiked = true
    }
}

For more information about the mutating keyword, and a broader discussion around value and reference types, check out this Basics article.

With the above in place, we can now make both Song and Album conform to Likeable without having to write any additional code — since they both already declare an isLiked property that’s mutable:

extension Song: Likeable {}
extension Album: Likeable {}

Besides enabling code reuse and unifying similar implementations, protocols can also be really useful when refactoring, or when we want to conditionally replace one implementation with another.

As an example, let’s say that we wanted to test a new implementation of our Player class from before — that enqueues songs and other playback items, rather than immediately starting to play them. One way to do that would of course be to add that logic to our original Player implementation, but that could quickly get messy — especially if we want to perform multiple tests and try out more kinds of variations.

Instead, let’s create an abstraction for our core playback API by implementing a protocol for it. In this case, we’ll simply name it PlayerProtocol, and make it require our single play method from before:

protocol PlayerProtocol {
    func play(_ resource: AudioURLConvertible)
}

Using our new protocol, we’re now free to implement as many different variants of our player as we wish — each of which can have their own private implementation details, while still being compatible with the exact same public API:

class EnqueueingPlayer: PlayerProtocol {
    private let avPlayer = AVQueuePlayer()

    func play(_ resource: AudioURLConvertible) {
        let item = AVPlayerItem(url: resource.audioURL)
        avPlayer.insert(item, after: nil)
        avPlayer.play()
    }
}

extension Player: PlayerProtocol {}

With the above in place, we can now conditionally use either of our player implementations by making whichever code that creates the app’s player return a PlayerProtocol conforming-instance, rather than a concrete type:

func makePlayer() -> PlayerProtocol {
    if Settings.useEnqueueingPlayer {
        return EnqueueingPlayer()
    } else {
        return Player()
    }
}

Finally, let’s go back to that initial statement of Swift being a “protocol-oriented language”. So far in this article, we’ve seen that Swift does indeed support many powerful protocol-based features — but what actually makes the language itself protocol-oriented?

In many ways, it comes down to how the standard library is designed — which leverages features like protocol extensions to both optimize its own internal implementation, and to enable us to write our own functionality on top of its many protocols using those same extensions.

As an example, here’s how we could take the standard library’s Collection protocol (which all collections, such as Array and Set, conform to) and give it a sum method whenever the elements that are being stored conform to Numeric — which is yet another standard library protocol that numeric types, such as Int and Double, conform to:

extension Collection where Element: Numeric {
    func sum() -> Element {
        // The reduce method is implemented using a protocol extension
        // within the standard library, which in turn enables us
        // to use it within our own extensions as well:
        reduce(0, +)
    }
}

To learn more about why we’re able to pass the + operator directly to reduce, check out the Swift Clips episode about first class functions.

With the above in place, we can now easily sum up any collection of numbers, for example an array of Int values:

let numbers = [1, 2, 3, 4]
numbers.sum() // 10

So what makes protocols so incredibly useful is both that they enable us to create abstractions that let us hide implementation details behind shared interfaces — which in turn makes it easier to share code that uses those interfaces — and also that they enable us to customize and extend the standard library’s various APIs.

Protocols also have many more aspects and features that this Basics article didn’t cover (such as how they relate to testing and architecture). For a brief look at generic protocols, check out the Basics article about generics — and for other kinds of protocol-focused content, check out this list.

Thanks for reading! 🚀