This is Part 3 of a 3-part series on Swift 6 Concurrency:
- Part 1: The Fundamentals - async/await, actors, Sendable, MainActor
- Part 2: Advanced Patterns - AsyncSequence, task priorities, SwiftUI integration, practical guidance
- Part 3: Migration Guide (you are here) - Strategic planning for production codebases
Table of Contents
Introduction
Learning Swift 6 concurrency patterns is one thing—migrating a production codebase is another entirely. While understanding async/await, actors, and MainActor isolation is essential, successfully migrating requires strategic planning, team coordination, and realistic expectations about timelines and complexity.
This guide focuses on the how and when of migration: which strategies work, how to enable Swift 6 features incrementally, and how to avoid common pitfalls that derail migration efforts. If you're looking to learn Swift 6 concurrency patterns and see detailed code examples, start with Part 1: The Fundamentals and Part 2: Advanced Patterns.
Important reality check: Migrating to Swift 6 is not a quick weekend project. For most production applications, this is a lengthy process that can take weeks or months depending on your codebase size, architecture, and team capacity. The key to success is not rushing—it's having a solid plan and executing it systematically.
Let's set you up for a successful migration.
Before You Begin: Assessment Phase
Before writing a single line of migration code, conduct a thorough assessment of your codebase and team. This upfront analysis will determine your migration strategy and timeline.
1. Modularization Analysis
Question: How componentized is your codebase?
A well-modularized codebase with clear boundaries between components makes migration dramatically easier. You can migrate module-by-module, enabling strict concurrency checking for isolated parts of your app without triggering warnings across your entire codebase.
If your app is a monolith with tight coupling between components, you'll face a more challenging migration. Looser coupling and greater modularity significantly ease the transition. You may want to invest in breaking up your monolith before attempting Swift 6 migration.
2. Current Concurrency Usage
Question: How much concurrency does your app actually need?
Most iOS applications run their primary logic on the main thread, with background operations handled automatically by networking layers (URLSession) and system frameworks. Understanding your existing concurrency patterns helps you avoid introducing unnecessary complexity during migration.
As Donny Wals points out: "You really don't need a ton of concurrency in your app code." Many developers mistakenly believe migrating to Swift 6 means adding actors and async functions everywhere. The reality? Most of your code should remain MainActor-isolated, running on the main thread just like before.
Important caveat: Understanding nonisolated async is crucial. A nonisolated async function is not bound to any actor, meaning it can be called from any concurrency context and isn't protected by actor isolation. If you mark functions as nonisolated async unnecessarily, you may introduce complexity and lose the safety guarantees of actor isolation.
3. Team Readiness
Question: Does your entire team understand Swift Concurrency fundamentals?
This is crucial. You cannot successfully migrate if only one or two team members understand async/await, actors, and Sendable. The entire team must eventually grasp these concepts because everyone will be writing and maintaining Swift 6 code.
Invest in team education before beginning migration work. Use resources like our Part 1: The Fundamentals, Apple's documentation, conference talks, books, or formal training courses. This upfront investment prevents costly mistakes and confusion during migration.
4. Dependency Audit
Question: Do your third-party dependencies support Swift 6?
Check your critical dependencies for Swift 6 compatibility. Libraries that haven't adopted strict concurrency checking may require workarounds using @preconcurrency (more on this later). Make a list of:
- Dependencies with Swift 6 support
- Dependencies still using older patterns
- Internal frameworks that need updating
- Apple frameworks you rely on (not all have complete Swift 6 annotations yet)
This audit helps you plan your migration path and identify potential blockers early.
Migration Strategies
Once you've assessed your codebase, choose a migration strategy. The wrong approach can overwhelm your team with thousands of compiler warnings and grind development to a halt.
Module-by-Module Migration (Recommended)
Strategy: Enable strict concurrency checking for one module at a time, fixing warnings before moving to the next.
Why it works:
- Isolated changes: Each module's migration is contained. Warnings don't cascade across your entire codebase.
- Manageable scope: You can complete one module in a sprint or two, maintaining momentum.
- Learning opportunity: Each module teaches your team patterns that apply to subsequent modules.
- Testable progress: You can verify each module works correctly before moving on.
How to identify boundaries:
- Start with modules that have few dependencies (utility libraries, UI components)
- Move to modules that depend on those (networking layers, data models)
- Finish with high-level modules (feature modules, app target)
Key enabler: The @preconcurrency attribute allows Swift 6-compatible packages to be used by code that hasn't enabled strict concurrency yet. This prevents forced cascading updates across your entire codebase.
Outside-In Migration Pattern
Strategy: Start with view layers and work backwards to backend components within a module.
Why consider this:
Within a given module, starting from the UI and working backwards helps you determine the correct isolation boundaries. For MVVM architectures, this often means:
- Update views to understand MainActor requirements
- Decide on ViewModel isolation (usually MainActor)
- Update backend services called by ViewModels
- Update data models and networking layers
This outside-in approach reveals what actually needs to be isolated versus what can remain on the main thread.
Project-Wide Migration (Not Recommended)
Strategy: Enable strict concurrency for the entire project at once.
Why it usually fails:
You'll be overwhelmed with hundreds or thousands of compiler warnings simultaneously. Your team won't know where to start, and fixing one warning often triggers five more. Development velocity plummets as everyone struggles to understand the warnings.
When it might work:
- Very small codebases (< 10,000 lines)
- New projects still in early development
- Apps with no dependencies and simple architecture
For most production apps, avoid this approach. The module-by-module strategy is more sustainable.
Modularization: The Secret Weapon
If your codebase isn't already modularized, consider breaking it into Swift packages before starting your Swift 6 migration. This investment pays significant dividends.
Benefits for Migration
Isolated strict concurrency: Turn on strict concurrency checking for only one module without forcing your entire app to opt into all the sendability and isolation checks. This is the single biggest factor in making migration manageable.
Clear boundaries: Modules have explicit import statements and APIs, making it obvious where data crosses concurrency boundaries.
Parallel work: Different team members can migrate different modules simultaneously without stepping on each other's toes.
Incremental rollout: Ship migrated modules to production as they're completed, reducing risk.
How @preconcurrency Helps
The @preconcurrency attribute is critical for module-by-module migration. It allows Swift 6-compatible modules to be used by code that hasn't enabled strict concurrency yet, preventing forced cascading updates across your entire codebase.
When you mark module boundaries with @preconcurrency import, you suppress Sendable and isolation warnings, making gradual migration possible. This means you can migrate one module at a time without breaking code that depends on it.
For detailed usage examples, common use cases, and best practices, see the dedicated @preconcurrency: Your Migration Friend section below.
Modularization Doesn't Have to Be Perfect
You don't need a perfectly architected module system. Even basic separation (networking module, UI components module, core business logic module) provides enough isolation to make migration tractable. Focus on creating boundaries, not perfection.
Enabling Strict Concurrency Step-by-Step
Understanding how and when to enable strict concurrency checking is critical. Do this wrong and you'll overwhelm your team. Do it right and migration becomes systematic.
Swift 5 Mode + Strict Concurrency (Safest Path)
Recommendation: Start here, not with Swift 6 language mode.
Stick with the Swift 5 language mode and enable strict concurrency checking. This is the safest bet because it allows you to write code that may not be fully Swift 6 compliant but works completely fine. You get most of the safety benefits without some of Swift 6's stricter requirements.
How to enable (Xcode):
- Select your project in the navigator
- Choose your target
- Build Settings tab
- Search for "Strict Concurrency Checking"
- Set to "Complete" (for full checking)
How to enable (Swift Package Manager):
let package = Package(
name: "MyPackage",
targets: [
.target(
name: "MyTarget",
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency")
]
)
]
)
Important: Use ONE flag only, not both. The exact flag varies by Swift version:
- Swift 5.7-5.9:
.enableExperimentalFeature("StrictConcurrency")
- Swift 5.10+:
.enableUpcomingFeature("StrictConcurrency")
Check your Swift version (swift --version) and consult the official migration guide for the correct flag.
Per-Module Activation (Key Strategy)
Critical insight: You can enable strict concurrency checking per module, not just project-wide.
For multi-module projects, enable strict concurrency for one target at a time. In Xcode, each target has its own build settings. In SPM, each target in your Package.swift can have different swift settings.
This prevents the overwhelming cascade of warnings. Fix one module, verify it works, then move to the next.
Swift 6 Language Mode (Eventual Goal)
Once you've migrated most or all of your modules with Swift 5 + strict concurrency, consider switching to Swift 6 language mode.
What changes:
- Some code that worked in Swift 5 + strict concurrency may need adjustments
- Default isolation assumptions may differ
- You're fully committed to Swift 6 semantics
When to switch:
- After your team is comfortable with concurrency patterns
- When your critical dependencies support Swift 6
- When you've addressed most strict concurrency warnings
Note: Swift 6 language mode includes strict concurrency by default. You don't need to enable both.
Default Actor Isolation (Swift 6.2+)
If you're using Swift 6.2 or later (Xcode 26+), consider enabling Default Actor Isolation. This setting assumes @MainActor by default for your code, significantly reducing the boilerplate of adding MainActor annotations everywhere.
How to enable:
For Xcode projects: In Build Settings, search for "Default Actor Isolation" and set it to MainActor. The exact location may vary by Xcode version, but it's typically under the Swift Compiler section.
For Swift Package Manager:
.target(
name: "MyTarget",
swiftSettings: [
.defaultIsolation(MainActor.self)
]
)
This makes migration smoother by aligning the compiler's assumptions with how most iOS code actually works (on the main thread).
@preconcurrency: Your Migration Friend
The @preconcurrency attribute is specifically designed to ease Swift 6 migration. Think of it as a bridge between old code and new code.
What It Is
@preconcurrency tells the compiler: "This code is from a pre-Swift 6 era, so don't apply strict concurrency warnings when it's used." It's essentially an opt-out from certain strict checking requirements.
Common Use Cases
1. Legacy Dependencies Without Swift 6 Support
@preconcurrency import OldNetworkingLibrary
class MyService {
let client = OldNetworkingClient()
}
2. Your Own Modules During Transition
@preconcurrency public protocol LegacyDelegate {
func didFinish()
}
3. Apple Frameworks with Incomplete Annotations
Not all Apple frameworks have complete Swift 6 concurrency annotations. Delegates, callbacks, and older APIs may trigger warnings. @preconcurrency import Foundation can suppress these while Apple completes their migration.
When to Remove It
@preconcurrency is a temporary measure. Once the underlying code (dependency, module, or Apple framework) is updated with proper concurrency annotations, remove the @preconcurrency attribute to get full type safety.
Think of it as scaffolding: necessary during construction, but removed when the building is complete.
Limitations
@preconcurrency import only affects modules compiled without strict concurrency. If the imported module already uses Swift 6 language mode, the attribute has no effect and the compiler will warn you. It also cannot suppress diagnostics that originate from how your own code captures or uses imported types — only those caused by missing annotations in the imported module. See Apple Framework Compatibility below for practical workarounds when @preconcurrency isn't enough.
Example: Gradual Module Migration
public actor DataStore {
public func save(_ data: Data) async throws { }
}
@preconcurrency import ModuleA
class LegacyViewController {
let store = DataStore()
func saveData() {
Task {
try? await store.save(data)
}
}
}
As you migrate ModuleB, you'll remove @preconcurrency and update the code to use proper async/await patterns.
Common Migration Pitfalls
Learn from others' mistakes. These pitfalls derail migration efforts more than any technical challenge.
1. Introducing Unnecessary Concurrency
The mistake: Adding actors and async functions everywhere because "that's what Swift 6 is about."
The reality: Most iOS app code should remain on the main thread, marked with @MainActor. You really don't need a ton of concurrency in your app code. System frameworks like URLSession already perform work off the main thread where appropriate. UI updates must be on the main thread.
The hidden complexity: Marking functions as nonisolated async removes actor isolation protection, meaning they can be called from any concurrency context and you lose the safety of actor isolation. Don't use nonisolated async unless you have a specific reason.
Important clarification: nonisolated is about isolation, not execution location. It doesn't mean "runs on a background thread"—it means the function isn't bound to any actor's isolation domain. The actual thread or executor used depends on the calling context and runtime decisions, not the nonisolated keyword itself.
nonisolated func calculateTotal(_ items: [Item]) async -> Decimal {
return items.reduce(0) { $0 + $1.price }
}
@MainActor
func calculateTotal(_ items: [Item]) -> Decimal {
return items.reduce(0) { $0 + $1.price }
}
Exception: SDK/Library Design — While app code should avoid nonisolated async, SDK and library public APIs are different. Apple recommends using nonisolated for public library APIs so consumers can call them from any isolation context. Library authors must accommodate unknown usage patterns rather than forcing consumers into a specific isolation context. Internal SDK code can still use actors and proper isolation.
Key principle: Only introduce concurrency where you have genuinely independent work that can benefit from parallelism. For more details, see the over-using actors pitfall in Part 2.
2. Not Modularizing First
The mistake: Attempting to migrate a monolithic 100,000-line codebase in one go.
Why it fails: You can't enable strict concurrency for part of a monolith. It's all or nothing. The resulting flood of warnings is paralyzing.
The solution: Spend 1-2 weeks breaking your monolith into modules first. Even rough boundaries (UI, Business Logic, Networking, Data) provide enough isolation to make module-by-module migration feasible.
3. Apple Framework Compatibility Issues
The reality: Not all Apple frameworks have complete Swift 6 concurrency annotations. You'll encounter:
- Delegate protocols without
@MainActor annotations
- Callbacks that should be isolated but aren't marked
- APIs that trigger false positive Sendable warnings
The workaround:
@preconcurrency import UIKit
@preconcurrency import Foundation
@MainActor
class MyDelegate: UIApplicationDelegate {
}
Important caveat: @preconcurrency import has limitations — it only works for modules compiled without strict concurrency, and can't suppress all diagnostics (see full details above). If it has no effect, prefer local annotations on the affected types, callbacks, or delegate conformances instead.
As Apple completes their Swift 6 work across iOS releases, these issues will resolve. Don't let them block your migration—work around them temporarily.
4. Rushing the Process
The mistake: Setting aggressive deadlines ("We're switching to Swift 6 next sprint!").
The reality: Swift 6 migration for production apps is measured in weeks or months, not days. Teams that rush make mistakes:
- Slapping
@MainActor everywhere without understanding isolation
- Using
@unchecked Sendable to silence warnings instead of fixing issues (⚠️ dangerous - treat like force unwrapping)
- Creating race conditions by misunderstanding actor semantics
The solution: Set realistic timelines based on your codebase complexity. Celebrate incremental progress (one module migrated) rather than fixating on "done by date X."
5. Ignoring Team Training
The mistake: Assuming developers will "figure it out" as they go.
Why it fails: Swift Concurrency introduces fundamentally new concepts (actor isolation, Sendable, nonisolated, async contexts). Developers who don't understand these concepts will write code that compiles but is subtly wrong, or waste time fighting the compiler.
The solution: Invest in education before migration. Team members should understand:
- async/await fundamentals (Part 1)
- Actor isolation and data race prevention (Part 1)
- MainActor for UI code (Part 1)
- Sendable protocol (Part 1)
Budget time for this education. It's not wasted—it's essential infrastructure for successful migration.
The Migration Journey
Rather than prescribing exact timelines (which vary wildly by codebase and team), here's a phase-based approach. Move through these phases at your own pace.
Phase 1: Foundation & Assessment
Goals: Build team knowledge and understand your codebase.
Activities:
- Team training: Everyone learns Swift Concurrency fundamentals. Use Part 1 and Part 2, Apple's docs, WWDC talks, or formal courses.
- Codebase assessment: Analyze modularization, concurrency needs, and dependency support (as described in Assessment Phase).
- Modularization planning: If needed, plan to break your monolith into modules.
- Enable warnings: Turn on Swift 5 + strict concurrency in warning mode (not errors) for one low-dependency module to see what you're dealing with.
Outcome: The team understands Swift Concurrency, and you have a migration plan tailored to your architecture.
Phase 2: First Module
Goals: Complete your first full module migration and establish patterns.
Activities:
- Choose wisely: Pick a module with few dependencies, low complexity, and limited Combine usage. Utility libraries, UI components, or data models are good candidates.
- Enable strict concurrency: Turn on strict checking for this module only.
- Fix warnings systematically:
- Add
@MainActor to UI classes
- Make data models Sendable (often just struct with immutable properties)
- Use actors for shared mutable state (if any)
- Apply
@preconcurrency for legacy dependencies
- Learn patterns: Document what works. "All ViewModels are
@MainActor" might become a team standard.
- Verify thoroughly: Run tests, stress test concurrent operations.
Outcome: One fully migrated module and a set of patterns your team can replicate.
Phase 3: Gradual Expansion
Goals: Migrate additional modules following your established patterns.
Activities:
- Follow your strategy: Use module-by-module or outside-in approach (as described in Migration Strategies).
- Use @preconcurrency at boundaries: Allow migrated modules to be used by not-yet-migrated code.
- Update shared components: Data models, networking clients, and utilities used across modules need early attention.
- Cross-reference patterns: For complex scenarios (AsyncSequence, task priorities), refer to detailed examples in Part 2.
Outcome: Multiple modules migrated, team velocity increasing as patterns become familiar.
Phase 4: Core Migration
Goals: Migrate your app's critical business logic and UI layer.
Activities:
- ViewModels: Apply
@MainActor to ViewModel classes. See Part 1: MainActor for details.
- Combine to async/await: Convert Combine publishers to async functions. See Part 2: Migrating from Combine for patterns.
- Business logic: Ensure proper isolation. Most logic should be
@MainActor, with only genuinely concurrent work using actors.
- Integration testing: Verify the app works correctly with migrated modules.
Outcome: Core functionality fully migrated, app running with strict concurrency enabled for most modules.
Phase 5: Completion & Polish
Goals: Finish the migration and enable full strict checking.
Activities:
- Switch to Swift 6 language mode: For each module, change the Swift Language Version from Swift 5 to Swift 6. This is what turns strict concurrency warnings into errors — there is no separate toggle.
- Remove @preconcurrency: As dependencies update or modules complete migration, remove temporary
@preconcurrency attributes.
- Final verification: Run comprehensive tests, including stress tests for concurrent operations (see Part 2: Conclusion).
Outcome: Fully migrated to Swift 6, with strict concurrency enforced project-wide.
Practical Tips
Beyond the structured phases, these practical tips help keep migration on track.
Start Small and Build Confidence
Don't begin with your most complex module. Start with something simple—a utility library, a UI component—to build team confidence and establish patterns. Early wins create momentum.
Document Patterns That Work
As you migrate, write down patterns your team settles on:
- "All ViewModels are
@MainActor"
- "Data models are immutable structs"
- "Networking responses are Sendable"
This documentation becomes your team's migration playbook, reducing decision paralysis as you move through modules.
Use Stress Tests to Verify Correctness
Concurrent bugs are notoriously hard to reproduce. Don't just check "code compiles." Write stress tests that run operations 1000 times concurrently to verify you haven't introduced data races. See Part 1 for stress testing patterns.
Don't Feel Pressured to Rush
Swift 6 is the future, but it's not going anywhere. Apple will support Swift 5 for years to come. Migrating thoughtfully and correctly is far more valuable than migrating quickly and introducing subtle concurrency bugs.
If your team needs six months for a thorough migration, take six months. The alternative—a rushed migration full of unsafe @unchecked Sendable hacks and misunderstood isolation—creates technical debt that's harder to fix later.
Remember: Swift Is Still Evolving
Swift 6 continues to evolve. New features, refinements to concurrency checking, and improvements to standard library APIs are ongoing. Be prepared to adjust your code as Swift matures.
This isn't a weakness—it's a strength. You're migrating to a modern, actively developed language feature. Stay connected to the Swift community, watch WWDC sessions, and update your patterns as best practices evolve.
Resources
Related Content
- Part 1: The Fundamentals - async/await, actors, Sendable, MainActor
- Part 2: Advanced Patterns - AsyncSequence, task priorities, SwiftUI integration, practical guidance
Migration Guides
- How to Plan a Migration to Swift 6 by Donny Wals - The primary inspiration for this guide, with excellent strategic advice
- Swift 6: What's New and How to Migrate by Antoine van der Lee - Practical step-by-step migration guide with Xcode-specific instructions
- Migrating to Swift 6 - Official Swift migration documentation
- Default Actor Isolation in Swift 6.2 - Understanding the new Swift 6.2 feature that simplifies migration
WWDC Sessions
- WWDC 2024: Migrate your app to Swift 6 - Official Apple migration guidance
- WWDC 2023: Beyond the basics of structured concurrency - Advanced patterns
- WWDC 2022: Eliminate data races using Swift Concurrency - Understanding the safety guarantees
Official Documentation
- Swift Concurrency Documentation - Comprehensive language guide
- Swift Evolution Proposals - Deep dives into concurrency features
- Swift Forums - Ask questions and learn from others' migration experiences
Video Tutorials
Ready to begin? Start with the Assessment Phase to understand your codebase, then pick your migration strategy. Remember: successful migration is about strategic planning and steady progress, not speed.
For questions about specific concurrency patterns, see Part 1 and Part 2. For migration strategy questions, the Swift community forums and Donny Wals' article are excellent resources.
Series Navigation