A delayed Swift calculation keeps its owner alive when its task strongly captures that owner. Use a weak capture to allow the owner to disappear during the wait, or capture an independent value snapshot when the calculation should finish without it.
For a Mac developer building a small desktop calculation preview, the practical task is to choose that lifetime deliberately. This example waits briefly, adds the synthetic values [4, 8, 12], and formats the total. Everything runs locally in memory.
Which lifetime should your calculation have?
Swift capture lists let you specify what a closure retains. A strong capture holds the referenced instance; a weak reference allows it to be deallocated and becomes nil afterward. These are documented language behaviors, independent of how long the calculation waits. Automatic Reference Counting
Choose a contract before choosing the capture syntax. The following decision table applies those language rules to a delayed desktop preview:
| Desired behavior | Capture strategy | What happens after the caller releases its owner? |
|---|---|---|
| Finish using the same owner | [self] |
The task keeps the owner available for its work. |
| Continue only if the owner remains available | [weak self] |
After waiting, the task checks whether the owner still exists. |
| Finish using the values selected when scheduled | [samples = self.samples] |
The task retains the captured values without retaining this owner. |
The snapshot row has an essential condition: this example captures an array of integers. Capture-list expressions are evaluated when the closure is created. Capturing a class instance instead would retain a reference to that instance; it would not create an independent copy of its mutable contents. Expressions
There is also a separate value question. The strong and weak versions below read the owner's values after the delay. The snapshot version uses the values from scheduling time. If the preview's inputs can change while waiting, decide which interpretation the result should represent.
Run a complete local comparison on your Mac
Use macOS 13 or later and an installed Swift 6 or later toolchain. The duration-based Task.sleep(for:) overload used here is documented for macOS 13 onward. It suspends the task without blocking the underlying thread and throws CancellationError if cancellation occurs before the wait ends. sleep(for:tolerance:clock:)
Save the following complete program as CaptureDemo.swift. The calculator, task closures, and demonstration run on @MainActor, which keeps their state access serialized. Swift documents both class-level main-actor isolation and the explicit closure annotation used here. Concurrency
import Foundation
enum CapturePolicy: String {
case strong, weak, snapshot
}
@MainActor
func formatTotal(_ samples: [Int]) -> String {
let total = samples.reduce(0, +)
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.usesGroupingSeparator = false
formatter.maximumFractionDigits = 0
guard let text = formatter.string(from: NSNumber(value: total)) else {
return "Formatting unavailable"
}
return "Total: \(text)"
}
@MainActor
final class Calculator {
var samples = [4, 8, 12]
func start(_ policy: CapturePolicy) -> Task<String, Error> {
switch policy {
case .strong:
return Task { @MainActor [self] in
try await Task.sleep(for: .milliseconds(300))
try Task.checkCancellation()
return formatTotal(self.samples)
}
case .weak:
return Task { @MainActor [weak self] in
try await Task.sleep(for: .milliseconds(300))
try Task.checkCancellation()
guard let self else {
return "Owner unavailable"
}
return formatTotal(self.samples)
}
case .snapshot:
return Task { @MainActor [samples = self.samples] in
try await Task.sleep(for: .milliseconds(300))
try Task.checkCancellation()
return formatTotal(samples)
}
}
}
}
@main
struct CaptureDemo {
@MainActor
static func demonstrate(
_ policy: CapturePolicy,
cancel: Bool = false
) async {
var owner: Calculator? = Calculator()
weak var probe = owner
let task = owner!.start(policy)
owner = nil
let label = cancel ? "\(policy.rawValue), cancelled" : policy.rawValue
print("\(label) retains owner: \(probe != nil)")
if cancel {
task.cancel()
}
do {
let result = try await task.value
print(result)
} catch is CancellationError {
print("Cancelled")
} catch {
print("Calculation failed: \(error)")
}
}
@MainActor
static func main() async {
await demonstrate(.strong)
await demonstrate(.weak)
await demonstrate(.snapshot)
await demonstrate(.snapshot, cancel: true)
}
}
The arithmetic uses standard-library reduce(0, +) to combine the integers into one sum. Foundation's NumberFormatter turns that sum into display text; its string(from:) method returns an optional string, so the program reports a formatting failure explicitly. The fixed sample adds to 24. reduce(::), NumberFormatter, string(from:)
In Terminal, change to the folder containing the file and run these commands. The target specifies macOS 13 for your Mac's processor architecture. The -parse-as-library option permits the file's explicit @main entry point. Swift's compiler accepts source files and command-line options; swiftc -help lists the options installed with your toolchain. Swift command-line manual
swift --version
swiftc -parse-as-library -swift-version 6 -target "$(uname -m)-apple-macos13.0" CaptureDemo.swift -o CaptureDemo
./CaptureDemo
The expected application output is shown below. It is derived from the capture and cancellation rules; this manuscript's research environment had no Swift compiler available, so the program was inspected but not executed here.
strong retains owner: true
Total: 24
weak retains owner: false
Owner unavailable
snapshot retains owner: false
Total: 24
snapshot, cancelled retains owner: false
Cancelled
The weak probe observes ownership without adding another strong reference. Each demonstration clears its own strong reference before awaiting the task. Because the demonstration and task closures use the same actor, the newly created closure cannot interleave with that synchronous setup. This makes the comparison independent of guessing how quickly the task starts. Concurrency
Why does the position of the weak check matter?
In the weak branch, guard let self appears after the delay. The task can therefore wait without holding the owner strongly. If the owner has disappeared, the explicit Owner unavailable result ends that calculation.
Moving the unwrap before the delay changes the lifetime. The following is an illustrative fragment replacing the weak branch's closure body; it is not a separate program:
// Inside the existing [weak self] task closure:
guard let self else {
return "Owner unavailable"
}
try await Task.sleep(for: .milliseconds(300))
try Task.checkCancellation()
return formatTotal(self.samples)
Here, successful optional binding creates a strong local reference that is needed again after suspension. The owner consequently remains available across the wait once that binding succeeds. This follows from Swift's strong-reference rules: the weak capture determines how the closure initially reaches the owner, while the unwrapped local determines how subsequent code holds it. Automatic Reference Counting
For the contract “wait without retaining the owner, then calculate only if it still exists,” keep the unwrap after the wait and perform the small calculation immediately afterward. Additional suspension points after unwrapping require another lifetime decision.
An unowned capture requires the referenced instance to remain valid whenever accessed. It does not provide the optional absence check needed by this example, where releasing the owner during the delay is intentional. Automatic Reference Counting
Does releasing the task handle stop the calculation?
A task continues running even if you discard its handle. Keeping the handle lets you await its result or request cancellation. Apple also documents that a completed task eagerly releases its operation closure and the references that closure held. A finite strong capture therefore extends the owner's lifetime through the work without necessarily producing an enduring reference cycle. Task
Cancellation is a separate contract from ownership. The final demonstration requests cancellation through the handle. The throwing sleep and subsequent Task.checkCancellation() provide places where the operation can stop, and the caller reports that outcome. Swift cancellation is cooperative: requesting it does not forcibly interrupt arbitrary calculation code. Task
Do not replace the throwing sleep with try? and then proceed unconditionally if cancellation should prevent a result. Suppressing that error would allow execution to reach the calculation unless another cancellation check stops it. Likewise, assigning nil to the owner's variable is not a cancellation request.
For a desktop preview, these distinctions give three useful controls: whether the owner remains allocated, which input values the calculation uses, and whether the operation should stop. Choose each explicitly rather than expecting a weak capture to implement all three.
What should you change for a desktop preview?
Use the weak version when a pending preview has value only while its owner remains available. Use the snapshot version when the selected synthetic inputs should produce a result independently. Use the strong version when completing the operation legitimately requires the owner.
Object lifetime also differs from interface visibility. A window closing does not itself prove that every strong reference to its calculator has been removed. If the application should stop pending work when a preview closes, connect that application event to the retained task handle's cancel() method; the calculation must still cooperate with cancellation. Task
Keep this example's workload small. Main-actor isolation makes the state access straightforward, and the wait suspends, but the synchronous sum still executes on that actor. Swift's concurrency guide explains that the main actor also handles interface work. A substantially larger calculation needs a separate execution design that passes suitable values to the worker. Concurrency
For this local calculation, the decisive test is simple: remove the caller's reference before the delayed operation runs and observe the result. Strong capture preserves the owner, weak capture permits an explicit unavailable outcome, and an integer-array snapshot preserves just the inputs needed to finish.
Sources
- Automatic Reference Counting — Swift.org
- Expressions — Swift.org
- Concurrency — Swift.org
- Task — Apple Developer
- sleep(for:tolerance:clock:) — Apple Developer
- reduce(::) — Apple Developer
- NumberFormatter — Apple Developer
- string(from:) — Apple Developer
- Swift command-line manual — Swift project