Dans notre vie de développeur iOS, on utilise fréquemment des closures. Ils sont tout simplement des fonctions sans nom qu’on l’utilise généralement pour effectuer des instructions suite à des appels de fonctions dites asynchrones. Longtemps, j’ai eu ce malaise de comprendre pourquoi Swift appelle les paramètres passés a une closure au moment de sa définition des captures list? qui capte quoi et pour quelles raisons, pourquoi on met weak ou unowned? Il y a t’il une différence entre un paramètre de type référence (classe) ou par valeur (struct, enum…) dans les captures list? J’essayerai d’expliquer succinctement ces sujets en retournant au bases de la compréhension de fonctionnement d’une closure et des mécanismes du gestion de mémoire en Swift.

  1. C’est quoi une closure (trailing closure)?

Une closure est tout simplement la signature d’une fonction . Comme toute fonction, une closure possède un domaine d’appel et un domaine de définition. La subtilité d’une closure que ses deux domaines sont inversés par rapport à la fonction qui va appeler à notre closure.

Un exemple :


func f (param1: type, ....., closure : (Type) -> Type') {
closure(value)
}

On a la définition de la fonction f avec ses paramètres dont l’un est une autre fonction qu’on a appelé closure qui retournera une valeur (trailing closure). Par exemple, value de type Type (On va laisser le Type’ ensuite).

Lorsqu’on exécute closure(value), on fait l’appel de la fonction closure à l’intérieur de la définition de la fonction f. La question qui se pose, ou se trouve la définition de la closure? En effet, la définition se trouve dans l’appel de la fonction f :).

Retournons à l’appel de la fonction f, ou on va définir qu’est ce qu’elle doit faire notre closure lorsqu’on va faire appel à elle:

f(value1,....{ value in 
/// que je veux faire avec mon value 
/// a l'intérieur de ma fonction closure
})

La notion de capture List intervient ici au moment de la définition de notre closure lors de l’appel de la fonction f.

  1. C’est quoi la capture list?

Lors de son appel, la closure a envoyé une valeur value pour être exploiter au moment de son exécution (la définition). Cependant, la closure aurait peut être besoin d’autres objets de son scope (la classe ou la structure dans laquelle elle s’exécute) pour exécuter ses instructions. Ces objets dont la closure aurait besoin seront ajouté par la closure dans une liste qu’on appelle capture list. En effet, la closure enregistre les références ou les valeurs des objets de son domaine pour les exploiter ensuite au moment de son appel.

class A {
    func f(value: Int, closure: (Int) -> Void) {
        closure(value)
    }
    
    func f2(value: Int) {
        print(value)
    }
}

var objA = A()
objA.f(value: 2) { [objA] value in
    objA.f2(value: value)
}

la closure a enregistré la référence de objA (la référence de l’instance de la classe A) dans son tableau pour éventuellement être exploiter ensuite au moment de l’appel de la closure. Mais, on peut ne pas capturer objA ici et ça fonctionnera parfaitement vu que dans ce cas notre objA ne peut pas être nil.


var objA = A()
objA.f(value: 2) { value in
    objA.f2(value: value)
}

Voyons un autre exemple ou il est indispensable de capturer un objet par une closure.

class ViewController: UIViewController {
  private lazy var button: UIButton = {
     let btn = UIButton()
     btn.translatesAutoresizingMaskIntoConstraints = false
     btn.setTitle("Open secondVC", for: .normal)
     (self, action: #selector(openVC), for: .touchUpInside)
        btn.heightAnchor.constraint(equalToConstant: 30).isActive = true
        btn.widthAnchor.constraint(equalToConstant: 50).isActive = true
        return btn
    }()
  override func viewDidLoad() {
        title = "First Screen"
        view.backgroundColor = UIColor.red
        view.addSubview(button)
        button.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = true
        button.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor).isActive = true 
    }
    
    @objc private func openVC() {
        let secondViewController = SecondViewController()
self.navigationController?.pushViewController(secondViewController, animated: true)
    }
}

class SecondViewController: UIViewController {

    override func viewDidLoad() {
        title = "Second Screen"
        view.backgroundColor = UIColor.green
        doSomething(value: 2)
    }

    func doSomething(value: Int) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [self] in
            self.showResult(value * value)
        }
    }

    private func showResult(_ result: Int) {
        print(result)
    }
 
    deinit {
        print("deinit VC2")
    }
}

Dans cette exemple, on a une première vue qui va afficher une autre suite à un clique sur un bouton. Au moment du lancement de la deuxième vue, elle récupérera la valeur passé 2, le met en puissance et l’imprime. Cependant, on peut imaginer que le user peut cliquer sur le back button de navigation avant que les 3 secondes passent et du coup le destructeur sera appelé et on aura pas d’affichage de notre value. Néanmoins, vu que l’instance de self est capturé par défaut comme strong, la deuxième ViewController ne sera libéré qu’après 3 secondes et on aura toujours l’affichage mais nous remarquons la mauvaise gestion mémoire vu que notre deuxième VC ne sera libéré réellement qu’après la closure sera exécuté (self ne peut pas être libéré a cause que son retain count est égale à 1)

public func asyncAfter(deadline: DispatchTime, qos: DispatchQoS = .unspecified, flags: DispatchWorkItemFlags = [], execute work: @escaping @convention(block) () -> Void)
  • Capturer avec weak:

Si on capture la valeur de self avec [weak self], on informe la closure que l’instance de self peut être libéré en moment de retour vers le premier contrôleur avant les 3 secondes (le retain count n’est pas incrémenté):

la closure sera execute après 3 secondes mais rien de grave vu que la valeur de self est optionnel et la méthode showResult() ne sera pas exécuté:

Capture List avec weak

La méthode AsynAfter n’a pas capturé à travers sa closure une référence strong de self alors le destructeur de SecondViewController peut être appeler et l’objet est libéré.

  • Capturer avec unowned:

Lorsque une closure capture un objet avec unowned, on doit être sur que ce dernier ne soit pas libéré avant son utilisation sinon on aura un crash systématique. C’est le cas dans notre cas si on retourne à notre viewController avant les 3 secondes vu que le compilateur va retenir une référence non strong mais non weak.

le compilateur détecte que l’objet a été libéré à l’execution et affiche cette exception:

Fatal error: Attempted to read an unowned reference but the object was already deallocated

Capture List avec Unowned
  1. Conclusion:

Dans cette première partie, on a présenté l’utilité de capture list, qui constitue un mécanisme adopté par les closures pour gérer les objets qu’ils vont utilisé lors de la définition de leur comportement vu que la définition devance l’appel. Au moment de l’appel de la closure, une mauvaise gestion des captures list peut engendrer des situations de crash ou une mauvaise gestion mémoire (l’objet ne sera libéré que si la closure est appelé dans le cas d’une capture strong de l’objet).