The Delegate Pattern In Swift

The Delegate Pattern In Swift

ยท

17 min read

Intro

Delegation is a commonly used pattern in Apple's frameworks, but I find that many people have a hard time wrapping their heads around it when they are first learning iOS development. And it is no wonder because the terms used to define it aren't particularly "beginner-friendly".

Take this quote from the Swift Language Guide:

This design pattern is implemented by defining a protocol that encapsulates the delegated responsibilities, such that a conforming type (known as a delegate) is guaranteed to provide the functionality that has been delegated.

Or this quote from John Sundell's article on the topic:

The core purpose of the delegate pattern is to allow an object to communicate back to its owner in a decoupled way. By not requiring an object to know the concrete type of its owner, we can write code that is much easier to reuse and maintain.

If you have been programming for a while, those sentences probably mean something to you. But if you are just starting out, there's a good chance you just experience some cognitive overload and don't really come away from it knowing anything more about what delegation is. So let's take a look at what delegation is, why we use it, and an example of how I use it in my apps.

What Is Delegation?

The concept itself is fairly simple. I can illustrate it with a metaphor. Imagine an executive has some work they would like done and a few questions they would like answered, but they either can't or won't do those things for themselves. Maybe they don't have the time, or they don't have the skill, or whatever, it doesn't really matter. This person is our "delegator". So what the delegator does is write a job description saying "I need someone who knows how to do this one task and answer these two questions." That person will be the "delegate". The delegate can be anyone in the world, as long as they sign the contract confirming that they know how to do the task and find the answers for the questions. Then, any time the delegator needs that task done or the questions answered, they just hand that off to the delegate who actually does the work and brings the result back.

In Swift, we call the "job description" a protocol. In other languages it is called an interface, so you might see that term as well. According to the Swift Language Guide: "A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality." Sounds a lot like our job description to me. And when a class adopts a protocol, that is signing the contract. It is telling me and you, and the rest of the code, and the compiler that it can do everything described in that protocol.

So if we write our example scenario as code it might look like this:

// This protocol will hold everything the manager class needs their assistant to do
protocol ManagerDelegate {
    func doLaundry(for manager: Manager, laundry: [Clothes]) -> [Clothes]
    func howMuchMoneyDidWeMake() -> Int
    func whatBearIsBest() -> String
}

class Manager {
    // the manager doesn't care what class their assistant is,
    // only that they can do what the manager needs them to do
    weak var assistant: ManagerDelegate? 

    // whenever the manager needs those tasks done, they just have the assistant do it
    func performEndOfDayTasks() {
        if today.isMonday {
            cleanClothes = assistant.doLaundry(for: self, laundry: dirtyClothes)
        } else if today.isFriday {
            let profit = assistant.howMuchMoneyDidWeMake()
            tellBoss("We made \(profit) dollars this week boss!")
        }
    }
}

// The assistant adopts the protocol and this code will not compile
// unless they meet all the requirements of the protocol
class Assistant: ManagerDelegate {
    func doLaundry(for manager: Manager, laundry: [Clothes]) -> [Clothes] {
        var cleanClothes = washClothes(laundry) 
        dryClothes(cleanClothes)
        foldClothes(cleanClothes)
        return cleanClothes
    }
    func howMuchMoneyDidWeMake() -> Int {
        return ledger.profit
    }
    func whatBearIsBest() -> String {
        return "There are basically two schools of thought..."
    }
}

Why Use Delegation?

So that explains what delegation is and what it might look like, but it doesn't explain why anyone would want to do it in the first place. Why can't the manager just do the work themselves? Like we said earlier, in the real world they might be constrained by time (not enough hours in the day) or constrained by capability (don't have the skill), or they might be constrained by desire (just don't want to do it). When we're talking about code, the metaphor breaks down here a little bit because our code is not (yet) sentient and it doesn't really have any desires. Here are a couple of reasons why we use delegation in our code: It promotes proper separation of concerns. Our Manager class can hold all the logic that is necessary for managing and not have to be cluttered up with logic for assisting. It has an assistant for that. This makes our code easier to read and easier to reason about. When you need to update the managing logic, or you are trying to track down an assisting bug, you know right where to look. It decouples our code. That means each class is not tied to the other directly. We can test each class's logic individually, we can debug them separately, and we can grow one's functionality without affecting the other. And if we want to write a new ExecutiveAssistant class, we can do that and swap it out without affecting anything about how the Manager operates, as long as it adopts the appropriate protocol(s). If we work on a team, one person can work on the Manager class and another can work on the Assistant class without conflicts. This is what people mean when they say two classes are "loosely coupled". Things would be much more "tightly coupled" if our Manager defined its assistant as var assistant: Assistant. In our real-world metaphor, loose coupling would be "I need an assistant who fits this job description" and tight coupling would be something like "I need Dwight as my assistant, no one else will do". It promotes reuse. In the real world we can (theoretically) pluck a manager from one position and place them in another, and as long as they know how to manage people in general, it shouldn't matter if they are managing these people or those people. They should continue to thrive. The same can be said for an assistant, whether they are assisting this person or that person. There are similar examples in our code. Think about a table view. There's a lot of logic used for rendering a vertically scrolling table on screen. You have to manage all the cells and it would probably be good if you make sure they are reused so that your app doesn't generate thousands of views for long lists, etc. There is a lot of logic there that you and I don't want to have to write every time we need a table in our app, so it would be great if there was a class that we could reuse for that. The hitch is that the content that we want to display in each table is different. As it happens Apple came up with a solution for us and it is called UITableView (although modern readers may want to prefer UICollectionView), and they used delegation to allow us to define the logic for our individual apps while keeping the logic for rendering tables wrapped up in its own class where it can be reused. If all those reasons aren't enough for you, I suppose you could write all of your own code without using delegation. You could use other patterns like target-action or callbacks to achieve many of the same things (with other trade-offs). But if you are developing iOS apps, you're going to have to implement at least a few delegates at some point because it is a common pattern in Apple's frameworks, so you may as well get comfortable with the pattern.

How I Use Delegation

I have been developing iOS apps for a few years, so I have implemented my fair share of UITableViewDelegates and UICollectionViewDataSources but I also like using the delegate pattern with my own views. I tend to lay out my UI in a UIView subclass, define a delegate for that view, and then pass up any events that happen to the delegate. So if there is a button in the view, the user's action of tapping on that button that gets passed to the delegate (most often its owning UIViewController). It usually looks something like this:

First, we define the delegate protocol. This will be all the messages our view needs to relay to its delegate. For this example, we'll just let them know that the button has been tapped, but in production code you would probably want to have a more specific and communicative name for this function.

protocol SomeDetailViewDelegate: AnyObject {
    func didTapButton()
}

Then the view itself. It has a delegate which is weak. The reason why is beyond the scope of this article, but the simple answer is it helps to prevent retention cycles by making sure SomeDetailView doesn't retain it's delegate. If you want, you can read more about how that works here. Also notice that we marked our protocol has being AnyObject. That is because the compiler won't let us mark it as weak in the view if it isn't known to be a reference type and that is exactly what AnyObject does.

class SomeDetailView: UIView {

    weak var delegate: SomeDetailViewDelegate?
    private let button = UIButton()

    // somewhere in the configuration and layout of button and other views
    button.addTarget(self, action: #selector(didTapButton), for: .touchUpInside)

    @objc private func didTapButton(_ sender: UIButton) {
        delegate?.didTapButton()
    }
}

You can see that the view itself is pretty dumb (meaning that it doesn't hold much, if any, business logic). It just defines what the UI looks like and then passes the messages it gets along to its delegate.

Next we have the view controller that will hold this view. It has a strong reference to the view and it is responsible for putting it on screen (not shown here). At some point, preferably before it is possible for the view to send any messages to its delegate, the view controller sets itself as the view's delegate.

class SomeDetailViewController: UIViewController {
    private lazy var detailView: SomeDetailView = {
        let detailView = SomeDetailView()
        detailView.delegate = self
        return detailView
    }()
}

Now to get that to compile, our view controller must adopt the delegate protocol. Here, I am doing it in an extension. This is not required, but it is a fairly common pattern that you'll see and it is a nice way to wrap up all the logic relating to that protocol in one place. It can even be put in a separate file if you want. Regardless of how you conform to the protocol, this is where the logic for what to do when the user taps that button lives.

extension SomeDetailViewController: SomeDetailViewDelegate {
    func didTapButton() {
        print("Do something when the user taps the button")
    }
}

This organization leads to all of the benefits discussed earlier. It separates the UI logic from the business logic, so if you have a layout bug you know where to look. It decouples our view from our view controller, allowing you to make changes to the view with no effect on the business logic and vice versa. And it promotes reuse. Any view controller (or even another view) could now plug this view in and all it needs to do is decide how to conform to the one new protocol method. And, if it is done consistently throughout a code base, anyone else can hop in to our code and know where to look with only a few minutes of explanation.

Wrap Up

So quick recap:

  • Delegation is just handing off some work to someone/something else who is able to do that work.
  • Use delegation to separate concerns, to decouple your classes, and to make your code more reusable
  • A good place to start with delegation is between your view and your view controller

I hope this helps, especially if you're just getting started in iOS development. And if it did, let me know! Maybe you're a more experienced developer who thinks the delegate pattern is boring, or outdated, and we shouldn't use it anymore. Feel free to let me know all your thoughts and opinions too.

Did you find this article valuable?

Support Dillon McElhinney by becoming a sponsor. Any amount is appreciated!