How to create a task group and add tasks to it
How to create a task group and add tasks to it êŽë š
Updated for Xcode 15
Swiftâs task groups are collections of tasks that work together to produce a single result. Each task inside the group must return the same kind of data, but if you use enum associated values you can make them send back different kinds of data â itâs a little clumsy, but it works.
Creating a task group is done in a very precise way to avoid us creating problems for ourselves: rather than creating a TaskGroup
instance directly, we do so by calling the withTaskGroup(of:)
function and telling it the data type the task group will return. We give this function the code for our group to execute, and Swift will pass in the TaskGroup
that was created, which we can then use to add tasks to the group.
First, I want to look at the simplest possible example of task groups, which is returning 5 constant strings, adding them into a single array, then joining that array into a string:
func printMessage() async {
let string = await withTaskGroup(of: String.self) { group -> String in
group.addTask { "Hello" }
group.addTask { "From" }
group.addTask { "A" }
group.addTask { "Task" }
group.addTask { "Group" }
var collected = [String]()
for await value in group {
collected.append(value)
}
return collected.joined(separator: " ")
}
print(string)
}
await printMessage()
I know itâs trivial, but it demonstrates several important things:
- We must specify the exact type of data our task group will return, which in our case is
String.self
so that each child task can return a string. - We need to specify exactly what the return value of the group will be using
group - > String in
â Swift finds it hard to figure out the return value otherwise. - We call
addTask()
once for each task we want to add to the group, passing in the work we want that task to do. - Task groups conform to
AsyncSequence
, so we can read all the values from their children usingfor await
, or by callinggroup.next()
repeatedly. - Because the whole task group executes asynchronously, we must call it using
await
.
However, thereâs one other thing you canât see in that code sample, which is that our task results are sent back in completion order and not creation order. That is, our code above might send back âHello From A Task Groupâ, but it also might send back âTask From A Hello Groupâ, âGroup Task A Hello Fromâ, or any other possible variation â the return value could be different every time.
Tasks created using withTaskGroup()
cannot throw errors. If you want them to be able to throw errors that bubble upwards â i.e., that are handled outside the task group â you should use withThrowingTaskGroup()
instead. To demonstrate this, and also to demonstrate a more real-world example of TaskGroup
in action, we could write some code that fetches several news feeds and combines them into one list:
struct NewsStory: Identifiable, Decodable {
let id: Int
let title: String
let strap: String
let url: URL
}
struct ContentView: View {
@State private var stories = [NewsStory]()
var body: some View {
NavigationView {
List(stories) { story in
VStack(alignment: .leading) {
Text(story.title)
.font(.headline)
Text(story.strap)
}
}
.navigationTitle("Latest News")
}
.task {
await loadStories()
}
}
func loadStories() async {
do {
stories = try await withThrowingTaskGroup(of: [NewsStory].self) { group -> [NewsStory] in
for i in 1...5 {
group.addTask {
let url = URL(string: "https://hws.dev/news-\(i).json")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([NewsStory].self, from: data)
}
}
let allStories = try await group.reduce(into: [NewsStory]()) { $0 += $1 }
return allStories.sorted { $0.id > $1.id }
}
} catch {
print("Failed to load stories")
}
}
}
In that code you can see we have a simple struct that contains one news story, a SwiftUI view showing all the news stories we fetched, plus a loadStories()
method that handles fetching and decoding several news feeds into a single array.
There are four things in there that deserve special attention:
- Fetching and decoding news items might throw errors, and those errors are not handled inside the tasks, so we need to use
withThrowingTaskGroup()
to create the group. - One of the main advantages of task groups is being able to add tasks inside a loop â we can loop from 1 through 5 and call
addTask()
repeatedly. - Because the task group conforms to
AsyncSequence
, we can call itsreduce()
method to boil all its task results down to a single value, which in this case is a single array of news stories. - As I said earlier, tasks in a group can complete in any order, so we sorted the resulting array of news stories to get them all in a sensible order.
Regardless of whether youâre using throwing or non-throwing tasks, all tasks in a group must complete before the group returns. You have three options here:
- Awaiting all individual tasks in the group.
- Calling
waitForAll()
will automatically wait for tasks you have not explicitly awaited, discarding any results they return. - If you do not explicitly await any child tasks, they will be implicitly awaited â Swift will wait for them anyway, even if you arenât using their return values.
Of the three, I find myself using the first most often because itâs the most explicit â you arenât leaving folks wondering why some or all of your tasks are launched then ignored.