iosNudge

What is a capture list in a Swift closure?

MediumAsked oftenSwiftMemory & ARC 3 min read

TL;DR

A capture list is the [ ... ] at the start of a closure. It says how the closure should capture things. Two main uses: break retain cycles with [weak self] or [unowned self], and copy a value at creation time with [value]. Without a capture list, a closure captures variables by reference and objects strongly.

Default capture: by reference

var count = 0
let printCount = { print(count) }

count = 5
printCount()     // 5, not 0

The closure sees the variable itself, not a snapshot of it.

Capture a copy with a capture list

var count = 0
let printCount = { [count] in print(count) }

count = 5
printCount()     // 0

[count] copies the value when the closure is created. Later changes do not affect it. The captured copy is a constant.

Break retain cycles with [weak self]

final class TimerViewModel {
    var ticks = 0
    var onTick: (() -> Void)?

    func start() {
        onTick = { [weak self] in
            guard let self else { return }
            self.ticks += 1
        }
    }

    deinit { print("TimerViewModel freed") }
}

Without [weak self]: the object holds onTick, and onTick holds the object. Neither is freed.

With [weak self]: the closure does not keep the object alive. self becomes an optional inside the closure.

[unowned self]

lazy var title: () -> String = { [unowned self] in
    "Ticks: \(self.ticks)"
}

unowned does not keep the object alive either. But it is not optional. If the object is gone when the closure runs, the app crashes.

Use unowned only when the closure can never outlive self.

weak vs unowned in closures

[weak self][unowned self]
Type inside closureSelf?Self
If object is goneself is nilCrash
Safe default?YesOnly if lifetime is guaranteed

Capture other things too

You can capture several values and give them new names.

let service = APIService()
loadButton.onTap = { [weak self, service, limit = pageSize * 2] in
    self?.load(using: service, limit: limit)
}
  • service is captured strongly (fine, it does not hold the closure).
  • limit is computed once when the closure is created.

You can also make other objects weak, like a delegate or a view:

{ [weak delegate, weak tableView] in
    tableView?.reloadData()
}

When you do NOT need [weak self]

  • Non-escaping closures, like map and filter.
  • Closures that run once and are not stored by self, like UIView.animate or a short DispatchQueue.main.async. They hold self a little longer, then release it.
  • Task { } that finishes quickly. It keeps self alive until it ends, but does not create a permanent cycle.

Be careful with a Task that never ends, such as one that loops over an AsyncStream. It keeps self alive forever. Store the task and cancel it.

final class FeedViewModel {
    private var task: Task<Void, Never>?

    func start(stream: AsyncStream<Int>) {
        task = Task { [weak self] in
            for await value in stream {
                self?.handle(value)
            }
        }
    }

    func handle(_ value: Int) { }
    deinit { task?.cancel() }
}

Note: writing guard let self at the top of a long-running loop would hold self strongly for the whole loop. Use self?. inside the loop instead.

Common follow-up questions

  • Does [weak self] work with structs? No. weak only works with class (reference) types. Structs are copied, so there is no cycle.
  • What does guard let self else { return } do? It makes a strong reference for the rest of the closure body, so self cannot disappear in the middle.
  • Is a captured value in [x] mutable? No. It is a constant copy inside the closure.
  • Do capture lists matter for value types? Yes, for snapshot behavior. [count] copies the current value instead of reading the variable later.
  • Is [unowned self] faster? Slightly, because there is no optional check. The safety risk is usually not worth it.