iToverDose/Software· 8 JUNE 2026 · 04:01

Swift 6’s Strict Concurrency Mode: Lessons from Migrating a Real App

Migrating a production macOS/iPad app to Swift 6’s strict concurrency mode exposed 44 warnings, evicted an uncooperative dependency, and revealed why compile-time safety demands more than just code changes.

DEV Community5 min read0 Comments

Moving a production app to Swift 6’s strict concurrency mode isn’t just a technical upgrade—it’s a philosophical shift. For developers accustomed to managing threads with mutexes and prayers, Swift 6 promises to prove concurrency correctness at compile time. But promises, as anyone who’s debugged a Heisenbug on a customer’s M1 under sync load knows, demand proof.

I learned that firsthand while migrating Ditto Edge Studio, a SwiftUI tool for debugging and querying the Ditto edge database. This wasn’t a to-do list; it was a real app with SQLCipher persistence, an embedded MCP server, SpriteKit graphs, and live sync over Bluetooth and WebSocket. Concurrency bugs don’t announce themselves in demo-friendly ways—they lurk in the cracks until a critical moment.

The migration was worth it, but the path was steeper than the WWDC talks suggested. The most valuable intervention didn’t come from a sweeping refactor—it came from the compiler stopping me in the one place I’d told it to ignore. Here’s what happened, and why it matters for any team eyeing Swift 6.

Dependency Hell: The Hidden Blockade in Swift 6 Migrations

Swift 6’s strict concurrency mode isn’t a per-file toggle. Even flawless actor isolation won’t save your build if a single dependency isn’t Swift 6-ready. That’s where my migration stalled.

The culprit was a beloved SwiftUI code editor I’d integrated for DQL query editing. It pulled in a syntax-highlighting library, both written in a pre-Swift 6 era. Neither would compile under strict concurrency without upstream changes I couldn’t wait for—and I refuse to own a syntax highlighter.

The choices were grim:

  • Pin the dependency and keep the app in Swift 5 forever.
  • Fork the dependency and maintain it myself.
  • Evict it entirely and replace it with something Swift 6-compatible.

I chose eviction. There’s no heroism in clinging to legacy code when the compiler is waving a red flag.

The replacement paired HighlightSwift with a custom DQLCodeEditor interface—an NSViewRepresentable on macOS and UIViewRepresentable on iPad, with a @MainActor coordinator handling debounced highlighting. The critical structure looked like this:

@MainActor final class Coordinator: NSObject, NSTextViewDelegate {
    private let highlighter = Highlight()
    private var highlightTask: Task<Void, Never>?

    func scheduleHighlight(_ source: String) {
        highlightTask?.cancel()
        highlightTask = Task { @MainActor [weak self] in
            try? await Task.sleep(for: .milliseconds(150)) // Debounce guard !Task.isCancelled else { return } await self?.applyHighlight(source)
        }
    }

    isolated deinit {
        highlightTask?.cancel()
    }
}

Key takeaway: In Swift 6, your dependency graph is your concurrency story. Audit it before flipping the language mode. The day you enable strict concurrency is the wrong day to discover a 2021-era dependency blocking your build.

The 44 Warnings: When the Compiler Demands Honesty

With the editor evicted, I flipped the switches:

SWIFT_VERSION = 6.0
SWIFT_STRICT_CONCURRENCY = complete

The build completed—then the compiler handed me 44 warnings.

Forty-four reasons my code wasn’t as isolated as I’d claimed. Forty-four opportunities to confront the difference between looking correct and being correct. Strict concurrency doesn’t care about changelogs or good intentions. It only cares about what the code actually does.

The warnings clustered into predictable (and humbling) categories:

  • Bridge code interfacing with @MainActor frameworks like SpriteKit, AppKit, and AVFoundation—where I’d assumed the main thread but never explicitly stated it.
  • Deprecated String(cString:) overloads finally exposed by strict mode.
  • Freshly written code that introduced new isolation gaps.
  • Tests calling @MainActor helpers from nonisolated test bodies.

Eradicating every warning required a methodical pass across both macOS and iPad targets. The platforms disagree on API availability in ways that will ambush you if you only build for one. Zero warnings on macOS didn’t guarantee zero warnings on iPad—and vice versa.

Not glamorous work. Just necessary.

The Swift 6 Concurrency Lexicon: A Developer’s Cheat Sheet

After hundreds of warnings, the compiler’s vocabulary stops feeling like syntax and starts feeling like intent. Here’s the working field guide to the annotations that earned their keep.

@MainActor: The Obvious (But Often Misapplied) Baseline

Most @MainActor fixes were straightforward: view models, UI glue, anything touching AppKit or UIKit. Mark it, move on.

The subtler cases involved SpriteKit. The SDK now annotates SpriteKit scenes and layers with @MainActor, rendering my old DispatchQueue.main.async calls redundant—and revealing how much of my "thread safety" was superstition rather than strategy.

// Before: A GCD hop born from "the main thread is a vibe" culture.
DispatchQueue.main.async { [weak self] in
    self?.onZoomChanged?(newScale)
}

// After: The compiler enforces the truth.

isolated Deinitializers: Cleanup Without the Hacks

Swift 6.2’s isolated deinit lets you cancel tasks safely on the main actor without the old "capture self in a detached Task" workaround. It’s a small joy, but one that eliminates entire classes of subtle bugs.

Actors vs. @MainActor: When to Choose Each

Actors are for shared mutable state across threads. @MainActor is for anything that must run on the main thread—UI updates, touch handling, animations. Confusing the two leads to warnings (or worse, runtime crashes).

Sendable and Structured Concurrency: The Compiler as Your Pair Programmer

Strict concurrency forces you to annotate types as Sendable where data crosses isolation boundaries. It’s tedious, but the compiler catches data races you’d never spot in testing. Structured concurrency—using async/await and Task—becomes the default, not an exotic pattern.

What Comes Next: Swift 6 Beyond the Compiler Warnings

Swift 6’s strict concurrency mode isn’t just a tool for catching bugs—it’s a shift toward writing code that proves its own correctness. For teams willing to invest the time, the payoff is real: fewer Heisenbugs, fewer late-night debugging sessions, and code that communicates its isolation guarantees at a glance.

But the migration isn’t a one-time event. It’s an ongoing discipline. Dependencies must be audited. Isolations must be explicit. Tests must be @MainActor-aware. The compiler will keep pushing you toward honesty—and that’s a feature, not a bug.

The real question isn’t whether Swift 6’s strict concurrency is worth adopting. It’s whether your team is ready to let the compiler hold you accountable—before your customers do.

AI summary

Learn how migrating a production SwiftUI app to Swift 6’s strict concurrency mode exposed 44 warnings, evicted a legacy dependency, and enforced compile-time safety.

Comments

00
LEAVE A COMMENT
ID #5C3M3J

0 / 1200 CHARACTERS

Human check

6 + 5 = ?

Will appear after editor review

Moderation · Spam protection active

No approved comments yet. Be first.