Design pattern in Swift - Delegation
The delegation pattern is one of the most common design pattern in iOS. You probably use it on daily basis without noticing, every time you create a UITableView or UICollectionView and implementing their delegates. Let’s see how it works and how to implement it in Swift.
The concept is simple, it’s designed to communicate information between two objects, one delegating responsibility to the other. Compared to the Observer design pattern, it’s mainly used for a one-to-one relationship.
Delegate pattern with protocols
Delegation design pattern is very often implemented using protocols which is a familiar approach for mobile developers: many public API are using protocols.
protocol PictureDownloaderDelegate : AnyObject {
func pictureDownloader(_ downloader: PictureDownloader, didDownloadImage image: Image)
func pictureDownloader(_ downloader: PictureDownloader, didFailDownloadWithError error: Error)
}
class PictureDownloader {
weak var delegate : PictureDownloaderDelegate?
...
}
Notice the naming convention of the protocol and its methods:
- The protocol is named after the object delegating (UITableView -> UITableViewDelegate …)
- The first argument of each method is the object delegating his responsibility.
Regarding what logic to delegate in your main class, it’s up to you as a Swift developer to define what’s necessary to keep a clean architecture and separation of concern.
Back in my example downloading images, we could image two methods to notice the delegate when the download start and finish, regardless of the result, to display a loader on UI for instance. Those could be optional, we not always want to show a loader for instance.
However, when it succeed, I definitely need to notify the delegate, same for an error. That makes the delegate essential for that class.
class PictureDownloader {
weak var delegate : PictureDownloaderDelegate?
func downloadImage(_ url: URL) {
guard let delegate = self.delegate else { return }
let urlSession = URLSession(configuration: .default)
let task = urlSession.downloadTask(with: url) { [weak self] (locationUrl, response, error) in
if let error = error {
self?.downloadDidFail(error)
return
}
self?.downloadDidSucceed(locationUrl)
}
task.resume()
}
private func downloadDidSucceed(_ locationUrl: URL?) {
guard let locationUrl = locationUrl,
let data = try? Data(contentsOf: locationUrl),
let image = UIImage(data: data) else {
return
}
self.delegate?.pictureDownloader(self, didDownloadImage: image)
}
private func downloadDidFail(_ error: Error) {
self.delegate?.pictureDownloader(self, didFailDownloadWithError: error)
}
The issue regarding above is what happen if we don’t have a delegate setup. What should happen if the image isn’t loaded from locationUrl
? Should it be same error delegation? Should it be a different one? The state of it becomes uncertain.
Delegate pattern with closures
A nice way to avoid some confusion is implementing a delegation with closures. I’m using here a struct to configure how I want my class to handle each success and error.
class PictureDownloader {
let configuration : PictureDownloader.Configuration
struct Configuration {
let completionHandler : ((UIImage) -> (Void))?
let errorHandler : ((Error) -> (Void))?
init(onSuccess completionHandler: ((UIImage) -> Void)?, onError errorHandler: ((Error) -> Void)?) {
self.completionHandler = completionHandler
self.errorHandler = errorHandler
}
}
init(configuration: Configuration) {
self.configuration = configuration
}
func downloadImage(_ url: URL) {
let urlSession = URLSession(configuration: .default)
let task = urlSession.downloadTask(with: url) { [weak self] (locationUrl, response, error) in
if let error = error {
self?.downloadDidFail(error)
return
}
self?.downloadDidSucceed(locationUrl)
}
task.resume()
}
private func downloadDidSucceed(_ locationUrl: URL?) {
guard let locationUrl = locationUrl,
let data = try? Data(contentsOf: locationUrl),
let image = UIImage(data: data) else {
// TODO
return
}
self.configuration.completionHandler?(image)
}
private func downloadDidFail(_ error: Error) {
self.configuration.errorHandler?(error)
}
}
We reduced some cases of uncertainty when the delegate wasn’t defined. I’ve chosen to leave the struct within the class to explicitly keep a connection while using it.
let configuration = PictureDownloader.Configuration(onSuccess: { image in
// success image!
}) { error in
// error no image!
}
let downloader = PictureDownloader(configuration: configuration)
We still need to handle the case when we don’t get the image from the locationUrl
, The easiest might be to define a different kind of erro and fall back to downloadDidFail
. We could also delegate this to another layer of logic.
In conclusion, we’ve learnt how to implement delegate design pattern in Swift using protocols as well as closures. Delegation was historically one of the core concept in mobile development and still is today, we have seen today how to make it a bit more swifty.
Thanks for reading!
Where to go from here