Logo for tanaschita.com

Memory management for async/await and tasks in Swift

Learn about weak self references when working with the async/await API.

03 Oct 2022 · 2 min read

Although ARC tracks and manages our app's memory usage automatically in most cases, we still need to keep an eye out to not cause memory issues in certain situations, especially when working with asynchronous code in Swift.

Sponsorship logo
Preparing for a technical iOS job interview - updated for iOS 17
Check out my book on preparing for a technical iOS job interview with over 200 questions & answers. Test your knowledge on iOS topics such as Swift, SwiftUI, Combine, HTTP Networking, Authentication, SwiftData & Core Data, Concurrency with async/await, Security, Automated Testing, Machine Learning and more.
LEARN MORE

A common scenario where we need to be careful not to cause retain cycles is when working with closures:

private var loadCategories: (([Category]) -> Void)?
func update() {
loadCategories { [weak self] categories in
self.categories = categories
}
}

In the example above, a closure is assigned to a property of a class instance and at the same time the body of that closure captures the instance by using self. To avoid a retain cycle in that situation, we capture self as a weak reference instead of a strong reference by using [weak self].

Now, let's look at [weak self] references when working with async/await functionality.

To call an async function from a synchronous context in Swift, we are using tasks providing a closure that contains the asynchronous code to perform:

func update() {
Task {
let categories = await loadCategories()
self.categories = categories
}
}

Objects used within a Task are automatically retained until it finishes. That means in our case, self is captured for a long as the categories are loading. The example doesn't create a retain cycle though - assuming that the task is guaranteed to finish at some point.

Even if not strictly necessary, we could still add a [weak self] reference so the object that started the task can be released without having to wait for the task to finish:

func update() {
Task { [weak self] in
let categories = await self?.loadCategories()
self?.categories = categories
}
}

An alternative to using [weak self] is cancelling the task, for example when a view is dismissed:

private var categoriesLoadingTask: Task<Void, Never>?
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
categoriesLoadingTask?.cancel()
}
func update() {
categoriesLoadingTask = Task {
...
}
}

In the example above, we are using the UIViewController's viewWillDisappear(_:) method to cancel the task. In case of a SwiftUI view, we could use its onDisappear(perform:) method.

Sponsorship logo
Preparing for a technical iOS job interview - updated for iOS 17
Check out my book on preparing for a technical iOS job interview with over 200 questions & answers. Test your knowledge on iOS topics such as Swift, SwiftUI, Combine, HTTP Networking, Authentication, SwiftData & Core Data, Concurrency with async/await, Security, Automated Testing, Machine Learning and more.
LEARN MORE

Newsletter

Image of a reading marmot
Subscribe

Like to support my work?

Say hi

Related tags

Articles with related topics

async/await

concurrency

swift

How to bridge completions handlers to Swift's async/await

Understand Swift continuations to create your own async/await functions.

05 Dec 2022 · 3 min read

Latest articles and tips

© 2024 tanaschita.com

Privacy policy

Impressum