Note
Good news! Apple has released a new version Xcode 16.
Swift 6 compiler, is the 1st version of Swift 6 language mode means, when you install Xcode 16, you get the new llvm compiler and you get Swift 6 language support with it. We can check your version by running swiftc –version
in terminal. Because Swift 6 language mode – is an opting feature (not a mandatory one).
Note
Either in your build settings or package manifest
You can begin leveraging new features from Swift 6, such as strict concurrency checking, without fully upgrading your project to Swift 6. This process, known as Incremental adoption,
It allows you to gradually implement these updates rather than adopting all the changes at once.
To enable new language features, navigate to your project's build settings, search for Swift Compiler - Upcoming Features, and set the features you want to adopt to Yes.
Lets you integrate Swift 6 features progressively, ensuring a smoother transition.
For Swift Package Manager (SPM) packages, you can achieve the same incremental adoption using the .enableUpcomingFeature
API.
Allows you to specify and enable individual features that you want to integrate into your package.
By using this approach, you can selectively update your package with new Swift 6 features while maintaining compatibility with earlier versions.
Note
Either in your build settings or package manifest
To upgrade your project to Swift 6 language mode, navigate to your build settings and select Swift Compiler - Language > Swift Language Version. This upgrade enables all of the new features introduced in Swift 6, including strict concurrency checking.
Note: After upgrading, you may notice that a few features planned for future Swift versions are still listed but disabled by default in Swift 6. For example, the ExistentialAny (SE-335), which was initially planned for Swift 6, was postponed by the Language Steering Group and is not enabled by default in this version.
For Swift Package Manager (SPM) packages, don’t confuse the swift-tools-version with the Swift language version. The swift-tools-version
specified at the top of your Package.swift
Package.swift file only sets the minimum Swift version required to build the package. To upgrade your package to Swift 6, you need to explicitly adopt the following Swift setting:
.swiftLanguageVersion(.v6)
Note
Either in your build settings or package manifest
If you only want to enable strict concurrency checking, you can update your build settings accordingly. Some of you might have already experimented with this, as it was available in Xcode 15 for projects using Swift 5.10. Has anyone tried it in Xcode 15 yet?
There are three levels of concurrency checking to choose from:
Minimal
: This enforces Sendable constraints only where they’ve been explicitly adopted and performs actor-isolation checks for code that has adopted concurrency.
Targeted
: This level performs actor-isolation checks and applies Sendable constraints to explicitly adopted code.
Completed
: Enforces Sendable constraints and actor-isolation checks across the entire project or module.
Each step introduces stricter checking, potentially leading to more warnings. It’s important to proceed gradually, adopting each level one at a time. After resolving the warnings for each level, you can open a pull request and move on to the next level.
Swift 6 brings significant advancements beyond Swift concurrency, with proposals focusing on improved type safety, performance optimizations, and language consistency.
Key changes include Strict Concurrency Checking, the introduction of ExistentialAny for clearer type usage, enhanced Sendable
and actor
isolation models, and refined macro support for code generation.
Swift 6 also delivers optimizations for generics, improved memory management, and a more expressive pattern matching system, making it a robust upgrade for modern Swift development. These changes set the stage for both safer code and better performance across projects.
Swift 6 introduces several concurrency-focused proposals that significantly enhance the language's safety and performance.
Swift 6 introduces several breaking changes, which are opt-in. Along with new features, many previously accepted proposals, marked as "upcoming features," will now be enabled by default.
In Xcode, under Build Settings > Upcoming Features, you can find the breaking changes listed on the left, with corresponding options on the right to help manage these updates in your project.
Swift 6 introduces a new language mode that eliminates data races at compile time by enforcing data isolation.
What is Data Isolation?
Mechanism - used to make data races impossible
Deals with accessing mutable state in unsafe ways not all data races
Isolation can change when you use await keyword
Isolation applies to variables, functions, closures, protocols
Why is Data Isolation Important?
To identify data races at compile time
How Does Data Isolation Work?
Compiler validates data passed over boundaries is either safe or mutually exclusive
Where Does Data Isolation Apply?
variables, functions, closures, protocols
Data Isolation is Similar to…?
Conceptually similar to a lock
Apart from flavours, we have important concepts in Isolation.
Methods and variables are isolated into distinct regions, where each is classified as non-isolated, actor-isolated, or global actor-isolated
The boundary between isolation domains
Moving values into or out of an isolation domain is known as crossing an isolation boundary
The addWithGlobalIsolation
method, which is actorIsolated, attempts to access the globallyIsolatedValue
, which is isolated by a global actor, asynchronously. This demonstrates crossing between two isolation domains. Additionally, values can also cross boundaries indirectly when captured by closures.
Remember, incremental migration is recommended when transitioning to Swift 6.
If you're using Xcode 15 and Swift 5.9, you'll need to enable the experimentalFeature
compiler flag to activate strict concurrency checking.
In Xcode 16 with Swift 5, you'll use the upcomingFeature
flag for this purpose.
In Xcode 16 with Swift 6, strict concurrency checks are enabled by default.
Let’s look at some important breaking changes and tips for preparing for strict concurrency checking:
Global state, such as static variables, has traditionally been accessible from anywhere in a program, making them vulnerable to concurrent access. Before Swift's data-race safety, it was up to the programmer to handle these carefully, without compiler support.
Now, in Swift 6, Global or Static Variables are not concurrency-safe in non-isolated contexts, and the compiler won't allow them either. To address this:
Convert global or static variables to let
constants and ensure the type conforms to Sendable
.
If the value is used only within a specific concurrency domain, you can add a global actor attribute (e.g., @MainActor
) to its declaration.
If the type's usage is concurrency-safe, you can mark it as nonisolated(unsafe) after auditing the type.
Swift achieves data race safety through two key concepts: actor isolation and region isolation. These mechanisms determine where a value is stored and where a piece of code executes, along with ensuring sendability checks.
In earlier versions of Swift, passing a non-Sendable type, like a reference type, across isolation boundaries would raise a potential data race issue. For instance, passing an object from the @MainActor
in OpenNewAccount
to the SharedActor
in ClientStore.shared
would trigger a compiler error before Swift 6. The compiler flagged this because concurrent access could occur, making the code unsafe.
However, in Swift 6, the region analysis introduces flow-sensitive analysis. This means that even though the object isn't Sendable, Swift 6 can detect that the value is passed to another actor and no longer accessed in the @MainActor
. As a result, Swift can safely move the value without worrying about data races, since the value is only owned and modified by the new actor.
This improvement allows for better handling of non-Sendable types when crossing isolation boundaries, making it easier to work with these types safely and efficiently.
Before Swift 6, isolated parameters couldn't express non-isolation. For example, if you wanted to call a function from a non-isolated context, you couldn't because an actor instance was required, but there was no way to represent "non-isolated."
Swift 6 fixes this by allowing isolated parameters to accept optional arguments, which solves this issue.
Additionally, Swift previously lacked a convenient way to capture the current isolation, requiring manual handling and boilerplate code. Swift 6 introduces #isolation, a new expression similar to #line
or #file
, allowing you to capture the current isolation context easily.
Let’s explore enforcing actor isolation checks dynamically at runtime in non-strict concurrency contexts.
Consider a protocol defined in an external module or package. When you import the package and conform to this protocol, the compiler raises an error: main actor instances cannot satisfy non-isolated protocol requirements.
How can we avoid this? One option is to add the nonisolated
keyword before the function. However, this removes static data race safety, which isn't ideal.
The correct solution in Swift 6 is to use the @preconcurrency
annotation. This allows you to maintain the same runtime behavior without sacrificing static isolation and data race safety.
Let’s take this example where we have a NonSendable class and a trySend()
method that accepts a non-Sendable value. We want to pass this value to another actor, such as @MainActor
, from a non-isolated async function.
Before Swift 6, this would raise an error. This makes sense because the compiler cannot determine how the non-Sendable variable is used across all possible call sites of trySend
. While some cases might be safe, others might not, so the compiler errs on the side of caution.
However, Swift 6 addresses this by introducing the Sending keyword. This keyword relates to Sendable protocol conformance but works differently. It signifies that when a non-Sendable value is passed into trySend()
, ownership of that value is transferred, allowing it to be safely passed to another concurrency domain.
Essentially, marking a function parameter or result with Sending
ensures the value is disconnected at the function boundary and can be safely passed between isolated contexts.
In Swift, every declaration has a well-defined static isolation, allowing us to determine its isolation. However, closures behave differently. Their isolation is influenced not just by where they are defined but also by what they capture. As a result, closures can lose static isolation information, like the context in which they were defined.
By adding @isolated(any) to a closure, you enforce that it must be called with await
, even if it doesn't cross an isolation boundary. This way, the closure can access isolation information from the surrounding context. Initially, it may seem odd to think of closures having properties, but it's just a specific type of closure that can carry such information.
Now, let's examine a few examples:
Non-isolated function: By default, everything in Swift is non-isolated unless specified otherwise.
Function isolated to a global actor: This one is isolated to the MainActor
.
Function isolated to a passed actor instance: This function runs in the context or isolation of the actor instance passed to it.
In Xcode 16, Apple made a significant adjustment to the View protocol in SwiftUI. Previously, only the body
property was annotated with @MainActor
, which could lead to confusion. Now, the entire View protocol is annotated with @MainActor
, not just body
. This change is backwards compatible, meaning that all types conforming to the View protocol will automatically inherit the @MainActor
annotation.
You might be wondering what this change means in practice. For example, if you run a check in Xcode 15, it will return false because only the body
is marked as @MainActor
. But in Xcode 16, this will return true since the entire view is now @MainActor
.
If you want to exit this isolation context, you'll need to use a non-isolated context, which will still return false in both Xcode versions.
Handling actor reentrancy, task cancellation, and managing related warnings and errors can be tricky in Swift. The migration from existing code to the new concurrency model may also feel unclear, as it involves refactoring for actor isolation and async tasks.
Challenges include:
Actor reentrancy: Preventing unexpected behavior when actors handle multiple tasks.
Cancellation: Managing task interruptions smoothly.
Migration: Refactoring older code to fit the new concurrency rules, which can be confusing.
Taking a gradual approach to migration can make the process easier.
You can track the adoption of Swift 6 in popular packages by visiting swiftpackageindex.com. This platform provides updates on package compatibility and how widely Swift 6 features are being integrated across the ecosystem.
Migrating to Swift 6 varies by project size. For a smoother process, adopt Swift 6 incrementally.
Start with an isolated part of your project—like a target or module. Enable Swift 6 features step by step, fixing warnings as you go. Gradually increase concurrency checks and, once issues are resolved, switch fully to Swift 6.
Focus on small, isolated parts like app extensions or individual modules. If your app has multiple modules, migrate one at a time, starting with the app itself and then its dependencies. This makes the process more manageable.
Checkout the YouTube link here - https://www.youtube.com/watch?v=dkhaZcL4wnY&ab_channel=SwiftPackageIndex
Warning-Free Compilation is Not the Ultimate Goal: The focus is on improving code safety and performance, not just eliminating warnings.
Clear Areas for Improvement: The migration process highlights key areas needing attention, making it easier to address potential issues.
Improved Safety: Swift 6 enhances safety, particularly with strict concurrency and isolation checks, making code more reliable.
Please check out basics of Swift Concurrency (introduced in Swift 5.5) - Strict Concurrency in Swift 6 - Part 1
This is a free third party commenting service we are using for you, which needs you to sign in to post a comment, but the good bit is you can stay anonymous while commenting.