Leave a rating/review
Notes: 15. Grouping Requests
Welcome back! At times, you may want to group requests together. Right now you download the song and album artwork as two separate requests, each from two different methods.
What if you wanted for SongDetailView to make a single method call and get back both at once? How would that work? Let’s take a look!
Open the Starter project for this episode and then go to SongDownloader.swift. Add a new method towards the bottom:
func download(songAt songURL: URL, artworkAt artworkURL: URL) async throws -> Data {
}
This method receives two URLs as parameters and returns a Data object. It’s asynchronous so it’s marked async and can throw errors so throw is also in the method declaration. Next, add this code:
typealias Download = (_ url: URL, _ response: URLResponse)
You declare a typealias for a tuple containing a URL object and a URLResponse object. Moving on to the actual requests. Add the following:
async let song: Download = try session.download(from: songURL)
async let artwork: Download = try session.download(from: artworkURL)
Oh my, what is this? You’re declaring asynchronous constants that will execute immediately. The results are stored in the constants themselves and can be awaited afterwards in order to proceed once all requests have completed.
This is exactly what you want in order to group our requests.
These two requests don’t need to be sequential and execute in a specific order, you just want to take advantage of the network and system resources to have them execute as fast as possible, regardless of the order in which they complete and then proceed.
To actually wait for both of these requests to complete you need to add the following:
let (songDownload, artworkDownload) = try await (song, artwork)
This awaits for both requests to complete, marks them with try in case any errors are thrown that need to be handled, and stores the resolts in a couple of constants.
You could also await an array of async properties and store the results in a single constant array. That could work if you want to download, say, many items at once.
For this scenario, it’s more convenient for you to have constants for each of the result tuples; one for the song and one for the artwork.
Add this code next in order to check the responses and their status codes:
guard let songHTTPResponse = songDownload.response as? HTTPURLResponse,
let artworkHTTPResponse = artworkDownload.response as? HTTPURLResponse,
songHTTPResponse.statusCode == 200,
artworkHTTPResponse.statusCode == 200
else {
throw SongDownloadError.invalidResponse
}
This should look familiar as it’s the same code you’ve used a couple of times when performing requests. Moving on to the FileManager side of things, add the following:
let fileManager = FileManager.default
guard let documentsPath = fileManager.urls(for: .documentDirectory,
in: .userDomainMask).first
else {
throw SongDownloadError.documentDirectoryError
}
Again, this is exactly the same code as written in the other download methods except combined to handle both the song and artwork downloads. Continuing, add this code next:
let lastPathComponent = songURL.lastPathComponent
let destinationURL = documentsPath.appendingPathComponent(lastPathComponent)
do {
if fileManager.fileExists(atPath: destinationURL.path) {
try fileManager.removeItem(at: destinationURL)
}
try await fileManager.copyItem(at: song.url, to: destinationURL)
} catch {
throw SongDownloadError.failedToStoreSong
}
With both items downloaded, time to wrap up this method. Add the following code at the bottom of the method:
await MainActor.run {
downloadLocation = destinationURL
}
This will set the downloadLocation property on the main thread. And then add this:
do {
return try Data(contentsOf: artworkDownload.url)
} catch {
throw ArtworkDownloadError.failedToDownloadArtwork
}
This attempts to initialize a Data object with the image’s contents. At this point you’ve done everything that’s necessary in order to handle both the song download and the artwork download. This method is, essentially, a combination of the two separate methods.
You leverage FileManager to store the downloads in a permanent location, set the downloadLocation property for the song, and return a Data object with the artwork. Build your project just to make sure everything is running correctly.
You’ll take care of updating the UI next, but before you do, it’s important to know that all three of the download methods have a lot of duplicate code in them.
You’re not going to worry about that for now as repetition is great when learning and trying to memorize new concepts, but should you be inclined to tidy things up a bit and remove some of the duplication, feel free to add helper methods to put some of the duplicate logic into, or refactor this class as you see fit.
Time to work on the UI side of things next. Open SongDetailView.swift. Comment out the onAppear modifier:
// .onAppear(perform: {
// Task {
// await downloadArtwork()
// }
// })
You used this to immediately download the album artwork when the view was shown, but now you want to use your new method that groups the song and artwork requests. To keep things separate, add a new method:
private func downloadSongTapped() async {
}
Then, add the following code inside:
if downloader.downloadLocation == nil {
guard let artworkURL = URL(string: musicItem.artwork),
let previewURL = musicItem.previewURL
else {
return
}
} else {
playMusic = true
}
First you check whether the song has already been downloaded or not as you don’t want to download it twice, and that’s the main item you want to download from the network.
Next up, you ensure you can construct the URLs for the song and artwork, oherwise you simply return from this method.
You could add some error-handling here in order to notify your users, but for now we skip that in order to focus on the concepts at hand. Finally, should the song already be downloaded, you set playMusic to true.
After the guard statement, add this code next:
isDownloading = true
defer {
isDownloading = false
}
This sets isDownloading to true, as you’ll beging the download next, and then sets it back to false but within a defer statement so this gets done before the method returns. Time use SongDownloader to actually download our resources:
do {
let data = try await downloader.download(songAt: previewURL, artworkAt: artworkURL)
guard let image = UIImage(data: data) else {
return
}
artworkImage = image
} catch {
print(error)
showDownloadFailedAlert = true
}
The new method returns a data object for the artwork image, and sets SongDownloader’s downloadLocation property for the song.
Knowing that, you store the returned data in a constant and try to create an image from it. If you succeed then you set the view’s artworkImage to your new image.
Nothing special needs to be done for the song itself as that’s taken care of via the downloadLocation property of SongDownloader.
Should the download method fail, you catch the error and, for now, print it and set the property that shows your error alert. Finally and to wrap it all up, update the Button action with the following:
await downloadSongTapped()
This simply replaces the method that gets called to perform the downloads. Voila, all done! Build and run the app and check out the results.
Faaantastic :) Your requests are now grouped :D
Note that you’re not worrying about the download progress or fancy error handling here, you’ve seen how to do that already so the focus is on showing you how to group requests.
In the next episode I’ll show you how to simulate different network speeds and conditions. That’s going to come in handy for properly testing the ProgressView you implemented in the previous episode. I’ll see ya in a bit! :)