Question 6
Task lifecycles in UIKit / SwiftUI
How do you tie the lifecycle of async tasks to a view (e.g. SwiftUI view or UIViewController) so you don't leak work or update deallocated UI?
Answer outline
SwiftUI: prefer .task { } on the view. This creates structured work that is cancelled when the view leaves the hierarchy (and can restart when id changes).
Avoid orphan Task.detached from views.
UIKit: store Task? (or a small coordinator) on the view controller, cancel() in viewWillDisappear / deinit, and [weak self] (or Task.checkCancellation) before touching UI after await.
Leaks happen when Task.detached or unowned self outlives the VC; structured children tied to a stored Task you cancel fix most cases. After await, assume self may be nil—guard weak self.
Principles
- SwiftUI: use
.task, notonAppear { Task { } }—.taskauto-cancels when the view leaves the hierarchy;onAppeardoes not. .task(id:)re-runs and cancels the previous task wheneveridchanges — the right tool for selection-driven loads.- UIKit: store the handle, cancel on disappear —
viewWillDisappearcancels in-flight work;deinitis your safety net. - Always
[weak self]afterawait— the task may still be running when the VC is gone.
struct ProfileView: View {
@StateObject private var model = ProfileModel()
let userId: String
var body: some View {
VStack {
// ...
}
.task(id: userId) {
await model.load(userId: userId)
}
}
}
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
guard let self else { return }
await self.viewModel.load()
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
loadTask?.cancel()
loadTask = nil
}
}