Skip to content
C.W.K.
Stream
Lesson 02 of 07 · published

Swift 5 Mode, Swift 6 Mode, and the Same Source Meaning Two Things

~15 min · concurrency, language-mode, swift-6, strict-concurrency

Level 0Bundle Opener
0 XP0/81 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete
"My app is on Swift 5" is not an answer to "will this trap".

One Compiler, Several Languages

The Swift 6.3 compiler accepts more than one language mode, chosen per target. In Swift 5 mode, most concurrency checking is off by default; you can raise it to warnings with strict concurrency checking. In Swift 6 mode, data-race safety is part of the language: sending a non-Sendable value across an isolation boundary while other code can still reach it, touching main-actor state from a background context, or sharing a mutable global are compile errors. A value that nothing else can reach may still cross, because the compiler tracks which region each value belongs to. Where the compiler cannot see the whole picture — typically a closure handed to an Objective-C API — Swift 6 also emits a runtime check that the code really is running where its isolation says.

You choose the mode in the build settings, not in the compiler version. A SwiftPM package declares swiftLanguageModes: [.v6] (a package created by swift package init with the 6.3 toolchain already does), and a single target can opt back with .swiftLanguageMode(.v5). An Xcode or XcodeGen target sets SWIFT_VERSION to 6.0 or 5.0.

The Family Ran Both, and Paid for It

For months the family's code lived in both modes at once. The shared Swift kit and several Mac apps compiled as Swift 6; other Mac apps and most of the iOS apps compiled in Swift 5 mode while linking that Swift 6 kit. On paper this is supported — modules in different modes interoperate. In practice the same source means different things on each side of the line.

The dictation code of an iOS app worked perfectly as app-local code in Swift 5 mode. The identical code, donated to the kit and adopted back the same evening, crashed the instant Dad tapped the microphone. Nothing in the source had changed. Compiled in Swift 6 mode, a closure written inside a @MainActor method was inferred to be main-actor isolated and received a runtime isolation check — and the audio framework called it on a realtime thread. In Swift 5 mode the same lines are a warning at most. Lesson four of this track takes that crash apart line by line.

The House Rule Now

The family's direction is Swift 6 language mode everywhere: packages, Mac apps, iOS and watch targets, and even bare swiftc helper builds (-swift-version 6). Migration is real work — you will write @Sendable, nonisolated, and isolated conformances you never needed before — but every one of those annotations states a fact the Swift 5 build was silently assuming. If a migration is too big for one step, turn on strict concurrency checking in Swift 5 mode first, drive the warnings to zero, then flip the mode.

Code

Package.swift: Swift 6 for the package, an explicit exception for one target·swift
// swift-tools-version: 6.2
import PackageDescription

let package = Package(
    name: "Spark",
    platforms: [.macOS(.v14), .iOS(.v17), .watchOS(.v10)],
    products: [
        .library(name: "SparkCore", targets: ["SparkCore"]),
    ],
    targets: [
        .target(name: "SparkCore"),
        // A temporary exception while it migrates — name it, date it, delete it.
        .target(name: "LegacyBridge", swiftSettings: [.swiftLanguageMode(.v5)]),
    ],
    swiftLanguageModes: [.v6]
)
project.yml: the same decision for an XcodeGen iOS target·yaml
settings:
  base:
    SWIFT_VERSION: "6.0"          # not "5.0": the app and the kit it links agree on the rules
    DEVELOPMENT_TEAM: EXAMPLE_TEAM

External links

Exercise

Create a small SwiftPM target in Spark with a global var counter = 0 and a function that increments it from a detached Task. Build it with .swiftLanguageMode(.v5) and note what the compiler says. Flip the target to Swift 6 and write down the error. Fix it two different ways (an actor, and a main-actor isolated value) and explain which one matches how the counter is really used.
Hint
In Swift 6 mode a mutable global is refused as not concurrency-safe. The right fix depends on who reads and writes it: UI-facing state wants @MainActor; state shared by background work wants an actor or a lock-protected type.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.