Don't make this mistake with a TaskGroup


You’re more of a video kind of person? I’ve got you covered! Here’s a video with the same content than this article 🍿


Advertisement

Authentication is critical, but it shouldn’t slow you down.

Clerk’s iOS SDK gives you secure sign-in, sessions, and user profiles out of the box, so you can focus on shipping great features

👉 Try Clerk for iOS 👈


Sponsors like Clerk really help me grow my content creation, so if you have time please make sure to check out their survey: it’s a direct support to my content creation ☺️


When you use a TaskGroup for the first time, there’s one mistake that’s very easy to make.

Luckily, it’s usually quite easy to notice that the code isn’t behaving as we would expect.

But it can be tricky to understand what’s actually causing the problem.

Let’s have a look at this piece of code and take it step-by-step:

I’ve implemented an asynchronous function that fetches some data.

And I’m making the function sleep for a random amount of time before returning its result.

This way, the function simulates the behavior of a network call that would also take an unpredictable amount of time before returning its result:

Then, I create a TaskGroup and in this TaskGroup I add several tasks, each of them making a call to fetchData() and returning the result:

Finally, I collect all the results in an Array and I return that Array:

At first glance everything looks good!

But if we print the Array to the console, we’ll see that the order of its values isn’t what we would have expected:

The order seems to be completely random, and it actually is!

What’s happening, and what’s key to understand, is that when we collect the results, we collect them in the order in which the tasks have returned them, and not in the order in which the tasks had been created!

So what do we do if we want to have the results in the same order that the tasks were created?

The most versatile approach is to slightly tweak the tasks that we create, so that instead of just returning the result of fetchData(), they now return a tuple with both the argument passed to fetchData() and its result:

And from there, once we’ve collected all the results, we can now use this extra piece of information to order them correctly:

Of course, depending on the situation, you can adapt this approach and add to the tuple any other value needed to correctly order the results 👌

That’s all for this article, here’s the code if you want to experiment with it!

import Foundation

func fetchData(id: Int) async -> String {
    try! await Task.sleep(for: .seconds(Int.random(in: 0..<5)))
    
    return "Result for \(id)"
}

let results = await withTaskGroup { taskGroup in
    for i in 0...5 {
        taskGroup.addTask {
            let result = await fetchData(id: i)
            return (i, result)        
        }
    }
    
    var results = [Int: String]()
    for await (index, result) in taskGroup {
        results[index] = result
    }
    return results
}

// ["Result for 0", "Result for 1", "Result for 2", "Result for 3", "Result for 4", "Result for 5"]
print(results
    .sorted(by: { $0.key < $1.key })
    .map { $0.value }
)
Next
Next

Why a custom ViewModifier is often useless