How to handle multiple async tasks in Swift

Mar 23, 2024#swift

In Swift, asynchronous tasks are operations that don’t block the main thread of execution. They allow your program to continue executing other tasks while waiting for a particular operation to complete, such as network requests, file I/O, or any other time-consuming operation that might cause your application to freeze if executed synchronously.

Swift offers a few ways to handle multiple asynchronous tasks, depending on whether you need them to run concurrently or wait for their results.

Using TaskGroup

Task groups are a powerful feature introduced in Swift 5.5 that allow you to combine multiple parallel tasks and wait for their results. They’re commonly used for scenarios like combining multiple API request responses into a single response object.

// Assume we have a function that lists photo URLs from a gallery
func listPhotoURLs(inGallery galleryName: String) async -> [URL] {
    // Implementation details...
    // For demonstration purposes, let's return some dummy URLs.
    return [
        URL(string: "https://example.com/photo1.jpg")!,
        URL(string: "https://example.com/photo2.jpg")!,
        // Add more URLs as needed...
    ]
}

// Assume we have a function to download a photo from a given URL
func downloadPhoto(url: URL) async -> UIImage {
    // Implementation details...
    // For demonstration purposes, let's return a placeholder image.
    return UIImage(named: "placeholder")!
}

// Create a task group for downloading photos
let photoGroup = TaskGroup<UIImage, Error>()

// Fetch photo URLs and download them concurrently
await withTaskGroup(of: UIImage.self) { taskGroup in
    let photoURLs = await listPhotoURLs(inGallery: "Amsterdam Holiday")
    
    for photoURL in photoURLs {
        taskGroup.addTask { await downloadPhoto(url: photoURL) }
    }
}

// Now we have all the downloaded images in the `photoGroup.results` array
let downloadedImages = photoGroup.results

// You can use the downloaded images as needed!

Using async let

This approach is useful when you need the results of multiple asynchronous tasks before proceeding further. You can launch them simultaneously using separate async let bindings and then await their results.

struct UserData {
    let name: String
    let friends: [String]
    let highScores: [Int]
}

func printUserDetails() async {
    async let username = getUser()
    async let scores = getHighScores()
    async let friends = getFriends()

    let user = await UserData(name: username, friends: friends, highScores: scores)
    print("Hello, my name is \(user.name), and I have \(user.friends.count) friends!")
}

// Call the function
await printUserDetails()

Using DispatchGroup

If you’re using an older version of Swift that doesn’t support async/await, you can use DispatchGroup to manage asynchronous tasks. It allows you to track the completion of tasks and execute a block of code once all tasks are finished.

func downloadFiles(urls: [URL], completion: @escaping ([Data]) -> Void) {
  let dispatchGroup = DispatchGroup()
  var results: [Data] = []
  
  for url in urls {
    dispatchGroup.enter()
    URLSession.shared.dataTask(with: url) { data, _, error in
      if let error = error {
        print(error.localizedDescription)
      } else if let data = data {
        results.append(data)
      }
      dispatchGroup.leave()
    }.resume()
  }
  
  dispatchGroup.notify(queue: .main) {
    completion(results)
  }
}