Modern Concurrency: Getting Started

Oct 18 2022 · Swift 5.5, iOS 15, Xcode 13.4

Part 2: Asynchronous Sequences

16. Concurrent Downloads

Episode complete

Play next episode

Next
About this episode

Leave a rating/review

See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 15. Using Combine Next episode: 17. Conclusion

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

Transcript: 16. Concurrent Downloads

Make sure the course server is running and continue with your project from the previous episode or open the starter project for this episode.

In this episode, you’ll implement the premium download plan Cloud 9. It downloads files super fast by running 4 concurrent tasks; each task downloads about 1/4 of the file. You’ll start by finishing the private version of downloadWithProgress in SuperStorageModel.

Open SuperStorageModel and locate the if let offset code you commented out in episode 13.

offset is nil for the Silver and Gold plans, so you didn’t need this if closure. For the Cloud 9 plan, you’ll pass in an actual offset value to create 4 tasks.

To start, uncomment these lines:

if let offset = offset {
  // Add code for Cloud 9 plan
}
else {
  result = try await URLSession.shared.bytes(from: url)
  guard (result.response as? HTTPURLResponse)?.statusCode == 200 else {
    throw "The server responded with an error."
  }
}

In the closure, create a URLRequest:

if let offset = offset {
  🟩
  let urlRequest = URLRequest(url: url, offset: offset, length: size)
  🟥
}

You probably haven’t seen this initializer before.

Command-click URLRequest and select Jump to Definition…. Select the second option to open Utility.swift.

This initializer is in a custom extension of URLRequest.

extension URLRequest {
  init(url: URL, offset: Int, length: Int) {
    self.init(url: url)
    addValue("bytes=\(offset)-\(offset + length - 1)", forHTTPHeaderField: "Range")
  }
}

The course server you’re running supports a capability called partial requests.

You can ask for a byte range of the response, instead of the entire response at once.

You do thi0 by setting the Range field in the HTTP header.

Here’s an example of offset, length and range values for a 77,345-byte file broken into 20,000-byte chunks:

0-19999 [offset 0, length 20000]
20000-39999 [offset 20000, length 20000]
40000-59999 [offset 40000, length 20000]
60000-77344 [offset 0, length 17345]

Three 20,000-byte chunks, and the remaining bytes in the last chunk.

When offset is 40,000 and length is 20,000, the range value is “bytes=40000-59999”

Don’t worry, the starter project code already handles all the calculations to create these chunks.

Now, click back to SuperStorageModel to send the request and store its result:

if let offset = offset {
  let urlRequest = URLRequest(url: url, offset: offset, length: size)
  🟩result = try await URLSession.shared.bytes(for: urlRequest)🟥
}

In episode 5, you used the from: url version of bytes with a custom LiveURLSession to get a never-ending sequence of stock prices.

Here, you use the for: URLRequest version of bytes.

You don’t need this session to stay open, so shared is fine.

This got rid of the error message about uninitialized result.

To finish this closure, check the status code almost as usual:

if let offset = offset {
  let urlRequest = URLRequest(url: url, offset: offset, length: size)
  result = try await URLSession.shared.bytes(for: urlRequest)
  🟩
  guard (result.response as? HTTPURLResponse)?.statusCode == 206 else {
    throw "The server responded with an error."
  }
  🟥
}

Status code 206 indicates a successful partial response.

Now, jump down to multiDownloadWithProgress(file:)

func multiDownloadWithProgress(file: DownloadFile) async throws -> Data {
  func partInfo(index: Int, of count: Int) -> (offset: Int, size: Int, name: String) {
    let standardPartSize = Int((Double(file.size) / Double(count)).rounded(.up))
    let partOffset = index * standardPartSize
    let partSize = min(standardPartSize, file.size - partOffset)
    let partName = "\(file.name) (part \(index + 1))"
    return (offset: partOffset, size: partSize, name: partName)
  }
  let total = 4
  let parts = (0..<total).map { partInfo(index: $0, of: total) }
  
  // Add code here, replacing placeholder return statement
  return Data()
}

This method already includes the code to break a file download into 4 parts.

This partInfo helper function calculates the offset, size and name values for each part and stores them in the parts array.

Jump back to the private downloadWithProgress signature.

name, size and offset are precisely the parameters you need to pass to this method:

downloadWithProgress(fileName: String, name: String, size: Int, offset: Int? = nil)

Jump back to multiDownloadWithProgress. Start replacing this dummy return statement with calls to downloadWithProgress(...).

Call multiDownloadWithProgress(file:)

Start with part0: Define a promise with async let:

async let part0 =
  downloadWithProgress(fileName: file.name, name: parts[0].name, size: parts[0].size, offset: parts[0].offset)

Then duplicate-and-edit this line:

async let part0 =
downloadWithProgress(fileName: file.name, name: parts[0].name, size: parts[0].size, offset: parts[0].offset)
🟩 
async let part1 =
downloadWithProgress(fileName: file.name, name: parts[1].name, size: parts[1].size, offset: parts[1].offset)
async let part2 =
downloadWithProgress(fileName: file.name, name: parts[2].name, size: parts[2].size, offset: parts[2].offset)
async let part3 =
downloadWithProgress(fileName: file.name, name: parts[3].name, size: parts[3].size, offset: parts[3].offset)
🟥 

Next, await an array of these 4 downloads:

try await [part0, part1, part2, part3]

This executes them concurrently.

Now, combine their data:

try await [part0, part1, part2, part3]
  🟩.reduce(Data(), +)🟥

The default initializer is the initial value for summing over the array items.

And now, return the complete file content instead of the dummy placeholder:

🟩return 🟥try await [part0, part1, part2, part3]
  .reduce(Data(), +)
❌return Data()

Finally, you just need to call multiDownloadWithProgress(file:) in DownloadView.

Find the code for downloadSingleAction and copy-paste-edit it into downloadMultipleAction:

isDownloadActive = true
Task {  // delete downloadTask =
  do {
    fileData = try await model.🟩multiDownloadWithProgress🟥(file: file) 
  } catch { }
  isDownloadActive = false
  // delete timerTask...
}

You don’t need to store the Task in downloadTask: The course server doesn’t slow down partial responses, so files download faster than you can move the cursor to Cancel Now.

Build and run, select a tiff file and tap Cloud 9.

How awesome is that?