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
loadMovies
runs on the identical actor as its callsite (that is whatnonisolated(nonsending)
would obtain) - Be sure that
loadMovies
runs 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.