Revealed on: September 11, 2025
Swift 6.2 comes with a some attention-grabbing Concurrency enhancements. One of the crucial notable modifications is that there is now a compiler flag that can, by default, isolate all of your (implicitly nonisolated) code to the primary actor. This can be a big change, and on this put up we’ll discover whether or not or not it is a good change. We’ll do that by looking at a number of the complexities that concurrency introduces naturally, and we’ll assess whether or not transferring code to the primary actor is the (right) resolution to those issues.
By the tip of this put up, it is best to hopefully have the ability to determine for your self whether or not or not major actor isolation is smart. I encourage you to learn by way of your entire put up and to fastidiously take into consideration your code and its wants earlier than you leap 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 trying on the defaults for major actor isolation in Xcode 26 and Swift 6. Then we’ll transfer on to figuring out whether or not we must always preserve these defaults or not.
Understanding how Major Actor isolation is utilized by default in Xcode 26
While you create a brand new undertaking in Xcode 26, that undertaking may have two new options enabled:
- World actor isolation is ready 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 put up on Approachable Concurrency.
The worldwide actor isolation setting will robotically 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).
Because of this all code that you simply write in a undertaking created with Xcode 26 might be remoted to the primary 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 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 major actor remoted by default is that your app will successfully be single threaded except you explicitly introduce concurrency. All the things you do will begin off on the primary thread and keep there except you determine it’s essential to depart the Major Actor.
Understanding how Major Actor isolation is utilized for brand spanking new SPM Packages
For SPM packages, it is a barely totally different story. A newly created SPM Package deal won’t have its defaultIsolation
flag set in any respect. Because of this a brand new SPM Package deal will not isolate your code to the MainActor by default.
You’ll be able to change this by passing defaultIsolation
to your goal’s swiftSettings
:
swiftSettings: [
.defaultIsolation(MainActor.self)
]
Notice {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. Because of this 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 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 in the event you’re calling your nonisolated
async
capabilities from the primary actor in your app goal they’ll 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
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 Major Actor by default
In a codebase that depends closely on concurrency, you may must cope with a variety of concurrency-related complexity. Extra particularly, a codebase with a variety of concurrency may have a variety of information race potential. Because of this Swift will flag a variety of potential points (whenever you’re utilizing the Swift 6 language mode) even whenever you by no means actually supposed 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 common rule you wish to handle the concurrency in your code fastidiously and keep away from introducing concurrency by default.
Let’s take a look at a code pattern the place we have now a view that leverages a job
view modifier to retrieve information:
struct MoviesList: View {
@State var movieRepository = MovieRepository()
@State var motion pictures = [Movie]()
var physique: some View {
Group {
if motion pictures.isEmpty == false {
Checklist(motion pictures) { film in
Textual content(film.id.uuidString)
}
} else {
ProgressView()
}
}.job {
do {
// Sending 'self.movieRepository' dangers inflicting information races
motion pictures = strive await movieRepository.loadMovies()
} catch {
motion pictures = []
}
}
}
}
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 have now 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 attainable information 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 Major Actor
Choice 2 makes a variety 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 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-Major Actor remoted perform internally on an object that is not Sendable nor remoted to the Major Actor.
Finally, we would find yourself with one thing that appears as follows:
class MovieRepository {
@MainActor
func loadMovies() async throws -> [Movie] {
let req = makeRequest()
let motion pictures: [Movie] = strive await carry out(req)
return motion pictures
}
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: Information) async throws -> T {
return strive JSONDecoder().decode(T.self, from: information)
}
}
We have @MainActor
remoted all async
capabilities apart 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 fastened by marking MovieRepository
as Sendable
. However let’s assume that we have now causes that forestall us from doing so. Perhaps the true object holds on to mutable state.
We may repair our downside by really making all of MovieRepository
remoted to the Major Actor. That method, we are able to safely move self
round even when it has mutable state. And we are able to nonetheless preserve our decode
perform as nonisolated
and async
to forestall it from operating 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 ultimately a complete class. This feels fallacious. It looks 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 attainable in parallel and issues might be nice.
That is nearly by no means true. Concurrency just isn’t the most effective default to have.
In code that you simply wrote pre-Swift Concurrency, most of your capabilities would simply run wherever they had been known as from. In follow, this meant that a variety of your code would run on the primary thread with out you worrying about it. It merely was how issues labored by default and in the event you wanted concurrency you’d introduce it explicitly.
The brand new default in Xcode 26 returns this habits each by operating your code on the primary actor by default and by having nonisolated
async
capabilities inherit the caller’s actor by default.
Because of this 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 are able to rewrite the code from earlier as follows:
class MovieRepository {
func loadMovies() async throws -> [Movie] {
let req = makeRequest()
let motion pictures: [Movie] = strive await carry out(req)
return motion pictures
}
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: Information) async throws -> T {
return strive JSONDecoder().decode(T.self, from: information)
}
}
Our code is way easier and safer, and we have inverted one key a part of the code. As a substitute of introducing concurrency by default, I needed to explicitly mark my decode
perform as @concurrent
. By doing this, I be certain that decode
just isn’t major actor remoted and I be certain 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 Major Actor. That is completely nice 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 affect of Major Actor by default
Whereas operating code concurrently can enhance efficiency, concurrency does not all the time enhance efficiency. Moreover, whereas blocking the primary thread is dangerous we should not be afraid to run code on the primary thread.
Each time 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 normally, but it surely’s a value both method.
It is usually 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 signifies that it is rather more express 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 am unable to determine 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 unintentionally and I believe that is a superb factor.
So, do you have to set your default isolation to the Major Actor?
On your app targets it makes a ton of sense to run on the Major Actor by default. It lets you write easier code, and to introduce concurrency solely whenever you want it. You’ll be able to 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 a superb instance of objects that you’re going to most likely mark nonisolated
). You should use @concurrent
to make sure sure async
capabilities do not run on the Major Actor, and you should use nonisolated
on capabilities that ought to inherit the caller’s actor. Discovering the proper key phrase can typically 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.
On your SPM Packages the choice is much less apparent. In case you have a Networking
package deal, you most likely don’t desire it to make use of the primary actor by default. As a substitute, you may 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.
If you happen to’re constructing UI Packages, you most likely do wish to isolate these to the Major Actor by default since just about every part that you simply do in a UI Package deal must be used from the Major Actor anyway.
The reply is not a easy “sure, it is best to”, however I do suppose that whenever you’re doubtful isolating to the Major Actor is an efficient default selection. While you discover that a few of your code must run on a background thread you should use @concurrent
.
Observe makes excellent, and I hope that by understanding the “Major Actor by default” rationale you may make an informed determination on whether or not you want the flag for a selected app or Package deal.