Zhivko Manchev

Senior iOS Engineer

iOS Team Lead

Swift & SwiftUI Expert

Article

Swift Concurrency: Parallel Tasks

February 1, 2025 Swift-Concurrency
Swift Concurrency Series
Intro to Parallel Resource Fetching

In the previous chapter, each resource (image, invites count, and friends list) was loaded one after the other. While this sequential approach is simple and readable, it can be inefficient when resources are independent. In this chapter, we’ll see how Swift Concurrency lets us fetch multiple resources in parallel, reducing total execution time while keeping the code clean and easy to understand.

Example 1. Updating multiple resources in parallel

This example updates multiple related resources for a single person, but fetches them in parallel instead of sequentially. Each tab shows the same operation implemented with GCD and Swift Concurrency, allowing you to compare the syntax and structure of parallel execution in both approaches.

func updatePersonInfoParallel(person: Person) {
    let dispatchGroup = DispatchGroup()
    var personDetailsTemp = PersonDetails()
    
    dispatchGroup.enter()
    person.loadImage { personImage in
        personDetailsTemp.personImage = personImage
        dispatchGroup.leave()
    }
    
    dispatchGroup.enter()
    person.loadInvitesCount { invitesCount in
        personDetailsTemp.invitesCount = invitesCount
        dispatchGroup.leave()
    }
    
    dispatchGroup.enter()
    person.loadFriends { friends in
        personDetailsTemp.friends = friends
        dispatchGroup.leave()
    }
    
    dispatchGroup.notify(queue: .main) { [weak self] in
        self? .personDetails = personDetailsTemp
    }
}
func updatePersonInfoParallel(person: Person) async {
    async let personImage = try? person.loadImage()
    async let invitesCount = try? person.loadInvitesCount()
    async let friends = try? person.loadFriends()

    let (image, count, friendsList) = await (personImage, invitesCount, friends)

    personDetails = .init(
        personImage: image,
        invitesCount: count,
        friends: friendsList
    )
}
Example overview

The same operation is performed in parallel using Grand Central Dispatch with a DispatchGroup and using Swift Concurrency. While the performance of both approaches is comparable, Swift Concurrency offers much clearer, linear syntax, eliminating the boilerplate and callback management required by GCD. This example demonstrates how async/await can make parallel code easier to read and maintain, without sacrificing efficiency.

Example 2. Token refresh

This example retrieves an authenticated user, refreshing the token if it has expired before continuing the request. The same control flow is implemented using GCD and Swift Concurrency to highlight how each approach handles branching logic and asynchronous dependencies.

func getAuthenticatedUser(token: Token, completion: (Result<Person, Error>) -> Void) {
    if !token.isValid() {
        TokenManager.shared.renewToken { result in
            switch result {
            case .success(let newToken):
                // Call same function again, with new token
                getAuthenticatedUser(token: newToken, completion: completion)
            case .failure(let tokenError):
                // Token refresh failed, call closure with error
                completion(.failure(tokenError))
            }
        }

        // Return from this execution because we entered a closure
        return
    }
    
    AuthManager.shared.getAuthenticatedUser(token: token) { result in
        switch result {
        case .success(let user):
            // Successfully retrieved user
            completion(.success(user))
        case .failure(let userError):
            // Error retrieving user
            completion(.failure(userError))
        }
    }
}
func getAuthenticatedUser(token: Token) async throws -> Person {
    var token = token
    do {
        if !token.isValid() {
            token = try await TokenManager.shared.renewToken()
        }
        
        return try await AuthManager.shared.getAuthenticatedUser(token: token)
    }
}
Example overview

This example shows the same operation implemented in two ways: using Grand Central Dispatch with nested callbacks, and using Swift Concurrency with async/await.

The GCD version requires managing multiple closures and has to include explanatory comments to help follow the flow, making it harder to read and understand. In contrast, the Swift Concurrency version expresses the same logic in linear, easy-to-follow syntax, eliminating the callback nesting and boilerplate while keeping the behavior identical.

Example 3. Fetching an array of data in parallel

This example fetches a collection of images for multiple people at the same time. Each implementation runs the work in parallel, allowing you to compare how GCD and Swift Concurrency handle multiple concurrent tasks and result aggregation.

func getAllPhotos(completion: @escaping ([UUID: UIImage?]) -> Void) {
    let dispatchGroup = DispatchGroup()
    var results: [UUID: UIImage?] = [:]

    for person in people {
        dispatchGroup.enter()
        DispatchQueue.global().async {
            person.loadImage() { image in
                results[person.id] = image
                dispatchGroup.leave()
            }
        }
    }

    dispatchGroup.notify(queue: .main) {
        completion(results)
    }
}
func getAllPhotos() async -> [UUID: UIImage?] {
    await withTaskGroup(of: (UUID, UIImage?).self) { group in
        var results: [UUID: UIImage?] = [:]
        
        for person in people {
            group.addTask {
                await (person.id, try? person.loadImage())
            }
        }
        
        for await (id, image) in group {
            results[id] = image
        }
        
        return results
    }
}
Example overview

This example fetches multiple images in parallel. The GCD version uses a DispatchGroup with nested closures, which requires careful handling of shared state and boilerplate. Swift Concurrency accomplishes the same work using a TaskGroup, allowing each task to run concurrently with linear, readable syntax.

Support This Content

If you found this helpful, you can buy me a coffee here.

Tags:
Related Posts
Swift Concurrency: Actors & Data Safety

Swift Concurrency Series Part 1: Basics – Synchronous code, GCD, and async/await fundamentals. Part 2: Parallel Execution – Running independent…

Swift Concurrency: Errors & Cancellation

Swift Concurrency Series Part 1: Basics – Synchronous code, GCD, and async/await fundamentals. Part 2: Parallel Execution – Running independent…

Write a comment