Swift & Objective-C

Swift Closure Capture: Variables, Snapshots, and References

Compare live variable capture, capture-list snapshots, and weak references, then store an escaping callback and execute it once after registration.

2 min read
Cover image for Swift Closure Capture: Variables, Snapshots, and References

A capture-list snapshot is not a deep object copy. An integer preserves its value at closure creation; a captured class reference still points to the same object. weak is a separate ownership choice that does not keep the target alive.

Mechanism and design choice

A closure capturing a mutable local variable can observe later changes. A capture list such as [number] stores the value at creation. A closure parameter stored for use after return needs @escaping, but escaping alone means neither asynchronous execution nor a memory leak. Primary source

Reproduce the experiment

Inside one function we create live and snapshot captures, then change number to 2. We capture a class reference before changing its object value to 3, and release an object captured weakly. Queue stores a callback, runs it once, and clears it to separate storage lifetime from invocation.

This standalone example was run on 2026-10-23 with Apple Swift 6.4 targeting arm64 macOS, in Swift 6 language mode. It needs no network or additional packages.

Download the complete Swift file

Run the command in the download directory. Preconditions stop execution if a check fails. Do not use -Ounchecked: it removes these checks.

swift -swift-version 6 swift-closure-capture-weak-self-escaping.swift
final class Box { var value = 1 }
final class Queue {
    private var action: (() -> Int)?
    func enqueue(_ action: @escaping () -> Int) { self.action = action }
    func run() -> Int? {
        let current = action
        action = nil
        return current?()
    }
}

func experiment() {
    var number = 1
    let live = { number }
    let snapshot = { [number] in number }
    number = 2
    precondition(live() == 2 && snapshot() == 1)

    let box = Box()
    let capturedReference = { [box] in box.value }
    box.value = 3
    precondition(capturedReference() == 3)

    var temporary: Box? = Box()
    let weakRead = { [weak box = temporary] in box?.value }
    precondition(weakRead() == 1)
    temporary = nil
    precondition(weakRead() == nil)

    let queue = Queue()
    queue.enqueue(snapshot)
    precondition(queue.run() == 1 && queue.run() == nil)
    print("variable=2 snapshot=1 captured-object=3 weak-after-release=nil queued=1 then=nil")
}
experiment()

Recorded output and interpretation

variable=2 snapshot=1 captured-object=3 weak-after-release=nil queued=1 then=nil

The live capture returns 2, the numeric snapshot 1, and the captured object reference observes 3. The weak capture returns nil after deallocation. Queue returns 1 once and nil next. This Queue is a synchronous storage example, not an asynchronous scheduler.

Scope and limitations

Capture does not necessarily imply a heap allocation; the compiler can optimize storage. [weak self] is not mandatory for every closure: decide whether skipping work after the owner disappears is valid. Investigate direct and indirect ownership paths to establish an actual cycle.

What to check next

For stored callbacks, document who owns them, when they are cleared, and what cancellation means. Do not mechanically weaken a reference if the operation must keep its owner alive. The ARC experiment demonstrates breaking a real ownership cycle.

Related experiment

Sources and verification

  • The Swift Programming Language: ClosuresSwift.org · Official documentation · Checked October 23, 2026Supports: Source for the implementation contract and explanation. The output was checked separately using the experiment included here.