Swift Concurrency arrived with a promise: simpler, safer, and faster code for asynchronous work. But moving from callbacks and completion handlers to async/await and actors isn't automatic. Teams often find that the new tools solve old problems while introducing new ones—if not used carefully. This checklist is for iOS and macOS developers who want a practical, step-by-step approach to adopting Swift Concurrency without breaking their apps or losing performance.
Who Needs This Checklist and Why Now
If you maintain an app that performs network requests, processes data in the background, or updates the UI asynchronously, you already deal with concurrency. The question is whether your current approach—be it Grand Central Dispatch (GCD), OperationQueue, or manual thread management—is sustainable. Swift Concurrency offers a language-level model that reduces boilerplate and eliminates many common bugs, but it requires a shift in mindset.
This checklist is for teams that have read the WWDC sessions and want to know: where do we start? It assumes you understand the basics of async/await but need guidance on structuring real-world code. We focus on practical decisions: which parts of your app to refactor first, how to test concurrent code, and how to avoid performance regressions.
We also address the elephant in the room: Swift Concurrency is not a silver bullet. It works best when you adopt its patterns fully—mixing it with legacy concurrency tools can lead to subtle bugs. Our goal is to help you make informed trade-offs, not to advocate for a wholesale rewrite.
What This Checklist Covers
We break down the adoption process into seven areas: assessing your current codebase, choosing between actors and other isolation mechanisms, structuring async tasks, handling cancellation, testing, performance profiling, and migrating incrementally. Each section includes concrete steps and common mistakes to avoid.
Who Should Read This
This guide is for Swift developers with at least a year of iOS or macOS experience. You should be comfortable with closures, protocols, and basic GCD. If you are new to concurrency concepts, we recommend reviewing Apple's Swift Concurrency documentation first.
The Concurrency Landscape: Three Approaches Compared
Before diving into the checklist, it helps to understand the main concurrency models available in Swift today. Each has strengths and weaknesses, and the right choice depends on your app's architecture and performance requirements.
Grand Central Dispatch (GCD)
GCD is the veteran. It uses dispatch queues to manage thread pools and execute work items asynchronously. It is flexible and well-understood, but it requires careful management of queue priorities, QoS classes, and thread safety. Common issues include thread explosion, priority inversion, and difficulty debugging deadlocks. GCD works well for fire-and-forget tasks but becomes unwieldy for complex dependency chains.
OperationQueue and Operations
OperationQueue builds on GCD with higher-level abstractions: dependencies, cancellation, and state management. It is ideal for workflows where tasks depend on each other, like image processing pipelines. However, it introduces overhead and can be verbose. It also does not integrate with Swift's async/await natively, though you can wrap operations in async tasks.
Swift Concurrency (async/await, Actors, Task Groups)
Swift Concurrency is the newest model, introduced in Swift 5.5. It provides language-level support for asynchronous functions, structured concurrency, and actor-based isolation. It eliminates many boilerplate patterns (like dispatch queues) and makes concurrent code easier to read and reason about. However, it requires iOS 13+ (with some features requiring iOS 15+), and mixing it with legacy code can cause issues. It also has a learning curve for teams accustomed to callback-based patterns.
Comparison Criteria
When choosing a concurrency model, consider these factors: deployment target (Swift Concurrency requires iOS 13 at minimum, with full features on iOS 15+), team familiarity, existing codebase (mixing models adds complexity), performance characteristics (actor overhead vs. dispatch queues), and testing support (Swift Concurrency integrates with XCTest). For new projects targeting iOS 15+, Swift Concurrency is often the best default. For existing apps, a phased migration is safer.
Decision Criteria: How to Choose Your Concurrency Model
Choosing a concurrency model is not a one-size-fits-all decision. We recommend evaluating your app against these criteria to determine the best path forward.
Deployment Target and Feature Availability
Swift Concurrency's async/await is available from Swift 5.5 and iOS 13, but some features—like actors, task groups, and global actors—require iOS 15 or later. If your app supports iOS 12 or earlier, you cannot use Swift Concurrency at all. For apps targeting iOS 13–14, you can use async/await but must avoid actors and structured concurrency. In that case, consider a hybrid approach: use async/await for simple async calls but stick with GCD for thread isolation.
Codebase Complexity and Refactoring Cost
If your app is heavily callback-based with deeply nested closures, the refactoring effort to switch to async/await can be significant. However, the payoff in readability and maintainability is often worth it. Start by identifying isolated modules (e.g., a networking layer) that can be converted independently. Avoid converting the entire app at once; incremental migration reduces risk.
Performance Sensitivity
Swift Concurrency's actor model adds overhead for each actor access due to runtime checks and priority management. For high-frequency, low-latency operations (e.g., audio processing), actors may introduce unacceptable latency. In such cases, consider using GCD with a dedicated high-priority queue or using unsafe, unchecked concurrency with careful manual locking. For most UI-bound apps, the overhead is negligible.
Testing and Debugging
Swift Concurrency integrates well with XCTest: you can write async test functions and use expectations for async work. This makes testing concurrent code more straightforward than with GCD, where you often need to use semaphores or wait for queues. However, debugging actor deadlocks can be tricky because the runtime may not always report them clearly. Invest in learning the debugger's concurrency view (Xcode 14+) to inspect task states.
Team Readiness
If your team is new to concurrency concepts, Swift Concurrency's structured approach can be easier to learn than GCD's low-level primitives. However, it still requires understanding of async/await, task cancellation, and actor reentrancy. Plan for a learning phase: pair programming, code reviews focused on concurrency, and small pilot projects.
Trade-offs Table: Swift Concurrency vs. GCD vs. OperationQueue
To help you compare at a glance, here is a structured overview of the trade-offs between the three main concurrency models in Swift. Use this as a reference when deciding which approach to use for a given task.
| Aspect | Swift Concurrency | GCD | OperationQueue |
|---|---|---|---|
| Deployment target | iOS 13+ (full: iOS 15+) | iOS 4+ | iOS 2+ |
| Readability | High (linear async code) | Low (nested callbacks) | Medium (verbose setup) |
| Thread safety | Built-in (actors) | Manual (locks, queues) | Manual (locks, queues) |
| Task cancellation | Built-in (structured) | Manual (dispatch blocks) | Built-in (Operation.cancel) |
| Dependency management | Manual (task groups) | Manual (barriers, groups) | Built-in (dependencies) |
| Performance overhead | Low to medium (actor checks) | Low (direct queue dispatch) | Medium (object overhead) |
| Debugging | Good (Xcode concurrency view) | Poor (thread explosion) | Good (state tracking) |
| Testing support | Excellent (async XCTest) | Poor (semaphores) | Good (XCTest expectations) |
As the table shows, Swift Concurrency excels in readability and safety but requires a higher deployment target. GCD remains a solid choice for simple, high-performance tasks, while OperationQueue is best for complex workflows with dependencies. In practice, many apps use a mix: Swift Concurrency for networking and UI updates, GCD for background processing, and OperationQueue for batch operations.
When to Use Each Model
Use Swift Concurrency for: network calls, database access, UI updates, and any task that benefits from structured error handling and cancellation. Use GCD for: lightweight, fire-and-forget tasks (e.g., logging), high-frequency operations where actor overhead is unacceptable, and code that must run on older OS versions. Use OperationQueue for: complex workflows with multiple dependencies, such as image processing pipelines or batch data exports.
Implementation Path: A Step-by-Step Checklist
Once you have chosen your concurrency model (or a hybrid), follow this checklist to implement it correctly. We assume you are adopting Swift Concurrency as the primary model, but the steps adapt to other models with minor adjustments.
Step 1: Audit Your Current Asynchronous Code
Identify all places where you use completion handlers, delegates, or GCD. Categorize them by complexity: simple callbacks (one async call), nested callbacks (multiple sequential calls), and concurrent callbacks (multiple parallel calls). This audit helps you prioritize which parts to convert first.
Step 2: Convert Simple Callbacks to async/await
Start with the simplest cases: a function that takes a completion handler and returns once. Replace the closure with an async function that throws or returns a value. For example, convert a URLSession data task from a completion handler to an async let. This gives your team practice with the syntax and builds confidence.
Step 3: Introduce Actors for Shared Mutable State
Identify any mutable state that is accessed from multiple threads—e.g., a cache, a user session, or a data store. Wrap that state in an actor to ensure thread-safe access. Be careful with actor reentrancy: if an actor method calls an async function, other tasks can run on the actor during the suspension. Use nonisolated or isolated parameters to control access granularity.
Step 4: Use Task Groups for Parallel Work
When you need to perform multiple independent tasks concurrently and wait for all of them, use a task group. For example, loading multiple images or fetching data from several endpoints. Task groups also support cancellation: if one task fails, you can cancel the others. Use withThrowingTaskGroup for error handling.
Step 5: Implement Proper Cancellation Handling
Cancellation is not automatic. Your async functions must check Task.isCancelled or call try Task.checkCancellation() at appropriate points. For long-running loops, check cancellation periodically. For tasks that create subtasks, propagate cancellation by passing the parent task's cancellation token or by using structured concurrency.
Step 6: Write Tests for Async Code
Use XCTest with async test functions. Test both success and cancellation paths. Use XCTestExpectation for older iOS versions if needed. Mock network calls to avoid flaky tests. For actor-based code, test that state is correctly isolated and that reentrancy does not cause data races.
Step 7: Profile and Optimize
Use Instruments' Swift Concurrency template to track task creation, actor contention, and priority inversions. Look for excessive actor hops (calls between actors) that can degrade performance. Consider using nonisolated functions for actor methods that do not access mutable state. For hot paths, benchmark actor vs. lock-based alternatives.
Risks of Skipping Steps or Choosing Wrong
Adopting Swift Concurrency without a plan can introduce subtle bugs that are hard to diagnose. Here are the most common risks and how to avoid them.
Priority Inversion
When a high-priority task waits for a low-priority task (e.g., through an actor), the system may temporarily boost the low-priority task's priority. This can cause unexpected performance degradation. To avoid it, ensure that actors do not block on external resources for long periods, and use Task.currentPriority to adjust behavior if needed.
Actor Reentrancy Bugs
Actors allow reentrancy: while an actor method is suspended (e.g., waiting for a network call), other tasks can run on the same actor. This can lead to state changes that the suspended method did not expect. To mitigate, avoid mutable state in actors that depends on the order of operations. Use isolated parameters to pass state explicitly.
Task Leaks
If you create a task but do not store a reference to it, the task may be cancelled prematurely or continue running after the parent object is deallocated. Always store tasks in a property or use structured concurrency (task groups) to manage their lifecycle. For fire-and-forget tasks, use Task.detached only when necessary.
Mixing GCD and Swift Concurrency Incorrectly
Calling an async function from a GCD queue can cause thread explosion if not handled carefully. Use withUnsafeCurrentTask or bridge through MainActor.run when mixing. Better yet, convert entire subsystems to Swift Concurrency to avoid the complexity.
Ignoring Main Actor Requirements
UI updates must happen on the main actor. If you call a method that updates the UI from a non-main actor, you risk crashes or undefined behavior. Mark UI-related classes with @MainActor and use await MainActor.run for one-off updates.
Mini-FAQ: Common Questions About Swift Concurrency
Can I use Swift Concurrency with iOS 12?
No. Swift Concurrency requires Swift 5.5 and a runtime that supports it, which is available from iOS 13. For iOS 12, you must use GCD or OperationQueue. Consider using a compatibility library like AsyncSwift for backporting, but be aware of limitations.
Should I convert all my code at once?
No. Incremental migration is safer. Start with isolated modules (networking, data fetching) and test thoroughly. Avoid converting the entire app in a single branch; instead, use feature flags or separate targets to validate each conversion.
How do I handle errors in task groups?
Use withThrowingTaskGroup and catch errors in the group's for try await loop. When one task throws, you can cancel the remaining tasks by calling group.cancelAll(). Alternatively, use withTaskGroup for non-throwing tasks and handle errors through result types.
What is the performance impact of actors?
Actors add overhead for each access due to runtime checks and priority management. In typical UI apps, this overhead is negligible (microseconds per access). For high-frequency calls (e.g., in a rendering loop), consider using a lock or an unsafe, unchecked approach. Profile with Instruments to determine if actors are a bottleneck.
How do I debug deadlocks in actors?
Xcode's concurrency view (Debug > Debug Workflow > Show Concurrency View) shows task states and actor queues. If a task is stuck, look for circular waits or long-running synchronous operations inside an actor. Use breakpoints and os_signpost to trace execution.
Can I use Swift Concurrency with Combine?
Yes. You can convert Combine publishers to async sequences using values property (iOS 15+) or by using AsyncStream. This allows you to use async/await with Combine pipelines, but be mindful of cancellation and backpressure.
What about thread safety for singletons?
Use a global actor, such as @MainActor or a custom global actor, to protect singleton state. Alternatively, wrap the singleton in an actor. Avoid using static let with mutable state accessed from multiple threads without isolation.
Next Steps: Your Action Plan
Now that you have the checklist, here are three concrete actions to take this week:
- Audit your current codebase for async patterns. List all completion handlers and GCD calls. Categorize them by complexity and deployment target requirements.
- Pick one module to convert first—ideally a self-contained networking layer or a data service. Convert it to async/await and actors. Write tests for the new code.
- Run Instruments with the Swift Concurrency template on your converted module. Look for actor contention, priority inversions, and task leaks. Adjust your implementation based on the data.
Remember, the goal is not to rewrite everything overnight. Swift Concurrency is a tool, not a mandate. Use it where it adds clarity and safety, and keep legacy tools where they serve better. Over time, as your team gains experience, you can expand the adoption. The checklist above will help you avoid common pitfalls and build performant, maintainable concurrent apps.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!