Migrating to Swift 6 Tutorial

The migration path to Swift 6 is now a lot smoother, with lots more guideposts. Work through this tutorial to find out how much easier it’s become. By Audrey Tam.

Leave a rating/review
Download materials
Save for later
Share

Swift 6 appeared at WWDC 2024, and all of us rushed to migrate all our apps to it … well, not really. We were pretty happy with what we got at WWDC 2021 — Swift 5.5’s shiny new structured concurrency framework that helped us write safe code more swiftly with async/await and actors. Swift 6 seemed to break everything, and it felt like a good idea to wait a while.

One year later, the migration path looks a lot smoother, with lots more guideposts. Keep reading to find out how much easier it’s become.

From Single-Thread to Concurrency

The goal of Swift 6.2 concurrency is to simplify your app development. It identifies three phases, where you introduce concurrency explicitly, as and when you need it:

  1. Run everything on the main thread: Start with synchronous execution on the main thread — if every operation is fast enough, your app’s UI won’t hang.
  2. async/await: If you need to perform a slow operation, create and await an async function to do the work. This function still runs on the main thread, which interleaves its work with work from other tasks, like responding to the user scrolling or tapping. For example, if your app needs to download data from a server, your asynchronous function can do some setup then await a URLSession method that runs on a background thread. At this point, your function suspends, and the main thread is free to do some other work. When the URLSession method finishes, your function is ready to resume execution on the main thread, usually to provide some new data to display to the user.
  3. Concurrency: As you add more asynchronous operations to the main thread, your app’s UI might become less responsive. Profile your app with Instruments to find performance problems and see if you can fix the problem — speed up the slow operation — without concurrency. If not, introduce concurrency to move that operation to a background thread and perhaps use async let or task groups to run sub-tasks in parallel to take advantage of the multiple CPUs on the device.

Isolation Domains

Swift 6.2 concurrency aims to eliminate data races, which happen when a process on one thread modifies data while a process on another thread is accessing that data. Data races can only arise when your app has mutable objects, which is why Swift encourages you to use let and value types like struct as much as possible.

The main tools to prevent data races are data isolation and isolation domains:

The critical feature of an isolation domain is the safety it provides. Mutable state can only be accessed from one isolation domain at a time. You can pass mutable state from one isolation domain to another, but you can never access that state concurrently from a different domain. This guarantee is validated by the compiler.

There are three categories of isolation domain:

  1. Actor
  2. Global actor
  3. Non-isolated

Actors protect their mutable objects by maintaining a serial queue for asynchronous requests coming from outside their isolation domain. A GlobalActor must have a static property called shared that exposes an actor instance that you make globally accessible — you don’t need to inject the actor from one type to another, or into the SwiftUI environment.

From Embracing Swift concurrency:

Nonisolated code is very flexible, because you can call it from anywhere: if you call it from the main actor, it will stay on the main actor. If you call it from a background thread, it will stay on a background thread. This makes it a great default for general-purpose libraries.

Data isolation guarantees that non-isolated entities cannot access the mutable state of other domains, so non-isolated functions and variables are always safe to access from any other domain.

Non-isolated is the default domain at swift.org because non-isolated code cannot mutate state protected in another domain. However, new Xcode 26 projects will have MainActor as the default isolation domain, so every operation runs on the main thread unless you do something to move work onto a background thread. The main thread is serial, so mutable MainActor objects can be accessed by at most one process at a time.

Migrating to Swift 6.2

Swift.org Migration Guide

The Swift Migration Guide suggests a process for migrating Swift 5 code to Swift 6. While in Swift 5 language mode, incrementally enable Swift 6 checking in your project’s Build Settings. Enable these settings one at a time, in any order, and address any issues that arise:

Upcoming Features suggested by swift.org’s migration strategy

Upcoming Features suggested by swift.org's migration strategy

Upcoming Features suggested by swift.org’s migration strategy

In your project’s Build Settings, these are in Swift Compiler — Upcoming Features:

Upcoming Features suggestions in Xcode Build Settings

Upcoming Features suggestions in Xcode Build Settings

Upcoming Features suggestions in Xcode Build Settings

Note: I don’t see an exact match for GlobalConcurrency, but it might be Isolated Global Variables.

Then, enable complete concurency checking to turn on the remaining data isolation checks. In Xcode, this is the Strict Concurrency Checking setting in Swift Compiler — Concurrency.

Xcode Build Settings: Swift Compiler — Concurrency

Xcode Build Settings: Swift Compiler — Concurrency

Xcode Build Settings: Swift Compiler — Concurrency

Xcode 26 Default Settings

New Xcode 26 projects will have these default settings for the other two Swift Compiler — Concurrency settings:

  • Approachable Concurrency: Yes: Enables a suite of upcoming features that make easier to work with concurrency.
  • Default Actor Isolation: MainActor: Isolates code on the MainActor unless you mark it as something else.

Enabling Approachable Concurrency enables several Upcoming Features, including two of the swift.org’s migration strategy suggestions:

Upcoming Features that Approachable Concurrency enables

Upcoming Features that Approachable Concurrency enables

Upcoming Features that Approachable Concurrency enables

If this raises too many issues, disable Approachable Concurrency and try the swift.org migration strategy instead.

Getting Started

Use the Download Materials button at the top or bottom of this article to download the starter project, then open it in Xcode 26 (beta).

TheMet is a project from SwiftUI Apprentice. It searches The Metropolitan Museum of Art, New York for objects matching the user’s query term.

TheMet app: search for Persimmon

TheMet app: search for Persimmon

TheMet app: search for Persimmon

TheMetService has two methods:

  • getObjectIDs(from:) constructs the query URL and downloads ObjectID values of art objects that match the query term.
  • getObject(from:) fetches the Object for a specific ObjectID.

TheMetStore instantiates TheMetService and, in fetchObjects(for:) calls getObjectIDs(from:) then loops over the array of ObjectID to populate its objects array.

ContentView instantiates TheMetStore and calls its fetchObjects(from:) method when it appears and when the user enters a new query term.

The sample app uses this Thread extension from SwiftLee’s post Swift 6.2: A first look at how it’s changing Concurrency to show which threads fetchObjects(for:), getObjectIDs(from:) and getObject(from:) are running on.

<code>nonisolated extension Thread {
  /// A convenience method to print out the current thread from an async method.
  /// This is a workaround for compiler error:
  /// Class property 'current' is unavailable from asynchronous contexts; 
  /// Thread.current cannot be used from async contexts.
  /// See: https://github.com/swiftlang/swift-corelibs-foundation/issues/5139
  public static var currentThread: Thread {
    return Thread.current
  }
}
</code>

In this tutorial, you’ll migrate TheMet to Swift 6.2 concurrency.

Build and run and watch the console:

Store and Service methods running on background threads

Store and Service methods running on background threads

Store and Service methods running on background threads

TheMetStore and TheMetService methods run entirely on background threads, except when fetchObjects(for:) appends an object to objects, which ContentView displays. However, in Swift 6.2’s three-phase app development process, only the URLSession method needs to run off the main thread. You’ll soon fix this!

Contributors

Audrey Tam

Author, Tech Editor, Editor&Final Pass Editor

Over 300 content creators. Join our team.