This is a repository that explains how to manage and cancel tasks.
- Xcode 13.2
I recommend you to read the following article to understand how to cancel tasks.
The following issues are related to Task.
- Even after the screen is dismissed, Task will continue to run.
- You have to write code to cancel tasks in a lot of files. It's boierplate code.
- Manage tasks in ViewModel
- Cancel tasks when the screen is dismissed.
The management method is similar to Disposable
in RxSwift and Cancellable
in Combine.
Define ViewModel as BaseClass and then SubClass can be inherited it.
@MainActor
class ViewModel: ObservableObject, TaskCancellable {
private var taskDict: [TaskID: [Task<Void, Never>]] = [:]
deinit {
taskDict.values.forEach { tasks in
for task in tasks where !task.isCancelled {
task.cancel()
}
}
}
}
extension ViewModel {
func addTask(
priority: TaskPriority? = nil,
operation: @Sendable @escaping () async -> Void
) {
_addTask(id: DefaultTaskID(), task: Task(priority: priority, operation: operation))
}
func addTask<ID: TaskIDProtocol>(
id: ID,
priority: TaskPriority? = nil,
operation: @Sendable @escaping () async -> Void
) {
_addTask(id: id, task: Task(priority: priority, operation: operation))
}
func _addTask<ID: TaskIDProtocol>(
id: ID,
task: Task<Void, Never>
) {
taskDict[id, default: []].append(task)
}
func cancelAll() {
taskDict.values.forEach { tasks in
for task in tasks where !task.isCancelled {
task.cancel()
}
}
taskDict = [:]
}
}
// SubClass
final class FeatureAViewModel: ViewModel { }
If viewWillDisappear
is called when the screen is poped or dismissed, tasks will be cancelled.
This can be done by using HostingViewController
.
@MainActor
class HostingViewController<Content: View, ViewModel: TaskCancellable>: UIHostingController<Content> {
let viewModel: ViewModel
init(rootView: Content, viewModel: ViewModel) {
self.viewModel = viewModel
super.init(rootView: rootView)
}
@available(*, unavailable)
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override open func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
let willDisappear = isBeingDismissed
|| isMovingFromParent
|| navigationController?.isBeingDismissed ?? false
if willDisappear {
viewModel.cancelAll()
}
}
}
I use two screens to show the different behavior of canceling.
- FeatureA Screen: cancel all tasks when the screen is dismissed(Using
HostingViewController
) - FeatureB Screen: not cancel all tasks when the screen is dismissed (Using
UIHostingControler
)
example code
final class FeatureAViewModel: ViewModel {
func sleep() async -> Bool {
do {
// wait 5 seconds
try await Task.sleep(nanoseconds: 5000000000)
return true
} catch {
return false
}
}
}
final class FeatureAViewController: HostingViewController<FeatureAView, FeatureAViewModel> {
override func viewDidLoad() {
super.viewDidLoad()
viewModel.addTask { [weak self] in
let success = await self?.viewModel.sleep() ?? false
// after waiting sleeping hours, print log
print("success is \(success)")
}
}
}
final class FeatureBViewModel: ViewModel {
func sleep() async -> Bool {
do {
// wait 5 seconds
try await Task.sleep(nanoseconds: 5000000000)
return true
} catch {
return false
}
}
}
/// Use UIHostingController instead of HostingViewController
/// not cancel all tasks after screen is dismissed
final class FeatureBViewController: UIHostingController<FeatureBView> {
private lazy var viewModel = FeatureBViewModel()
override func viewDidLoad() {
super.viewDidLoad()
viewModel.addTask { [weak self] in
let success = await self?.viewModel.sleep() ?? false
// after waiting sleeping hours, print log
print("success is \(success)")
}
}
}
Both screens print log in 5 seconds after viewDidLoad
is called.
display_featurea.mov
display_featureb.mov
If the screen is displayed and then dismissed immediately, the behavior will be different.
- FeatureA: cancel all tasks immediatly after the screen is dismissed.
- FeatureB: Tasks will continue to run even after the screen is dismissed.
cancel_featurea.mov
cancel_featureb.mov
I have written about how to manage tasks using ViewModel.
If you're interested, have a look at my project.
funzin
twitter: @_funzin