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.
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...
}
}
ImageDownloader
is responsible for breaking the retain cycle by holding a weak
reference to it’s delegateHow 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
}
}
}
ImageViewer
is responsible for making sure that it’s references itself weakly in the callbackIt 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…
What if one class needs use multiple ImageDownloader
s, 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 UITableView
s. 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:
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...
}
}
dataSource
exists, then we know that it has implemented all of the methods that we requireinit
method, so we’re making it clear to the user of this class that a data source is requiredNow 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.
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!
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 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:
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
}
}
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.
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.
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.
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
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.
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 UITableView
s!
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.