← All topics/Concurrency

Question 6

Task lifecycles in UIKit and SwiftUI

How do you tie the lifecycle of async tasks to a view (such as a SwiftUI view or a UIViewController) so you don't leak work or update deallocated UI?

Answer outline

In SwiftUI, prefer .task { } on the view. It creates work scoped to the view's lifetime that SwiftUI cancels when the view leaves the hierarchy, and .task(id:) restarts it when the id changes. Avoid orphan Task.detached calls from views.

In UIKit, store the Task handle on the view controller and call cancel() in viewWillDisappear and deinit. Capture [weak self] so the task doesn't keep the controller alive.

Work leaks when a detached task or a strong capture of self outlives the view controller. A stored Task that you cancel on disappear fixes most cases. After an await, assume the controller may already be gone, so guard the weak reference or check Task.isCancelled before touching UI.

Principles

  • In SwiftUI use .task rather than onAppear { Task { } }, because .task auto-cancels when the view disappears and onAppear doesn't.
  • .task(id:) cancels the previous task and re-runs whenever the id changes, which suits selection-driven loads.
  • In UIKit store the handle and cancel in viewWillDisappear, with deinit as the safety net.
  • Capture [weak self] in the task, because it may still be running after the view controller is gone.
SwiftUI: lifetime-scoped .task modifier
struct ProfileView: View {
    @StateObject private var model = ProfileModel()
    let userId: String

    var body: some View {
        VStack {
            // ...
        }
        .task(id: userId) {
            await model.load(userId: userId)
        }
    }
}
UIKit: store the handle, cancel on disappear
final class FeedViewController: UIViewController {
    private var loadTask: Task<Void, Never>?

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        loadTask?.cancel()
        loadTask = Task { [weak self] in
            // Strong self until load() returns, so cancel on disappear matters
            guard let self else { return }
            await self.viewModel.load()
        }
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        loadTask?.cancel()
        loadTask = nil
    }

    deinit { loadTask?.cancel() }
}