Concurrency Demystified

Jun 20 2024 · Swift 5.10, iOS 17, Xcode 15.3

Lesson 02: Taming Network Calls with Async/Await

Image Downloader Demo

Episode complete

Play next episode

Next

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

In this demo, you’ll review the implementation of the image downloader you saw in the lesson.

Downloading Images With URLSession bytes(for:)

In this demo, you’ll take a quick look at the implementation of an image downloader with a progress indicator using URLSession’s bytes(for:).

struct ArticleImageView: View {
  let url: URL?

  @State private var imageService = ImageService()
var body: some View {
  innerView()
    .padding()
    .task {
      try? await imageService.downloadImage(url: url)
    }
}
@MainActor
@ViewBuilder
private func innerView() -> some View {
  if let image = imageService.image {
    image
      .resizable()
      .aspectRatio(contentMode: .fit)
      .background(.clear)
      .mask(RoundedRectangle(cornerRadius: 8))
  } else {
    if imageService.progress < 1 {
      ProgressView()
    } else {
      Image(systemName: "photo")
    }
  }
}
class ImageService {
  var progress: Double = 0
  var image: Image?
  ...
}
func downloadImage(url: URL?) async throws {
  progress = 0
  image = nil

  // Simulate image download
  try await Task.sleep(for: .seconds(Int.random(in: 2..<4)))

  progress = 1
  image = Image(systemName: "photo")
}
guard let url else {
  Logger.main.error("URL is nil returning empty image")
  return
}
progress = 0
image = nil
let (bytes, response) = try await URLSession.shared.bytes(for: URLRequest(url: url))
guard let httpResponse = response as? HTTPURLResponse, httpResponse.isOK else {
  Logger.main.error("Network response error")
  return
}
let length = Int(httpResponse.expectedContentLength)
var data = Data(capacity: length)
for try await byte in bytes {
  data.append(byte)
}
  progress = 1
  image = Image(uiImage: UIImage(data: data) ?? UIImage())
}

Adding the Download Progress

Now that you’ve verified that the image downloaded fine, you’ll add the download progress indicator. This is a two-step process.

for try await byte in bytes {
  data.append(byte)
  progress = Double(data.count) / Double(length)
}
@MainActor
@ViewBuilder
private func innerView() -> some View {
  if let image = imageService.image {
    image
      .resizable()
      .aspectRatio(contentMode: .fit)
      .background(.clear)
      .mask(RoundedRectangle(cornerRadius: 8))
  } else {
    if imageService.progress < 1 {
      ProgressView(value: imageService.progress)
    } else {
      Image(systemName: "photo")
    }
  }
}

Fixing the Download Progress

Reduce the rate of the UI update to fix the performance issue with the download progress. You’ll update the UI just on changes in the percent value.

var bytesAccumulator = 0
let bytesForUpdate = length / 100
for try await byte in bytes {
  data.append(byte)
  bytesAccumulator += 1

  if bytesAccumulator > bytesForUpdate {
    progress = Double(data.count) / Double(length)
    bytesAccumulator = 0
  }
}
See forum comments
Cinema mode Download course materials from Github
Previous: Discovering URLSession Async API Next: Conclusion