---
title: 'Capturing in Closures'
source: 'https://academia.sh/en/courses/swift/capturing-in-closures'
course: 'The Swift Language'
language: en
updated: '2026-10-21T05:03:12+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Reads from the deinit output that a closure stored in a property builds a retain cycle when it captures self strongly'
    - 'Breaks the cycle with a weak self capture list and observes the closure run empty after the object is gone'
    - 'Measures that a capture list is evaluated once when the closure is created, while a variable captured without a list is read with its current value on every call'
    - 'Distinguishes that a closure that is not stored does not build a cycle when it uses self'
---

# Capturing in Closures

Measures that a closure stored in an object's own property blocks deinit when it captures self, that a weak self capture list resolves it, and that a capture list is evaluated once when the closure is created.

The previous lesson built a retain cycle between two class properties. The same cycle can be built through a **closure** that a class stores in its own property, and in practice this is the form met most often. A closure takes the outside values it uses in its body into itself; this is called **capturing**. If the body of the closure uses `self`, the closure captures the instance with a strong reference. If the instance also holds the closure in a property, the two sides hold each other.

This lesson builds the cycle once, breaks it with a `[weak self]` **capture list**, and then measures another property of the capture list: the expression inside the list is evaluated not on every call of the closure but once, while the closure is being created. At the end comes a measurement that draws the limit of the question: not every closure builds a cycle.

```swift
// Probe.swift — records when an expression is evaluated
final class Probe {
    let label: String
    private(set) var evaluations = 0
    private(set) var uses = 0
    private var evaluatedBeforeUse = false

    init(_ label: String) {
        self.label = label
    }

    func eval<T>(_ value: T) -> T {
        evaluations += 1
        if uses == 0 { evaluatedBeforeUse = true }
        return value
    }

    func use<T>(_ access: () -> T) -> T {
        uses += 1
        return access()
    }

    var moment: String {
        if evaluations == 0 { return "never" }
        if evaluations > 1 { return "on-every-use" }
        return evaluatedBeforeUse ? "immediately" : "on-first-use"
    }

    func report() {
        print("\(label): \(evaluations) evaluations, \(uses) uses -> \(moment)")
    }
}

func tally(_ probes: [Probe]) {
    for moment in ["immediately", "on-first-use", "on-every-use", "never"] {
        let count = probes.filter { $0.moment == moment }.count
        print("\(moment): \(count)")
    }
}
```

## When the Closure Holds the Instance

The `Button` class stores an optional closure named `onTap`. The `install` method assigns to this property a closure whose body reads `self.name`. In each round a `Button` is built, its closure is installed, it is called once, and the name is released.

```swift
// main.swift — a closure that captures self strongly blocks deinit
final class Button {
    let name: String
    let owner: Probe
    var onTap: (() -> Void)?

    init(_ name: String, owner: Probe) {
        self.name = name
        self.owner = owner
    }

    func install() {
        onTap = { print("tapped \(self.name)") }
    }

    deinit {
        print("deinit button \(name)")
        _ = owner.eval(name)
    }
}

let trapped = Probe("closure-cycle")

for round in 1...2 {
    var button: Button? = Button("button-\(round)", owner: trapped)
    button?.install()
    button?.onTap?()
    _ = trapped.use { button = nil }
    print("released \(round): \(trapped.evaluations) deinit")
}

trapped.report()
```

```
tapped button-1
released 1: 0 deinit
tapped button-2
released 2: 0 deinit
closure-cycle: 0 evaluations, 2 uses -> never
```

The closure was called and the name was written, but after the release no `deinit` line ever appears. `Button` holds the closure through `onTap`, and the closure holds `Button` through `self`. When the outside name is gone, the counter drops to one, not to zero. The rule of the previous lesson holds here too: `closure-cycle` records two uses and zero evaluations, and its moment is `never`.

## Breaking It with Weak Self

The fix is to put a capture list at the head of the closure. The `[weak self]` list gives the closure a weak copy of `self`; the closure no longer holds the instance, but once the instance is gone, `self` becomes optional and can be `nil`. The body therefore first checks with `guard let self` whether the instance is still alive. The measurement calls the closure twice: before the name is released and after. For the call after the release, the closure is saved in a separate variable.

```swift
// main.swift — [weak self] gets ahead of the cycle
final class Button {
    let name: String
    let owner: Probe
    var onTap: (() -> Void)?

    init(_ name: String, owner: Probe) {
        self.name = name
        self.owner = owner
    }

    func install() {
        onTap = { [weak self] in
            guard let self else {
                print("button gone")
                return
            }
            print("tapped \(self.name)")
        }
    }

    deinit {
        print("deinit button \(name)")
        _ = owner.eval(name)
    }
}

let broken = Probe("weak-capture")

for round in 1...2 {
    var button: Button? = Button("button-\(round)", owner: broken)
    button?.install()
    let saved = button?.onTap
    saved?()
    _ = broken.use { button = nil }
    saved?()
    print("released \(round): \(broken.evaluations) deinit")
}

broken.report()
```

```
tapped button-1
deinit button button-1
button gone
released 1: 1 deinit
tapped button-2
deinit button button-2
button gone
released 2: 2 deinit
weak-capture: 2 evaluations, 2 uses -> on-every-use
```

This time the `deinit` line appears in every round right after the release. The closure did not hold `Button`, so when the outside name was gone, the counter dropped to zero. After the release the same closure can still be called, but `guard let self` fails and `button gone` is written. The closure now runs empty: the instance is gone, the closure remains. `weak-capture` records two uses and two evaluations; over the two rounds a `deinit` runs at every use, and its moment is `on-every-use`.

## When a Capture List Is Evaluated

`[weak self]` is only one use of the capture list. A list can also hand the closure a value: each entry written in the form `[snapshot = expression]` is evaluated **once, while the closure is being created**, and becomes a fixed copy inside the closure. A variable captured without a list, on the other hand, is read with its current value on every call of the closure. The measurement below shows this difference with two `Probe` objects. First both closures are created while `level` is 1, then `level` becomes 5, and then each closure is called twice.

```swift
// main.swift — a capture list at creation, an unlisted read on every call
let listed = Probe("capture-list")
let live = Probe("no-capture-list")

do {
    var level = 1
    let withList = { [snapshot = listed.eval(level)] in snapshot }
    let withoutList = { live.eval(level) }
    level = 5
    print("listed: \(listed.use { withList() }) \(listed.use { withList() })")
    print("unlisted: \(live.use { withoutList() }) \(live.use { withoutList() })")
}

listed.report()
live.report()
```

```
listed: 1 1
unlisted: 5 5
capture-list: 1 evaluations, 2 uses -> immediately
no-capture-list: 2 evaluations, 2 uses -> on-every-use
```

The closure with the list returns 1 in both calls: the copy was taken while `level` was still 1, and the later assignment did not change it. The closure without the list returns 5 in both calls, because it reads the variable again on every call. The Probe output records these as two different moments. `capture-list` records a single evaluation, and because that evaluation happened before the first use, its moment is **immediately**. `no-capture-list` records two evaluations, so it is `on-every-use`. This lesson's moment is **immediately**: the expression in the capture list is evaluated once, at the point where the closure is created.

## Two Closures Share a Variable

Capturing is often not the copying of a value but the sharing of a variable. Two closures that capture the same local variable look at the same box of that variable. This measurement builds an incrementing closure and a reading closure that capture a `total` variable. The incrementer makes an evaluation through `Probe` on every call, and the question is whether the reader sees the change made by the incrementer.

```swift
// main.swift — two closures capture the same variable
let shared = Probe("shared-variable")
do {
    var total = 0
    let add = { total += shared.eval(10) }
    let read = { total }
    print("start \(read())")
    shared.use { add() }
    print("first increase \(read())")
    shared.use { add() }
    print("second increase \(read())")
}
shared.report()
```

```
start 0
first increase 10
second increase 20
shared-variable: 2 evaluations, 2 uses -> on-every-use
```

The `read` closure sees the change made by the incrementer: `start 0`, then `first increase 10` and `second increase 20`. If the two closures held separate copies of the value of `total`, the reader would always give 0. The closures look at the same box, and every call that changes the box is visible to the other. `shared-variable` counts two evaluations in two uses and says `on-every-use`. The capture list of the previous section cuts this sharing: a closure written with `[total]` takes the value at that moment, not the variable. Sharing is the default; copying is a behavior that has to be asked for explicitly.

## Escaping and Non-Escaping Closures

A closure can outlive the function that receives it. If it will, its parameter is marked `@escaping`; an unmarked closure has to finish before the function returns. These two forms are measured with `Probe`. The non-escaping closure is called immediately in the `run` function. The escaping closure is added to an array in the `store` function and is called only later. After two `store` calls, and before the closures are called, `later.evaluations` is read.

```swift
// main.swift — non-escaping evaluates now, escaping evaluates later
let now = Probe("non-escaping")
let later = Probe("escaping")
do {
    var pending: [() -> Int] = []

    func run(_ work: () -> Int) -> Int {
        work()
    }

    func store(_ work: @escaping () -> Int) {
        pending.append(work)
    }

    for round in 1...2 {
        _ = now.use { run { now.eval(round) } }
        store { later.eval(round) }
        print("round \(round): non-escaping \(now.evaluations), escaping \(later.evaluations)")
    }
    for work in pending {
        _ = later.use { work() }
    }
    print("escaping after calls \(later.evaluations)")
}
now.report()
later.report()
```

```
round 1: non-escaping 1, escaping 0
round 2: non-escaping 2, escaping 0
escaping after calls 2
non-escaping: 2 evaluations, 2 uses -> on-every-use
escaping: 2 evaluations, 2 uses -> on-every-use
```

In each round the `non-escaping` counter rises before `run` returns: 1 in the first round, 2 in the second. The `escaping` counter, on the other hand, is still 0 after both `store` calls, because `store` only adds the closure to the array and does not call it. The closures are called only in the next loop, and the counter becomes `escaping after calls 2`. Both `Probe` objects say `on-every-use`, but the place of the evaluation differs: the evaluation of the non-escaping closure is inside the call of the function that receives it. The evaluation of the escaping closure can come long after the line where the closure was created. This difference also explains why a closure forces `self` to be written explicitly: an escaping closure can outlive the instance it captures. A non-escaping closure can never hold the instance, because it ends when the function returns.

## The Strong Temporary Copy

The `guard let self` line does not only check whether `self` is `nil`; it also sets up a strong temporary reference that lives through the body. This reference holds the instance until the closure ends. The measurement tests it as follows: the closure of a `Job` instance calls a function in the middle of its body that releases the outside name. Does `deinit` run at the release line, or after the body finishes?

```swift
// main.swift — guard let self holds the instance through the body
final class Job {
    let name: String
    let owner: Probe
    var run: (() -> Void)?

    init(_ name: String, owner: Probe) {
        self.name = name
        self.owner = owner
    }

    func install(release: @escaping () -> Void) {
        run = { [weak self] in
            guard let self else {
                print("job gone")
                return
            }
            print("body started \(self.name)")
            release()
            print("body ending \(self.name)")
        }
    }

    deinit {
        print("deinit job \(name)")
        _ = owner.eval(name)
    }
}

let held = Probe("temporary-strong")
for round in 1...2 {
    var job: Job? = Job("job-\(round)", owner: held)
    job?.install { job = nil }
    let saved = job?.run
    _ = held.use { saved?() }
    print("closure returned: \(held.evaluations) deinit")
}
held.report()
```

```
body started job-1
body ending job-1
deinit job job-1
closure returned: 1 deinit
body started job-2
body ending job-2
deinit job job-2
closure returned: 2 deinit
temporary-strong: 2 evaluations, 2 uses -> on-every-use
```

In each round `release()` is called between the lines `body started` and `body ending`, and the outside `job` name becomes `nil`. Even so, the line `deinit job job-1` comes after the line `body ending job-1`. The temporary strong reference set up by `guard let self` keeps the instance up until the body ends; the last reference is released only as the closure returns. The line `closure returned: 1 deinit` confirms it: at the moment the closure returns, the counter is 1. `temporary-strong` counts two evaluations in two rounds and says `on-every-use`. The practical consequence runs in two directions. `[weak self]` keeps the closure from holding the instance permanently, but the instance does not disappear while the body is running. The work done in the body does not have to be split for fear that the instance will vanish.

## The Bounding Measurement

Not every closure builds a cycle. For a cycle, the closure has to be **stored** in the instance. A closure that is created inside a method and finishes before the method returns, for example a closure given to a transformation of an array, is stored nowhere, and its capturing `self` does not hold the instance. The measurement shows this in a `Report` instance: the `joined` method reads the `name` property inside `parts.map { ... }`, the result is returned, and the instance is released.

```swift
// main.swift — a closure that is not stored builds no cycle
final class Report {
    let name: String
    let owner: Probe
    let parts = ["a", "b", "c"]

    init(_ name: String, owner: Probe) {
        self.name = name
        self.owner = owner
    }

    func joined() -> String {
        parts.map { "\($0)-\(name)" }.joined(separator: ",")
    }

    deinit {
        print("deinit report \(name)")
        _ = owner.eval(name)
    }
}

let passing = Probe("temporary-closure")

for round in 1...2 {
    var report: Report? = Report("report-\(round)", owner: passing)
    print(report?.joined() ?? "none")
    _ = passing.use { report = nil }
    print("released \(round): \(passing.evaluations) deinit")
}

passing.report()
```

```
a-report-1,b-report-1,c-report-1
deinit report report-1
released 1: 1 deinit
a-report-2,b-report-2,c-report-2
deinit report report-2
released 2: 2 deinit
temporary-closure: 2 evaluations, 2 uses -> on-every-use
```

In each round `deinit` runs at the moment of the release. The `map` closure read `self`, but the closure ended the moment the method returned; no reference to it remained. The leak does not come from capturing itself but from the capturing closure continuing to live in the instance. The only place that needs a capture list is a stored closure.

**MA3.** Every measured class stores its closure in a single property. That several stored closures, or an object that keeps its closure in an outside list, follow the same rules was not measured in this lesson. Having a single closure is a design choice.

## Summary

- If a closure stored in a property captures `self` strongly, the instance and the closure hold each other and `deinit` never runs.
- The `[weak self]` capture list does not hold the instance; when the instance is gone, `self` is `nil` and `guard let self` makes the closure run empty.
- `[snapshot = expression]` in a capture list is evaluated once while the closure is created; a variable captured without a list is read with its current value on every call.
- A closure that is not stored, such as the ones inside `map`, builds no cycle even when it uses `self`.
- This lesson's moment: **immediately**. The expression in the capture list is evaluated once, at the point where the closure is created.

## Next Step

In this lesson the closure was a box that took a value or an instance into itself and stored it. Property wrappers do something similar for a property itself: they move how a value is stored and read to a separate type. The next lesson measures when the `wrappedValue` getter runs and when the initial value of the wrapper is set up.
