Revealed on: September 11, 2025
Swift 6.2 comes with a some attention-grabbing Concurrency enhancements. One of the notable modifications is that there is now a compiler flag that may, by default, isolate all of your (implicitly nonisolated) code to the principle actor. It is a enormous 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 among the complexities that concurrency introduces naturally, and we’ll assess whether or not transferring code to the principle actor is the (appropriate) resolution to those issues.
By the tip of this put up, you must hopefully be capable of resolve for your self whether or not or not fundamental actor isolation is smart. I encourage you to learn by means of all the put up and to rigorously take into consideration your code and its wants earlier than you leap to conclusions. In programming, the best reply to most issues depends upon the precise issues at hand. That is no exception.
We’ll begin off by trying on the defaults for fundamental actor isolation in Xcode 26 and Swift 6. Then we’ll transfer on to figuring out whether or not we should always maintain these defaults or not.
Understanding how Most important Actor isolation is utilized by default in Xcode 26
If you create a brand new undertaking in Xcode 26, that undertaking could have two new options enabled:
- International 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 Most important Actor or no actor in any respect (nil
and MainActor.self
are the one two legitimate values).
Which means all code that you just write in a undertaking created with Xcode 26 will likely be remoted to the principle 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 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 fundamental actor remoted by default is that your app will successfully be single threaded until you explicitly introduce concurrency. All the things you do will begin off on the principle thread and keep there until you resolve it’s essential to depart the Most important Actor.
Understanding how Most important 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 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 there’s an attention-grabbing distinction between code in your SPM Packages and your app goal.
In your app goal, every little thing will run on the Most important 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 should you’re calling your nonisolated
async
features from the principle actor in your app goal they are going to run on the Most important Actor. Name them from elsewhere and so they’ll run there.
In your SPM Packages, the default is in your code to not run on the Most important 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 operating code on the Most important Actor by default
In a codebase that depends closely on concurrency, you will 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 knowledge race potential. Which means Swift will flag quite a lot of potential points (if you’re utilizing the Swift 6 language mode) even if 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 need to handle the concurrency in your code rigorously 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 activity
view modifier to retrieve knowledge:
struct MoviesList: View {
@State var movieRepository = MovieRepository()
@State var motion pictures = [Movie]()
var physique: some View {
Group {
if motion pictures.isEmpty == false {
Listing(motion pictures) { film in
Textual content(film.id.uuidString)
}
} else {
ProgressView()
}
}.activity {
do {
// Sending 'self.movieRepository' dangers inflicting knowledge races
motion pictures = strive await movieRepository.loadMovies()
} catch {
motion pictures = []
}
}
}
}
This code has a difficulty: sending self.movieRepository dangers inflicting knowledge 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 principle actor. That is an issue as a result of within 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 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 Most important Actor
Choice 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 Most important Actor anyway.
Relying on the contents of loadMovies
and the features 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-Most important Actor remoted operate internally on an object that is not Sendable nor remoted to the Most important 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 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 (knowledge, _) = strive await URLSession.shared.knowledge(for: request)
// Sending 'self' dangers inflicting knowledge races
return strive await decode(knowledge)
}
nonisolated func decode(_ knowledge: Information) async throws -> T {
return strive JSONDecoder().decode(T.self, from: knowledge)
}
}
We have @MainActor
remoted all async
features apart from decode
. At this level we will not name decode
as a result of we will not 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 we have now causes that forestall us from doing so. Possibly the true object holds on to mutable state.
We might repair our downside by truly making all of MovieRepository
remoted to the Most important Actor. That method, we are able to safely move self
round even when it has mutable state. And we are able to nonetheless maintain our decode
operate as nonisolated
and async
to stop it from operating on the Most important 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 flawed. It looks like we’re having to lower the standard of our code simply to make the compiler completely satisfied.
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 will likely be nice.
That is nearly by no means true. Concurrency is just not one of the best default to have.
In code that you just wrote pre-Swift Concurrency, most of your features would simply run wherever they had been known as from. In follow, this meant that quite a lot of your code would run on the principle thread with out you worrying about it. It merely was how issues labored by default and should 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
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 Most important 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 (knowledge, _) = strive await URLSession.shared.knowledge(for: request)
return strive await decode(knowledge)
}
@concurrent func decode(_ knowledge: Information) async throws -> T {
return strive JSONDecoder().decode(T.self, from: knowledge)
}
}
Our code is far 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
operate as @concurrent
. By doing this, I be certain that decode
is just not fundamental 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 features in MoviesRepository
run on the Most important Actor. That is completely effective 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 Most important Actor can do different work till the operate I am awaiting returns.
Efficiency influence of Most important 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 dangerous we should not be afraid to run code on the principle thread.
Every time a program runs code on one thread, then hops to a different, after which again once more, there is a efficiency value to be paid. It is a small value often, but it surely’s a value both method.
It is typically cheaper for a fast operation that began on the Most important 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 Most important Actor. Being on the Most important Actor by default implies that it is way more express if you’re leaving the Most important Actor which makes it simpler so that you can decide whether or not you are able to pay the price for thread hopping or not. I am unable to resolve for you what the cutoff is for it to be price paying a value, I can solely let you know that there’s a value. And for many apps the price might be sufficiently small for it to by no means matter. By defaulting to the Most important Actor you may keep away from paying the price unintentionally and I feel that is factor.
So, do you have to set your default isolation to the Most important Actor?
On your app targets it makes a ton of sense to run on the Most important Actor by default. It lets you write easier code, and to introduce concurrency solely if you want it. You may nonetheless mark objects as nonisolated
if you discover that they must be used from a number of actors with out awaiting every interplay with these objects (fashions are instance of objects that you will in all probability mark nonisolated
). You should use @concurrent
to make sure sure async
features do not run on the Most important Actor, and you should utilize nonisolated
on features 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 in all probability don’t need it to make use of the principle actor by default. As a substitute, you will need to make every little thing within the Package deal Sendable
for instance. Or perhaps you need to design your Networking
object as an actor
. Its’ solely as much as you.
For those who’re constructing UI Packages, you in all probability do need to isolate these to the Most important Actor by default since just about every little thing that you just do in a UI Package deal needs to be used from the Most important Actor anyway.
The reply is not a easy “sure, you must”, however I do assume that if you’re doubtful isolating to the Most important 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 “Most important 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.