Articles, podcasts and news about Swift development, by John Sundell.

Is using [weak self] always required when working with closures?

Answered on 14 Jul 2020
Basics article available: Memory Management

When an object or value is referenced within an escaping closure, it gets captured in order to be available when that closure is executed. By default, when an object gets captured, it will be retained using a strong reference — which in the case of self could lead to retain cycles in certain situations.

For example, the following code captures self strongly within a closure that’s also itself ultimately retained by that same object, leading to a retain cycle:

class ProductViewController: UIViewController {
    private lazy var buyButton = Button()
    private let purchaseController: PurchaseController
    
    ...

    override func viewDidLoad() {
        super.viewDidLoad()

        // Since our buyButton retains its closure, and our
        // view controller in turn retains that button, we'll
        // end up with a retain cycle by capturing self here:
        buyButton.handler = {
            self.showShoppingCart()
            self.purchaseController.startPurchasingProcess()
        }
    }

    private func showShoppingCart() {
        ...
    }
}

To fix the above problem we can, for instance, use a [weak self] capture list — which will break our retain cycle by now capturing self using a weak reference:

buyButton.handler = { [weak self] in
    guard let self = self else { return }
    self.showShoppingCart()
    self.purchaseController.startPurchasingProcess()
}

However, that doesn’t mean that capturing self always results in a retain cycle — it’s really only in the above type of situation that such a problem could occur. For example, the following code does not cause a retain cycle:

class HomeViewController: UIViewController {
    ...

    override func viewDidLoad() {
        super.viewDidLoad()

        if shouldShowPromotion {
            // Capturing self within this closure does not cause
            // a retain cycle, since our view controller does not
            // retain this dispatch queue in any way:
            DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                self.showPromotionView()
            }
        }
    }

    private func showPromotionView() {
        ...
    }
}

One thing that’s important to consider though, is that when capturing self like we do above, we are retaining it for as long as the closure itself will remain in memory (in this case, for two seconds). So while we might not technically cause a retain cycle in those situations, we could end up prolonging the lifetime of certain objects, which might lead to unexpected behavior.

So, to sum up:

There are also sometimes ways that we can avoid using capture lists entirely, some of which were covered in “Capturing objects in Swift closures”.