TL;DR: Here’s how we migrated 3 production Swift apps to Swift 6 Concurrency in 12 weeks — without pausing feature delivery.

In Part 1 — Swift Concurrency: What I Wish Someone Had Told Me — I shared the chaos of adopting Swift 6’s strict concurrency: @ surprises, Sendable pitfalls, and debugging sessions that stretched into the night.
This time, I’ll show you what came next — how we turned that pain into a repeatable framework that migrated three live iOS apps (~400K lines) in 12 weeks while still shipping new features every sprint.
If your org is facing the same modernization challenge — how to evolve without pausing delivery — this playbook is for you.
Where We Started
Our starting point looked painfully familiar:
- Recurring concurrency-related crashes and UI freezes
- QA cycles taking 3–4 days
- Mixed async styles — GCD, Combine, callbacks, RxSwift
- Devs hesitant to touch threaded code
The roadmap was locked. No pause window. We had to modernize while shipping.

1. Architecture First
Our layered stack (Domain → Data → Presentation / MVVM) became the foundation for concurrency ownership and visibility.

Three ground rules
- Actors own mutable state — no shared data across task boundaries.
- Async APIs stop at layer edges — Presentation layer calls async, never dips into Data internals.
- Legacy async code stays behind adapters — safe wrapping over rewrites.
@MainActor
final class UserViewModel: ObservableObject {
@Published var users: [User] = []
private let service: UserService
init(service: UserService) {
self.service = service
}
func fetchUsers() async {
do {
users = try await service.load()
} catch {
print("Error: \(error)")
}
}
}This made concurrency visible. Developers now had a clear mental map:
“When you cross a layer boundary, you cross a concurrency boundary.”
New hires were productive in days, not weeks.

2. Testing Before Refactoring
Before touching production code, we expanded our concurrency regression suite. Goal: not perfect coverage — just catching the bugs that matter.
func testConcurrentAccess() async throws {
let manager = DataManager()
await withThrowingTaskGroup(of: Void.self) { group in
for i in 0..<100 {
group.addTask { try await manager.load(id: i) }
}
for try await _ in group {}
}
}These stress tests found 12+ hidden race conditions static analysis never flagged. Confidence came before code.
3. Adapters Keep Delivery Flowing
No big-bang rewrites. We wrapped legacy callbacks behind async boundaries.
// MARK: - Legacy Service (Callback-based)
class LegacyNetworkService {
func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
// Legacy implementation
}
}
Adapters bought us time. Old code stayed safe, new code shipped modern. Velocity never stopped.Adapters bought us time. Old code stayed safe, new code shipped modern. Velocity never stopped.
// MARK: - Modern Service (Async/Await)
actor ModernNetworkService {
private let legacyService = LegacyNetworkService()
/// Fetches data using modern async/await while wrapping legacy callback API
func fetchData() async throws -> Data {
try await withCheckedThrowingContinuation { continuation in
self.legacyService.fetchData { result in
continuation.resume(with: result)
}
}
}
}// MARK: - Usage Example
func example() async {
let service = ModernNetworkService()
do {
let data = try await service.fetchData()
print("Data fetched successfully: \(data.count) bytes")
} catch {
print("Error fetching data: \(error)")
}
}
// MARK: - Alternative: More Flexible Implementation
actor FlexibleNetworkService {
private let legacyService = LegacyNetworkService()
private var cache: Data?
/// Fetches data with optional caching
func fetchData(useCache: Bool = true) async throws -> Data {
if useCache, let cached = cache {
return cached
}
let data = try await withCheckedThrowingContinuation { continuation in
self.legacyService.fetchData { result in
continuation.resume(with: result)
}
}
self.cache = data
return data
}
func clearCache() {
cache = nil
}
}
}
4. Leadership & Momentum
We introduced a modernization sprint track running parallel to feature work:
one module migrated → tested → shipped every three weeks.

Every sprint ended with visible wins — compiler warnings down, crashes down, coverage up. That visibility built trust with leadership.
5. Measurable Impact in 12 Weeks
(5 engineers • 3 production apps • ~400K LOC)

6. Why It Matters
This wasn’t just a technical upgrade — it was operational maturity.
• Fewer production incidents → measurable KPI improvements
•Clearer ownership → onboarding cut from 2 weeks → 3 days
• Documented ROI → stronger executive confidence
When you can tie refactors to delivery metrics, technical work stops being a cost and becomes a strategic advantage.
7. The Human Result
Eight weeks in, a mid-level engineer shipped a new real-time module using only Swift 6 Concurrency. No escalations. No thread issues. Just clean, reliable async code.
That’s when it clicked — we hadn’t just migrated code. We had migrated how the team thinks.
Key Takeaways
• Architecture clarity + testing discipline + visible metrics = predictable migration success
• Async/Await simplifies logic without slowing delivery
• Visibility builds trust — data beats opinion every time
• Progress > perfection — sustainable modernization wins long-term
We didn’t just modernize Swift code — we modernized how we deliver software. From “fix fast” → ship smart.
Who This Helps
• Product Teams: Learn how to deliver modernization safely under real deadlines.
• Engineering Leaders: Case study–level visibility on ROI-driven refactors.
• Developers: Practical Swift 6 playbooks and testing strategies for real teams.Follow & Connect
Follow & Connect
Follow Swift in Practice for proven Swift architecture patterns, modernization frameworks, and real-world concurrency results from production apps. I share lessons from leading iOS migrations and team transformations — the kind that keep delivery on track while evolving the tech stack.
☕ Enjoying Swift in Practice? It's free and reader-supported. If an edition saved you some debugging time, you can buy me a coffee: https://buymeacoffee.com/swiftinpractice
Originally published as a Swift in Practice edition on LinkedIn. Comments and discussion happen there.