Home From Chaos to Clarity - Using TaskGroup Instead of DispatchGroup
Post
Cancel

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.

This post is licensed under CC BY 4.0 by the author.

Simplify Strategy (Behavioural Pattern)

-

Comments powered by Disqus.