Swift Concurrency: The Basics
Part 1: Basics - Synchronous code, GCD, and async/await fundamentals.
Part 2: Parallel Execution - Running independent tasks concurrently.
Part 3: Errors & Cancellation - Propagation and task cancellation.
Part 4: Actors - Protecting shared state with actor isolation.
- XC Project - Download the project with all examples used in this series
- 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
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).
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
/
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.
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 }
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.
These examples compare structured concurrency and unstructured tasks in Swift.
Both sets demonstrate two common patterns:
Creating work inside an
asyncfunctionUpdating the UI on the
MainActor
The goal is to show the behavioral difference between:
Awaiting work (
await)Spawning a new task (
Task {}orTask.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") }
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.
Swift Concurrency Series Part 1: Basics – Synchronous code, GCD, and async/await fundamentals. Part 2: Parallel Execution – Running independent…
Swift Concurrency Series Part 1: Basics – Synchronous code, GCD, and async/await fundamentals. Part 2: Parallel Execution – Running independent…