Retain cycles in closures

Memory & ARC Mid-level Swift 5.0+ iOS 17+ Reviewed By Alok Singh

Spot the bug

The screen is never freed after it closes. Tap the line that creates the cycle.

  1. final class ProfileViewController: UIViewController {
  2. let model = ProfileModel()
  3. var titleText = ""
  4. override func viewDidLoad() {
  5. super.viewDidLoad()
  6. model.onUpdate = { name in
  7. self.titleText = name
  8. }
  9. }
  10. deinit { print("freed") }
  11. }
Hint 1 of 1

Who owns the closure stored in onUpdate?

Choose an answer, then open Show answer below to check it.

Show answer
  • Answer: line 6 model.onUpdate = { name in
  • Answer: line 7 self.titleText = name

The controller owns model, and model.onUpdate stores a closure that captures self strongly (lines 6–7). Use model.onUpdate = { [weak self] name in self?.titleText = name }.

30-second answer

A closure captures self strongly by default. If the object stores that closure (directly, or through something it owns), the object keeps the closure alive and the closure keeps the object alive. Capture [weak self] and unwrap with guard let self, or capture only the values you need.

Practise saying it · 45 s. No recording, just a timer

Explain closure retain cycles.

Say your answer out loud, then check which of these points you covered:

  • Closures capture self strongly
  • Stored closure creates a loop
  • [weak self] + guard
  • Capture values instead

Deep dive

Non-escaping closures and one-shot structured async work rarely need [weak self].

[unowned self] is only safe when the closure can't outlive self.

Capture specific values ([model]) instead of self when that is all you need.

Combine sink closures stored in cancellables are a classic cycle.

Common mistakes

  • Adding [weak self] everywhere without thinking.
  • Using [unowned self] in network callbacks.

Senior take & follow-ups

Seniors explain the ownership loop, not just 'always use weak self'.

Follow-up questions

Think of your answer first, then open the hint.

  1. Does Task { self.load() } in a view controller leak?

    Show hint

    It keeps self alive until the task ends; long-running tasks should be cancelled.

  2. When is capturing self fine?

    Show hint

    When nothing self owns retains the closure.

Suggest a fix if something here is wrong or out of date.