Leave a rating/review
Make sure the course server is running and continue with your project from the previous episode or open the starter project for this episode.
Build and run, then tap a filename.
SuperStorage app
The download view downloads the file using your choice of subscription plan: silver, gold or Cloud 9.
In this episode, you’ll implement the slowest plan: Silver.
For this plan, the server can pretend to be slow. There’s nothing wrong with your network connection.
Download data
In SuperStorageModel, locate download(file:). It already contains the code you know so well:
guard let url = URL(
string: "http://localhost:8080/files/download?\(file.name)") else {
throw "Could not create the URL."
}
let (data, response) = try await URLSession.shared.data(from: url)
guard (response as? HTTPURLResponse)?.statusCode == 200 else {
throw "The server responded with an error."
}
This code creates an endpoint, sends a request to the server, waits for the response, then checks the response status code.
All you need to add is some housekeeping to keep track of downloaded files.
Look down in the model’s extension to see two helper methods: addDownload(name:) and updateDownload(name:).
Go back to download(file:), add this line before sending the URL request:
addDownload(name: file.name)
You add the file’s download info to the published downloads array. The download view uses this to show ongoing download statuses onscreen.
Then add this line after sending the URL request (before checking statusCode):
updateDownload(name: file.name, progress: 1.0)
After data(from:) completes, you set progress to 1.0, to indicate the download finished.
In later episodes, you’ll implement the Gold and Cloud 9 plans, where you’ll update the progress value during the download. To finish download(file:), replace its return value:
return data
Call download(file:)
Now, where do you call this asynchronous method? Open DownloadView. Go to its body.
This view’s body is a List, and the main list item is a FileDetails view. This view’s initializer has 3 action parameters, one for each of the subscription plans.
Right-click FileDetails and jump to its definition.
Here’s the Silver button, with action downloadSingleAction.
Click back to DownloadView.
You need to make downloadSingleAction call download(file:).
Try this, even though you feel in your bones it won’t work:
fileData = try await model.download(file: file)
Sure enough, the parameter expects a synchronous closure. You could change FileDetails to make it accept asynchronous closures…
Click back to FileDetails and add the async keyword:
let downloadSingleAction: () 🟩async🟥 -> Void
But the Button action must be synchronous. Also, you don’t always have access to the source code of an API.
Remove the async keyword and go back to DownloadView.
You can’t use the .task view modifier: That would execute this code when the view appears.
To pass an asynchronous closure to a Button action, you need something that lets you run asynchronous code in a synchronous context … you did this right at the start, in episode 2.
That’s right, you wrap the asynchronous code in a capital-T Task:
Task {
fileData = try await model.download(file: file)
}
Build and run, select a jpeg file (they’re smaller) then tap Silver.
A progress bar appears and, after a while, it updates and a preview appears.
But here in Xcode, there’s a console message that Publishing changes from background threads is not allowed. You might also get a purple flag from the Main Thread checker.
Why is this happening? You passed download(file:) to FileDetails, which is a SwiftUI view so, when you tap Silver, download(file:) starts running on the main thread.
Jump to the definition of download(file:) to take another look.
Here’s the problem: This await suspends your code. When it resumes and calls updateDownload, it could be executing on any thread. Probably not the main thread.
Jump down to updateDownload: This method updates downloads … Jump to downloads … It’s a Published property. So you need to make sure updateDownload runs on the main thread.
And look at addDownload … It also updates downloads, so it also must run on the main thread. Look again at download(file:) …
Because you call addDownload before the await, it’s already running on the main thread, but that’s really a coincidence, and you can’t count on this always happening. You’ll call it for the other download methods, and there’s no guarantee it will always run before code suspends. So you need to do something for both addDownload and updateDownload.
In episode 5, when you assigned a new value to tickerSymbols, you enclosed that line of code in a MainActor.run closure. This time, just tell Xcode that these two methods belong to MainActor.
Annotate them with @MainActor:
@MainActor
func addDownload(name: String) { ... }
@MainActor
func updateDownload(name: String, progress: Double) { ... }
Now both methods will always run on the main thread.
By default, code that belongs to an actor runs asynchronously, so you need to call it with await. This lets the runtime suspend your call and resume it on the correct actor.
Scroll back to download(file:), and there are two new error messages.
The fix “Insert await” is correct, so go ahead and click Fix.
Build and run again, pick a file and tap Silver. This time, there’s no error message in the console. Tap Back and pick another file.
Hmm, no buttons, just the progress bar from your first download. This one’s a quick fix.
In DownloadView, add this modifier below toolbar:
.onDisappear {
fileData = nil
model.reset()
}
You reset the view’s fileData and also the model’s downloads array.
Build and run again, to see that this works. Yup, that’s fine!
You’re almost finished with the Silver plan. There’s just one more feature to add. In the next episode, your challenge is to show a ProgressView spinner while the file is downloading.