Version Compatibility

Swift moves fast, but it takes backward compatibility seriously. This chapter untangles two ideas people often confuse — the version of the compiler you run versus the language mode it interprets your code with — and explains how to upgrade safely and what "Swift 6 mode" really means.

Compiler version vs. language mode

There are two separate version numbers in play, and keeping them straight avoids most confusion:

A single modern compiler can build code in several language modes. So you can run the latest Swift 6 compiler while still compiling a package in language mode 5. This is what lets the whole ecosystem upgrade gradually instead of all at once.

Note: A new compiler can introduce new features (like a new macro) independently of the language mode. Modes mainly gate the source-breaking changes, while opt-in features can often be enabled à la carte.

Swift language versions and modes

Swift's source-breaking changes are grouped into numbered language modes. The major ones are 4, 4.2, 5, and 6. Code written for an older mode keeps compiling under newer compilers, because each mode preserves the source-level behavior it promised.

The headline change in mode 6 is that data-race safety is enforced at compile time. Patterns that mode 5 only warned about — sharing non-Sendable state across concurrency boundaries — become hard errors in mode 6.

Source compatibility

Source compatibility means existing code still compiles unchanged. Swift's rule is that a project pinned to a given language mode keeps compiling under newer compilers. Apple even maintains a public Source Compatibility Suite of open-source projects that are continuously rebuilt to catch accidental breakage.

What is not guaranteed across major compiler releases is binary framework compatibility for everything — but Swift's ABI stability on Apple platforms (since Swift 5) means apps can rely on the Swift runtime built into the OS rather than bundling their own.

Selecting a mode with -swift-version

On the command line, you choose the language mode with the -swift-version flag:

# Compile a file using Swift 6 language mode
swiftc -swift-version 6 main.swift

# Or stay in the Swift 5 mode for now
swiftc -swift-version 5 main.swift

The compiler picks the rule set from this flag.

In a Swift package you set it declaratively per target with the swiftLanguageModes setting in Package.swift:

// Package.swift
let package = Package(
    name: "MyLibrary",
    targets: [ .target(name: "MyLibrary") ],
    swiftLanguageModes: [.v6]
)

Pinning a package to Swift 6 mode.

In Xcode the same choice appears as the Swift Language Version build setting, which you can set per target.

What "Swift 6 mode" means

Turning on Swift 6 mode is mostly about one thing: complete concurrency checking is on, and its diagnostics are errors rather than warnings. The compiler now proves, at build time, that values shared between tasks and actors are safe to share.

Concretely, mode 6 expects:

// In Swift 6 mode this is an error: shared mutable global state.
var counter = 0

func bump() { counter += 1 }   // ⚠️ not concurrency-safe

// One fix: isolate it to the main actor.
@MainActor var safeCounter = 0

Swift 6 mode pushes you toward provably safe state.

Watch out: Adopting Swift 6 mode is not just a flag flip — it can surface real concurrency issues in existing code. That's the point, but plan for it as a focused migration rather than a free upgrade.

Upgrading gradually

Swift is designed so you don't have to jump to a new mode all at once. The recommended path is incremental:

  1. Update the compiler

    Move to the latest Swift 6 toolchain while keeping your project in language mode 5. Your code should build as before.

  2. Enable upcoming features

    Turn on individual concurrency checks as warnings using upcoming-feature flags, one at a time, and clean up what they report.

  3. Migrate module by module

    Flip targets to mode 6 one at a time. A package can mix modes across targets, so a dependency doesn't have to migrate in lockstep with you.

  4. Adopt Swift 6 mode

    Once a target is clean, set its language mode to 6 so the checks become enforced errors and stay that way.

Xcode also offers a migration assistant that can apply many of the mechanical changes for you as you move through these steps.

Backward deployment

A common worry: "If I write modern Swift, will my app still run on older iPhones?" Largely, yes. Swift language features generally work on any OS your tools support, because the syntax is resolved at compile time. What's gated is access to new runtime APIs and SDK types, which depend on the OS version.

You guard OS-dependent code with availability checks rather than language version checks:

if #available(iOS 18, *) {
    // Use an API introduced in iOS 18
} else {
    // Fallback for earlier systems
}

@available(iOS 18, *)
func newFeature() { /* ... */ }

#available and @available manage OS-version access.

Some newer library features even ship with back-deployment support, meaning Apple bundles the implementation so the API works on OS versions released before it existed. The @backDeployed attribute is how library authors opt in to that.

Tip: Keep the two checks mentally separate. -swift-version chooses the language rules; #available guards access to OS APIs. They solve different problems.

Putting it together

Swift lets you adopt the newest compiler immediately, choose your language mode independently, and deploy to older systems with availability checks. That layered approach is why the ecosystem can keep advancing without leaving existing apps behind. With versioning understood, you're ready for a fast, hands-on tour of the language itself.