"Every annotation you add under Swift 6 is a fact the old build was silently assuming."
The Three Words
- An actor owns mutable state and serializes access to it. Code outside the actor reaches in with
await; inside, methods run one at a time.@MainActoris the one global actor you already know: "the main thread", as a type. Sendablemarks a value that is safe to hand to another isolation domain. A struct or enum whose fields are allSendableis inferredSendableas long as it is notpublic, and actors always are. Afinalclass with only immutableSendablestate can beSendabletoo, but only when it declares the conformance.@unchecked Sendableis your promise instead of the compiler's — legitimate for a small type that protects its state with a lock, dishonest for anything else.
Inside your own code, these fit together neatly. The friction is at the boundary with frameworks designed before Swift concurrency existed. Three patterns come up again and again in the family's apps.
1. A Main-Actor Class Conforming to an Old Protocol
A file browser's @MainActor view controller adopted NSServicesMenuRequestor and Swift 6 refused: conformance crosses into main actor-isolated code and can cause data races. The protocol does not say where AppKit calls it, so the compiler cannot assume main. You can: AppKit always calls it on the main thread. Say so on the conformance itself — extension LibraryController: @MainActor NSServicesMenuRequestor — an isolated conformance. @preconcurrency is the fallback that turns the question into a runtime check.
2. A Nonisolated Delegate That Always Arrives on Main
A terminal library's delegate protocol was declared nonisolated, but the library delivered every callback on the main queue. The Mac terminal app declared its witnesses nonisolated and wrapped each body in MainActor.assumeIsolated { … }: a statement of the delivery contract that the runtime checks, while an isolated conformance is checked by the compiler, and at run time only when a dynamic cast looks it up (off its actor, the cast fails). One caution from the same app: calling assumeIsolated inside a high-frequency mouse-moved event monitor crashed the Swift 6.3 runtime. There, a plain Task { @MainActor in … } hop was the safe shape. Note too that NSEvent is not Sendable, so the closure cannot return the event — return a Bool and map it outside.
3. An Actor Behind a Synchronous Protocol Requirement
An iOS client's network layer was an actor conforming to a protocol whose requirement was a synchronous function returning an AsyncThrowingStream. An actor method is isolated, and an isolated method cannot satisfy a synchronous requirement: "conformance crosses into actor-isolated code". The fix is to make the witness nonisolated, and do every touch of actor state inside the stream's task through a private isolated helper. Immutable lets of Sendable type stay readable from the nonisolated method.