Skip to main content

How to handle different result types in a task group

About 4 minSwiftArticle(s)bloghackingwithswift.comcrashcourseswiftxcodeappstore

How to handle different result types in a task group 관련

Swift Concurrency by Example

Back to Home

How to handle different result types in a task group | Swift Concurrency by Example

How to handle different result types in a task group

Updated for Xcode 15

Each task in a Swift task group must return the same type of data as all the other tasks in the group, which is often problematic – what if you need one task group to handle several different types of data?

In this situation you should consider using async let for your concurrency if you can, because every async let expression can return its own unique data type. So, the first might result in an array of strings, the second in an integer, and so on, and once you’ve awaited them all you can use them however you please.

However, if you need to use task groups – for example if you need to create your tasks in a loop – then there is a solution: create an enum with associated values that wrap the underlying data you want to return. Using this approach, each of the tasks in your group still return a single data type – one of the cases from your enum – but inside those cases you can place the unique data types you’re actually using.

This is best demonstrated with some example code, but because it’s quite a lot I’m going to add inline comments so you can see what’s going on:

// A struct we can decode from JSON, storing one message from a contact.
struct Message: Decodable {
    let id: Int
    let from: String
    let message: String
}

// A user, containing their name, favorites list, and messages array.
struct User {
    let username: String
    let favorites: Set<Int>
    let messages: [Message]
}

// A single enum we'll be using for our tasks, each containing a different associated value.
enum FetchResult {
    case username(String)
    case favorites(Set<Int>)
    case messages([Message])
}

func loadUser() async {
    // Each of our tasks will return one FetchResult, and the whole group will send back a User.
    let user = await withThrowingTaskGroup(of: FetchResult.self) { group -> User in
        // Fetch our username string
        group.addTask {
            let url = URL(string: "https://hws.dev/username.json")!
            let (data, _) = try await URLSession.shared.data(from: url)
            let result = String(decoding: data, as: UTF8.self)

            // Send back FetchResult.username, placing the string inside.
            return .username(result)
        }

        // Fetch our favorites set
        group.addTask {
            let url = URL(string: "https://hws.dev/user-favorites.json")!
            let (data, _) = try await URLSession.shared.data(from: url)
            let result = try JSONDecoder().decode(Set<Int>.self, from: data)

            // Send back FetchResult.favorites, placing the set inside.
            return .favorites(result)
        }

        // Fetch our messages array
        group.addTask {
            let url = URL(string: "https://hws.dev/user-messages.json")!
            let (data, _) = try await URLSession.shared.data(from: url)
            let result = try JSONDecoder().decode([Message].self, from: data)

            // Send back FetchResult.messages, placing the message array inside
            return .messages(result)
        }

        // At this point we've started all our tasks,
        // so now we need to stitch them together into
        // a single User instance. First, we set
        // up some default values:
        var username = "Anonymous"
        var favorites = Set<Int>()
        var messages = [Message]()

        // Now we read out each value, figure out 
        // which case it represents, and copy its
        // associated value into the right variable.
        do {
            for try await value in group {
                switch value {
                case .username(let value):
                    username = value
                case .favorites(let value):
                    favorites = value
                case .messages(let value):
                    messages = value
                }
            }
        } catch {
            // If any of the fetches went wrong, we might
            // at least have partial data we can send back.
            print("Fetch at least partially failed; sending back what we have so far. \(error.localizedDescription)")
        }

        // Send back our user, either filled with
        // default values or using the data we
        // fetched from the server.
        return User(username: username, favorites: favorites, messages: messages)
    }

    // Now do something with the finished user data.
    print("User \(user.username) has \(user.messages.count) messages and \(user.favorites.count) favorites.")
}

await loadUser()

Download this as an Xcode projectopen in new window

I know it’s a lot of code, but really it boils down to two things:

  1. Creating an enum with one case for each type of data you’re expecting, with each case having an associated value of that type.
  2. Reading the results from your group’s tasks using a switch block that reads each case from your enum, extracts the associated value inside, and acts on it appropriately.

So, it’s not impossible to handle heterogeneous results in a task group, it just requires a little extra thinking.

Similar solutions…
How to cancel a task group | Swift Concurrency by Example

How to cancel a task group
How to create a task group and add tasks to it | Swift Concurrency by Example

How to create a task group and add tasks to it
How to get a Result from a task | Swift Concurrency by Example

How to get a Result from a task
What’s the difference between a task and a detached task? | Swift Concurrency by Example

What’s the difference between a task and a detached task?
How to create and use task local values | Swift Concurrency by Example

How to create and use task local values

이찬희 (MarkiiimarK)
Never Stop Learning.