UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Swift Protocols: Tips and Tricks

Discover the power of Swift protocols with hands-on examples.

Paul Hudson       @twostraws

At their core, Swift’s protocols are a way of letting us define functionality and optionally provide default implementations where needed. They are a fundamental feature of the language, allowing us enforce requirements in types, create unit test mocks, share functionality easily, and more.

In this article we’re going to look at some of the useful features of protocols, such as composition, extension points, and subtype existentials.

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

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

Protocol composition

Protocols work best when they are small and easily composable, because you can then build bigger protocols by combining smaller ones. For example, Swift’s Comparable protocol actually inherits from Equatable, because if two objects can be compared it means they can also be checked for equality.

Protocol inheritance looks just like class inheritance. We might have protocols to define various jobs that a developer might do: programming, debugging, attending meetings, and so on:

protocol Programming { }
protocol Debugging { }
protocol MeetingAttending { }

We could then define some types that use those protocols, like this:

struct JuniorDeveloper: Programming, Debugging, MeetingAttending { }
struct SeniorDeveloper: Programming, Debugging, MeetingAttending { }
struct LeadDeveloper: Programming, Debugging, MeetingAttending { }

Protocol inheritance lets us create a new protocol that combines those existing ones, saving us from repetition and the chance of missing something off. So instead of the above we could write this:

protocol Developer: Programming, Debugging, MeetingAttending { }
struct JuniorDeveloper: Developer { }
struct SeniorDeveloper: Developer { }
struct LeadDeveloper: Developer { }

Swift also lets us combine two protocols together using a type alias, and in fact that’s exactly how the Codable protocol is implemented:

typealias Codable = Decodable & Encodable

Even better, Swift lets us use the same syntax for class and subtype existentials, which means we can combine protocols with classes to be even more precise about what we accept.

For example, you might have a UserHandling protocol that can be used on any type in your app that is able to work with user data. You’re also likely to have lots of view controllers in your app. Using subtype existentials we can write function requirements that allow us to combine those two together: a parameter that gets passed in must conform to UserHandling and also be a subclass of UIViewController:

func showUserDetails(on vc: UserHandling & UIViewController) {
    ...
}

Protocol extensions

Bare protocols let us define requirements for conforming types, but it’s useful and indeed common to provide default implementations for those requirements so that protocols can be used more like building blocks.

For example, here’s a simple protocol that logs messages:

protocol Logging {
    func log(_ message: String)
}

Any conforming type must implement the log() method, deciding for itself how it should happen. However, we could also add a default implementation that every type will inherit, and will be used if they don’t override it:

extension Logging {
    func log(_ message: String) {
        print("\(Date()): \(message).")
    }
}

Helpfully, protocol extensions can also provide default values. For example, our Logging protocol might want to know the filename to use for writing, like this:

protocol Logging {
    var filename: String { get }
}

We can use a protocol extension to provide a default value:

extension Logging {    
    var filename: String {
        return "app.log"
    }
}

Where things get more fuzzy is how we declare extension points. To demonstrate this we need to write out some code, so here’s a Developer protocol that requires conforming types to implement one method:

protocol Developer {
    func attendMeeting()
}

We’re going to provide a default implementation of that method, because our developers love going to meetings:

extension Developer {
    func attendMeeting() {
        print("OK, let's go!")
    }
}

Now we’re going to create a conforming type called SeniorDeveloper. This senior developer thinks they are too cool for meetings, so they’ll refuse to go:

struct SeniorDeveloper: Developer {
    func attendMeeting() {
        print("No way!")
    }
}

Finally, we’re going to create an instance of SeniorDeveloper and ask them to attend a meeting:

let developer = SeniorDeveloper()
developer.attendMeeting()

That code will print out “No way!”, because the senior developer thinks they are too important for meetings.

Let’s try modifying the code just a little – I’m going to comment out the method in our protocol, like this:

protocol Developer {
   // func attendMeeting()
}

Our code still prints out “No way!” Now let’s add an explicit type for the senior developer:

let developer: SeniorDeveloper = SeniorDeveloper()
developer.attendMeeting()

And that still prints “No way!” For our last change, let’s give our instance the type Developer – we’re referring to it by its protocol rather than by its concrete type:

let developer: Developer = SeniorDeveloper()
developer.attendMeeting()

With that change we get a different result: the code now prints out “OK, let's go!” You’ll hit this same situation if you create an array of Developer instances.

Some consider this a feature, and others consider it a bug. But it’s been around for a couple of years now, so it’s not going away.

Declaring our method inside the protocol creates what Swift calls an extension point – a method that we encourage conforming types to override. If we put methods into the extension but not the protocol, all conforming types still get the method, except now we’re making it harder for them to override.

That doesn’t mean it’s impossible to override, and in fact it comes down to the difference between these two lines of code:

let developer: SeniorDeveloper = SeniorDeveloper()
let developer: Developer = SeniorDeveloper()

The former will use your overridden method and the latter will not.

Broadly speaking, it’s a good idea to put methods into your protocol definition only if you want them to be overridden. If you put everything in there you’re losing another piece of clarity in your code, because it’s impossible to distinguish between code that specifically needs to be there, and code that just happens to be there.

Protocols for the template method pattern

If you read my book Swift Design Patterns you’ll know that protocol extensions allow us to make a really clean implementation of the template method pattern. This is sometimes called the Hollywood pattern because it’s built around the saying, “don’t call us, we’ll call you” – it’s where we write methods that are designed to be used as one or more parts of an existing algorithm, rather than called directly by us.

Using protocols for this is pretty straightforward. First, we define a protocol that has at least one method requirement. In this case we’re going to make a general ImageRenderer protocol that does some basic image set up before calling a drawCustomComponents() method to do any custom work.

Here’s that in code:

protocol ImageRenderer {
    func drawCustomComponents()
}

Next, we go ahead and write an extension for that protocol adding whatever methods are required for our algorithm to work. They can then call drawCustomComponents() whenever they want, as part of their algorithm. Remember, these protocol extension methods don’t need to be declared inside the protocol – only the ones you want conforming types to override ought to be there.

For our image renderer, we’re going to write a protocol extension that adds a render() method. This will set up an image context, start rendering an image, then draw a blue gradient. Finally, it will call drawCustomComponents() so that conforming types can add their own customizations on top.

Here’s that in code:

extension ImageRenderer {
    func render() -> UIImage {
        let drawRect = CGRect(x: 0, y: 0, width: 400, height: 200)
        let renderer = UIGraphicsImageRenderer(bounds: drawRect)

        return renderer.image { ctx in
            let colorSpace = ctx.cgContext.colorSpace ?? CGColorSpaceCreateDeviceRGB()
            let colors = [UIColor(red: 27/255.0, green: 215/255.0, blue: 253/255.0, alpha: 1), UIColor(red: 30/255.0, green: 98/255.0, blue: 241/255.0, alpha: 1)]
            let cgColors = colors.map { $0.cgColor } as CFArray

            guard let gradient = CGGradient(colorsSpace: colorSpace, colors: cgColors, locations: [0, 1]) else {
                fatalError("Failed creating gradient.")
            }

            ctx.cgContext.drawLinearGradient(gradient, start: CGPoint.zero, end: CGPoint(x: 0, y: 200), options: [])
            drawCustomComponents()
        }
    }
}

We can call drawCustomComponents() safely because Swift will always ensure that conforming types implement it.

To try that out, let’s create and use a struct that conforms to the ImageRenderer protocol:

struct TextRenderer: ImageRenderer {
    func drawCustomComponents() {
        let text = "Hello, world!"
        let attrs: [NSAttributedString.Key: Any] = [.foregroundColor: UIColor.white, .font: UIFont.boldSystemFont(ofSize: 22)]
        let str = NSAttributedString(string: text, attributes: attrs)

        str.draw(at: CGPoint(x: 50, y: 50))
    }
}

let renderer = TextRenderer()
let image = renderer.render()

As you can see, it doesn’t need to worry about implementing the render() method – it just implements the parts that are custom.

Class-only protocols

Sometimes it’s useful to be able to restrict your protocols so that only classes can conform to it. This is most common when you want to call mutating methods: classes can mutate their variable properties freely, whereas structs cannot.

Sometimes you’ll see older Swift code create class-only protocols like this:

protocol MyProtocol: class { }

However, the correct way to declare class-only protocols in modern Swift code is to make them conform to the AnyObject protocol:

protocol MyProtocol: AnyObject { }

Once that’s done, you can modify properties in the protocol freely.

Resolving conflicts

One of the features I love most about Swift’s protocols is the simple, clear, and consistent way naming conflicts are resolved. There are only two rules:

  1. If two protocols declare the same method, e.g. sort(), the one that is most constrained is used. So if you have a sort() for all users, a sort() method for logged-in users, and a sort() method for logged-in users who are premium subscribers, the last one will be used.
  2. If you create an extension in your project code, and one of your frameworks declares the same extension, Swift will always prefer yours.

If the two rules above don’t completely disambiguate a method call, Swift will refuse to compile your code.

What about starting with a protocol?

At the infamous “Crusty talk” at WWDC 2015 (officially, “Protocol-Oriented Programming in Swift”), Apple offered some simple advice: start with a protocol.

While I agree that protocols offer many advantages, and are pretty much a staple in medium to large Swift projects, personally I find it easier to think about concrete types when I’m just starting out. Then, if I need to expand to do more or to share functionality, fine: I create a protocol and move functionality across to protocol extensions. But I only do that when needed – I don’t try to anticipate ahead of time what kinds of protocols I might need for future shared functionality.

I already talked about this at length elsewhere (see my presentation at Swift & Fika for example) so I’m not going to repeat myself here. The TL;DR version is this: protocols are wonderful tools that offer us a huge range of functionality, but we have lots of other wonderful tools in our toolkit – don’t imagine that protocols are a silver bullet.

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

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

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI 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 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 Beyond Code

Was this page useful? Let us know!

Average rating: 4.2/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.