Swift Concurrency Task Management
This article explains how to manage Task in Swift Concurrency.
Repository
This is a repository that explains how to manage and cancel tasks.
https://github.com/funzin/swift-concurrency-task-management
Environment
- Xcode 13.2
Reference
I recommend you to read the following article to understand how to cancel tasks.
Task issues
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.
Measures
- Manage tasks in ViewModel
- Cancel tasks when the screen is dismissed.
1. Manage tasks in ViewModel
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 { }
Cancel tasks when the screen is dismissed
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()
}
}
}
Demo
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
)
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)")
}
}
}
Behavior
viewDidLoad
Both screens print log in 5 seconds after viewDidLoad
is called.
FeatureA
FeatureB
Dismiss immediatly
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.
FeatureA
FeatureB
Conclusion
I have written about how to manage tasks using ViewModel.
If you’re interested, have a look at my project.