
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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Migrating to Swift 6 Tutorial
15 mins
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:
- 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.
-
async/await: If you need to perform a slow operation, create and
await
anasync
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 thenawait
aURLSession
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 theURLSession
method finishes, your function is ready to resume execution on the main thread, usually to provide some new data to display to the user. -
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:
There are three categories of isolation domain:
- Actor
- Global actor
- 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:
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
In your project’s Build Settings, these are in Swift Compiler — Upcoming Features:
Upcoming Features suggestions in Xcode Build Settings
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 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
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
TheMetService
has two methods:
-
getObjectIDs(from:)
constructs the queryURL
and downloadsObjectID
values of art objects that match the query term. -
getObject(from:)
fetches theObject
for a specificObjectID
.
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
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!