Swift · · 11 min read

A Beginner's Guide to Protocols and Protocol Extensions in Swift

A Beginner's Guide to Protocols and Protocol Extensions in Swift

Welcome to the tutorial on protocols and protocol oriented programming in Swift! In this tutorial, we’re going to talk about what protocols are and how you can use them in conjunction with protocol oriented programming.

We’ll start by explaining what protocols are, highlighting the key differences between protocols and classes / structures. Next, we’ll compare and contrast protocols and subclassing with examples, showing you the advantages and disadvantages of each approach. After this, we’ll talk about abstraction and polymorphism, important concepts in object and protocol oriented programming. We’ll then move onto discussing protocol extensions, which are a feature of Swift that allow you to provide default and extended implementations for your protocols. We’ll wrap the tutorial up by discussing how protocol oriented programming and object oriented programming can be used together to solve a wide variety of problems in programming.

Note: This tutorial assumes that you have a working knowledge of the Swift programming language. It doesn’t focus on iOS, macOS, or watchOS as platforms. Rather, it focuses on Swift and teaches you skills that can be used in any environment where you use Swift. However, it’s important to have a solid grasp on basic Swift concepts before diving in. Let’s get started!
Editor’s note: If you’re new to iOS programming and Swift, please refer to our free starter guide.

What’s a Protocol?

If you’re unfamiliar with protocols, your first question most likely involves understanding what exactly a protocol is.

A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality. The protocol can then be adopted by a class, structure, or enumeration to provide an actual implementation of those requirements. Any type that satisfies the requirements of a protocol is said to conform to that protocol.

The Swift Programming Language Guide by Apple

So, according to the creators of the Swift language, protocols are a good way to define a set of required functionality that other types can adopt. When I think of protocols, I like to think that protocols provide information about what a type can do, not necessarily what it is. Classes and structs provide you with information about what an object is, but protocols provide you with information about what an object does.

For example, you may have a variable called str that has a type of String. You know as a developer that str is a String. But what if we defined a protocol called StringProtocol that had all of String‘s various APIs as requirements? We could extend any type to conform to StringProtocol (meaning meet all of its requirements) and we could use that object like it was a String without really knowing what it is! If it looks like a duck, swims like a duck, and quacks like a duck, then it’s a duck. Our new StringProtocol type can be used to provide information about what its conformers do, without providing information about what they are.

So, protocols are abstractions then. You can use a protocol to add abstraction about what something really is to your code. Now that you know what protocols are and what they do, let’s see how you can use them in practice.

Protocols vs. Subclassing

One of the most common design patterns in object oriented programming is called subclassing. Subclassing allows you to define parent / child relationships between classes:

class MyClass { }
class MySubclass: MyClass { }

In the above relationship, MySubclass is a child class (a subclass) of MyClass. MySubclass will automatically inherit all of MyClass‘ functionality, including properties, functions, initializers, etc, meaning that all of MyClass‘ members will automatically be available on MySubclass:

class MyClass {
    var myProperty: String {
        return "property"
    }
    
    func myFunction() -> String {
        return "function"
    }
    
    init(string: String) {
        print("Initializing an instance of MyClass with \(string)!")
    }
}

class MySubclass: MyClass() { }

let s = MySubclass(string: "Hello world") //prints "Initializing an instance of MyClass with hello, world!"
print(s.myProperty) //prints "property"
print(s.myFunction()) //prints "function"

Subclasses may also override their superclass’ functionality, meaning they may replace it with their own:

class MySubclass: MyClass() { 
    override var myProperty: String {
        return "overriden property"
    }
    
    override func myFunction() -> String {
        return "overriden function"
    }
    
    override init(string: String) {
        print("Initializing an instance of MySubclass with \(string)!")
    }
}

let s = MySubclass(string: "Hello world") //prints "Initializing an instance of MySubclass with hello, world!"
print(s.myProperty) //prints "overriden property"
print(s.myFunction()) //prints "overridden function"

As you can see, this is a very powerful design pattern, and rightly so. It allows developers to create incredibly detailed and rich relationships between classes. But, as powerful as subclassing is, it doesn’t solve every single problem that we come across when building apps. Take a look at this:

class Animal {
    func makeSound() { }
}

We just defined a class, Animal, to represent different kinds of animals within our app. Cool! We included a function, makeSound(), to represent the functionality that our animals can possess. However, there’s a problem. Animals don’t have a uniform sound that they all make, so what can we put in the makeSound() function? In practice, we might do something like this:

class Animal {
    func makeSound() { fatalError("Implement me!") }
}

That works, right? It adds a fatalError to the function defined in Animal, effectively making Animal an abstract base class, or a class that isn’t instantiated directly. Abstract base classes can only be instantiated through their subclasses. Now, we can subclass Animal and define our own animals:

class Dog: Animal {
    override func makeSound() { print("Woof!") }
}

Cool! Now we have a Dog class. We can do things like this:

let rex = Dog()
rex.makeSound() //prints "Woof!"

What happens if we forget to override makeSound() though? Or what if we attempt to directly instantiate an Animal? Let’s see:

let tim = Animal()
tim.makeSound() //CRASH
class Cat: Animal { }
let ginger = Cat()
ginger.makeSound() //CRASH

So this is a situation where subclassing isn’t ideal. We see situations like this all the time. Take a look at UITableViewDataSource and UITableViewDelegate, for example. We can’t use subclassing, because there’s no good way to define the default behavior of a table view’s delegate / data source. In essence, subclassing fails whenever there isn’t a default implementation to use in a superclass. Let’s revisit our animal example, but with protocols:

protocol Sound {
    func makeSound() 
}

Great. We defined a protocol, Sound, that specifies that something must have a makeSound() function. It doesn’t matter what the object really is, because we only care about whether or not it meets our requirements for the Sound protocol. Let’s see how this works in practice:

struct Dog: Sound {
    func makeSound() {
        print("Woof")
    }
}

struct Tree: Sound {
    func makeSound() {
        print("Susurrate")
    }
}

struct iPhone: Sound {
    func makeSound() {
        print("Ring")
    }
}

Here Sound is a protocol, we can extend any type to conform to Sound, like we did in the above example. While it’s true that dogs are animals, trees and iPhones are not. It doesn’t make sense if we subclass them from Animal. However, in case of the Sound protocol, it doesn’t matter though. We decided that the only requirement we had for an object to be able to make a sound is to adopt the protocol and implement the required method.

Abstraction and Extension With Protocols

We learned about how protocols can replace subclassing in some cases, but let’s explore another use: abstraction! We know that protocols allow us to define blueprints of functionality that other types can conform to, so let’s see what happens if we creatively use that ability to abstract away type information. We already saw somewhat of an example through the use of the Sound protocol. We even added the ability for trees and iPhones to behave like animals!

But, what if we could take a bunch of types that are logically related, but don’t share a common superclass, and expose them through a single interface? If you’re not following me here, think about the various number types within Swift. We have Double, Float, Int and its various widths (Int8, Int16, etc.), and UInt and its various widths. Have you ever tried combining them during arithmetic operations? For example, have you ever divided an Int and a Float, or divided a Double and a UInt? It’s messy. Look at this snippet of code, which fails to compile in Swift:

let x: Float = 1.2345
let y: Double = 1.3579
let q = x / y

While the Swift Standard Library defines all of the various numerical types as completely separate types, to humans, all of the numerical types fit in one logical group: numbers.

Float, Double, Int, and UInt are all numbers to the average reader. What if we used protocols to take all of the various numerical types within Swift and expose them through a common interface: Number? Let’s see if that’s an approach we can take. We’ll start by defining a protocol called Number:

protocol Number {
    var floatValue: Float { get } // the { get } means that the variable must be read only
}

Our new protocol has one requirement: floatValue. As its name suggests, floatValue is a variable that takes its underlying type and converts it into a Float. So, we’ve defined a Number protocol with a floatValue requirement. That means that as far as we’re concerned, anything that has a valid implementation of floatValue is a number. Cool! Now, how do we apply this protocol to existing types within Swift?

The answer is the extension. Extensions in Swift allow us to extend types that we may or may not have defined ourselves. Take a look:

extension Float: Number {
    var floatValue: Float {
        return self
    }
}

extension Double: Number {
    var floatValue: Float {
        return Double(self)
    }
}

//repeat for Int and UInt

Thanks to our extensions, every single Double, Float, Int, and UInt within our app is now also a Number. We can now treat them as such:

let x: Double = 1.2345
let f = x.floatValue

Pretty cool, right? Let’s do one last thing to finish off our Number type: define custom operators that accept instances of Number and return Float!

//MARK: operator definitions
public func +(lhs: Number, rhs: Number) -> Float {
    return lhs.floatValue + rhs.floatValue
}

public func -(lhs: Number, rhs: Number) -> Float {
    return lhs.floatValue - rhs.floatValue
}

//repeat for * and /

Thanks to our clever use of protocols and extensions, we can now mix number types in Swift and use them to perform arithmetic computations:

let x: Double = 1.2345
ley y: Int = 5
let q = x / y //compiles properly

Neat! We just learned another awesome way to use protocols: abstractions and extensions. Through the power of extensions, we can define protocols and then modify existing Swift Standard Library types to conform to them, making them even more powerful than they already are.

Though you may not have noticed it, this approach also enabled something called polymorphism. While polymorphism isn’t specific to protocol oriented programming (it’s something we used with OOP all the time), it allows us to go back to the familiar object oriented programming principles we know and love, and use them within our protocol oriented environments. In our example with numbers, we were able to make Int, Float, Double, and UInt behave as instances of Number, while still retaining the functionality they had as their original types!

In OOP, polymorphism occurred when classes would inherit functionality from each other, allowing them to behave as instances of their superclasses, along with instances of themselves. POP takes that one step further, allowing us to apply polymorphic principles across our entire app with just a few lines of code. With protocols, we don’t need to subclass Double, Float, etc. to add functionality to them. We can just extend them and add functionality to every single instance of these types, not just instances of our subclasses. While it’s a very abstract concept to understand, it’s immensely useful, and the ability to apply polymorphic principles in your apps will help you write safe, powerful, and clean code.

Protocol Extensions and Multiple Inheritance

Now you’ve learned all about protocols, what they are, and the benefits they provide in your code. Great! However, there’s one huge feature of protocols that we haven’t talked about yet. It’s the feature that takes Swift from being an object oriented programming language to a protocol oriented one. After all, we had protocols in Objective C, too. So why is Swift considered a protocol oriented language when Objective C isn’t? The answer lies in the protocol extension. Like we saw in the previous section, we can extend classes and structs in Swift to add functionality to them. However, with protocols, extensions are even more powerful, because they allow you to provide default functionality for your protocols. What this means is that you can declare protocols with requirements that are automatically fulfilled.

Basically, protocol extensions allow you to retain one of the best features of subclassing (inheritance) while gaining all of the best features of protocols. Let’s go back to our animals again!

Imagine that we’re in a new world where each and every animal makes its own respective sound (like woof, meow, moo, etc.) but they all make a universal sound. We’ll call this universal sound “Wow”. We can now create an extension to our Sound protocol:

extension Sound {
    func makeSound() {
        print("Wow")
    }
}

So now, all we need to do to make something an animal is this:

extension MyType: Sound { }

We don’t need to do anything aside from adding the conformance declaration! We can also override default functionality (just like with classes):

extension Snake: Sound {
    func makeSound() {
        print("hiss")
    }
}

So, that’s how you use protocol extensions. But wait a minute. This looks and feels a lot like subclassing. We’re defining a blueprint for our functionality, providing a default implementation of it, and then selectively overriding it. Why would we use protocols in a situation like this, as opposed to subclassing?

The answer is called multiple inheritance. When you define a class, it can have either 0 or 1 superclasses. You can’t define a class with two superclasses, inheriting functionality from both. Protocols don’t have this contraint. An object can conform to as many protocols as you want, inheriting default functionality from all of them. Additionally, classes can selectively override functionality they inherit from protocols, just like with classes. Take a look:

protocol Sound {
    func makeSound()
}

extension Sound {
    func makeSound() {
        print("Wow")
    }
}

protocol Flyable {
    func fly()
}

extension Flyable {
    func fly() {
        print("✈️")
    }
}

class Airplane: Flyable { }
class Pigeon: Sound, Flyable { }
class Penguin: Sound { }


let pigeon = Pigeon()
pigeon.fly()  // prints ✈️
pigeon.makeSound() // prints Wow

In our example, we defined two protocols, Sound and Flyable. We already know what Sound does, but we now know that anything that has the ability to fly() is Flyable. Then, we defined a class called Airplane. Suppose this Airplane doesn’t make any sound, so Airplane only conforms to Flyable and inherits the default functionality that comes with it.

Conversely, penguins can’t fly. So our Penguin class adopts the Sound protocol, but due to its inability to fly, Penguin is not Flyable.

The interesting bit about this example lies in Pigeon. Pigeon makes sounds and it flies. Our Pigeon class is automatically inheriting the ability to fly and the ability to make a sound. If Sound and Flyable were classes, Pigeon would only be able to inherit functionality from one of them, not both. Extensions are arguably one of the most useful features of protocols, as they allow classes and structures to inherit functionality from numerous other types. This is something that simply isn’t possible with a conventional class / subclass structure, no matter how cleverly it’s designed.

By combining protocols and protocol extensions, we get to use one of our favorite OOP features (inheritance) while getting all the added benefits of protocols. Protocols are safer, easier to work with, and they keep our class structure simple. Additionally, using protocols allows us to inherit from multiple parents simultaneously. Cool, right?

Protocols and OOP

We’ve learned a lot about protocols in this tutorial. However, it’s important to remember one thing. No single programming paradigm will solve every single one of your problems. Since the inception of iOS, we’ve seen object oriented programming, functional reactive programming, protocol oriented programming, and countless other programming paradigms.

While many of these paradigms have been simple fads, a select few have stuck around. You have to remember that as a software engineer, you have a lot of tools at your disposal, and it would be unwise to use just one. Protocols and protocol oriented programming are not a replacement for OOP; rather, they are a supplement. When you’re building your next app, website, or backend service with Swift, remember not to fall into the trap of trying to make a single programming methodology your holy grail. You must be adaptive and flexible. Analyze each situation to determine the right kind of solution to use.

I hope you gained a lot of valuable insight from this tutorial, and if you enjoyed it, please take the time to share it with your friends and your programming community. Thank you!

Read next