Concurrency Demystified

Sep 20 2025 · Swift 6.2, iOS 26, Xcode 26

Lesson 02: Taming Network Calls with Async/Await

Image Downloader Demo

Episode complete

Play next episode

Next
Transcript

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:).

If you’re following along, start Xcode and open the starter project in the 02-taming-network-calls folder.

This project uses the ImageService class to perform the actual download and ArticleImageView to manage the service and display the image.

ArticleImageView has a single parameter, url, that contains the image URL address and has an instance of the ImageService class:

struct ArticleImageView: View {
  let url: URL?

  @State private var imageService = ImageService()

When ArticleImageView is loaded, the .task {} modifier runs, and the closure invokes the ImageService.downloadImage(url:) function to start downloading the image from the URL:

var body: some View {
  innerView()
    .padding()
    .task {
      try? await imageService.downloadImage(url: url)
    }
}

The innerView variable defines a view that displays the image if it is available or a progress view if it isn’t available, i.e., it’s being downloaded.

@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")
    }
  }
}

The @MainActor attribute ensures that the updates of the image variable occur on the main actor: the main thread.

ImageService has two member variables:

class ImageService {
  var progress: Double = 0
  var image: Image?
  ...
}
  • progress indicates the download progress.
  • image contains the downloaded image.

For now, the downloadImage(url:) function just contains some sample code that loads a placeholder symbol after a random time that simulates the delay of the network access:

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")
}

Start by implementing the download process in the function downloadImage(url:).

First of all, you want to check that url contains a valid URL address with the following code:

guard let url else {
  Logger.main.error("URL is nil returning empty image")
  return
}

Once you’ve verified the URL is valid, the real process starts by resetting the member variables:

progress = 0
image = nil

Then, you call the URLSession async method bytes(for:) to start downloading the resource and receive updates during the process:

let (bytes, response) = try await URLSession.shared.bytes(for: URLRequest(url: url))

To parse the response, first check that it contains a valid answer:

guard let httpResponse = response as? HTTPURLResponse, httpResponse.isOK else {
  Logger.main.error("Network response error")
  return
}

And then, you initialize the data structure that will hold the downloaded image:

let length = Int(httpResponse.expectedContentLength)
var data = Data(capacity: length)

Since URLSession.bytes(for:) returns an AsyncSequence, you iterate over the bytes received from the download using a for try await loop and append each byte to the data object:

for try await byte in bytes {
  data.append(byte)
}

As an asynchronous sequence, bytes is a sequence of values produced asynchronously over time. The for try await loop iterates over the elements of an asynchronous sequence. It suspends execution of the loop until the next element of the sequence becomes available.

Finally, once the download is complete, you update the progress and the image property:

  progress = 1
  image = Image(uiImage: UIImage(data: data) ?? UIImage())
}

In this first stage, you don’t update the progress yet; you want to verify that the download process works OK first. You’ll add the progress soon.

Run the project and verify that the download process works correctly.

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.

First, update the progress variable in the for try await loop:

for try await byte in bytes {
  data.append(byte)
  progress = Double(data.count) / Double(length)
}

Then, change the ProgressView() in ArticleImageView by using the imageService.progress value:

@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")
    }
  }
}

Rerun the project to verify the progress updates.

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.

Open ImageService and the following variables before entering the for loop.

var bytesAccumulator = 0
let bytesForUpdate = length / 100

The bytesForUpdate value represents the bytes needed to update the progress by 1%. This value is determined by dividing the total expected content length by 100.

In the async for loop, add the following code:

for try await byte in bytes {
  data.append(byte)
  bytesAccumulator += 1

  if bytesAccumulator > bytesForUpdate {
    progress = Double(data.count) / Double(length)
    bytesAccumulator = 0
  }
}

If the number of accumulated bytes (bytesAccumulator) exceeds the bytesForUpdate value, it means that enough bytes have accumulated to update the progress by 1%. So, a new progress value is computed, and the bytes accumulator is reset to 0 to start a new cycle.

Compared to the previous implementation, this code produces fewer, more significant UI updates, making the download process smooth again.

See forum comments
Cinema mode Download course materials from Github
Previous: Discovering URLSession Async API Next: Conclusion