Swift’s concurrency system, introduced in Swift 5.5, simplifies the writing and understanding of asynchronous and parallel code. In Swift 6, language updates further enhance this system by enabling the compiler to ensure that concurrent programs are free of data races. With this update, compiler safety checks, which were previously optional, are now mandatory, providing safer concurrent code by default.
Sooner or later, this will be something every iOS developer will need to adopt in their projects. The migration process can be carried out incrementally and iteratively. The aim of this post is to present concrete solutions for addressing specific issues encountered while migrating code to Swift 6. Keep in mind that there are no silver bullets in programming.
Step 0. Plan your strategy
First, plan your strategy, and be prepared to roll back and re-plan as needed. That was my process, after two rollbacks 🤷:
- Set the “Strict Concurrency Checking” compiler flag on your target. This will bring up a myriad of warnings, giving you the chance to tidy up your project by removing or resolving as many warnings as possible before proceeding.
- Study Migration to Swift 6 (from swift.org), Don’t just skim through it; study it thoroughly. I had to roll back after missing details here.Â
- Set the “Strict Concurrency Checking” compiler flag to Complete. This will trigger another round of warnings. Initially, focus on moving all necessary elements to
@MainActor
to reduce warnings. We’ll work on reducing the main-thread load later.
- Expect
@MainActor
propagation. As you apply@MainActor
, it’s likely to propagate. Ensure you also mark the callee functions as@MainActor
where needed - In protocol delegate implementations, verify that code runs safely on
@MainActor
. In some cases, you may need to make a copy of parameters to prevent data races. - Repeat the process until you’ve resolved all concurrency warnings.
- Check your unit tests, as they’ll likely be affected. If all is clear, change targets and repeat the process..
- Expect
- Set the Swift Language Version to Swift 6 and run the app on a real device to ensure it doesn’t crash. I encountered a crash at this stage..
- Reduce
@MainActor
usage where feasible. Analyze your code to identify parts that could run in isolated domains instead. Singletons and API services are good candidates for offloading from the main thread.
Â
On following sections I will explain the issues that I had found and how I fix them.
Issues with static content
Static means that the property is shared across all instances of that type, allowing it to be accessed concurrently from different isolated domains. However, this can lead to the following issue:
Static property ‘shared’ is not concurrency-safe because non-‘Sendable’ type ‘AppGroupStore’ may have shared mutable state; this is an error in the Swift 6 language mode

The First-Fast-Fix approach here is to move all classes to @MainActor
.
@MainActor
final class AppGroupStore {
let defaults = UserDefaults(suiteName: "group.jca.EMOM-timers")
static let shared = AppGroupStore()
private init() {
}
}
Considerations:
Moving to @MainActor
forces all calls to also belong to @MainActor
, leading to a propagation effect throughout the codebase. Overusing @MainActor
may be unnecessary, so in the future, we might consider transitioning to an actor
instead.
For now, we will proceed with this solution. Once the remaining warnings are resolved, we will revisit and optimize this approach if needed.
Issues with protocol implementation in the system or third-party SDK.
First step: focus on understanding not the origin, but how this data is being transported. How is the external dependency handling concurrency? On which thread is the data being dellivered? As a first step, check the library documentation — if you’re lucky, it will have this information. For instance:
- CoreLocation: Review the delegate specification in the
CLLocationManager
documentation. At the end of the overview, it specifies that callbacks occur on the same thread where you initialized theCLLocationManager
. - HealthKit: Consult the
HKWorkoutSessionDelegate
documentation. Here, the overview mentions that HealthKit calls these methods on an anonymous serial background queue.
In my case, I was working with WatchKit and implementing the WCSessionDelegate
. The documentation states that methods in this protocol are called on a background thread.
Once I understand how the producer delivers data, I need to determine the isolated domain where this data will be consumed. In my case, it was the @MainActor
due to recursive propagation from @MainActor
.
Now, reviewing the code, we encounter the following warning:
Main actor-isolated instance method ‘sessionDidBecomeInactive’ cannot be used to satisfy nonisolated protocol requirement; this is an error in the Swift 6 language mode.

In case this method had no implementation just mark it as nonisolated and move to next:
nonisolated func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
}
The next delegate method does have an implementation and runs on the @MainActor
, while the data is delivered on a background queue, which is a different isolated domain. I was not entirely sure whether the SDK would modify this data.

My adaptation at that point was create a deep copy of data received, and forward the copy to be consumed..
nonisolated func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) {
doCopyAndCallUpdateInMainActor(userInfo)
}
private nonisolated func doCopyAndCallUpdateInMainActor(_ dictionary: [String: Any] = [:]) {
nonisolated(unsafe) let dictionaryCopy = dictionary.deepCopy()
Task { @MainActor in
await self.update(from: dictionaryCopy)
}
}
Issues with default parameter function values
I have a function that receives a singleton as a default parameter. The singletons are working in an isolated domain; in this case, it was under @MainActor
. I encountered the following issue:
Main actor-isolated static property ‘shared’ can not be referenced from a nonisolated context; this is an error in the Swift 6 language mode

To remove this warning, I made it an optional parameter and handled its initialization:
init(audioManager: AudioManagerProtocol? = nil,
extendedRuntimeSessionDelegate: WKExtendedRuntimeSessionDelegate? = nil) {
self.audioManager = audioManager ?? AudioManager.shared
self.extendedRuntimeSessionDelegate = extendedRuntimeSessionDelegate
}
Decoupling non-UI logic from @MainActor for better performance.
There are components in your application, such as singletons or APIs, that are isolated or represent the final step in an execution flow managed by the app. These components are prime candidates for being converted into actors.
Actors provide developers with a means to define an isolation domain and offer methods that operate within this domain. All stored properties of an actor are isolated to the enclosing actor instance, ensuring thread safety and proper synchronization.
Previously, to expedite development, we often annotated everything with @MainActor
.
@MainActor
final class AppGroupStore {
let defaults = UserDefaults(suiteName: "group.jca.XYZ")
static let shared = AppGroupStore()
private init() {
}
}
Alright, let’s move on to an actor.
actor AppGroupStore {
let defaults = UserDefaults(suiteName: "group.jca.XYZ")
static let shared = AppGroupStore()
private init() {
}
}
Compiler complains:
Actor ‘AppGroupStore’ cannot conform to global actor isolated protocol ‘AppGroupStoreProtocol’
That was because protocol definition was also @MainActor, so lets remove it:
import Foundation
//@MainActor
protocol AppGroupStoreProtocol {
func getDate(forKey: AppGroupStoreKey) -> Date?
func setDate(date: Date, forKey: AppGroupStoreKey)
}
At this point, the compiler raises errors for two functions: one does not return anything, while the other does. Therefore, the approaches to fixing them will differ.

We have to refactor them to async/await
protocol AppGroupStoreProtocol {
func getDate(forKey: AppGroupStoreKey) async -> Date?
func setDate(date: Date, forKey: AppGroupStoreKey) async
}
There are now issues in the locations where these methods are called.

This function call does not return any values, so enclosing its execution within Task { ... }
is sufficient.
func setBirthDate(date: Date) {
Task {
await AppGroupStore.shared.setDate(date: date, forKey: .birthDate)
}
}
Next isssue is calling a function that in this case is returning a value, so the solution will be different.

Next, the issue is calling a function that, in this case, returns a value, so the solution will be different.
func getBirthDate() async -> Date {
guard let date = await AppGroupStore.shared.getDate(forKey: .birthDate) else {
return Calendar.current.date(byAdding: .year, value: -25, to: Date()) ?? Date.now
}
return date
}
Changing the function signature means that this change will propagate throughout the code, and we will also have to adjust its callers.

Just encapsulate the call within a Task{...}
, and we are done.
.onAppear() {
Task {
guard await AppGroupStore.shared.getDate(forKey: .birthDate) == nil else { return }
isPresentedSettings.toggle()
}
}
Conclusions
To avoid a traumatic migration, I recommend that you first sharpen your saw. I mean, watch the WWDC 2024 videos and study the documentation thoroughly—don’t read it diagonally. Once you have a clear understanding of the concepts, start hands-on work on your project. Begin by migrating the easiest issues, and as you feel more comfortable, move on to the more complicated ones.
At the moment, it’s not mandatory to migrate everything at once, so you can start progressively. Once you finish, review if some components could be isolated into actors.
Related links
- Migrating to Swift 6
Official Swift.org guide
- Migrate your app to Swift 6
WWDC 2024 video
- CoreLocation
Apple Developer Documentation
- HealthKit
Apple Developer Documentation
- Watch Connectivity
Apple Developer Documentation