Leave a rating/review
Notes: 14. Show Download Progress
When downloading images or large over the network, the user will often times want to know the progress of the total download. If using the closure-based task downloads on URLSession, you have delegate methods for this.
If you want to use the asynchronous functions of URLSession, then switching over from the data or download methods over to the bytes methods is what you need.
The delegate method is called urlSession-downloadTask-didWriteData. This passes in three important values: 1. The bytesWritten. 2. The totalBytesWritten. 3. And the totalBytesExpectedToWrite.
When a file is saved, it’s downloaded over time. Each time this method is called, the bytesWritten is the number of bytes that were saved to disk, the totalBytesWritten is the total bytes that have been saved to disk and the totalBytesExpectedToWrite is the actual size of file.
When using the asynchronous APIs, you retrieve the contents of a URL or URLRequest and get back an asynchronous sequence of bytes for you to store in memory or on disk. We’ll be working with the asynchronous APIs in this episode, so let’s get started!
Let’s add some better error-handling to the app. Start by opening the Starter project for this episode.
Open SongDetailView.swift and add a property to keep track of the download progress:
@MainActor @State private var downloadProgress: Float = 0.0
And with this property, update your ProgressView:
if isDownloading {
ProgressView(value: downloadProgress)
}
To comply with ProgressView‘s expectations for value you’ll also keep the property from 0 to 1, to represent 0 to 100%.
Open SongDownloader.swift In here you’re going to add a new method that downloads a song using URLSession’s bytes method. Add the following code:
func downloadSongBytes(at url: URL, progress: Binding<Float>) async throws {
}
This method works similar to downloadSong by taking a download URL, but it also has a progress property that’s a Float Binding for you to keep track of the download progress. Similarly, this method is async and can throw errors.
Switch back to SongDetailView.swift real quick and replace the call to downloadSong with:
try await downloader.downloadSongBytes(at: previewURL, progress: $downloadProgress)
You call the new method on SongDownloader and pass downloadProgress as the binding that will be used to keep track of the download.
Back in SongDownloader, it’s time to implement the new downloadSongBytes method. Start by adding this code:
let (asyncBytes, response) = try await session.bytes(from: url)
This is very similar to the methods you’ve been using, except it downloads bytes that are returned as an asynchronous sequence. Moving on, add the following:
let contentLength = Float(response.expectedContentLength)
var data = Data(capacity: Int(contentLength))
This code will help you check the response’s expected content length which you then use to create a new Data object with a reserved capacity of the expected size of the bytes to download.
Because the tuple returned from the asynchronous bytes call contains the sequence of bytes with your data, iterate through these bytes with the following:
for try await byte in asyncBytes {
}
Note how you use try await to loop through this async sequence. In the for loop you want to append the bytes that are downloaded to the data object you created earlier:
data.append(byte)
And to actually see the progress of the download you want to update the progress parameter:
progress.wrappedValue = Float(data.count) / Float(contentLength)
Note that, because the progress parameter is a Binding, you want to update its wrappedValue with the actual floating point number, from 0 to 1, that corresponds to your download progress. Build and run the app.
Tap the Download button and check out the results. Yay, you see the progress view in action even though the actual bytes downloaded aren’t being handled, so your button reverts back to saying Download.
You may have also noticed that the download might felt incredibly slow when compared to the code you were previously using.
This is because updating the progress value on every iteration of the loop isn’t the most optimal thing to do, so time to tweak the code a little bit to improve its performance. Remove the line where you set the progress parameter with:
let currentProgress = Float(data.count) / contentLength
This stores the current progress of the download. Then, add the following:
if Int(progress.wrappedValue * 100) != Int(currentProgress * 100) {
progress.wrappedValue = currentProgress
}
What this does is set the progress parameter only if the current progress is different, otherwise the loop continues. Build and run the app and give things a try one more time.
That was much, much faster. It might be so fast that you barely even got a chance to enjoy your progress view.
Don’t worry, I’ll show you how to test different network connections in a couple of episodes, but for now it’s time to actually store those downloaded bytes into a file on disk. Add this code at the bottom of your for loop:
let fileManager = FileManager.default
guard let documentsPath = fileManager.urls(for: .documentDirectory,
in: .userDomainMask).first
else {
throw SongDownloadError.documentDirectoryError
}
As before you are working with FileManager to acquire the URL for the app’s Documents directory. If something goes wrong in the process then you throw an error. Time to construct the URL for where to save the song:
let lastPathComponent = url.lastPathComponent
let destinationURL = documentsPath.appendingPathComponent(lastPathComponent)
And the write the actual bytes to a file on disk:
do {
if fileManager.fileExists(atPath: destinationURL.path) {
try fileManager.removeItem(at: destinationURL)
}
try data.write(to: destinationURL)
} catch {
throw SongDownloadError.failedToStoreSong
}
Excellent. This should look very similar to what you wrote in the downloadSong method. Finally, and in order for your app to know that the song was downloaded successfully, you want to update the downloadLocation property:
await MainActor.run {
downloadLocation = destinationURL
}
Build and run your app, tap the Download button, and check out the results. Excellent. Everything is working as expected, and just as it was before, but now you actually see the download progress in your UI.
Great work! Not only are you making the app a lot better and more functional for users, but you’re also learning more concepts and functionality that URLSession provides.
In the next episode I’ll show you how you can group multiple requests together so you can treat the song and artwork downloads as a single operation, even if they are performed individually and concurrently. See ya there! :)