Leave a rating/review
Notes: 14. Canceling Tasks
Xcode 14 beta detects a race condition for stopDownloads when you tap Cancel Now while using the Cloud 9 plan. Move stopDownloads and reset() to @MainActor and await the variable in the private downloadWithProgress method:
while await !stopDownloads, !accumulator.checkCompleted() {
...
if await stopDownloads, !Self.supportsPartialDownloads {
...
@MainActor var stopDownloads = false
...
@MainActor func reset() {
Make sure the course server is running and continue with your project from the previous episode or open the starter project for this episode.
Canceling Tasks
Why is it important to cancel tasks in a timely manner? Try this:
Now, tap Gold. Before it finishes, tap Back.
Look in the console: The download keeps going. But users expect this action to cancel the download.
In LittleJohn, when you tapped the Back button from the TickerView, it canceled the task view modifier, which canceled its child tasks.
Look in the FileDetails view: The download button actions aren’t in a task view modifier, so there’s no parent task to cancel them. You must cancel them manually.
First, go to DownloadView and add a State property:
@State var downloadTask: Task<Void, Error>?
You’re just creating a place to store a Task to give it a name you can cancel.
Task is a type like any other, so you can store it in your view, model or any other scope.
downloadTask is an asynchronous task that returns no result and could throw an error.
The Task type doesn’t return anything if it’s successful, so success is Void, and it returns an Error if there’s a failure.
The Task you want is in downloadWithUpdatesAction: Store it in your new State property:
downloadTask = 🟥Task {
The time to cancel this Task is when the user navigates back to the main screen, which triggers this onDisappear view modifier.
So add a line to this closure:
downloadTask?.cancel()
Canceling downloadTask cancels all its child tasks and their child tasks and so on.
Build and run, select a tiff file, tap Gold then tap Back before it finishes.
In the console, the download stops.
You’ve implemented a simple way to manually cancel a task: name the task. Decide where to cancel it. And call its cancel() method.
Partial Image Preview
What if you want to cancel a download task without leaving DownloadView? Look in SuperStorageModel.
Look in the jump bar: There’s already a Boolean property stopDownloads
You’re keeping track of this flag in downloadWithProgress.
You’ll set this property in DownloadView.
To make life interesting, you’ll implement a fun use case: You’ll display a partial image when the user cancels the download of a JPEG image file.
The JPEG format allows for partially decoding images, but other formats, including TIFF, don’t.
To support partial preview only for canceled JPEG files, you need each task to have a local property you can set, to indicate whether it supports partial downloads.
TaskLocal
Say hello to the TaskLocal property wrapper!
At the top of SuperStorageModel, add a TaskLocal property:
@TaskLocal static var supportsPartialDownloads = false
Task-local properties must be either static for the type or global variables.
Now back to DownloadView, to set this property: In downloadWithUpdatesAction, replace the do closure code:
First, wrap the fileData line in a try await closure:
try await SuperStorageModel {
fileData = try await model.downloadWithProgress(file: file)
}
then add these modifiers:
try await SuperStorageModel
.$supportsPartialDownloads
.withValue(file.name.hasSuffix(".jpeg")) {
fileData = try await model.downloadWithProgress(file: file)
}
The TaskLocal property wrapper has a method withValue(_:) to set the property’s value. You use this to set the value to true when the user starts a JPEG download.
You can bind multiple values this way, and you can also overwrite the values from inner bindings, but this can become hard to read. Task storage is more useful for binding complete configuration objects or whole data models, rather than separate single values or flags as in this example.
Now, scroll down to the toolbar view modifier, where the Cancel Now button needs an action:
Button(action: { 🟩model.stopDownloads = true
Instead of canceling the download task directly, like you did in onDisappear, you turn on the stopDownloads flag on SuperStorageModel.
You’ll observe this flag while downloading. If it changes to true, you need to cancel download tasks that don’t support partial downloads.
In SuperStorageModel, in the private downloadWithProgress, insert this code just before the return line:
if stopDownloads, !Self.supportsPartialDownloads {
throw CancellationError()
}
After each downloaded batch of bytes, you check stopDownloads then supportsPartialDownloads. If the downloading file isn’t JPEG, you throw a CancellationError to exit and stop the download.
If the downloading file is JPEG, you stop downloading, but continue execution to return the partially downloaded file.
Build and run to try this out: First, tap a TIFF file, tap Gold, then tap Cancel Now. Next, use a JPEG — wait until about a third of the JPEG file has downloaded before you cancel it, or there won’t be much to see. Tap Gold, then tap Cancel Now.
And there’s your partial preview!
Congratulations! You’ve used task cancellation and a TaskLocal property to add a useful feature to your app.
The next fun feature you’ll add is a timer, to show how long a download is taking.