Revealed on: September 11, 2025
Swift 6.2 comes with a some attention-grabbing Concurrency enhancements. One of the vital notable modifications is that there is now a compiler flag that may, by default, isolate all of your (implicitly nonisolated) code to the primary actor. This can be 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 few of the complexities that concurrency introduces naturally, and we’ll assess whether or not shifting code to the primary actor is the (appropriate) answer to those issues.
By the tip of this publish, it is best to hopefully be capable to resolve for your self whether or not or not most important actor isolation is smart. I encourage you to learn by way of the complete publish and to fastidiously take into consideration your code and its wants earlier than you soar to conclusions. In programming, the appropriate reply to most issues relies on the precise issues at hand. That is no exception.
We’ll begin off by trying on the defaults for most important actor isolation in Xcode 26 and Swift 6. Then we’ll transfer on to figuring out whether or not we must always maintain these defaults or not.
Understanding how Major Actor isolation is utilized by default in Xcode 26
If you create a brand new venture in Xcode 26, that venture could have two new options enabled:
- World actor isolation is ready to
MainActor.self
- Approachable concurrency is enabled
If you wish to be taught 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 Major Actor or no actor in any respect (nil
and MainActor.self
are the one two legitimate values).
Which means all code that you simply write in a venture created with Xcode 26 will likely be remoted to the primary actor (until 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 perform is @MainActor remoted by default
}
nonisolated func performOtherWork() async {
// this perform is nonisolated so it isn't @MainActor remoted
}
}
// this actor and its members will not be @MainActor remoted
actor Counter {
var depend = 0
}
The results of your code bein most important actor remoted by default is that your app will successfully be single threaded until you explicitly introduce concurrency. Every thing you do will begin off on the primary thread and keep there until you resolve that you must go away the Major Actor.
Understanding how Major Actor isolation is utilized for brand spanking new SPM Packages
For SPM packages, it is a barely completely different story. A newly created SPM Bundle is not going to have its defaultIsolation
flag set in any respect. Which means a brand new SPM Bundle 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)
]
Be aware {that a} newly created SPM Bundle additionally will not have Approachable Concurrency turned on. Extra importantly, it will not have NonIsolatedNonSendingByDefault
turned on by default. Which means 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 Major Actor by default. Any features that you’ve got outlined in your app goal and are marked as nonisolated
and async
will run on the caller’s actor by default. So if you happen to’re calling your nonisolated
async
features from the primary actor in your app goal they may run on the Major Actor. Name them from elsewhere they usually’ll run there.
In your SPM Packages, the default is to your code to not run on the Major Actor by default, and for nonisolated
async
features to run on a background thread it doesn’t matter what.
Complicated is not it? I do know…
The rationale for working code on the Major Actor by default
In a codebase that depends closely on concurrency, you may should take care of quite a lot of concurrency-related complexity. Extra particularly, a codebase with quite a lot of concurrency could have quite a lot of information race potential. Which means Swift will flag quite a lot of potential points (whenever you’re utilizing the Swift 6 language mode) even whenever you by no means actually meant to introduce a ton of concurrency. Swift 6.2 is significantly better at recognizing code that is secure though it is concurrent however as a normal rule you wish to handle the concurrency in your code fastidiously and keep away from introducing concurrency by default.
Let us take a look at a code pattern the place we’ve a view that leverages a process
view modifier to retrieve information:
struct MoviesList: View {
@State var movieRepository = MovieRepository()
@State var films = [Movie]()
var physique: some View {
Group {
if films.isEmpty == false {
Checklist(films) { film in
Textual content(film.id.uuidString)
}
} else {
ProgressView()
}
}.process {
do {
// Sending 'self.movieRepository' dangers inflicting information races
films = strive await movieRepository.loadMovies()
} catch {
films = []
}
}
}
}
This code has a difficulty: sending self.movieRepository dangers inflicting information races
.
The rationale we’re seeing this error is because of us calling a nonisolated
and async
methodology on an occasion of MovieRepository
that’s remoted to the primary actor. That is an issue as a result of inside loadMovies
we’ve 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 inside our view at the very same time so we’re certainly making a doable information race.
There are two methods to repair this:
- Ensure that
loadMovies
runs on the identical actor as its callsite (that is whatnonisolated(nonsending)
would obtain) - Ensure that
loadMovies
runs on the Major Actor
Possibility 2 makes quite a lot of sense as a result of, so far as this instance is anxious, we all the time name loadMovies
from the Major Actor anyway.
Relying on the contents of loadMovies
and the features that it calls, we’d merely be shifting our compiler error from the view over to our repository as a result of the newly @MainActor
remoted loadMovies
is looking a non-Major Actor remoted perform internally on an object that is not Sendable nor remoted to the Major Actor.
Finally, we’d 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: "https://instance.com")!
return URLRequest(url: url)
}
@MainActor
func carry out(_ request: URLRequest) async throws -> T {
let (information, _) = strive await URLSession.shared.information(for: request)
// Sending 'self' dangers inflicting information races
return strive await decode(information)
}
nonisolated func decode(_ information: Knowledge) async throws -> T {
return strive JSONDecoder().decode(T.self, from: information)
}
}
We have @MainActor
remoted all async
features 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
perform decode
.
On this particular case, the issue may very well be mounted by marking MovieRepository
as Sendable
. However let’s assume that we’ve causes that stop us from doing so. Perhaps the actual object holds on to mutable state.
We may repair our downside by truly making all of MovieRepository
remoted to the Major Actor. That means, we will safely go self
round even when it has mutable state. And we will nonetheless maintain our decode
perform as nonisolated
and async
to forestall it from working on the Major 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 finally a complete class. This feels incorrect. It appears like we’re having to lower the standard of our code simply to make the compiler joyful.
In actuality, the default in Swift 6.1 and earlier was to introduce concurrency by default. Run as a lot as doable in parallel and issues will likely be nice.
That is virtually by no means true. Concurrency is just not the perfect default to have.
In code that you simply wrote pre-Swift Concurrency, most of your features would simply run wherever they have been referred to as from. In observe, this meant that quite a lot of your code would run on the primary thread with out you worrying about it. It merely was how issues labored by default and if you happen to wanted concurrency you’d introduce it explicitly.
The brand new default in Xcode 26 returns this conduct each by working your code on the primary actor by default and by having nonisolated
async
features inherit the caller’s actor by default.
Which means 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 Major 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: "https://instance.com")!
return URLRequest(url: url)
}
func carry out(_ request: URLRequest) async throws -> T {
let (information, _) = strive await URLSession.shared.information(for: request)
return strive await decode(information)
}
@concurrent func decode(_ information: Knowledge) async throws -> T {
return strive JSONDecoder().decode(T.self, from: information)
}
}
Our code is far 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
perform as @concurrent
. By doing this, I make sure that decode
is just not most important actor remoted and I make sure that it all the time runs on a background thread. In the meantime, each my async
and my plain features in MoviesRepository
run on the Major Actor. That is completely tremendous as a result of as soon as I hit an await
like I do in carry out
, the async
perform I am in suspends so the Major Actor can do different work till the perform I am awaiting returns.
Efficiency influence of Major Actor by default
Whereas working code concurrently can enhance efficiency, concurrency would not all the time enhance efficiency. Moreover, whereas blocking the primary thread is unhealthy we should not be afraid to run code on the primary 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, nevertheless it’s a value both means.
It is typically cheaper for a fast operation that began on the Major Actor to remain there than it’s for that operation to be carried out on a background thread and handing the outcome again to the Major Actor. Being on the Major Actor by default implies that it is way more specific whenever you’re leaving the Major Actor which makes it simpler so that you can decide whether or not you are able to pay the fee for thread hopping or not. I can not resolve for you what the cutoff is for it to be price paying a value, I can solely inform you that there’s a price. And for many apps the fee might be sufficiently small for it to by no means matter. By defaulting to the Major Actor you may keep away from paying the fee by chance and I believe that is an excellent factor.
So, do you have to set your default isolation to the Major Actor?
In your app targets it makes a ton of sense to run on the Major Actor by default. It permits you to write easier code, and to introduce concurrency solely whenever you want it. You may nonetheless mark objects as nonisolated
whenever you discover that they must be used from a number of actors with out awaiting every interplay with these objects (fashions are an excellent instance of objects that you will in all probability mark nonisolated
). You should utilize @concurrent
to make sure sure async
features do not run on the Major Actor, and you should utilize nonisolated
on features 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 sometimes use both @concurrent
or nothing (@MainActor
by default). Needing nonisolated
is extra uncommon in my expertise.
In 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 primary actor by default. As an alternative, you may wish to make every part within the Bundle Sendable
for instance. Or possibly you wish to design your Networking
object as an actor
. Its’ completely as much as you.
In case you’re constructing UI Packages, you in all probability do wish to isolate these to the Major Actor by default since just about every part that you simply do in a UI Bundle needs to be used from the Major Actor anyway.
The reply is not a easy “sure, it is best to”, however I do assume that whenever you’re unsure isolating to the Major Actor is an efficient default alternative. If you discover that a few of your code must run on a background thread you should utilize @concurrent
.
Apply makes good, and I hope that by understanding the “Major Actor by default” rationale you can also make an informed resolution on whether or not you want the flag for a selected app or Bundle.