Revealed on: September 11, 2025
Swift 6.2 comes with some attention-grabbing Concurrency enhancements. Some of the notable adjustments is that there is now a compiler flag that can, by default, isolate all of your (implicitly nonisolated) code to the principle actor. It is a large change, and on this publish we’ll discover whether or not or not it is a good change. We’ll do that by having a look at a number of the complexities that concurrency introduces naturally, and we’ll assess whether or not transferring code to the principle actor is the (appropriate) answer to those issues.
By the tip of this publish, you need to hopefully be capable to determine for your self whether or not or not principal actor isolation is sensible. I encourage you to learn by the complete publish and to rigorously take into consideration your code and its wants earlier than you soar to conclusions. In programming, the precise reply to most issues will depend on the precise issues at hand. That is no exception.
We’ll begin off by wanting on the defaults for principal actor isolation in Xcode 26 and Swift 6. Then we’ll transfer on to figuring out whether or not we should always hold these defaults or not.
Understanding how Foremost Actor isolation is utilized by default in Xcode 26
While you create a brand new venture in Xcode 26, that venture could have two new options enabled:
- World actor isolation is about to
MainActor.self - Approachable concurrency is enabled
If you wish to study extra about approachable concurrency in Xcode 26, I like to recommend you examine it in my publish on Approachable Concurrency.
The worldwide actor isolation setting will routinely isolate all of your code to both the Foremost Actor or no actor in any respect (nil and MainActor.self are the one two legitimate values).
Which means that all code that you just write in a venture created with Xcode 26 might be remoted to the principle actor (except it is remoted to a different actor otherwise you mark the code as nonisolated):
// this class is @MainActor remoted by default
class MyClass {
// this property is @MainActor remoted by default
var counter = 0
func performWork() async {
// this operate is @MainActor remoted by default
}
nonisolated func performOtherWork() async {
// this operate is nonisolated so it is not @MainActor remoted
}
}
// this actor and its members will not be @MainActor remoted
actor Counter {
var depend = 0
}The results of your code bein principal actor remoted by default is that your app will successfully be single threaded except you explicitly introduce concurrency. Every little thing you do will begin off on the principle thread and keep there except you determine it is advisable to depart the Foremost Actor.
Understanding how Foremost Actor isolation is utilized for brand new SPM Packages
For SPM packages, it is a barely totally different story. A newly created SPM Package deal is not going to have its defaultIsolation flag set in any respect. Which means that a brand new SPM Package deal will not isolate your code to the MainActor by default.
You may change this by passing defaultIsolation to your goal’s swiftSettings:
swiftSettings: [
.defaultIsolation(MainActor.self)
]Observe {that a} newly created SPM Package deal additionally will not have Approachable Concurrency turned on. Extra importantly, it will not have NonIsolatedNonSendingByDefault turned on by default. Which means that there’s an attention-grabbing distinction between code in your SPM Packages and your app goal.
In your app goal, every part will run on the Foremost Actor by default. Any capabilities that you have outlined in your app goal and are marked as nonisolated and async will run on the caller’s actor by default. So when you’re calling your nonisolated async capabilities from the principle actor in your app goal they may run on the Foremost Actor. Name them from elsewhere and so they’ll run there.
In your SPM Packages, the default is on your code to not run on the Foremost Actor by default, and for nonisolated async capabilities to run on a background thread it doesn’t matter what.
Complicated is not it? I do know…
The rationale for operating code on the Foremost Actor by default
In a codebase that depends closely on concurrency, you will must cope with lots of concurrency-related complexity. Extra particularly, a codebase with lots of concurrency could have lots of knowledge race potential. Which means that Swift will flag lots of potential points (once you’re utilizing the Swift 6 language mode) even once you by no means actually meant to introduce a ton of concurrency. Swift 6.2 is a lot better at recognizing code that is protected regardless that it is concurrent however as a common rule you wish to handle the concurrency in your code rigorously and keep away from introducing concurrency by default.
Let us take a look at a code pattern the place now we have a view that leverages a job view modifier to retrieve knowledge:
struct MoviesList: View {
@State var movieRepository = MovieRepository()
@State var films = [Movie]()
var physique: some View {
Group {
if films.isEmpty == false {
Listing(films) { film in
Textual content(film.id.uuidString)
}
} else {
ProgressView()
}
}.job {
do {
// Sending 'self.movieRepository' dangers inflicting knowledge races
films = strive await movieRepository.loadMovies()
} catch {
films = []
}
}
}
}This code has a problem: sending self.movieRepository dangers inflicting knowledge races.
The explanation we’re seeing this error is because of us calling a nonisolated and async technique on an occasion of MovieRepository that’s remoted to the principle actor. That is an issue as a result of within loadMovies now we have entry to self from a background thread as a result of that is the place loadMovies would run. We even have entry to our occasion from within our view at the very same time so we’re certainly making a attainable knowledge race.
There are two methods to repair this:
- Be sure that
loadMoviesruns on the identical actor as its callsite (that is whatnonisolated(nonsending)would obtain) - Be sure that
loadMoviesruns on the Foremost Actor
Possibility 2 makes lots of sense as a result of, so far as this instance is worried, we all the time name loadMovies from the Foremost Actor anyway.
Relying on the contents of loadMovies and the capabilities that it calls, we would merely be transferring our compiler error from the view over to our repository as a result of the newly @MainActor remoted loadMovies is looking a non-Foremost Actor remoted operate internally on an object that is not Sendable nor remoted to the Foremost Actor.
Ultimately, we would find yourself with one thing that appears as follows:
class MovieRepository {
@MainActor
func loadMovies() async throws -> [Movie] {
let req = makeRequest()
let films: [Movie] = strive await carry out(req)
return films
}
func makeRequest() -> URLRequest {
let url = URL(string: "
return URLRequest(url: url)
}
@MainActor
func carry out(_ request: URLRequest) async throws -> T {
let (knowledge, _) = strive await URLSession.shared.knowledge(for: request)
// Sending 'self' dangers inflicting knowledge races
return strive await decode(knowledge)
}
nonisolated func decode(_ knowledge: Knowledge) async throws -> T {
return strive JSONDecoder().decode(T.self, from: knowledge)
}
} We have @MainActor remoted all async capabilities aside from decode. At this level we won’t name decode as a result of we won’t safely ship self into the nonisolated async operate decode.
On this particular case, the issue could possibly be fastened by marking MovieRepository as Sendable. However let’s assume that now we have causes that stop us from doing so. Possibly the true object holds on to mutable state.
We may repair our drawback by really making all of MovieRepository remoted to the Foremost Actor. That manner, we will safely cross self round even when it has mutable state. And we will nonetheless hold our decode operate as nonisolated and async to forestall it from operating on the Foremost Actor.
The issue with the above…
Discovering the answer to the problems I describe above is fairly tedious, and it forces us to explicitly opt-out of concurrency for particular strategies and ultimately a whole class. This feels unsuitable. It seems like we’re having to lower the standard of our code simply to make the compiler pleased.
In actuality, the default in Swift 6.1 and earlier was to introduce concurrency by default. Run as a lot as attainable in parallel and issues might be nice.
That is virtually by no means true. Concurrency will not be the very best default to have.
In code that you just wrote pre-Swift Concurrency, most of your capabilities would simply run wherever they had been known as from. In follow, this meant that lots of your code would run on the principle thread with out you worrying about it. It merely was how issues labored by default and when you wanted concurrency you’d introduce it explicitly.
The brand new default in Xcode 26 returns this conduct each by operating your code on the principle actor by default and by having nonisolated async capabilities inherit the caller’s actor by default.
Which means that the instance we had above turns into a lot easier with the brand new defaults…
Understanding how default isolation simplifies our code
If we flip set our default isolation to the Foremost Actor together with Approachable Concurrency, we will rewrite the code from earlier as follows:
class MovieRepository {
func loadMovies() async throws -> [Movie] {
let req = makeRequest()
let films: [Movie] = strive await carry out(req)
return films
}
func makeRequest() -> URLRequest {
let url = URL(string: "
return URLRequest(url: url)
}
func carry out(_ request: URLRequest) async throws -> T {
let (knowledge, _) = strive await URLSession.shared.knowledge(for: request)
return strive await decode(knowledge)
}
@concurrent func decode(_ knowledge: Knowledge) async throws -> T {
return strive JSONDecoder().decode(T.self, from: knowledge)
}
} Our code is way easier and safer, and we have inverted one key a part of the code. As an alternative of introducing concurrency by default, I needed to explicitly mark my decode operate as @concurrent. By doing this, I be sure that decode will not be principal actor remoted and I be sure that it all the time runs on a background thread. In the meantime, each my async and my plain capabilities in MoviesRepository run on the Foremost Actor. That is completely tremendous as a result of as soon as I hit an await like I do in carry out, the async operate I am in suspends so the Foremost Actor can do different work till the operate I am awaiting returns.
Efficiency impression of Foremost Actor by default
Whereas operating code concurrently can improve efficiency, concurrency does not all the time improve efficiency. Moreover, whereas blocking the principle thread is unhealthy we should not be afraid to run code on the principle thread.
At any time when a program runs code on one thread, then hops to a different, after which again once more, there is a efficiency price to be paid. It is a small price often, however it’s a value both manner.
It is usually cheaper for a fast operation that began on the Foremost Actor to remain there than it’s for that operation to be carried out on a background thread and handing the consequence again to the Foremost Actor. Being on the Foremost Actor by default implies that it is rather more specific once you’re leaving the Foremost Actor which makes it simpler so that you can decide whether or not you are able to pay the associated fee for thread hopping or not. I can not determine for you what the cutoff is for it to be value paying a value, I can solely inform you that there’s a price. And for many apps the associated fee might be sufficiently small for it to by no means matter. By defaulting to the Foremost Actor you’ll be able to keep away from paying the associated fee by accident and I feel that is an excellent factor.
So, must you set your default isolation to the Foremost Actor?
To your app targets it makes a ton of sense to run on the Foremost Actor by default. It means that you can write easier code, and to introduce concurrency solely once you want it. You may nonetheless mark objects as nonisolated once you discover that they should be used from a number of actors with out awaiting every interplay with these objects (fashions are an excellent instance of objects that you’re going to in all probability mark nonisolated). You need to use @concurrent to make sure sure async capabilities do not run on the Foremost Actor, and you should use nonisolated on capabilities that ought to inherit the caller’s actor. Discovering the proper key phrase can generally be a little bit of a trial and error however I usually use both @concurrent or nothing (@MainActor by default). Needing nonisolated is extra uncommon in my expertise.
To your SPM Packages the choice is much less apparent. When you’ve got a Networking package deal, you in all probability don’t need it to make use of the principle actor by default. As an alternative, you will wish to make every part within the Package deal Sendable for instance. Or possibly you wish to design your Networking object as an actor. Its’ solely as much as you.
Should you’re constructing UI Packages, you in all probability do wish to isolate these to the Foremost Actor by default since just about every part that you just do in a UI Package deal must be used from the Foremost Actor anyway.
The reply is not a easy “sure, you need to”, however I do assume that once you’re unsure isolating to the Foremost Actor is an efficient default alternative. While you discover that a few of your code must run on a background thread you should use @concurrent.
Apply makes good, and I hope that by understanding the “Foremost Actor by default” rationale you can also make an informed choice on whether or not you want the flag for a particular app or Package deal.

