Skip to content

This is a repository that explains how to manage and cancel tasks.

Notifications You must be signed in to change notification settings

funzin/swift-concurrency-task-management

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 

Repository files navigation

Overview

This is a repository that explains how to manage and cancel tasks.

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

  1. Manage tasks in ViewModel
  2. 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)
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)")
        }
    }
}

Behavior

viewDidLoad

Both screens print log in 5 seconds after viewDidLoad is called.

FeatureA
display_featurea.mov
FeatureB
display_featureb.mov

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
cancel_featurea.mov
FeatureB
cancel_featureb.mov

Conclusion

I have written about how to manage tasks using ViewModel.
If you're interested, have a look at my project.

Author

funzin

twitter: @_funzin

About

This is a repository that explains how to manage and cancel tasks.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages