Before Xcode 26, we have 2 global functions:

func syncMethod() {} func asyncMethod() async {}

The behavior is that syncMethod is non-isolated, and will be called in the same isolation context as the caller. The asyncMethod is also non-isolated, but it will be called in the coorperative thread pool.

Let's say we set "Default actor isolation" to main actor, this code will just be auto annotated like this:

@MainActor func syncMethod() { // now i can access main actor stuff } @MainActor func asyncMethod() { // now i can access main actor stuff }

This makes sense.

However, if I enable "nonisolated(sending) by Default" as well, isn't it gonna conflict? Will the global funcs become nonisolated or will they still be @MainActor?

HL666's user avatar

Why not try it and see?

func asyncMethod() async { let iso = #isolation print(iso as Any) } actor MyActor { func test() async { await asyncMethod() } } class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() Task { await asyncMethod() } Task { @concurrent in await asyncMethod() } Task { await MyActor().test() } } }

Prints (with Default Actor Isolation set to MainActor and nonisolated(nonsending) By Default set to YES):

Optional(Swift.MainActor) Optional(Swift.MainActor) Optional(Swift.MainActor)

matt's user avatar

There are two very separate issues at play:

You have identified the historic asymmetry with regard to nonisolated functions, namely where synchronous ones ran on the current thread but asynchronous ones introduced a context hop. “Approachable concurrency” arguably rectifies this, bringing these two into line, where all nonisolated functions simply avoid unnecessary context hops, whether synchronous or asynchronous.

And, in those cases where one actually want the context hop in the async functions, use @concurrent.

This behavior is dictated by the “nonisolated(nonsending) by default” build setting. If “Yes”, it follows the behavior I outline above. If “No”, we have the legacy behavior.

The above notwithstanding, “approachable concurrency” acknowledges a separate, but deep truth: Writing thread-safe code can be complicated, but calling asynchronous API doesn’t need to be. They realized that the bulk of applications operate perfectly well if the whole thing is isolated to the main actor (and merely await asynchronous API). And the beauty of defaulting everything to the main actor is that vast swaths of thread-safety concerns (Sendable types, region-based isolation, etc.) just evaporate.

So “approachable concurrency” basically steers us towards “keep it simple and default to the main actor where reasonable.” They suggest only introducing thread-safety complexities when it is really needed (notably, where you might be doing something slow and synchronous). And even then, in those rare cases where we need to introduce multithreading (e.g., actors, @concurrent functions, etc.), we are no worse off than we were before Swift 6.2.

But, now we can simply sidestep this complexity when it is not needed.

This behavior is dictated by the “Default actor” build setting. If “MainActor”, we get this new (easier) behavior. If “nonisolated”, we get the legacy behavior.

Now, both of these build settings reference “nonisolated”, but there is no inherent conflict between them; they are simply solving two very different problems. But, for new apps, at least, the default build settings are probably well advised:

Build setting Default value in new apps Description
“nonisolated(nonsending) by default” “Yes” Consistency of nonisolated functions, whether synchronous or asynchronous
“Default actor” “MainActor” Eliminate many unnecessary multithreaded considerations

Rob's user avatar

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.