Browse Questions
Answer:
Unstructured Task { ... } blocks retain captured objects for the entire duration of the asynchronous operation, which can lead to delayed deallocation or memory leaks if long-running tasks are not cancelled.
When you create an unstructured Task or Task.detached, the task closure captures referenced instances strongly by default. If the task performs a long network request, an infinite loop, or listens to an AsyncStream, the captured object (self) remains retained in memory until the task completes.
Best Practices for Swift Concurrency:
- Cancel Tasks on Lifecycle Teardown: Store the
Taskreference and calltask.cancel()indeinit,viewWillDisappear, or SwiftUI.onDisappear. - Use
[weak self]in Long-Running Tasks: Prevent holdingselfin memory during extended background operations. - Structured Concurrency: Prefer structured concurrency (
async let,withTaskGroup,withThrowingTaskGroup) because child tasks automatically cancel when the parent scope exits.
Code Example:
class LiveStreamViewModel {
private var streamTask: Task<Void, Never>?
func startListening(to stream: AsyncStream<String>) {
// ❌ Leaks self indefinitely if stream never terminates
// streamTask = Task { for await item in stream { self.handle(item) } }
// ✅ Safe: Use [weak self] and check for cancellation
streamTask = Task { [weak self] in
for await item in stream {
guard !Task.isCancelled, let self else { break }
self.handle(item)
}
}
}
func stop() {
streamTask?.cancel()
streamTask = nil
}
deinit {
streamTask?.cancel()
}
private func handle(_ item: String) {}
}
Key Points:
- Structured tasks tie child lifetimes to the enclosing lexical scope; unstructured
Taskoutlives the scope unless explicitly cancelled - Always check
Task.isCancelledinside long loops or afterawaitsuspension points