Apple introduced some changes regarding Swift Concurrency in WWDC 25. While these changes may not affect the old projects you are working on, there is a chance you could accidentally hang your apps without modifying your codebase.
Before reading this blog, it’s recommended that you watch the following WWDC sessions first:
To start, consider the following code. Can you identify which threads these two functions (process()
& processInternal()
) run on?
Note:
isMain
is annonisolated
static property in my Thread extension to avoid concurrency warnings.
Prior to this year’s Xcode 26 and Swift 6.2, you can tell at least both are NOT in the main thread, as they are marked as asynchronized functions with async
, and the TaskProcessor
class is not bound to any Actor. To dive more into this case, the whole class is nonisolated
. Calling nonisolated
async functions will result in switching to global generic executor in your Xcode 16 projects.
Default Actor Isolation in Xcode 26 Beta
Now, if you run the same code on a new project created by Xcode 26 Beta, with the following settings being set by default:
The Default Actor Isolation
is set to MainActor
instead of nonisolated
, while the Approachable Concurrency
is still No
by default, which is a new feature and will be covered later.
The Default Actor Isolation being set to MainActor
means that the class has an implicit @MainActor
annotation.
If you run the code, you will notice that both of the functions are running on Main Thread. If you do the heavy-lifting work in those method, the UI will be blocked since the main thread is busy doing the work.
This is a huge deal. As this may hang your apps easily.
To switch off the main thread (or main actor if you like), you can mark the functions with the nonisolated
keyword.
In this case, both functions will be run off the main thread. This is still a behavior that is easily understandable, as Default Actor Isolation
setting to MainActor
just implicitly adds @MainActor
to the class definition.
However, with the following setting, nonisolated
functions can become tricky—or clearer, depending on your perspective on Swift evolution.
Approachable Concurrency in Xcode 26 Beta
Now, set the Approachable Concurrency
to YES
, and let’s see what would happen.
Now, both nonisolated
async
functions will now run on the Main Thread!
Said by Main Thread 💔: I just can’t catch a break, can I?
Under the hood, the Approachable Concurrency will enable the NonisolatedNonsendingByDefault
flag, which is an upcoming language feature. When this feature is enabled, nonisolated(nonsending)
is the default behavior for all functions, which means that:
Nonisolated async functions always run on the caller’s actor by default.
In this case, the caller’s actor is the Main Actor with Default Actor Isolation setting to MainActor
. More cases:
If you call the nonisolated async functions from the views’ action like the
.task
modifier and the SwiftUI buttons, the caller’s actor is the Main Actor.
If you call the nonisolated async functions in a task created by
Task.detached
, then it’s running in a background thread.
In short, there is NO guarantee that an nonisolated async function will be always run in a background thread, until you add the new @concurrent
annotation:
The second method, processInternal()
is marked as nonisolated only without the new @concurrent
annotation, this mean that it’s run on the caller’s actor, which is the process()
method that runs on a background thread.
The nonisolated
keyword in the process()
function can be omitted, as @concurrent
implies nonisolated
.
My thoughts and conclusions
The whole changes to the concurrency in Swift 6.2 is to let developers adopt concurrency with a more approachable way: by default all should be isolated to the Main Actor, which runs on the Main Thread under the hood to avoid data racing. If you do want to adopt concurrency, then try making async functions and utilizing the new @concurrent
annotation to specifically tell the async functions should run on a background thread.
That said, it’s still easily to break your code and hang you app by simply flipping a few settings and using Xcode 26 to create a new project. Make sure to check the Approachable Concurrency
and Default Actor Isolation
settings in Xcode when working on your old and new projects.
For conclusions, I made a table for you to reference.
Note: the
foo()
method is assumed to be called from theMainActor
. In this case, it’s called in the.task
modifier. For nonisolated functions withNonisolatedNonsendingByDefault
setting, it can run on a background thread based on the caller context.
Links
https://github.com/swiftlang/swift-evolution/blob/main/visions/approachable-concurrency.md
https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-18-release-notes
https://developer.apple.com/videos/play/wwdc2025/268
https://www.donnywals.com/exploring-concurrency-changes-in-swift-6-2/
https://docs.swift.org/compiler/documentation/diagnostics/nonisolated-nonsending-by-default/
Apple did such a mess honestly (