There are several options for sending messages from a view controller to its coordinator. Up until now we exclusively used closures to notify the coordinator. Another option is delegation and that's the pattern we explore in this episode.

Starter Project

The starter project I use for this episode is the finished project of Working With Tab Bar Controllers. Open PhotosCoordinator.swift and navigate to the showPhotos() method. We use the PhotosViewController class as an example. The advantages of closures are obvious if you explore the implementation of the showPhotos() method. The instantiation and configuration of the PhotosViewController instance is encapsulated in the showPhotos() method. The API is intuitive and it's clear how the photos view controller communicates with its coordinator.

private func showPhotos() {
    // Initialize Photos View Controller
    let photosViewController = PhotosViewController.instantiate()

    // Install Handlers
    photosViewController.didSignIn = { [weak self] in
        self?.showSignIn()
    }

    photosViewController.didSelectPhoto = { [weak self] (photo) in
        self?.showPhoto(photo)
    }

    photosViewController.didBuyPhoto = { [weak self] (photo) in
        self?.buyPhoto(photo, purchaseFlowType: .vertical)
    }

    // Push Photos View Controller Onto Navigation Stack
    navigationController.pushViewController(photosViewController, animated: true)
}

Implementing a Delegate Protocol

Let's refactor the PhotosViewController class. Open PhotosViewController.swift and remove the didSignIn, didBuyPhoto, and didSelectPhoto properties. We replace these properties with a delegate protocol.

import UIKit

class PhotosViewController: UIViewController, Storyboardable {

    // MARK: - Properties

    @IBOutlet var tableView: UITableView! {
        didSet {
            // Configure Table View
            tableView.delegate = self
            tableView.dataSource = self
        }
    }

    // MARK: -

    private lazy var dataSource: [Photo] = [
        ...
    ]

    ...

}

Define a protocol with name PhotosViewControllerDelegate below the import statement for the UIKit framework.

import UIKit

protocol PhotosViewControllerDelegate {

}

class PhotosViewController: UIViewController, Storyboardable {

    ...

}

We replace each handler with a delegate method. The didSignIn handler is replaced with the controllerDidSignIn(_:) method. The method accepts a reference to the delegating object, an instance of the PhotosViewController class. It's a best practice to pass the delegating object as the first argument of the delegate method.

protocol PhotosViewControllerDelegate {

    func controllerDidSignIn(_ controller: PhotosViewController)

}

The didBuyPhoto handler is replaced with the controller(_:didBuyPhoto:) method. The method accepts the delegating object as its first argument and a Photo object as its second argument. The Photo object is the photo the user is intending to buy.

protocol PhotosViewControllerDelegate {

    func controllerDidSignIn(_ controller: PhotosViewController)
    func controller(_ controller: PhotosViewController, didBuyPhoto photo: Photo)

}

The didSelectPhoto handler is replaced with the controller(_:didSelectPhoto:) method. The method resembles the controller(_:didBuyPhoto:) method. It accepts the delegating object as its first argument and a Photo object as its second argument. The Photo object is the photo selected by the user.

protocol PhotosViewControllerDelegate {

    func controllerDidSignIn(_ controller: PhotosViewController)
    func controller(_ controller: PhotosViewController, didBuyPhoto photo: Photo)
    func controller(_ controller: PhotosViewController, didSelectPhoto photo: Photo)

}

Defining the delegate protocol is only the first step. The delegating object needs to hold a reference to the delegate object. Define a weak, variable property, delegate, of type PhotosViewControllerDelegate?. Notice that the photos view controller weakly references the delegate object and that's only possible if the delegate property is of an optional type.

class PhotosViewController: UIViewController, Storyboardable {

    // MARK: - Properties

    @IBOutlet var tableView: UITableView! {
        didSet {
            // Configure Table View
            tableView.delegate = self
            tableView.dataSource = self
        }
    }

    // MARK: -

    weak var delegate: PhotosViewControllerDelegate?

    // MARK: -

    ...

}

The compiler throws an error because it isn't possible to declare the delegate property weakly if it isn't a reference type.

Compiler Error

What does that mean? Any type, including value types, can conform to the PhotosViewControllerDelegate protocol. The weak keyword can only be applied to reference types, not value types. The solution is simple, though.

The PhotosViewControllerDelegate protocol needs to inherit the AnyObject protocol. AnyObject is a special protocol defined in the Swift standard library. It's actually a type alias. Every class implicitly conforms to the AnyObject protocol. By inheriting the AnyObject protocol, only classes can conform to the PhotosViewControllerDelegate protocol.

protocol PhotosViewControllerDelegate: AnyObject {

    func controllerDidSignIn(_ controller: PhotosViewController)
    func controller(_ controller: PhotosViewController, didBuyPhoto photo: Photo)
    func controller(_ controller: PhotosViewController, didSelectPhoto photo: Photo)

}

Implementing the Delegate Protocol

The next step is replacing the handlers in the PhotosViewController class with the delegate methods of the PhotosViewControllerDelegate protocol. Navigate to the signIn(_:) method. When the user taps the Sign In button, the photos view controller calls the controllerDidSignIn(_:) method on its delegate property, passing in a reference to itself. We use optional chaining because the delegate property is of an optional type.

@objc func signIn(_ sender: UIBarButtonItem) {
    // Notify Delegate
    delegate?.controllerDidSignIn(self)
}

Navigate to the tableView(_:cellForRowAt:) method of the UITableViewDataSource protocol. We need to update the didBuy handler of the photo table view cell. Because self is weakly referenced in the didBuy handler, we use a guard statement to safely access the reference to the PhotosViewController instance. The photos view controller calls the controller(_:didBuyPhoto:) method on its delegate property, passing in a reference to itself and the photo the user is intending to buy.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: PhotoTableViewCell.reuseIdentifier, for: indexPath) as? PhotoTableViewCell else {
        fatalError("Unable to Dequeue Photo Table View Cell")
    }

    // Fetch Photo
    let photo = dataSource[indexPath.row]

    // Configure Cell
    cell.configure(title: photo.title, url: photo.url, didBuyPhoto: UserDefaults.didBuy(photo))

    // Install Handler
    cell.didBuy = { [weak self] in
        guard let self = self else {
            return
        }

        // Notify Delegate
        self.delegate?.controller(self, didBuyPhoto: photo)
    }

    return cell
}

The last method we need to update is the tableView(_:didSelectRowAt:) method of the UITableViewDelegate protocol. When the user taps a row in the table view, the photos view controller calls the controller(_:didSelectPhoto:) method on its delegate property, passing in a reference to itself and the photo selected by the user.

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    tableView.deselectRow(at: indexPath, animated: true)

    // Fetch Photo
    let photo = dataSource[indexPath.row]

    // Notify Delegate
    self.delegate?.controller(self, didSelectPhoto: photo)
}

Conforming to the Delegate Protocol

The last step is conforming the PhotosCoordinator class to the PhotosViewControllerDelegate protocol. Open PhotosCoordinator.swift and navigate to the showPhotos() method. We remove the handlers and set the photos coordinator as the delegate of the photos view controller.

private func showPhotos() {
    // Initialize Photos View Controller
    let photosViewController = PhotosViewController.instantiate()

    // Configure Photos View Controller
    photosViewController.delegate = self

    // Push Photos View Controller Onto Navigation Stack
    navigationController.pushViewController(photosViewController, animated: true)
}

The compiler throws an error because the PhotosCoordinator class doesn't conform to the PhotosViewControllerDelegate protocol. Let's fix that.

Compiler Error

Create an extension for the PhotosCoordinator class at the bottom of PhotosCoordinator.swift. We use the extension to conform the PhotosCoordinator class to the PhotosViewControllerDelegate protocol. The compiler helps us out by adding protocol stubs for each of the delegate methods.

Compiler Error

The implementation of the delegate methods is identical to the implementation of the handlers we started with at the beginning of this episode.

extension PhotosCoordinator: PhotosViewControllerDelegate {

    func controllerDidSignIn(_ controller: PhotosViewController) {
        showSignIn()
    }

    func controller(_ controller: PhotosViewController, didBuyPhoto photo: Photo) {
        showPhoto(photo)
    }

    func controller(_ controller: PhotosViewController, didSelectPhoto photo: Photo) {
        buyPhoto(photo, purchaseFlowType: .vertical)
    }

}

Build and run the application to make sure we didn't break anything. Tapping the Sign In button takes the user to the sign in view, tapping a row takes the user to the photo detail view, and tapping a buy button triggers the purchase flow.

Closures or Delegation

It's up to you to decide which option you prefer, closures or delegation. Closures come with little overhead and I like their elegant syntax. Delegation requires more work. You need to define a protocol, the delegating object needs to keep a reference to the delegate object, and the delegate object needs to implement the delegate protocol.

The only compelling advantage of delegation is clarity. The delegate protocol clearly describes the interface or contract between the delegating object and the delegate object.

With the introduction of blocks in Objective-C, almost ten years ago, Apple has been moving away from delegation in favor of handlers. Delegation has its advantages, but closures are often the better choice.

What About Notifications?

There's a third option we didn't discuss in this series. Notifications. Are notifications a viable alternative to closures and delegation? The short answer is "No." Use notifications if an object wants to notify one or more objects of an event. The sender of the notification doesn't care which object or objects subscribe to the notification or how they handle the broadcast.

A view controller and a coordinator have a clearly defined relationship. A view controller needs the ability to notify its coordinator, but it doesn't know about its coordinator. That is why closures and delegation are a perfect fit.

Notifications will work fine, but they aren't a good fit. Notifications are useful if the sender and the receiver don't have a clearly defined relationship.

What's Next?

Closures and delegation work well with the coordinator pattern. It's up to you to decide which option you prefer. Don't take my word for it. Give each option a try and stick with the one you like best.