From Chaos to Clarity - Using TaskGroup Instead of DispatchGroup
Coder Orangu is working on a travel planner app and encounters a classic problem:
How to fetch data from multiple sources and update the UI only after all are ready? The app shows flight deals, hotel recommendations and weather forecasts at once.
The challenge?
Each piece of data comes from a different API, and the screen should update only when all responses have arrived.
Coder Orangu also had an old-school solution for this: using bitwise operations to track the completion state of each API call. But in the world of modern Swift, such low-level tricks feel out of place. So he turned to an inbuilt approach: DispatchGroup.
DispatchGroup
Coder Orangu first tried to solve the problem using DispatchGroup, a traditional way to wait for multiple asynchronous tasks to complete.
It involves three basic steps:
Ā Ā Call group.enter() before starting each API fetch.
Ā Ā Call group.leave() when each fetch finishes.
Ā Ā Use group.notify() to update the UI once everything completes.
Hereās how Coder Orangu used it to fetch flight deals, hotel options, and weather data:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func fetchAllService(completion: () -> Void) {
let group = DispatchGroup()
group.enter()
self.fetchFlights { result in
group.leave()
}
group.enter()
self.fetchHotels { result in
group.leave()
}
group.enter()
self.fetchWeather { result in
group.leave()
}
group.notify(queue: .main) {
// All data fetched ā update UI
completion()
}
}
While this approach technically works, Coder Orangu always felt it was too easy to mess up, too hard to trust. Hereās why:
Ā Ā Forgetting a single leave() call causes the group to wait indefinitely, leading to UI freezes.
Ā Ā Debugging becomes tedious because thereās no compiler help to ensure the balance between enter() and leave().
Ā Ā As more APIs were added, the code started looking more like a checklist than a clean Swift function.
Coder Orangu was looking for a cleaner solution, and while going through Swift 5.5 enhancements, something caught his eye: structured concurrency.
More specifically, TaskGroup was giving exactly the feeling he had been longing for in a solution.
TaskGroup
TaskGroup allows you to run multiple asynchronous tasks in parallel, all within a structured, type-safe, and readable block of code. It is the āSwiftyā replacement for DispatchGroup when working with async/await.
Hereās how Coder Orangu changes the flow:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func fetchAllTaskGroupService() async {
await withTaskGroup(of: Void.self) { group in
group.addTask {
await self.fetchFlights()
}
group.addTask {
await self.fetchHotels()
}
group.addTask {
await self.fetchWeather()
}
}
// All data fetched ā update UI
}
The first thing Coder Orangu liked here was how compact and elegant the code becameāalmost like writing what you mean, and nothing more.
Ā Ā No fear of missing a leave()āthe Swift compiler helps ensure correctness.
Ā Ā It embraces Swiftās structured concurrency model.
Ā Ā It offers great flexibility for dynamic concurrency.
How To Implement
š§ Step 1: Create Task Group
1
2
3
await withTaskGroup(of: String.self) { group in
// add child tasks here
}
š§µ Step 2: Add Child Tasks to the Group
1
2
3
4
5
6
7
8
await withTaskGroup(of: String.self) { group in
//Task 1
group.addTask {
}
//Task 2
group.addTask {
}
}
šµļø Step 3: Get the result
1
2
3
4
5
6
7
8
9
10
11
await withTaskGroup(of: String.self) { group in
//Task 1
group.addTask {
}
//Task 2
group.addTask {
}
for await result in group {
print("Fetched: \(result)")
}
}
šµļø Step 4: Bonus Step Coder Orangu realised one important thing: if the user moves away from the screen before the data is fully fetched, the ongoing tasks shouldnāt continue running in the background. Thankfully, TaskGroup supports cancellation propagationābut not directly.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private var fetchTask: Task<Void, Never>? = nil
func loadTripData() {
fetchTask = Task {
await withTaskGroup(of: String.self) { group in
//Task 1
group.addTask {
}
//Task 2
group.addTask {
}
}
}
}
func cancelLoading() {
fetchTask?.cancel()
}
The solution is to wrap the TaskGroup inside a parent Task. When this parent task is cancelled, all child tasks inside the group are also automatically cancelled.
š Bonus: Even Lighter with async let
If you have a limited number of asynchronous tasks to run in parallel, Swift offers an even simpler and more concise option: async let
Hereās how Coder Orangu uses async let to fetch flights, hotels, and weather data simultaneously:
1
2
3
4
5
6
7
8
9
10
func fetchAllAsyncLet() async {
async let flights = self.fetchFlights()
async let hotels = self.fetchHotels()
async let weather = self.fetchWeather()
// Await all tasks and get their results
let (flightsResult, hotelsResult, weatherResult) = await (flights, hotels, weather)
// Update UI here
}
This approach is elegant for a fixed number of tasks, but when you need more flexibility or dynamic concurrency, TaskGroup remains the better choice.
KeyTakeways
Ā Ā DispatchGroup helps manage multiple concurrent tasks but can be error-prone and lacks compiler checks.
Ā Ā For simple and limited concurrent tasks, async let offers the most concise and readable alternative.
Ā Ā Use TaskGroup when you have a dynamic number of tasks. It provides compiler-enforced safetyāno need to manually balance enter()/leave() calls.
Comments powered by Disqus.