Skip to content

Latest commit

 

History

History
230 lines (155 loc) · 6.6 KB

Protocols-and-Delegates.md

File metadata and controls

230 lines (155 loc) · 6.6 KB

Protocols & Delegates

An example.

protocol LogoDownloaderDelegate: AnyObject {
    func didFinishDownloading(_ sender:LogoDownloader)
} 

class LogoDownloader {

    weak var delegate: LogoDownloaderDelegate?
    
    func downloadLogo() {
    	delegate?.didFinishDownloading(self)
    }
    
} 

extension ViewController: LogoDownloaderDelegate {

    override func viewDidLoad() {
        logoDownloader?.delegate = self
    }
    
    func didFinishDownloading(_ sender: LogoDownloader) {
    } 
}

Naming

There is a definite convention around naming your protocol methods. Start the name of your protocol with the name of the sender, followed by the state of the thing that changed.

sender / state that changed

For example here we return the switch of the button followed by the state after.

protocol InternetTileViewControllerDelegate: AnyObject {
    func internetTileViewController(_ viewController: InternetTileViewController, didSetInternetPackage package: InternetPackage?)
}

protocol SavedPaymentSectionViewDelegate: AnyObject {
    func savedPaymentInfoSwitch(_ infoSwitch: CheckBoxButton, didChange state: Bool)
    func savePaymentInfoButtonWasTapped(_ infoButton: UIButton)
}

If there is no state change, include the action with the sender in the name. Here is another.

protocol FileImporterDelegate: AnyObject {
    func fileImporter(_ importer: FileImporter,
                      shouldImportFile file: File) -> Bool

    func fileImporter(_ importer: FileImporter,
                      didAbortWithError error: Error)

    func fileImporterDidFinish(_ importer: FileImporter)
}

class FileImporter {
    weak var delegate: FileImporterDelegate?
}

And some, like UITableView use a combination of each. Naming convention is ClassDelegate or ClassDataSource.

protocol UITableViewDataSource : NSObjectProtocol {

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell

    func numberOfSections(in tableView: UITableView) -> Int // Default is 1 if not implemented

}

class UITableView : UIScrollView, NSCoding, UIDataSourceTranslating {
	weak open var dataSource: UITableViewDataSource?
}

Which every way you go, remember to make your protocol reference weak in your delegate class. This will avoid any cyclomatic references and retains cycles in your code.

Class-only weak var and retain cycles

If you are defining a protcol that needs to work with a class, you need to mark your protocol AnyObject and make the reference a weak var.

protocol WeatherServiceDelegate: AnyObject {
    func didFetchWeather()
}

struct WeatherService {
    weak var delegate: WeatherServiceDelegate?
}

Even if you container is a struct using value semantics, it will still retain a strong reference to a class via the var. So if you push and pop this class repeatedly in a view controller, creating a new instance of ViewController2 every time, it will leak and retain an instance of WeatherService even after it has been dismissed.

That doesn't mean you can't use protocol-delegate purely between two structs and not require the weak var - you can.

But if your protocol is going to touch any class, it needs the weak var - else it has the potential to leak.

This demo project shows how ViewController2 can leak if repeatedly pushed and popped and a weak var on the container struct is not used.

import UIKit

class ViewController2: UIViewController {

    let button = makeButton(withText: "Pop")
    var weatherService = WeatherService()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemRed
        weatherService.delegate = self

        button.addTarget(self, action: #selector(buttonTapped), for: .primaryActionTriggered)

        view.addSubview(button)
        
        NSLayoutConstraint.activate([
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
        ])
    }

    @objc func buttonTapped() {
        dismiss(animated: true, completion: nil)
    }
}

extension ViewController2: WeatherServiceDelegate {
    func didFetchWeather() {

    }
}

class ViewController: UIViewController {

    let button = makeButton(withText: "Push")

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white

        button.addTarget(self, action: #selector(buttonTapped), for: .primaryActionTriggered)

        view.addSubview(button)

        NSLayoutConstraint.activate([
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
        ])
    }

    @objc func buttonTapped() {
        present(ViewController2(), animated: true, completion: nil)
    }
}



// GOOD
/*
protocol WeatherServiceDelegate: AnyObject {
    func didFetchWeather()
}

struct WeatherService {
    weak var delegate: WeatherServiceDelegate?
}
*/



// BAD - pattern - avoid doing this.
protocol WeatherServiceDelegate {
    func didFetchWeather()
}

struct WeatherService {
    var delegate: WeatherServiceDelegate?
}



func makeButton(withText text: String) -> UIButton {
    let button = UIButton()
    button.translatesAutoresizingMaskIntoConstraints = false
    button.setTitle(text, for: .normal)
    button.titleLabel?.adjustsFontSizeToFitWidth = true
    button.contentEdgeInsets = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16)
    button.backgroundColor = .systemBlue
    button.layer.cornerRadius = 8
    return button
}

Subtlties around language

Describing these things can sometimes be problematic. Here are some sentences to help me explain.

A retain cycle is created when two instances of classes reference each other.

This is true. But that doens't mean you still can have a memory leak between a struct and a class as we just saw above.

The best way to think of this is if you touch a class, define your protocol using AnyObject. If you never touch a class, go a head without, but be careful. The compiler will allow you do use a container without a weak var and that will leak.

So when in doubt. Make it a weak var.

Links that help