Swift
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Optionals
-
Section 4: Object-Oriented and Value Types
-
Section 5: Memory Management
-
Section 6: Generics and Error Handling
-
Section 7: Concurrency
-
Section 8: Working with Collections
-
Section 9: Codable and Data Handling
-
Section 10: Protocol-Oriented Programming
-
Section 11: Testing and Tooling
-
Section 12: Practical Projects
-
Section 13: Interview Practice
-
Section 14: More Practice Exercises
-
Section 15: More Standard Library
-
Section 16: Advanced Concurrency
-
Section 17: Foundation Framework Deep Dive
-
Section 18: URLSession and Networking Deep Dive
-
Section 19: Combine Framework
-
Section 20: SwiftUI Fundamentals for Swift Developers
-
Section 21: Server-Side Swift with Vapor
-
Section 22: Swift Package Manager Deep Dive
-
Section 23: Swift Concurrency Deep Dive
-
Section 24: More Language Features
-
Section 25: Error Handling Deep Dive
-
Section 26: Testing Deep Dive
-
Section 27: Data Structures and Algorithms in Swift
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Swift Macros (Swift 5.9+)
-
Section 31: Property Wrappers Ecosystem
-
Section 32: Swift Interop Deep Dive
-
Section 33: iOS App Architecture Patterns
-
Section 34: Performance and Debugging
-
Section 35: App Distribution and CI/CD
-
Section 36: More Practical Projects
-
Section 37: SwiftData and Persistence
-
Section 38: More Design Patterns
-
Section 39: More Review and Practice
-
Section 40: More Foundation Deep Dive
-
Section 41: Advanced Collections in Swift
-
Section 42: Advanced Generics Practice
-
Section 43: UIKit for Legacy and Hybrid Apps
-
Section 44: watchOS and visionOS Development Basics
-
Section 45: More Networking Patterns
-
Section 46: More Testing Practice
-
Section 47: Accessibility in Swift Apps
-
Section 48: Localization
-
Section 49: More Practical Projects Round 2
-
Section 50: Swift Charts Framework
-
Section 51: More Interview and Algorithm Practice
-
Section 52: Final Practice and Mastery
-
Section 53: Swift Compiler and Build System
-
Section 54: More Concurrency Practice
-
Section 55: App Store Guidelines and Review
-
Section 56: More Design and Architecture
34: Task Groups and Structured Concurrency
Imagine you're the head chef in a busy kitchen preparing a massive banquet. You have five different appetizers that need to be prepped. You could do them one by one—chop the onions, then mince the garlic, then slice the peppers—but you'd be there all night. Instead, you call over five junior cooks. You give each one a specific ingredient to prep and tell them, "Let me know as soon as your pile is ready."
You aren't just throwing tasks into the void; you're managing a coordinated effort. If the banquet is suddenly canceled, you don't let the junior cooks keep chopping; you tell everyone to stop immediately. This is exactly how Task Groups work in Swift. They provide a way to launch a dynamic number of concurrent operations while keeping them "structured," meaning the parent task stays in control of its children.
Orchestrating Dynamic Work with withTaskGroup
You've already seen async let, which is great when you know exactly how many things you're waiting for (like "fetch user profile" AND "fetch user posts"). But what happens when you have an array of ten different cities and you need to fetch the current weather for all of them? You can't write async let ten times.
That's where withTaskGroup (or withThrowingTaskGroup if the work can fail) comes in. It creates a scope where you can spawn as many child tasks as you need.
struct WeatherReport {
let city: String
let temperature: Double
}
func fetchAllWeather(for cities: [String]) async throws -> [WeatherReport] {
// We use withThrowingTaskGroup because network calls can fail
try await withThrowingTaskGroup(of: WeatherReport.self) { group in
var reports = [WeatherReport]()
for city in cities {
// We "spawn" a child task for each city
group.addTask {
// This runs concurrently with other tasks in the group
return try await self.fetchWeather(for: city)
}
}
// Now we collect the results as they finish
for try await report in group {
reports.append(report)
}
return reports
}
}
// Mock function for the example
func fetchWeather(for city: String) async throws -> WeatherReport {
try await Task.sleep(nanoseconds: 1_000_000_000) // Simulate network lag
return WeatherReport(city: city, temperature: Double.random(in: 15...30))
}
Collecting Results in Any Order
Notice the for try await report in group line. This is where the magic happens. Unlike async let, where you wait for specific variables, a Task Group acts like an asynchronous sequence. The results arrive in the order they finish, not the order you started them. If the weather for Tokyo returns faster than the weather for London, Tokyo hits your loop first.
I've seen developers try to use a shared array inside the addTask block to collect results. Don't do that. You'll run straight into data races because multiple child tasks would be trying to mutate that array at the same time. The pattern I used above—returning a value from the child task and collecting it in the parent loop—is the safe, Swift-native way to do it.
The Safety Net of Structured Concurrency
The reason we call this "structured" concurrency is because of the hierarchy. The withTaskGroup block creates a boundary. The function fetchAllWeather cannot return until every single child task inside that group has either finished or been cancelled.
Here is the real kicker: if one of your child tasks throws an error and you don't catch it inside addTask, the entire group is notified. Swift can automatically cancel the remaining tasks in the group because it knows the overall operation has already failed. It's like the head chef seeing a fire in one pan and telling the whole kitchen to stop everything immediately. It prevents "zombie tasks" from eating up your CPU and battery in the background.
📋 Practical Task
Build a Concurrent Product Price Aggregator
You are building a price comparison tool. You have a list of "Store" objects, and each store has a method to fetch the current price of a specific product. Because some stores have slow APIs, you need to fetch these prices concurrently using a Task Group.
Requirements:
- Create a
ProductPricestruct withstoreName: Stringandprice: Double. - Implement a function
aggregatePrices(for productID: String, stores: [Store]) async throws -> [ProductPrice]. - Use
withThrowingTaskGroupto fetch the price from each store simultaneously. - Ensure the function returns an array of all fetched prices.
- Handle the scenario where a store's API might throw an error; the aggregator should still try to collect prices from the other stores (Hint: wrap the
await store.fetchPrice()call in ado-catchblock insideaddTaskand returnnilor a default value for failed fetches).
There are no comments for now.