Give your simulator superpowers

RocketSim: An Essential Developer Tool
as recommended by Apple

Swift 6: Incrementally migrate your Xcode projects and packages

Apple announced Swift 6 during WWDC 2024 as a major release of their programming language. It aims to create a fantastic development experience, and many of the latest features we know today are part of the road toward this major version bump.

The Swift development team shared their focus areas for 2023 and released a detailed road to Swift 6 post earlier. Both share a vision of Swift’s future and helped us prepare and migrate our project gracefully. In this article, I’ll guide you in incrementally migrating your Xcode projects and packages to Swift 6.

What to expect from Swift 6?

All the Swift 5.x releases build up toward the release of Swift 6 and progress on the goals presented on the road to Swift 6. Those releases might look minor initially, but examples like the async/await release in Swift 5.5 prove the opposite. They’ve all been small steps toward this major sixth release.

This significant release also introduces several new features like typed throws, but the primary focus has been on eliminating all data races.

Eliminating all data races

Swift 6’s goal has always been to eliminate all data races. Once you migrate your projects to Swift 6, you’ll notice several warnings related to Sendable and concurrency. These warnings guide you towards making your project thread-safe, eliminating data races and race conditions (they’re not the same).

Some of your app’s crashes are likely related to data races, while you have no clue how to reproduce them. Don’t be surprised to see them disappear after migrating successfully.

What is the release date of Swift 6?

Swift 6 was released during WWDC 2024 on June 11th and became officially available with Xcode 16.

Incrementally migrating your Xcode Projects

The time it takes to migrate your projects to Swift 6 depends on the type and size of your project. I recommend incremental adoption in all cases to isolate the changes and to enable you to open pull requests that aim to be small enough for reviews.

You’ll follow the same steps for migrating modules (Swift Packages) as for Xcode projects, but you’ll apply build settings inside the Package.swift file. Each migration follows the following steps:

  • Determine an isolated part of your project. This will either be an individual target, test target, or module.
  • Enable upcoming language features for Swift 6, one by one.
  • Increase the strict concurrency checking from minimal to targeted and finally to complete.
  • After fixing all warnings in each step, you can change the language mode to Swift 6.

Before diving into details, here’s a bit of mental guidance since it can be quite the ride for more extensive projects:

  • Take your time
  • Don’t panic
  • It’s fine to have warnings in your project during the migration

With that in mind, we’re ready to dive into more details.

1. Determining an isolated part of your project

It’s generally recommended to focus on an isolated piece of code when doing large refactors. Migrating to Swift 6 is definitely a potentially large refactor, so it’s essential to pick a piece of isolated code. By this, I mean code that can be compiled in isolation. Examples are targets or individual modules.

If you can, try to pick an app extension with fewer code files. This will allow you to familiarize yourself with migrating a part of your code to Swift 6.

2. Enable upcoming Swift 6 language features, one by one

The next step would be to enable upcoming language features one by one. You can do this by going into build settings and searching for “Upcoming features”:

You can incrementally adopt Swift 6 by enabling upcoming features one by one.
You can incrementally adopt Swift 6 by enabling upcoming features one by one.

The filtered list of build settings shows available upcoming language features and the strict concurrency checking setting. I recommend focusing on the build settings that contain the $(SWIFT_UPCOMING_FEATURE_6_0) variable, as these relate to Swift 6 directly. These features will also be enabled automatically when you change the project’s language feature to version six.

You’ll likely see new warnings after enabling one of the upcoming features. Some of these warnings will become errors when you’ve updated your language version, so try to fix as many as you can. Once done, open a pull request with just these changes before moving towards the next upcoming feature.

For Swift packages, you can enable upcoming features as follows:

.target(
    name: "WindowMonitoring",
    dependencies: [],
    swiftSettings: [
        .enableUpcomingFeature("SWIFT_UPCOMING_FEATURE_FORWARD_TRAILING_CLOSURES")
    ]
)

The key for each upcoming feature can be found inside Xcode’s Quick Help after selecting the build setting:

You can find the key to use inside Swift packages inside Xcode's Quick Help.
You can find the key to use inside Swift packages inside Xcode’s Quick Help.

3. Enabling Strict Concurrency Checking

Enabling upcoming features one by one prepares your project for strict concurrency checking. The strict concurrency checking build setting controls the level of Sendable enforcement and actor-isolation checking performed by the Swift compiler.

There are three levels to pick from:

  • Minimal: Enforce Sendable constraints only where they have been explicitly adopted and perform actor-isolation checking wherever code has adopted concurrency.
  • Targeted: Enforce Sendable constraints and perform actor-isolation checking wherever code has adopted concurrency, including code that has explicitly adopted Sendable.
  • Complete: Enforce Sendable constraints and actor-isolation checking throughout the entire project or module.

Each step results in stricter checking and potentially more warnings. Don’t go too fast here, and adopt each level individually. After fixing the warnings for each level, you can open a pull request and continue to the next level.

If you’re using Swift packages, you can change the strict concurrency level as follows:

.target(
    name: "CoreExtensions",
    dependencies: ["Logging"],
    path: "CoreExtensions/Sources",
    swiftSettings: [
        /// Used to be like this in Xcode 14:
        SwiftSetting.unsafeFlags(["-Xfrontend", "-strict-concurrency=complete"]),

        /// Xcode 15 & 16. Remove `=targeted` to use the default `complete`. Potentially isolate to a platform to further reduce scope.
        .enableExperimentalFeature("StrictConcurrency=targeted", .when(platforms: [.macOS]))
    ]
)

You can find more details in my dedicated article for this build setting. The warnings (or errors) triggered after enabling this setting give you insights into areas of improvement. As a team, I recommend enabling this setting by default to migrate your codebase gracefully. Code implementations like networking layers can be a great start since they’ll likely allow you to adopt async/await higher up in more places.

4. Change the Swift Language Version to Swift 6

The final step of the migration requires you to change the Swift Language Version to Swift 6. Go into the build settings and search for Swift Language Version:

The final step of the migration requires you to change the Swift Language Version to Swift 6.
The final step of the migration requires you to change the Swift Language Version to Swift 6.

You might still run into new warnings and errors after enabling, but you’ve likely eliminated a bunch of warnings already due to the incremental migration steps.

For packages, make sure to not get confused by the swift-tools-version:

// swift-tools-version:6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

This is the first line inside your Package.swift files and only determines the minimum version of Swift required to build the package.

To actually update your package’s language version, you’ll need to adopt the following Swift setting:

.target(
    name: "WindowMonitoring",
    dependencies: [],
    swiftSettings: [
        .swiftLanguageVersion(.v6)
    ]
)

Note that you can remove any other Swift Settings for upcoming features or strict concurrency checking since they’ll be enabled by default after updating the language version.

Stay updated with the latest in Concurrency

The 2nd largest newsletter in the Apple development community with 19,429 developers. Don't miss out – Join today:

You can always unsubscribe, no hard feelings.

Frequently Asked Questions (FAQ)

I’m sure many questions pop up when you consider migrating your project. Here’s a list of commonly asked questions to help you out.

Can I only adopt Swift 6 if all my dependencies are migrated?

No, all projects, packages, and dependencies can migrate independently. That also means that you can migrate your projects before any 3rd party dependencies do.

What if a dependency updates to Swift 6 and my project didn’t migrate yet?

Even in this case, you won’t notice anything. You can migrate your project when you are ready, independently from any dependencies.

Isn’t Swift 6 all about async/await?

It’s essential to understand it’s not only about getting rid of closures in favor of async/await. By using the concurrency framework, you’ll allow the compiler to validate your code for thread safety. The concurrency strictness warnings will indicate which types must become sendable, preventing you from creating data races and runtime exceptions.

How do existentials relate to Swift 6?

As described in my article about existential any, Swift 6 will force you to use any in front of existentials to indicate the impact on performance. I recommend reading up on existentials and deciding whether you want to start using an explicit indication of existentials today using the upcoming language feature.

Conclusion

With the major release of Swift 6, it’s time to update our project and packages and eliminate all data races. By performing the migration incrementally, you allow yourself to open pull requests with smaller changes. Ultimately, you’ll benefit from compile-time safety and stricter concurrency checking, preventing nasty bugs and crashes.

If you like to learn more tips on Swift, check out the Swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.

Thanks!