Steve On Stuff

Delegates vs Closure Callbacks

16 Aug 2018

Remember how awesome the delegate pattern was in Objective-C? Enabling classes to be super reusable by delegating out the bits that you might want control over. There’s a reason that it’s so ubiquitous across Cocoa Touch, it’s just so damn good!

But there’s a new pattern that seems to be quietly replacing the delegate pattern. I’m not sure if it has an official name, but I call it:

The Closure Callback Pattern

You probably know which one I mean, but in case you need a refresher, here’s the delegate way of doing things:

protocol ImageDownloaderDelegate: class {
    func imageDownloader(_ downloader: ImageDownloader, didDownloadImage image: UIImage)
}

class ImageDownloader {
    
    weak var delegate: ImageDownloaderDelegate?
    
    func downloadImage(url: URL) {
        // download the image asynchronously then...
        delegate?.imageDownloader(self, didDownloadImage: theImage)
    }
}

and here’s the closure callback way:

class ImageDownloader {
    
    var didDownload: ((UIImage?) -> Void)?
    
    func downloadImage(url: URL) {
        // download the image asynchronously then...
        didDownload?(theImage)
    }
}

Like everyone else, when Swift came out I dabbled with the closure callback pattern, using it where I might have previously used a delegate in a quest for ever increasing ‘Swifty-ness’. I was left unsatisfied though. Sometimes it felt like an improvement, and sometimes it didn’t. Is the delegate or the closure callback the superior pattern? I guess, well, it depends.

Depends on what, though??? Let’s compare these two approaches and see if we might learn something.

Breaking the retain cycle

Whenever two objects reference each other, one of them has to hold a weak reference to the other or we get a retain cycle. The handling of this couldn’t be more different between these two patterns.

How the delegate pattern does it:

class ImageDownloader {
    weak var delegate: ImageDownloaderDelegate?
    //...
}

class ImageViewer: ImageDownloaderDelegate {
    
    let downloader: ImageDownloader()
    
    init(url: URL) {
        downloader.delegate = self
        downloader.downloadImage(url: url)
    }
    
    func imageDownloader(_ downloader: ImageDownloader, didDownloadImage image: UIImage) {
        // view the downloaded image...
    }
}

How the closure callbacks do it:

class ImageDownloader {
    var didDownload: ((UIImage?) -> Void)?
	//...    
}

class ImageViewer {
    
    let downloader: ImageDownloader
    
    init(url: URL) {
        downloader = ImageDownloader()
        downloader.downloadImage(url: url)
        downloader.didDownload = { [weak self] image in
            // view the image
        }
    }
}

It may be the old way of doing things, but the delegate pattern is a clear winner on this front. The weak relationship is only defined once, and it’s much harder to make a mistake.

There’s plenty more to consider though, let’s see if the closure callbacks can redeem themselves…

One to many relationships

What if one class needs use multiple ImageDownloaders, how might our two patterns fair then?

First up, the delegate pattern:

class ProfilePage: ImageDownloaderDelegate {
    
    let profilePhotoDownloader = ImageDownloader()
    let headerPhotoDownloader = ImageDownloader()

    init(profilePhotoUrl: URL, headerPhotoUrl: URL) {
        
        profilePhotoDownloader.delegate = self
        profilePhotoDownloader.downloadImage(url: profilePhotoUrl)
        
        headerPhotoDownloader.delegate = self
        headerPhotoDownloader.downloadImage(url: headerPhotoUrl)
    }
    
    func imageDownloader(_ downloader: ImageDownloader, didDownloadImage image: UIImage) {
        
        if downloader === profilePhotoDownloader {
            // show the profile photo...
        } else if downloader === headerPhotoDownloader {
            // show the profile photo...
        }
    }
}

We have to check which instance of the ImageDownloader is calling us in each callback. If you have a whole bunch of delegate methods this is going to get really tedious. Plus you’re likely to make a mistake.

I’m sure we’ve all worked on an app before where the same object was the delegate for multiple UITableViews. Not cool.

Let’s see if the closure callback pattern can save us:

class ProfilePage {
    
    let profilePhotoDownloader = ImageDownloader()
    let headerPhotoDownloader = ImageDownloader()
    
    init(profilePhotoUrl: URL, headerPhotoUrl: URL) {
        
        profilePhotoDownloader.didDownload  = { [weak self] image in
            // show the profile image
        }
        profilePhotoDownloader.downloadImage(url: profilePhotoUrl)
        
        headerPhotoDownloader.didDownload  = { [weak self] image in
            // show the header image
        }
        headerPhotoDownloader.downloadImage(url: headerPhotoUrl)
    }
}

It’s a clear win. The callbacks for the two instances are completely separate, so there’s no chance of us getting them mixed up.

The closure callbacks win this one. It’s 1-1. Let’s see what’s next:

Datasources

Ok, these aren’t strictly delegate patterns, but I’ve seen the closure callbacks used to supply information to an object too, so I’m including them.

Lets look at a protocol based datasource pattern:

protocol SerialImageUploaderDataSource: class {
    var numberOfImagesToUpload: Int { get }
    func image(atIndex index: Int) -> UIImage
    func caption(atIndex index: Int) -> String
}

class SerialImageUploader {
    
    weak var dataSource: SerialImageUploaderDataSource?
    
    init(dataSource: SerialImageUploaderDataSource) {
        self.dataSource = dataSource
    }
    
    func startUpload() {
        
        guard let dataSource = dataSource else { return }
        
        for index in 0..<dataSource.numberOfImagesToUpload {
            let image = dataSource.image(atIndex: index)
            let caption = dataSource.caption(atIndex: index)
            upload(image: image, caption: caption)
        }
    }
    
    func upload(image: UIImage, caption: String) {
        // Upload the image...
    }
}

Now a Datasource implemented with closures:

class SerialImageUploader {
    
    var numberOfImagesToDownload: (() -> Int)?
    var imageAtIndex: ((Int) -> UIImage)?
    var captionAtIndex: ((Int) -> String)?
    
    func startUpload() {
        
        guard
            let numberOfImagesToDownload = numberOfImagesToDownload,
            let imageAtIndex = imageAtIndex,
            let captionAtIndex = captionAtIndex
            else {
                return
        }
        
        for index in 0..<numberOfImagesToDownload() {
            let image = imageAtIndex(index)
            let caption = captionAtIndex(index)
            upload(image: image, caption: caption)
        }
    }
    
    func upload(image: UIImage, caption: String) {
        // Upload the image...
    }
}

This one’s a bit of a train-wreck. We rely on all three closures being non-nil, so I had to guard against all of them. I could have passed them all in the init and made them non-optional, but I think that would be a bit ridiculous.

If you just require a single closure then you can supply it in the init as a non-optional, otherwise using a protocol is clearly the better approach.

Scalability

So, we’ve just got one method now, but what if we have 10 in the future? Our protocol now looks like this:

Delegate:

protocol ImageDownloaderDelegate: class {
    func imageDownloader(_ downloader: ImageDownloader, didDownloadImage image: UIImage)
    func imageDownloaderDidFail(_ downloader: ImageDownloader)
    func imageDownloaderDidPause(_ downloader: ImageDownloader)
    func imageDownloaderDidResume(_ downloader: ImageDownloader)
}

extension ViewController: ImageDownloaderDelegate {

    func imageDownloader(_ downloader: ImageDownloader, didDownloadImage image: UIImage) {
    }
    
    func imageDownloaderDidFail(_ downloader: ImageDownloader) {
    }
    
    func imageDownloaderDidPause(_ downloader: ImageDownloader) {
    }
    
    func imageDownloaderDidResume(_ downloader: ImageDownloader) {
    }
}

Closure callbacks:

class ImageDownloader {
    var didDownload: ((UIImage?) -> Void)?
    var didFail: (() -> ())?
    var didPause: (() -> ())?
    var didResume: (() -> ())?
}

class ViewController: UIViewController {
    
    let downloader = ImageDownloader()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        downloader.didDownload = {
            //...
        }
        
        downloader.didFail = {
            //...
        }
        
        downloader.didPause = {
            //...
        }
        
        downloader.didResume = {
            //...
        }
    }
}

I don’t like this at all. It’s not clear where we should even put the setup code for all the callbacks. Maybe we could make a method called setupDelegateCallbacks()? It’s all a bit messy for my liking.

Another win for the delegate pattern.

And on to our last test!

Enforcing the contract

Any type that works with another type should expect the other type to adhere to a contract. That way, if that contract isn’t fulfilled then the compiler can let us know at compile time, and we can avoid nasty surprises at runtime.

Lets look at how type-safe these two approaches are.

Delegates:

Add a new method, get a compiler error, sweet!

Closures callbacks:

Add a new callback and you’ll be blissfully unaware that you haven’t implemented it. Which might have bad consequences. Hope it wasn’t important!

So which is better?

So which is best? As we already knew, it depends! But hopefully we have a better idea on what now, so lets try to lay down some guidelines:

Scenario 1:

You have a single callback

The callback closures pattern is the best here. Pass it in the initialiser, and you can even make it non-optional:

class ImageDownloader {
    
    var onDownload: (UIImage?) -> Void
    
    init(onDownload: @escaping (UIImage?) -> Void) {
        self.onDownload = onDownload
    }
}

Scenario 2:

Your callbacks are more like notifications

If your callbacks are more like ‘notifications’ or ‘triggers’ that fire when other things happen, then the closure callbacks can be a less invasive option. Keep them optional, and you can just implement the ones that you are interested in.

If your delegator is saying Hey I just did this thing btw, letting you know, rather than I really need you to do something now! an optional closure can let you subscribe to that callback if you need it, or not if you don’t.

Scenario 3:

You need to become the delegate to multiple instances

The closure callback is the better pattern here. Be careful that this is really what you require though. You can always have an object that is a dedicated delegate or datasource, and have many instances of those.

Scenario 3:

Your delegate is actually a datasource

Use a protocol, it enforces a stronger contract between the two types, and the compiler can help you find bugs.

Scenario 4:

Your have many callbacks, and they might change in the future

Use a protocol. If you forget to implement new methods in the future, the compiler will tell you rather than your users.

Anything else, or you’re not sure:

Anything else, or you’re not sure

If in doubt, use a protocol. Defining a protocol guarantees that conforming types will have implemented the specified methods. If the protocol requirements change the future, the compiler will require you to update your types. It also simplifies the weak / strong relationship, allowing you to define it in a single place.

Conclusion

The callback closures pattern seems to be creeping in everywhere. It can be a great way to reduce complexity, handle one to many relationships, and make code more readable. I still think that a protocol is more appropriate for the majority of cases, though.

Choose the write tool for the right job, and if you only remember one thing from this post - never become the delegate to two UITableViews!

Further reading

This post by Oleg Dreyman presents a nice solution for avoiding the pitfalls of the weak / strong dance using closure callbacks

This post by John Sundell has some nice examples of closure callbacks vs. delegates.

Next »


« Previous




I'm Steve Barnegren, an iOS developer based in London. If you want to get in touch: