Hello there,
I write this question because I can't really find an answer. As a lot of developers, since the release of Xcode 13.2, I'd like to use the Swift Concurrency in my company projects (at last :D).
However, I'm facing an architecture problem when it comes to cancel all tasks currently running (not really cancel them, but I'd want to get the isCancel boolean to true to respect the "Cooperative Cancellation" process). The problem I'm trying to figure out is : how can I cancel my tasks running in a ViewModel when the ViewController is deinitialized ?
I can summarize the question with the code below :
// ViewController
class SomeViewController: UIViewController {
var viewModel: SomeViewModel?
deinit {
print("SomeViewController instance deinit")
}
override func viewDidLoad() {
super.viewDidLoad()
viewModel?.doAsyncStuff()
}
}
// ViewModel
class SomeViewModel {
deinit {
print("SomeViewModel instance deinit")
}
func doAsyncStuff() {
Task {
print("Begin async stuff")
try await Task.sleep(nanoseconds: 1_000_000_000 * 5) // 5 seconds
simulateAPICall()
print("Async stuff has been done successfully")
}
}
func simulateAPICall() {
// API Call and update the ViewModel properties
}
}
Console result :
Begin async stuff // Async code is called
SomeViewController instance deinit // SomeViewController instance is not shown anymore
Async stuff has been done successfully // SomeViewModel instance is retained in memory until the task has finished
SomeViewModel instance deinit // Now that the task has finished, the ViewModel can be automatically deinit
In this example, when the SomeViewController instance is not shown anymore (dismissed, pop, etc.) it is well deinitialized and does not retain anything in memory.
However, the SomeViewModel is implicitly retained in memory because of the Task (it's normal because the Tasks behave like this, they need to retain anything they use to perform their functions until they've finished). And the problem is here. For example, if I use a router at the end of the Task to show another ViewController, then the router stuff will be executed even if the SecondViewController that have asked the initial async stuff doesn't exist anymore.
I don't know how to inform the Task that it needs to be cancelled without doing a boilerplate? I'd have liked to make a system like RxSwift / Combine and their Disposable / Cancellable, so that I don't need to call the cancel() Task function manually. An explicit code can be easily forgotten or broken in a project life.
The problem might also exists with SwiftUI when we don't use a modifier that implicitly cancel tasks when the view is not shown anymore, like the task modifier.