From completion handlers to structured concurrency — actors, async/await, Sendable, and a phased migration strategy.
Welcome everyone. Today we're diving into Swift 6's concurrency model and how to migrate your existing codebase from the old completion handler patterns to structured concurrency. We'll cover the architectural changes, see real code transformations, and walk through a battle-tested migration strategy that minimizes risk. Whether you're maintaining a large production app or just getting started with Swift's modern concurrency features, this talk will give you the practical knowledge you need to make the transition successfully. Let's get started.
Let's look at how Swift's concurrency system is architected. The diagram shows three major components working together. At the top, we have structured concurrency with Tasks, async await syntax, TaskGroups for parallel work, and built-in cancellation support. In the middle, the actor model provides actor isolation, the MainActor for UI work, and the Sendable protocol for type safety. At the bottom, the runtime layer includes executors and a cooperative thread pool that manages execution efficiently. Notice how tasks connect to executors, and how the MainActor specifically routes work to the appropriate executor. This layered architecture is what gives Swift 6 its compile-time safety guarantees while maintaining excellent runtime performance.
Here's a side-by-side comparison that really shows the power of Swift 6. On the left, we have the Swift 5 approach with completion handlers. Look at all that nesting: we're checking for errors, validating data, handling JSON decoding, all within nested closures. The code is 20-plus lines deep with four levels of nesting, and every error case needs manual Result handling. Now look at the Swift 6 version on the right. The same functionality is just 12 lines. We use try await to get data, validate the response with a simple guard statement, and decode directly. The call site is even cleaner: just try await fetchUser. The table below quantifies this: we've reduced lines by over 40 percent, collapsed nesting from four levels to one, and replaced manual Result types with standard try-catch error handling.
This is a real migration diff from a production networking layer. We're looking at the APIClient class where we refactored the fetchProfile method. The old version had 30 lines of nested error handling inside a dataTask completion handler, with weak self capture and manual result construction. After migration, it's crystal clear: make the request, await the data, validate the response, and decode. The stats tell the story: we removed 23 lines, added only 14, resulting in a 39 percent reduction in code. But it's not just about fewer lines—the new code is dramatically more readable and maintainable. There's no callback hell, no forgotten resume calls, and the compiler enforces correct error propagation.
Looking at the migration scope for a real project: we have 147 total Swift files to evaluate. The good news shown in the callout is that only 38 files actually need migration—that's just 26 percent of the codebase. Of those, 12 are high priority, typically your core networking and data layers that heavily use completion handlers. Another 26 files only need Sendable or MainActor annotations without major refactoring. This breakdown is typical for iOS projects: most of your SwiftUI views and model types won't need significant changes. The key is identifying those high-priority files early—usually your API clients, database managers, and image caching layers—and tackling them first to establish patterns for the rest of your team.
Let's walk through the compiler flags you'll use during migration. Step one is enabling targeted warnings with the strict-concurrency=targeted flag. This is safe—it won't break your build, just shows warnings where you're capturing non-Sendable types. See that yellow warning about UserManager? That's exactly the kind of issue we need to find. Step two uses strict-concurrency=complete to surface all concurrency issues across your entire codebase. In this example, we're seeing 23 warnings including MainActor isolation problems. Step three is the big one: switching to Swift version 6 mode. Now those warnings become errors. Notice the red error about sending self.cache risking data races—the build fails until we fix it. This three-step progression lets you migrate incrementally without breaking your entire project at once.
Let's look at the real-world impact of this migration. The key stats are impressive: 34 percent reduction in lines of code, 100 percent compile-time data race safety, and most importantly, a 72 percent reduction in concurrency-related crashes in production. The table breaks down what actually changed: we modified 38 files, removed 127 completion handlers, and created 94 new async functions. We added 43 Sendable conformances to make our types thread-safe, applied the MainActor attribute 31 times for UI isolation, and converted 8 classes to actors for protected mutable state. These numbers come from a medium-sized production iOS app, and your mileage may vary, but they give you a realistic sense of the scope and impact of a Swift 6 concurrency migration.
The warning callout here is critical: do not enable strict mode globally before auditing your codebase. Let's talk about the four most common pitfalls. First, Sendable conformance is viral—when you make one type Sendable, all its properties must also be Sendable, cascading through your type graph. Plan this from your model layer upward. Second, marking a class as MainActor means every method runs on the main thread. Use the nonisolated keyword for background work to avoid performance issues. Third, understand task isolation: a plain Task inherits the current actor context, but Task.detached does not. This affects how self is captured and can cause subtle bugs. Finally, all global and static mutable state must be isolated to an actor or explicitly marked nonisolated unsafe. Address these early to avoid cascading compiler errors later.
Here's the four-phase migration strategy we recommend, spanning about 10 weeks for a typical project. Phase one is Audit: spend the first two weeks enabling targeted warnings, cataloging all your completion handlers, identifying Sendable violations, and mapping out which types need actor isolation. Phase two is Annotate: weeks three and four focus on adding Sendable conformances, marking MainActor on all your UI types, adding nonisolated where needed, and converting appropriate classes to actors. Phase three is Refactor: this is weeks five through eight where you actually replace those completion handlers, adopt async-await patterns throughout your networking and data layers, implement structured concurrency with task groups, and update all your unit tests. Finally, phase four is Strict Mode: weeks nine and ten you enable full Swift 6 language mode, fix the remaining errors that become visible, run performance benchmarks, and ship to production. This phased approach lets you validate each step before moving forward.
Let's recap the four key features that make Swift 6 concurrency so powerful. First, Actors are reference types with built-in serialization—all access is automatically isolated, eliminating data races at compile time. Second, Structured Concurrency means child tasks inherit context and are automatically cancelled when the parent scope exits, so you never leak background operations. Third, AsyncSequence lets you process values over time with the familiar for-await-in syntax, replacing both delegate callbacks and Combine publishers with a single unified pattern. And fourth, Distributed Actors extend the actor model across network boundaries, giving you the same isolation guarantees whether you're calling local or remote code. Together, these features form a cohesive concurrency model that's both safe and ergonomic.
Let's wrap up with the key takeaways. Async-await replaces completion handlers, giving you fewer lines of code and flatter, more readable structure. Actors provide compile-time data race safety—the compiler actually prevents you from writing concurrent code with race conditions. Structured concurrency automatically cancels child tasks when their parent scope exits, eliminating a huge class of resource leaks. And critically, migrate incrementally using the three-stage flag approach: targeted warnings first, then complete warnings, then finally strict mode. Don't try to flip the switch all at once. Take your time, follow the phased strategy we outlined, and you'll end up with safer, cleaner, more maintainable concurrent code. Thanks for your attention, and happy migrating.
Hands-on implementation guides with detailed code examples, step-by-step instructions, and expanded explanations for each topic.