Skip to Content
Course content

166: The Delegate Pattern In Depth

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Why use a delegate instead of just passing in a closure?

I get this question a lot because, on the surface, closures seem easier. You just pass a block of code and you're done. But closures are best for "one-and-done" events—like a single network request finishing. When you have a complex relationship where one object needs to report multiple different types of events over a long period, a delegate is much cleaner.

Imagine we're building a MusicPlayer. It needs to tell the UI when the song starts, when it pauses, when it finishes, and when an error occurs. If you used closures, your MusicPlayer initializer would look like a nightmare of five or six different closure properties. With a delegate, you just define one protocol that groups all those behaviors together.

protocol MusicPlayerDelegate: AnyObject {
    func musicPlayerDidStartPlaying(_ player: MusicPlayer)
    func musicPlayerDidPause(_ player: MusicPlayer)
    func musicPlayerDidFinishSong(_ player: MusicPlayer, songTitle: String)
    func musicPlayerDidEncounterError(_ player: MusicPlayer, error: Error)
}

class MusicPlayer {
    weak var delegate: MusicPlayerDelegate?
    
    func play() {
        // ... playback logic ...
        delegate?.musicPlayerDidStartPlaying(self)
    }
}

See how the MusicPlayer doesn't care who is listening? It just knows that whoever the delegate is, they've promised to handle those specific methods.

Why do I always see "weak var" used for the delegate?

If you forget the weak keyword here, you are almost certainly creating a retain cycle (a memory leak). Let's trace it: usually, your View Controller owns the MusicPlayer (a strong reference). If the MusicPlayer also has a strong reference back to the View Controller via the delegate, neither object can ever be deleted from memory. They're essentially holding each other hostage.

By marking the delegate as weak, you tell Swift: "I want to talk to this object if it exists, but I don't want to be the reason it stays alive." This is why the protocol must be constrained to AnyObject—weak references only work with classes, not structs or enums.

What if I don't want to implement every single method in the protocol?

In the early days of Swift, we had to use @objc optional, but that required us to inherit from NSObject, which feels clunky in pure Swift. The modern way to handle this is through Protocol Extensions.

I usually provide a "default implementation" in an extension. This makes the methods optional in practice; the delegate can choose to override them if they care about that event, or just ignore them if they don't. It keeps your View Controller code from being cluttered with empty functions.

extension MusicPlayerDelegate {
    // Default empty implementations
    func musicPlayerDidStartPlaying(_ player: MusicPlayer) {}
    func musicPlayerDidPause(_ player: MusicPlayer) {}
    func musicPlayerDidFinishSong(_ player: MusicPlayer, songTitle: String) {}
    func musicPlayerDidEncounterError(_ player: MusicPlayer, error: Error) {}
}

Now, if your SettingsViewController only cares about errors, you only have to write the musicPlayerDidEncounterError method. The rest are handled by the extension.

How do I actually "connect" the two objects?

This is the part where learners often trip up. You have to do two things: make the class conform to the protocol, and then explicitly assign that class as the delegate.

I like to do this in viewDidLoad or the initializer. If you forget the assignment step, your code will compile perfectly, but your delegate methods will never fire because the MusicPlayer is shouting into a void.

class PlayerViewController: UIViewController, MusicPlayerDelegate {
    let player = MusicPlayer()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // This is the critical line!
        player.delegate = self 
    }
    
    func musicPlayerDidFinishSong(_ player: MusicPlayer, songTitle: String) {
        print("Updating UI: \(songTitle) has ended.")
    }
}



📋 Practical Task

Exercise: Building a Custom Image Downloader Notifier

Your goal is to create a system where a background downloader notifies a UI component about its progress using the Delegate pattern.

  • Create a protocol called ImageDownloaderDelegate. It should have two methods: downloaderDidUpdateProgress(_ progress: Float) and downloaderDidComplete(imageName: String).
  • Create an ImageDownloader class. It should have a weak var delegate.
  • Inside ImageDownloader, write a method called startDownload(name: String). Use a loop or a timer to simulate progress (e.g., calling the progress delegate method 3 times with 0.3, 0.6, and 1.0) before finally calling the completion method.
  • Create a class ImageGalleryView that conforms to ImageDownloaderDelegate.
  • Implement the delegate methods in ImageGalleryView to print the progress and the final image name to the console.
  • Instantiate both classes and connect them so that starting a download in the ImageDownloader triggers the prints in ImageGalleryView.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.