Modern Concurrency: Getting Started

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

Part 2: Asynchronous Sequences

13. Downloading Chunks

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: 12. Displaying a Progress View Next episode: 14. Canceling Tasks

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.

Heads up... You’re accessing parts of this content for free, with some sections shown as obfuscated text.

Heads up... You’re accessing parts of this content for free, with some sections shown as obfuscated text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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

Downloading chunks

You’ve coded the Silver download option, which fetches the complete file in one go and presents an onscreen preview.

AsyncSequence

You’ll do this by reading the file as an asynchronous sequence of bytes from the server. This is similar to what you did in episodes 4 and 5, but now you’ll implement some helper methods to work with chunks, or batches, of bytes. This lets you update the progress bar one chunk at a time, as you’re receiving the file’s contents.

return try await downloadWithProgress(fileName: file.name, name: file.name, size: file.size)
private func downloadWithProgress(fileName: String, name: String, size: Int, offset: Int? = nil) async throws -> Data {
  guard let url = URL(string: "http://localhost:8080/files/download?\(fileName)") else {
    throw "Could not create the URL."
  }
  await addDownload(name: name)

  let result: (downloadStream: URLSession.AsyncBytes, response: URLResponse)
  if let offset = offset {
    // 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."
    }
  }

  // Add code here, replacing placeholder return statement
  return Data()
}
else {
  result = try await URLSession.shared.bytes(from: url)
  guard (result.response as? HTTPURLResponse)?.statusCode == 200 else {
    throw "The server responded with an error."
  }
}
var asyncDownloadIterator = result.downloadStream.makeAsyncIterator()
//    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."
      }
//    }

ByteAccumulator

You won’t update the progress bar after every byte. Instead, you’ll process a batch of bytes at a time and update the progress bar after each batch.

let accumulator = ByteAccumulator(name: name, size: size)
while !stopDownloads,  // you'll set this in the next episode
 !accumulator.checkCompleted() {  // accumulator can still collect more bytes

}
while !accumulator.isBatchCompleted,  // nested while runs until this batch is full
  let byte = try await asyncDownloadIterator.next() {  // or the byte sequence completes
  accumulator.append(byte)
}

Updating the progress bar

After a batch completes, it’s time to update the download progress bar.

await updateDownload(name: name, progress: accumulator.progress)
Task.detached(priority: .medium) {
  await self.updateDownload(name: name, progress: accumulator.progress)  
  // need self. in a closure
}
let progress = accumulator.progress
let progress = accumulator.progress
Task.detached(priority: .medium) {
  await self.updateDownload(name: name, progress: progress)  // delete accumulator.
}
let progress = accumulator.progress
Task.detached(priority: .medium) {
  await self.updateDownload(name: name, progress: progress)
}
🟩print(accumulator.description)

Returning accumulated result

And finally, replace the dummy return value:

return accumulator.data
isDownloadActive = true
Task {
  do {
    fileData = try await model.download🟩WithProgress🟥(file: file)
  } catch { }
  isDownloadActive = false
}