Swift 6.2: default MainActor and @concurrent

Concurrency Senior Swift 6.2+ iOS 17+ Reviewed By Alok Singh

Predict the output

The app target uses Xcode 26 defaults (MainActor isolation). A button awaits maker.make(from:). Where does make run?

struct ThumbnailMaker {
    func make(from data: Data) async -> UIImage? {
        // expensive decode and resize
        UIImage(data: data)?.preparingThumbnail(of: .init(width: 300, height: 300))
    }
}
Options
Hint 1 of 2

What is the default isolation in this target?

Hint 2 of 2

Does async imply a background thread?

Choose an answer, then open Show answer below to check it.

Show answer

Answer: B On the main actor, so the heavy decode can stall the UI

Correct. With default MainActor isolation the struct and its method are main-actor isolated. Mark make @concurrent to move it off.

Why not the others?
  • A On a background thread, because the function is async. async means the function can suspend; it says nothing about which thread runs it.
  • C On whichever thread happens to be free. Swift concurrency runs isolated code on its actor's executor; for the main actor that is the main thread.
  • D On a new thread created for each call. Swift concurrency never creates a thread per call; it uses a fixed pool plus the main thread.

30-second answer

In Xcode 26 app targets, code is MainActor-isolated by default, so most UI code needs no annotations. Nonisolated async functions now run on the caller's actor instead of hopping to the background (nonisolated(nonsending)). When you really want work on the concurrent pool, mark the function @concurrent.

Practise saying it · 45 s. No recording, just a timer

Summarize Swift 6.2's concurrency defaults.

Say your answer out loud, then check which of these points you covered:

  • Default MainActor isolation in app targets
  • nonisolated async runs on the caller's actor
  • @concurrent opts into the concurrent pool
  • async ≠ background

Deep dive

Default isolation is a per-module setting (-default-isolation MainActor); packages and libraries usually keep nonisolated.

nonisolated(nonsending) became the default for nonisolated async functions, which removes many 'sending non-Sendable value' errors.

@concurrent makes the function nonisolated and always runs it on the concurrent pool.

async still doesn't mean background: under these defaults, an async function you wrote runs on the main actor unless marked otherwise.

Opting heavy work out Type-checked · Swift 6.4
struct ThumbnailMaker {
    @concurrent
    func make(from data: Data) async -> UIImage? {
        UIImage(data: data)?.preparingThumbnail(of: .init(width: 300, height: 300))
    }
}

Common mistakes

  • Assuming every async function runs in the background.
  • Turning on default MainActor isolation in a library module.
  • Sprinkling @concurrent everywhere instead of only on heavy CPU work.

Senior take & follow-ups

Seniors explain the trade: fewer annotations and errors, but heavy work must now be moved off main on purpose with @concurrent.

Follow-up questions

Think of your answer first, then open the hint.

  1. Where does a nonisolated async function run in Swift 6.2?

    Show hint

    On the caller's actor, unless it is @concurrent.

  2. Why keep libraries nonisolated by default?

    Show hint

    Their callers may not be on the main actor.

Suggest a fix if something here is wrong or out of date.