← Alle Beitrage

Swift 6.2 Concurrency in der Praxis: Standardmäßig MainActor, gezielt ausbrechen

Swift 6.0 machte Data Races zu einem Compile-Fehler und ließ dafür ein Jahr lang alle bezahlen. Der Prüfer für strikte Concurrency verwandelte gewöhnlichen UI-Code in eine Mauer aus Sendable-Verstößen und Fehlern der Art „main actor-isolated property can not be referenced from a nonisolated context”. Die Diagnose war korrekt (dieser Code konnte tatsächlich racen), aber die schiere Menge begrub das Signal, und viele Teams blieben entweder im Swift-5-Modus oder streuten so lange @MainActor ein, bis die Fehler verstummten.

Swift 6.2 ändert nicht die Regeln, sondern den Standard. Die Garantie für Data-Race-Sicherheit bleibt dieselbe; geändert hat sich, wo der Compiler ansetzt. Übernehmen Sie die neuen Standardwerte, und der Großteil der Mauer verschwindet, denn der Compiler nimmt nun an, was für Ihre App ohnehin schon galt: Der größte Teil Ihres Codes läuft auf dem Main Actor, und Sie verlassen ihn gezielt, an benannten Stellen. Dieses Modell setze ich über die Apps der 941-Familie hinweg ein1, und es ist ein Baustein eines größeren Entwicklungsbogens, den der What’s New in Swift Roundup für 2026 durch die nachfolgenden Proposals zu 6.3 und 6.4 nachzeichnet. So funktioniert es – und das sind die sechs Fehler, die einen weiterhin beißen, sobald man es aktiviert.

TL;DR

  • SE-0466 erlaubt es, ein ganzes Modul standardmäßig auf @MainActor zu setzen. Setzen Sie SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor und hören Sie auf, jede View, jedes Model und jedes View-Model zu annotieren2.
  • SE-0461 sorgt dafür, dass nonisolated async-Funktionen standardmäßig auf dem Actor des Aufrufers laufen (nonisolated(nonsending)), sodass der Aufruf von async-Code keinen Actor-Hop mehr erzwingt – und ebenso wenig die Sendable-Fehler über die Grenze hinweg, die damit einhergingen3.
  • @concurrent ist die Notausstiegsluke. Markieren Sie eine Funktion mit @concurrent, um rechenintensive CPU-Arbeit (Decodierung, Bildverarbeitung) gezielt und sichtbar auf einen Hintergrund-Thread zu verlagern4.
  • Xcode 26 aktiviert beide Standardwerte für neue Projekte. Der übergeordnete Schalter ist SWIFT_APPROACHABLE_CONCURRENCY = YES5.
  • Das Modell kehrt die Last um: Statt für jede Zeile zu beweisen, dass sie sicher außerhalb des Main Actor laufen kann, behalten Sie alles auf dem Main Actor und beweisen nur die wenigen Stellen, an denen Sie ihn verlassen.
  • Sechs konkrete Fehler überleben den Umstieg. Alle sechs lassen sich mit einer einzigen Zeile beheben, sobald Sie das Muster erkennen.

Die Umkehrung – und warum sie wichtig ist

Altes Modell (Swift 6.0): Code ist nonisolated, bis Sie ihn isolieren. Jeder Typ, der UI-State berührte, brauchte @MainActor, jeder async-Aufruf über eine Isolationsgrenze hinweg brauchte Sendable-Konformität, und der Compiler markierte jede Lücke. Bei einer App, deren Code zu 95 Prozent ohnehin schon auf dem Main-Thread läuft, verbrachten Sie Ihre Zeit damit, diese 95 Prozent zu annotieren, um eine Tatsache zu beschreiben, an der nie ein Zweifel bestand.

Neues Modell (Swift 6.2): Code ist auf dem Main Actor, bis Sie ihn verlassen. SE-0466 erlaubt es, die Main-Actor-Isolation als Modulstandard zu deklarieren, sodass eine View, ihr Model und ihre Hilfsfunktionen allesamt @MainActor sind, ohne eine einzige Annotation2. SE-0461 beseitigt dann die zweite Abgabe: Eine nonisolated async-Funktion läuft nun auf demjenigen Actor, der sie aufgerufen hat, statt zum Global Executor zu springen. Sie darauf zu awaiten zieht Sie also nicht über eine Isolationsgrenze und verlangt nicht Sendable für alles im Scope3.

Das mentale Modell ist genau jenes, das dem tatsächlichen Verhalten von UI-Apps entspricht. Standardmäßig auf dem Main-Thread zu sein ist kein Kompromiss; es ist die Wahrheit über eine App, deren State ihre Views sind. Concurrency wird zur Ausnahme, nach der Sie gezielt greifen – benannt und eingegrenzt – statt zum allgegenwärtigen Zustand, gegen den Sie sich in jeder Zeile verteidigen. Die Aufgabe des Compilers kippt von „beweise, dass dies sicher nebenläufig laufen kann” zu „du hast gesagt, dies läuft nebenläufig, also beweise, dass es sicher ist” – und die zweite Frage wird an weitaus weniger Stellen gestellt.

Das Swift-Kernteam hat genau das gesagt. Im Swift Group Lab der WWDC 2026 schilderte Holly, wie nonisolated async-Funktionen, würde man das Concurrency-Modell heute entwerfen, von Anfang an im Kontext des Aufrufers gelaufen wären; sinngemäß sagte sie, dass sie lange am Modell des globalen Concurrent-Pools festgehalten habe und erst durch Probleme in realen Projekten vom Gegenteil überzeugt worden sei. Doug fügte hinzu, dass der alte Standard zu viele Typen in Richtung Sendable gedrängt habe. (Beide Zitate sind sinngemäß aus einer lokal transkribierten Lab-Aufnahme wiedergegeben; Apple hat für die Session keine Untertitel veröffentlicht.)9

Aktivierung

Zwei Build Settings, beide in den Standardwerten von Xcode 26 für neue Projekte enthalten und beide wert, in einem bestehenden Projekt explizit zu setzen5:

SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor
SWIFT_APPROACHABLE_CONCURRENCY = YES

Das erste ist SE-0466: Das Modul ist standardmäßig auf Main-Actor-Isolation eingestellt. Das zweite ist der Schalter, der den Funktionsumfang „approachable concurrency” aktiviert, einschließlich des SE-0461-Verhaltens „der Aufrufer bestimmt, wo es läuft”. In einem Swift Package setzen Sie dieselben Standardwerte über swiftSettings mit den passenden Upcoming-Feature-Flags statt über das Xcode-Build-Setting6.

Aktivieren Sie beide in einem bestehenden Projekt, und die Fehlerzahl fällt drastisch, denn das meiste, was der Prüfer markierte, war Main-Thread-Code, von dem er zuvor nicht annehmen konnte, dass er auf dem Main-Thread lief. Übrig bleibt eine kurze Liste echter Grenzfälle. Es lohnt sich, sie beim Namen zu kennen, denn jeder ist eine Stelle, an der Ihr Code den Main Actor tatsächlich verlässt, und die Lösung besteht darin, das präzise auszusprechen.

Die sechs Fehler, die den Umstieg überleben

Dies sind die Fehler der strikten Concurrency, die unter SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor weiterhin auftreten, entnommen aus der Migration der Apps der 941-Familie auf dieses Modell1. Jeder ist eine echte Grenze, kein False Positive – und genau deshalb ist die Lösung eine präzise Annotation und keine Unterdrückung.

1. Reine Funktionen auf Sendable-Typen. Eine reine Methode auf einem Sendable-Enum (ein URL-Builder, ein Formatter) erbt unter dem Modulstandard die Main-Actor-Isolation und wirft dann beim Aufruf aus einem nonisolated-Kontext einen Fehler: „Call to main actor-isolated instance method in a synchronous nonisolated context.” Die Methode berührt keinerlei State, sie auf den Main Actor zu isolieren ist also falsch. Markieren Sie sie als nonisolated7:

nonisolated func searchURL(for query: String) -> URL? { ... }

2. Singleton-Statics in Standardparametern. static let shared = Foo() ist unter dem Standard main-actor-isoliert, doch Werte von Standardparametern werden im Kontext des Aufrufers ausgewertet, der oft nonisolated ist: „Main actor-isolated static property ‘shared’ can not be referenced from a nonisolated context.” Machen Sie die Static nonisolated. Wenn der Typ Sendable ist (oder @unchecked Sendable, weil Sie seinen State selbst absichern), benötigen Sie keinen unsicheren Qualifizierer:

final class KeychainProxySecretStore: @unchecked Sendable {
    nonisolated static let shared = KeychainProxySecretStore()
}

3. Unveränderliche primitive Konstanten. Dasselbe Standardparameter-Problem trifft eine schlichte Konstante: ein static let defaultInterval, das aus einem nonisolated-Standardargument referenziert wird. Die Lösung ist identisch, und die Konstante lässt sich trivial sicher teilen:

nonisolated static let defaultInterval: TimeInterval = 15 * 60

4. Ein Task-Body, der erfasstes self liest. Eine äußere Closure erfasst [weak self]; darin liest ein Task { @MainActor in self?.foo() } dieses erfasste Optional: „Reference to captured var ‘self’ in concurrently-executing code.” Der Task liest eine var-Bindung aus dem umschließenden Scope nebenläufig – das ist der Race. Erfassen Sie self an der Task-Grenze erneut, damit der Task eine unveränderliche Bindung besitzt:

NotificationCenter.default.addObserver(...) { [weak self] _ in
    Task { @MainActor [weak self] in
        self?.value = next
    }
}

5. KVO-Callbacks, die Main-Actor-State lesen. Ein webView.observe(\.canGoBack) { wv, _ in ... }-Callback ist @Sendable und damit nonisolated, doch WKWebView.canGoBack ist main-actor-isoliert: „Main actor-isolated property ‘canGoBack’ can not be referenced from a Sendable closure.” KVO liefert synchron auf dem Thread, der den Wert mutiert hat, und der Navigationsstate von WKWebView mutiert ausschließlich auf dem Main-Thread, der Lesezugriff ist also korrekt. Versichern Sie das mit MainActor.assumeIsolated, was den Task-Hop vollständig entfernt und synchron bleibt8:

let pushNav: @Sendable (WKWebView?) -> Void = { [weak self] webView in
    MainActor.assumeIsolated {
        guard let self else { return }
        // safe to read webView?.canGoBack synchronously
    }
}

assumeIsolated ist ein Versprechen an den Compiler, keine Frage. Verwenden Sie es nur dort, wo die Laufzeitinvariante wirklich gilt (ein dokumentierter Main-Thread-Callback), denn ein falsches Versprechen ist ein Crash, keine Warnung.

6. Rechenintensive Arbeit, die nicht auf dem Main laufen sollte. Diese markiert der Prüfer nicht, und sie ist die wichtigste, die Sie selbst abfangen müssen. Unter einem Main-Actor-Standard läuft eine synchrone, CPU-gebundene Methode (das JSON-Decodieren einer großen Payload, das Skalieren eines Bildes) auf dem Main Actor und lässt Ihre UI ruckeln. Der Standard behält Sie auf dem Main; @concurrent ist, wie Sie ihn gezielt verlassen4:

@concurrent
func decodeLargePayload(_ data: Data) async throws -> Report {
    try JSONDecoder().decode(Report.self, from: data)
}

@concurrent lagert die Funktion auf den Global Executor aus und ist konstruktionsbedingt gegenseitig ausschließend mit @MainActor, einem benutzerdefinierten Global Actor und nonisolated(nonsending): Eine Funktion läuft entweder dort, wo ihr Aufrufer ist, oder läuft gezielt von ihm weg, niemals mehrdeutig4. Die Disziplin, die das neue Modell verlangt, steckt vollständig in diesem Muster. Bleiben Sie auf dem Main für alles, was die UI berührt, und greifen Sie nur dann zu @concurrent, wenn die Arbeit nachweislich einen Hintergrund-Thread braucht.

Wenn die Standardwerte für Sie falsch sind

Standardmäßig auf dem Main Actor zu sein passt zu Apps: SwiftUI- und UIKit-Code läuft überwältigend auf dem Main-Thread, und der Standard entspricht der Realität. In zwei Fällen passt er weniger gut, und etwas anderes vorzugeben verschwendet Ihre Zeit.

  • Ein Library- oder Framework-Target ohne UI. Eine Networking-Schicht, ein Parser oder eine Daten-Engine hat keinen Grund, standardmäßig auf dem Main Actor zu sein, und genau das zu tun erzwingt @concurrent oder nonisolated auf nahezu allem. Lassen Sie SWIFT_DEFAULT_ACTOR_ISOLATION für solche Targets ungesetzt und isolieren Sie gezielt – auf die alte Art herum.
  • Ein actor-lastiges nebenläufiges System. Wenn Ihr Design tatsächlich vieles parallel laufen lässt (eine echte Pipeline, nicht eine App mit ein paar Hintergrund-Tasks), arbeitet der Main-Actor-Standard gegen Sie. Sie wollen explizite Actors und nonisolated-Code, und der SE-0466-Standard ist der falsche Ausgangspunkt.

Für eine App ist die Entscheidung jedoch einfach: Aktivieren Sie beide Einstellungen, lassen Sie die Fehlerzahl in sich zusammenfallen und behandeln Sie die wenigen verbleibenden als Landkarte genau jener Stellen, an denen Ihr Code den Main-Thread verlässt. Diese Landkarte lohnt sich. Das alte Modell gab Ihnen tausend Warnungen und keine Karte; das neue gibt Ihnen sechs ehrliche Grenzen und einen Standard, der endlich dazu passt, wie die App läuft.

Eine letzte Anmerkung zu Rauschen versus Signal. SourceKit zeigt dateiübergreifende Index-Fehler („Cannot find type X in scope”, „No such module”) im Editor an, während Xcode seinen Index neu aufbaut, besonders direkt nach dem Neugenerieren eines Projekts. Das sind Index-Artefakte, keine Concurrency-Fehler. Wenn xcodebuild BUILD SUCCEEDED meldet, ist das Concurrency-Modell erfüllt und der Editor hinkt nur hinterher1. Index-Gespenstern hinterherzujagen ist der schnellste Weg, einen Nachmittag an einer Migration zu verschwenden, die bereits funktioniert hat.

FAQ

Wie aktiviere ich in Swift 6.2 standardmäßig Main-Actor-Concurrency?

Setzen Sie zwei Build Settings, beide standardmäßig in neuen Xcode-26-Projekten aktiviert: SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor und SWIFT_APPROACHABLE_CONCURRENCY = YES5. In einem Swift Package setzen Sie die entsprechenden Upcoming-Feature-Flags über swiftSettings statt über das Xcode-Build-Setting6.

Was genau ändert SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor?

Es macht den Code eines Moduls standardmäßig main-actor-isoliert (SE-0466), sodass eine View, ihr Model und ihre Hilfsfunktionen allesamt @MainActor sind, ohne Annotationen2. Concurrency wird zur benannten Ausnahme, nach der Sie greifen, statt zum allgegenwärtigen Zustand, gegen den Sie in jeder Zeile annotieren.

Was ist @concurrent und wann sollte ich es verwenden?

@concurrent lagert eine async-Funktion gezielt auf den Global Executor aus, weg vom Actor ihres Aufrufers4. Unter einem Main-Actor-Standard verwenden Sie es für Arbeit, die nachweislich einen Hintergrund-Thread braucht (das Decodieren einer großen Payload, das Skalieren eines Bildes); es ist gegenseitig ausschließend mit @MainActor, sodass eine Funktion entweder dort läuft, wo ihr Aufrufer ist, oder gezielt von ihm weg.

Warum erhalte ich „Main actor-isolated … can not be referenced from a nonisolated context”?

Unter dem Main-Actor-Standard erben reine Funktionen, Singleton-Statics und Konstanten die Main-Actor-Isolation und werfen dann einen Fehler, wenn sie aus einem nonisolated-Kontext aufgerufen werden, etwa aus der Auswertung eines Standardparameters. Die Lösung ist eine präzise nonisolated-Annotation am Member, keine Unterdrückung; der Wert lässt sich sicher teilen, weil er keinen Actor-State berührt7.

Sollte ich Main-Actor-by-default für ein Library- oder Framework-Target verwenden?

Nein. Eine Networking-Schicht, ein Parser oder eine Daten-Engine hat keinen Grund, standardmäßig auf dem Main Actor zu sein, und genau das zu tun erzwingt @concurrent oder nonisolated auf nahezu allem. Lassen Sie SWIFT_DEFAULT_ACTOR_ISOLATION für Nicht-UI-Targets ungesetzt und isolieren Sie gezielt auf die alte Art herum.

Springt das Awaiten einer nonisolated async-Funktion in Swift 6.2 noch zwischen Actors?

Nein. SE-0461 sorgt dafür, dass eine nonisolated async-Funktion auf demjenigen Actor läuft, der sie aufgerufen hat, statt zum Global Executor zu springen. Sie darauf zu awaiten zieht Sie also nicht mehr über eine Isolationsgrenze und verlangt nicht Sendable für alles im Scope3.



  1. Produktivcode des Autors über die iOS-Apps der 941-Familie (Ki, Return, Get Bananas), die allesamt mit SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor und SWIFT_APPROACHABLE_CONCURRENCY = YES ausgeliefert werden. Die sechs Fehlermuster und Lösungen unten waren der vollständige Aufräumsatz für strikte Concurrency in Ki 1.0.0. 

  2. Swift Evolution, SE-0466: Control default actor isolation inference. Erlaubt es einem Modul, standardmäßig auf @MainActor-Isolation zu setzen, sodass UI- und App-Targets auf dem Main Actor laufen, sofern Code nicht über @concurrent oder einen expliziten Actor aussteigt. 

  3. Swift Evolution, SE-0461: Run nonisolated async functions on the caller’s actor by default. Eine nonisolated async-Funktion ist standardmäßig nonisolated(nonsending) und läuft auf dem Actor des Aufrufers statt zum Global Executor zu springen, was die Sendable-Anforderungen über die Grenze hinweg beseitigt, die mit dem Hop einhergingen. 

  4. Swift Evolution, SE-0461 führt das Attribut @concurrent ein, mit dem eine Funktion sich dafür entscheidet, auf dem Global Executor (einem Hintergrund-Thread) zu laufen. @concurrent und nonisolated(nonsending) sind die beiden gegensätzlichen Isolationsmodi für eine nonisolated async-Funktion: Eine Funktion läuft entweder dort, wo ihr Aufrufer ist, oder läuft gezielt von ihm weg. @concurrent lässt sich nicht mit @MainActor oder einem benutzerdefinierten Global Actor kombinieren. 

  5. SWIFT_APPROACHABLE_CONCURRENCY ist das übergeordnete Xcode-Build-Setting, das die Upcoming Features der approachable concurrency aktiviert (einschließlich des SE-0461-Verhaltens), und SWIFT_DEFAULT_ACTOR_ISOLATION wählt die Standard-Isolation des Moduls. Neue Xcode-26-Projekte aktivieren beide mit dem Main-Actor-Standard. Dokumentiert bei Donny Wals, „Exploring concurrency changes in Swift 6.2”, und Paul Hudson, „What’s new in Swift 6.2”, beide gegen die zugrunde liegenden Proposals SE-0461 und SE-0466 abgeglichen. 

  6. Swift, Swift Concurrency Migration Guide, „Enabling Complete Concurrency Checking” und Konfiguration des Sprachmodus. In einem Swift Package werden die Standard-Isolation und die Features der approachable concurrency über die Upcoming-Feature-Flags in swiftSettings statt über das Xcode-Build-Setting gesetzt. 

  7. Swift, Migration Guide: global actor isolation and nonisolated. Die Methoden eines @MainActor-Typs erben die Main-Actor-Isolation; nonisolated nimmt eine Methode davon aus, was für reine Funktionen korrekt ist, die keinen isolierten State berühren. 

  8. Apple Developer, MainActor.assumeIsolated(_:). Versichert, dass die aktuelle Ausführung bereits auf dem Main Actor läuft, und führt die Closure synchron ohne Actor-Hop aus. Die Versicherung führt zur Laufzeit zu einem Trap, wenn die Invariante nicht gilt; sie ist daher nur dort gültig, wo der Aufrufer garantiert auf dem Main-Thread ist. 

  9. Apple, WWDC 2026 Swift Group Lab. Bemerkungen, die Holly und Doug aus dem Swift-Kernteam zugeschrieben werden, sind sinngemäß aus einer lokal transkribierten Aufnahme wiedergegeben; Apple hat für die Session kein offizielles Transkript und keine Untertitel veröffentlicht, der Wortlaut ist daher ungefähr und kein wörtliches Zitat. 

Verwandte Beiträge

Was Apples Swift-Team im WWDC26-Lab gesagt hat

Apples Swift Group Lab auf der WWDC26 lief ohne Untertitel. Wir haben es lokal transkribiert. Offene Antworten der Ingen…

12 Min. Lesezeit

Was ist neu in Swift (2026): Das WWDC26-Update

Swift 6.3 und 6.4 von der WWDC26: anyAppleOS-Verfügbarkeit, Modulselektoren, borrow/mutate-Accessoren, das Iterable-Prot…

15 Min. Lesezeit

Wie zwei MCP-Server Claude Code in ein iOS-Build-System verwandelten

XcodeBuildMCP und Apples Xcode-MCP verschaffen Claude Code strukturierten Zugriff auf iOS-Builds, Tests und Debugging. E…

14 Min. Lesezeit