HomeiOS DevelopmentDo you have to opt-in to Swift 6.2’s Major Actor isolation? –...

Do you have to opt-in to Swift 6.2’s Major Actor isolation? – Donny Wals


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:

  1. Ensure that loadMovies runs on the identical actor as its callsite (that is what nonisolated(nonsending) would obtain)
  2. 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.

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

- Advertisment -
Google search engine

Most Popular

Recent Comments