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()
Download this as an Xcode project
I know it’s trivial, but it demonstrates several important things:
String.self
so that each child task can return a string.group -> String in
– Swift finds it hard to figure out the return value otherwise.addTask()
once for each task we want to add to the group, passing in the work we want that task to do.AsyncSequence
, so we can read all the values from their children using for await
, or by calling group.next()
repeatedly.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")
}
}
}
Download this as an Xcode project
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:
withThrowingTaskGroup()
to create the group.addTask()
repeatedly.AsyncSequence
, we can call its reduce()
method to boil all its task results down to a single value, which in this case is a single array of news stories.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:
waitForAll()
will automatically wait for tasks you have not explicitly awaited, discarding any results they return.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.
SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.