Zhivko Manchev

Senior iOS Engineer

iOS Team Lead

Swift & SwiftUI Expert

Article

Swift Concurrency: The Basics

January 29, 2025 Swift-Concurrency
Swift Concurrency Series
Pre async/await
  • Grand Central Dispatch
    • Uses queues (DispatchQueue) to organize tasks.
    • Main Queue: For UI updates (runs on the main thread).
    • Global Queues: For background tasks (e.g., .global(), .global(qos: .background)).
    • DispatchGroup: for doing multiple works in parallel
  • Completion handler (closure)
    • Notifies us when async work has been completed
Overview

What is Swift Concurrency?

  • Swift Concurrency is a set of language features and APIs introduced in Swift 5.5 (iOS 15+) to simplify asynchronous programming.
  • It includes:
    • async/await: Write asynchronous code that looks like synchronous code.
    • Task: A unit of asynchronous work.
    • Actors: Protect shared state with thread-safe data access.

Why Use Swift Concurrency?

  • Simpler Code: No more callback hell (nested closures).
  • Better Readability: Asynchronous code looks linear and easy to follow.
  • Built-in Error Handling: Use try/catch for errors in async functions.
  • Structured Concurrency: Tasks are automatically managed and cancelled when no longer needed.
  • Performance: Less overhead compared to GCD (Grand Central Dispatch).
Example 1. Updating multiple resources

This example updates a view model by fetching multiple related resources for a single person, such as an image, invite count, and friends list. Each tab shows a different implementation of the same operation, along with a comparison of the pros and cons of using that approach.

func updatePersonInfoSync(_ person: Person) {
    let personImage = person.loadImage()
    let invitesCount = person.loadInvitesCount()
    let friends = person.loadFriends()
    
    personDetails = .init(
        personImage: personImage,
        invitesCount: invitesCount,
        friends: friends
    )
}

Good

  • Easy to read and follow
  • Predictable
  • Simple error handling

Bad

  • Serial
  • Blocking
  • Single thread
  • Poor resource utilization
func updatePersonInfoSequentially(person: Person) {
    person.loadImage { personImage in
        person.loadInvitesCount { invitesCount in
            person.loadFriends { friends in
                self.personDetails = .init(
                    personImage: personImage,
                    invitesCount: invitesCount,
                    friends: friends
                )
            }
        }
    }
}

Good

  • Concurrent
  • Non-blocking Multi-threaded
  • Superior resource optimization

Bad

  • Difficult to follow
  • (sometimes) unpredictable
  • Handling errors is involved / error-prone
func updatePersonInfoSequentially(person: Person) async {
    let personImage = try? await person.loadImage()
    let invitesCount = try? await person.loadInvitesCount()
    let friends = try? await person.loadFriends()
    
    personDetails = .init(
        personImage: personImage,
        invitesCount: invitesCount,
        friends: friends
    )
}

Good

  • Easy to read and follow
  • Predictable
  • Simple error handling
  • Concurrent
  • Non-blocking Multi-threaded
  • Superior resource optimization

Bad

/

Example overview

Each tab presents the same operation implemented with synchronous code, Grand Central Dispatch, and Swift Concurrency, with a brief summary of the strengths and trade-offs of each approach.

Swift Concurrency combines the readability of synchronous syntax with non-blocking execution, providing the benefits of both previous approaches while avoiding most of their drawbacks.

Example 2. GCD vs Async/Await Basics

This example demonstrates common concurrency patterns: running work in the background, returning to the main thread, and executing work after a delay. The goal is to show how these familiar GCD patterns translate directly into Swift Concurrency equivalents.

func doStuffInBackground() {
    DispatchQueue.global().async {
        // Do background work
    }
}

func doStuffAndNotifyMainThread() {
    DispatchQueue.global().async {
        // Do stuff
        
        DispatchQueue.main.async {
            // Notify main thread
        }
    }
}

func doStuffAfterDelay() {
    DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
        // Do stuff
    }
}
// Called from @MainActor
@MainActor
func doStuffInBackground1() {
    Task.detached {
        // Do background work
    }
}

func doStuffAndNotifyMainThread() async {
    // Do stuff
    
    await MainActor.run {
        // Notify main thread
    }
}

func doStuffAfterDelay() async {
    try? await Task.sleep(for: .seconds(5))
    // Do stuff
}
Example overview

This example maps common GCD patterns to their Swift Concurrency counterparts.

  • DispatchQueue.global().async → Task.detached{…} or just Task {...} if calling from non-main actor context
  • DispatchQueue.main.async → Task { @MainActor in … } or await MainActor.run{...}, difference explained in the next example
  • DispatchQueue.main.asyncAfter(deadline:) → try await Task.sleep(for:)

The goal is to give beginners a reference for translating familiar GCD syntax into async/await, making the code more linear and readable while keeping the same behavior.

Note: Task {} inherits the current actor. Detaching is only needed when calling from @MainActor to do background work, but it’s harmless to always detach until you’re more familiar with actors. More about Actors in Chapter 4.

Example 3. Structured Concurrency vs Unstructured Tasks

These examples compare structured concurrency and unstructured tasks in Swift.

Both sets demonstrate two common patterns:

  • Creating work inside an async function

  • Updating the UI on the MainActor

The goal is to show the behavioral difference between:

  • Awaiting work (await)

  • Spawning a new task (Task {} or Task.detached {})

Although the syntax may look similar, the execution model is fundamentally different.

func performWork() async {
    let result = await Task {
        // Background work
        return 42
    }.value
    
    print(result)
}

func updateUI() async {
    // Background work
    
    await MainActor.run {
        // UI update
    }
    
    print("UI updated")
}
func performWorkUnstructured() {
    Task {
        // Background work
        print(42)
    }
    
    print("Continues immediately")
}

func updateUI() async {
    // Background work

    Task { @MainActor in
        // UI update
    }
    
    print("UI update scheduled")
}
Example overview

The key distinction demonstrated in these samples is

Structured Concurrency

  • Execution suspends and waits
  • Work remains part of the same async call chain
  • Errors propagate
  • Cancellation propagates
  • Ordering is predictable
  • Preferred in most cases

Unstructured Concurrency

  • Creates a new independent task
  • Execution continues immediately
  • No automatic cancellation propagation
  • Errors do not bubble up
  • Ordering is not guaranteed
  • Should be used intentionally

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