Discovering URLSession Async API

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

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

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

Unlock now

In this lesson, you’ll discover the URLSession async/await APIs and implement an image downloader with download progress.

URLSession Async/Await APIs

URLSession offers several APIs to retrieve and upload data from and to the internet. These methods cover different scenarios and also provide different levels of abstraction to be used in them. You can use each one based on your current application and/or the feature you’re trying to implement.

Retrieving data from the internet

You already encountered this first method in the previous lesson to retrieve data.

func data(from url: URL) async throws -> (Data, URLResponse)
func data(for request: URLRequest) async throws -> (Data, URLResponse)
let (data, response) = try await URLSession.shared.data(from: Self.newsURL)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.isOK /* 200 */ else {
  throw NewsServiceError.serverResponseError
}
...

Uploading data to the internet

The following two methods are equivalent to the previous ones, just for the upload direction:

func upload(for request: URLRequest, from data: Data) async throws -> (Data, URLResponse)
func data(for request: URLRequest, fromFile url: URL) async throws -> (Data, URLResponse)
var request = URLRequest(url: Self.postURL)
request.httpMethod = "POST"

let (data, response) = try await URLSession.shared.upload(for: request, fromFile: fileURL)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.isCreated /* 201 */ else {
  throw NewsServiceError.serverResponseError
}
...

Downloading files from the internet

This third method is dedicated to file downloading and has peculiarities in the content management that makes it different from the previous ones.

func download(from url: URL) async throws -> (URL, URLResponse)
func download(for request: URLRequest) async throws -> (URL, URLResponse)
func download(resumeFrom resumeData: Data) async throws -> (URL, URLResponse)
let (location, response) = try await URLSession.shared.download(from: Self.downloadURL)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.isOK /* 200 */ else {
  throw NewsServiceError.serverResponseError
}

try FileManager.default.moveItem(at: location, to: finalLocation)

Getting session updates for downloads

Last but not least, this family of APIs allows you to download data from a server as in the first case, though they provide a sort of update mechanism using an AsyncSequence:

func bytes(from url: URL) async throws -> (URLSession.AsyncBytes, URLResponse)
func bytes(for request: URLRequest) async throws -> (URLSession.AsyncBytes, URLResponse)

Adding the Article Image

Enough with the theory. :] Now, it’s time to get your hands dirty and implement some fancy stuff to enrich Apple News with a shining article preview image.

import Foundation
import SwiftUI
import OSLog

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

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

Downloading the Image

It’s time to put the theory into practice and implement the image downloading.

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

    progress = 0
    image = nil

    // 1. Here the execution pauses
    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)

    // 2. The execution pauses and resumes several times (each byte received)
    for try await byte in bytes {
      data.append(byte)
    }

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

Adding a Download Progress Bar

As you can see, when the app downloads the image, it just shows a loading spinner. Wouldn’t it be nice to let the user know the download’s status?

progress = Double(data.count) / Double(length)
ProgressView(value: imageService.progress)

Fixing the Download Progress

The download progress is a great addition that let the user understand what’s happening. Using async/await, you do the download on a background task, so you don’t block the main thread, allowing the user interface to remain fluid.

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
Download course materials from Github
Previous: Introduction Next: Image Downloader Demo