Home Articles

Swift 6 Unleashed in Xcode 16: Enabling New Features and Strict Concurrency

Note

Good news! Apple has released a new version Xcode 16.

Xcode 16 and Swift 6 Compiler

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).

Xcode 16 and Swift 6

Xcode 16 and Swift 6

How to enable Swift 6 upcoming features?

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.

Enabling Swift 6 upcoming features

Enabling Swift 6 upcoming features

How to update the language version?

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)

Setting Swift language version in SPM

Update Swift Language Version Mode

Update Swift Language Version Mode

How to enable the strict concurrency checking?

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:

  1. Minimal: This enforces Sendable constraints only where they’ve been explicitly adopted and performs actor-isolation checks for code that has adopted concurrency.

  1. Targeted: This level performs actor-isolation checks and applies Sendable constraints to explicitly adopted code.

  1. 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.

Enable Strict Concurrency Check

Enable Strict Concurrency Check

Swift Evolution Proposals

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.

SE Proposals - Swift 6

SE Proposals - Swift 6

Swift 6 Breaking Changes

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.

Data Isolation

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.

Data Isolation

Data Isolation

Isolation Domain

Methods and variables are isolated into distinct regions, where each is classified as non-isolated, actor-isolated, or global actor-isolated

Example of Isolation Domain

Example of Isolation Domain

Isolation Boundary

The boundary between isolation domains

Example of Isolation Boundary

Example of Isolation Boundary

Crossing Isolation Boundaries

Moving values into or out of an isolation domain is known as crossing an isolation boundary

Crossing Isolation Boundary

Crossing 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.

Incremental migration to concurrency checking

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.

Incremental Migration

Incremental Migration

Strict concurrency for global variables

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.

Strict concurrency for global variables

Strict concurrency for global variables

Region Based Isolation

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.

Region Based Isolation

Region Based Isolation

Inheritance of actor isolation

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.

Inheritance of actor Isolation

Inheritance of actor Isolation

Dynamic actor isolation enforcement

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.

Dynamic actor isolation enforcement

Dynamic actor isolation enforcement

Sending parameter and result values

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.

Sending parameter and result values

Sending parameter and result values

@isolated(any) Function Types

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:

  1. Non-isolated function: By default, everything in Swift is non-isolated unless specified otherwise.

  1. Function isolated to a global actor: This one is isolated to the MainActor.

  1. Function isolated to a passed actor instance: This function runs in the context or isolation of the actor instance passed to it.

@isolated(any) function types

@isolated(any) function types

SwiftUI implications

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.

SwiftUI Implications

SwiftUI Implications

Challenges of Isolation

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.

Challenges of Isolation

Challenges of Isolation

Decide when to upgrade to strict checking

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.

When to Upgrade to strict checking

When to Upgrade to strict checking

How to ensure a smoother migration?

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.

Ensure smoother migration

Ensure smoother migration

Swift Package Index Podcast

Checkout the YouTube link here - https://www.youtube.com/watch?v=dkhaZcL4wnY&ab_channel=SwiftPackageIndex

Swiftpackageindex Podcast

Swiftpackageindex Podcast

Takeaways

  • 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.

Takeaways

Takeaways


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.