Approachable Concurrency in Swift 6.2: Der komplette Migrations-Guide für Xcode 26

Swift 6.2 macht Concurrency endlich zugänglich. Lerne, wie @MainActor als Standard, @concurrent und nonisolated(nonsending) funktionieren — und migriere dein bestehendes Projekt Schritt für Schritt mit diesem Guide.

Mit Swift 6.2 und Xcode 26 hat Apple die Concurrency-Landschaft grundlegend umgekrempelt. Das Ganze nennt sich Approachable Concurrency — und nein, das ist nicht einfach nur ein Marketing-Buzzword. Es ist ein konkretes Build-Setting, das verändert, wie dein Code kompiliert und wie er sich zur Laufzeit verhält.

Wenn du bisher mit Swift Concurrency gekämpft hast — endlose Sendable-Warnungen, überraschende Thread-Wechsel bei nonisolated async-Funktionen, oder schlicht zu viele Annotations — dann bist du hier richtig. Wir gehen Schritt für Schritt durch, was sich geändert hat, warum, und wie du dein Projekt sicher migrierst.

Das Problem mit Swift 6.0 und 6.1

Swift Concurrency wurde mit Swift 5.5 eingeführt und mit Swift 6.0 auf der WWDC 2024 als vollständiges Modell mit strikter Data-Race-Safety ausgeliefert. Das Konzept war technisch beeindruckend — aber mal ehrlich: die Praxis sah ganz anders aus.

Viele Entwickler standen vor einem Berg an Compiler-Fehlern, sobald sie strict concurrency checking aktivierten. Selbst einfache Apps, die eigentlich rein sequenziell arbeiteten, erforderten plötzlich Dutzende von @MainActor-, @Sendable- und nonisolated-Annotations. Das war frustrierend.

Das Kernproblem: Swift 6.0 ging davon aus, dass Code standardmäßig nicht isoliert ist. Der Compiler vermutete bei jeder Übergabe von Daten zwischen Kontexten potenzielle Data Races — selbst wenn der Code nie tatsächlich parallel lief.

Besonders verwirrend war das Verhalten von nonisolated async-Funktionen:

  • Eine synchrone nonisolated-Funktion lief auf dem Actor des Aufrufers (z. B. dem MainActor)
  • Eine asynchrone nonisolated-Funktion wechselte automatisch auf einen Hintergrund-Thread

Gleiches Keyword, zwei völlig unterschiedliche Verhaltensweisen. Das hat für jede Menge Bugs gesorgt, die richtig schwer zu finden waren.

Was ist Approachable Concurrency?

Approachable Concurrency basiert auf dem Prinzip der progressiven Offenlegung (Progressive Disclosure): Swift soll nur so viel Concurrency-Wissen von dir verlangen, wie du tatsächlich brauchst. Nicht mehr, nicht weniger.

Konkret heißt das:

  1. Sequenzieller Code bleibt sequenziell — ohne zusätzliche Annotations
  2. Async/await kommt hinzu, wenn du auf asynchrone APIs zugreifst
  3. Actors und Sendability werden erst relevant, wenn du bewusst Parallelismus einführst

In Xcode 26 ist Approachable Concurrency keine abstrakte Vision mehr. Es ist ein konkretes Build-Setting, das du pro Target aktivieren kannst. Bei neuen Projekten ist es bereits standardmäßig aktiv — endlich.

@MainActor als Standard: Die wichtigste Änderung

Okay, jetzt wird's spannend. Die größte Neuerung in Swift 6.2 ist, dass der gesamte Code eines Moduls standardmäßig auf dem @MainActor isoliert wird (SE-0466). Klingt radikal? Ist es auch.

Aber schau dir an, was das in der Praxis bedeutet:

// Swift 6.0/6.1: Diese Klasse ist nonisolated by default
class ViewModel: ObservableObject {
    @Published var items: [String] = []

    func loadItems() async {
        // Läuft auf einem Hintergrund-Thread!
        let data = await fetchFromAPI()
        items = data // ⚠️ Compiler-Fehler: Zugriff auf MainActor-Property von Hintergrund-Thread
    }
}

// Swift 6.2 mit Approachable Concurrency: Alles ist @MainActor by default
class ViewModel: ObservableObject {
    @Published var items: [String] = []

    func loadItems() async {
        // Läuft auf dem MainActor — kein Thread-Wechsel!
        let data = await fetchFromAPI()
        items = data // ✅ Kein Problem — wir sind bereits auf dem MainActor
    }
}

Für die meisten App-Entwickler ist das genau das, was sie ohnehin erwartet hatten. UI-Code läuft auf dem Main Thread. Punkt. Keine überraschenden Hops mehr.

Xcode 26 Build Settings

Bei einem neuen Projekt in Xcode 26 findest du zwei relevante Build Settings:

  • Approachable Concurrency: Yes (Standard bei neuen Projekten)
  • Default Actor Isolation: MainActor (Standard bei neuen Projekten)

Für bestehende Projekte stehen diese Settings auf No bzw. nonisolated. Du musst sie manuell ändern — und genau dafür ist dieser Migrations-Guide da.

nonisolated(nonsending): Das neue Standardverhalten

Eine der subtilsten, aber wirkungsvollsten Änderungen in Swift 6.2 ist das neue Schlüsselwort nonisolated(nonsending). Es löst das Inkonsistenz-Problem, das wir weiter oben besprochen haben.

Mit dem Feature-Flag NonisolatedNonsendingByDefault (Teil von Approachable Concurrency) erben alle nonisolated async-Funktionen die Isolation des Aufrufers:

// Swift 6.2 mit NonisolatedNonsendingByDefault
nonisolated func processData(_ input: String) async -> String {
    // Wird aufgerufen vom MainActor? → Läuft auf dem MainActor
    // Wird aufgerufen von einem Hintergrund-Kontext? → Läuft im Hintergrund
    return input.uppercased()
}

// Explizit das gleiche Verhalten:
nonisolated(nonsending) func processData(_ input: String) async -> String {
    return input.uppercased()
}

Der große Vorteil: Da kein Isolation-Wechsel stattfindet, müssen übergebene Typen auch nicht Sendable sein. Das eliminiert auf einen Schlag eine enorme Menge an Compiler-Warnungen. Ehrlich gesagt hätte das von Anfang an so funktionieren sollen.

@concurrent: Bewusst in den Hintergrund wechseln

Wenn jetzt alles standardmäßig auf dem MainActor läuft — wie führst du dann rechenintensive Arbeit im Hintergrund aus? Gute Frage. Hier kommt das neue @concurrent-Attribut ins Spiel (SE-0461).

// Diese Funktion läuft IMMER auf dem globalen Concurrent Executor (Hintergrund-Thread)
@concurrent
func decodeImage(from data: Data) async throws -> UIImage {
    // Schwere Arbeit — gehört nicht auf den MainActor
    guard let image = UIImage(data: data) else {
        throw ImageError.decodingFailed
    }
    return image
}

// Aufruf von einem @MainActor-Kontext:
@MainActor
func loadProfileImage() async {
    do {
        let data = await downloadImageData()
        let image = try await decodeImage(from: data) // → wechselt automatisch in den Hintergrund
        self.profileImage = image // → zurück auf dem MainActor
    } catch {
        self.errorMessage = error.localizedDescription
    }
}

Die Philosophie dahinter ist eigentlich ganz simpel: Concurrency ist opt-in, nicht opt-out. Du markierst explizit, was parallel laufen soll. Alles andere bleibt sicher auf dem MainActor.

Übrigens: @concurrent ist als Compiler-Direktive rückwärtskompatibel bis iOS 15. Du brauchst allerdings Xcode 26 und Swift 6.2, um es zu kompilieren.

Wann @concurrent verwenden?

Verwende @concurrent für:

  • JSON-Decoding großer Datenmengen
  • Bildverarbeitung und -komprimierung
  • Komplexe Berechnungen oder Algorithmen
  • Datei-I/O-Operationen
  • Netzwerk-Response-Parsing

Verwende es nicht für einfache Property-Zugriffe oder UI-Updates — die gehören auf den MainActor.

Inferred Isolated Conformances (SE-0470)

Ein häufiges Ärgernis in Swift 6.0 war die Protokoll-Konformität bei Actor-isolierten Typen. Wenn eine @MainActor-Klasse Equatable implementieren wollte, musste die ==-Funktion als nonisolated markiert werden — was die eigentliche Isolation untergrub. Das war nervig und hat zu unschönen Workarounds geführt.

Swift 6.2 löst das mit Inferred Isolated Conformances:

@MainActor
class UserProfile: Equatable {
    let name: String
    let email: String

    // Swift 6.2: Diese Konformität ist automatisch MainActor-isoliert
    static func == (lhs: UserProfile, rhs: UserProfile) -> Bool {
        lhs.name == rhs.name && lhs.email == rhs.email
    }
}

// Funktioniert auf dem MainActor:
@MainActor
func compareProfiles(_ a: UserProfile, _ b: UserProfile) -> Bool {
    a == b // ✅ Kein Problem
}

Der Compiler erkennt automatisch, dass die Konformität im Kontext des MainActors verwendet wird, und erzwingt diese Einschränkung. Eine ganze Klasse von Workarounds fällt damit weg.

Schritt-für-Schritt: Bestehendes Projekt migrieren

So, jetzt zum eigentlichen Kern dieses Guides. Die Migration eines bestehenden Projekts zu Approachable Concurrency erfordert etwas Sorgfalt. Aktiviere nicht einfach alle Settings auf einmal — das kann zu subtilen Laufzeit-Änderungen führen, die erst spät auffallen (und dann richtig wehtun).

Schritt 1: Auf Swift 6.2 und Xcode 26 aktualisieren

Stelle sicher, dass dein Projekt mit Xcode 26 kompiliert und Swift 6.2 als Sprachversion verwendet. Behebe alle bestehenden Compiler-Fehler, bevor du weitermachst. Das klingt offensichtlich, aber du wärst überrascht, wie viele Projekte noch mit unterdrückten Warnungen herumlaufen.

Schritt 2: Approachable Concurrency aktivieren

Gehe in die Build Settings deines Targets und suche nach „Approachable Concurrency". Setze es auf Yes. Das aktiviert mehrere Upcoming Features gleichzeitig, darunter:

  • InferIsolatedConformances
  • GlobalActorIsolatedTypesUsability

Schritt 3: Default Actor Isolation setzen

Setze „Default Actor Isolation" auf MainActor. Achtung: Dieser Schritt verändert das Laufzeitverhalten deines Codes. Funktionen, die vorher implizit nonisolated waren, laufen jetzt auf dem MainActor.

Prüfe insbesondere:

  • Service-Klassen, die Netzwerk-Calls oder Datenbank-Operationen durchführen
  • ViewModels mit asynchronen Methoden
  • Utility-Funktionen, die rechenintensive Arbeit verrichten

Schritt 4: @concurrent-Annotations hinzufügen

Identifiziere Funktionen, die bewusst im Hintergrund laufen sollen, und markiere sie mit @concurrent:

// Vorher (Swift 6.0): Lief implizit im Hintergrund
func parseResponse(_ data: Data) async throws -> [Item] {
    try JSONDecoder().decode([Item].self, from: data)
}

// Nachher (Swift 6.2): Explizit im Hintergrund
@concurrent
func parseResponse(_ data: Data) async throws -> [Item] {
    try JSONDecoder().decode([Item].self, from: data)
}

Schritt 5: NonisolatedNonsendingByDefault aktivieren

Dieses Feature-Flag ist in Xcode 26 standardmäßig nicht aktiviert — auch nicht bei neuen Projekten. Apple empfiehlt es aber ausdrücklich. Suche in den Build Settings nach „nonisolated" und aktiviere es.

Nach der Aktivierung wechseln nonisolated async-Funktionen nicht mehr automatisch auf einen Hintergrund-Thread. Überprüfe, ob Funktionen, die bisher im Hintergrund liefen, jetzt mit @concurrent annotiert werden müssen.

Schritt 6: Testen mit Instruments

Verwende den Thread Sanitizer und das Main Thread Checker Tool in Instruments, um sicherzustellen, dass keine unerwarteten Main-Thread-Blockierungen auftreten. Achte besonders auf:

  • UI-Ruckler durch schwere Operationen, die jetzt auf dem MainActor laufen
  • Deadlocks bei verschachtelten Actor-Aufrufen

Dieser Schritt ist nicht optional. Nimm dir die Zeit.

Swift Packages migrieren

Für Swift Packages gelten andere Regeln — und das ist ein wichtiger Punkt, der gerne übersehen wird. Approachable Concurrency wird bei Packages nicht automatisch aktiviert, auch nicht in Xcode 26. Du musst die Features manuell in der Package.swift setzen:

// swift-tools-version: 6.2
import PackageDescription

let package = Package(
    name: "MeineLibrary",
    platforms: [.iOS(.v17), .macOS(.v14)],
    products: [
        .library(name: "MeineLibrary", targets: ["MeineLibrary"]),
    ],
    targets: [
        .target(
            name: "MeineLibrary",
            swiftSettings: [
                .defaultIsolation(MainActor.self),
                .enableUpcomingFeature("NonisolatedNonsendingByDefault"),
                .enableUpcomingFeature("InferIsolatedConformances")
            ]
        ),
    ]
)

Wichtig: Wenn dein Package auch mit älteren Xcode-Versionen kompilieren muss, kannst du diese Settings noch nicht aktivieren. Plane die Migration entsprechend.

Häufige Stolperfallen und Lösungen

Problem: Service-Klassen blockieren den Main Thread

Das ist vermutlich die häufigste Falle nach der Migration. Wenn deine Netzwerk- oder Datenbank-Services jetzt auf dem MainActor laufen, kann das zu spürbaren UI-Rucklern führen:

// ❌ Problem: parseJSON blockiert jetzt den MainActor
class APIService {
    func fetchUsers() async throws -> [User] {
        let (data, _) = try await URLSession.shared.data(from: url)
        return try parseJSON(data) // Schwere Arbeit auf dem MainActor!
    }

    func parseJSON(_ data: Data) throws -> [User] {
        try JSONDecoder().decode([User].self, from: data)
    }
}

// ✅ Lösung: Parsing explizit in den Hintergrund
class APIService {
    func fetchUsers() async throws -> [User] {
        let (data, _) = try await URLSession.shared.data(from: url)
        return try await parseJSON(data)
    }

    @concurrent
    func parseJSON(_ data: Data) async throws -> [User] {
        try JSONDecoder().decode([User].self, from: data)
    }
}

Problem: nonisolated hebt MainActor-Isolation nicht mehr auf

In Swift 6.0 konnte nonisolated verwendet werden, um eine Funktion aus dem MainActor-Kontext herauszulösen. In Swift 6.2 mit NonisolatedNonsendingByDefault erbt die Funktion stattdessen die Isolation des Aufrufers. Das ist ein subtiler, aber wichtiger Unterschied:

// ❌ Swift 6.2: nonisolated async erbt den MainActor-Kontext
nonisolated func heavyComputation() async -> Result {
    // Läuft trotzdem auf dem MainActor, wenn von dort aufgerufen!
}

// ✅ Lösung: @concurrent verwenden
@concurrent
func heavyComputation() async -> Result {
    // Läuft garantiert im Hintergrund
}

Problem: Apple-Frameworks sind noch nicht vollständig aligned

UIKit und Core Data sind noch nicht komplett an die strikte Concurrency angepasst. Das ist ärgerlich, aber leider Realität. Workarounds wie @preconcurrency import oder gezielte nonisolated(unsafe)-Annotations können nötig sein. Prüfe die Release Notes von Xcode 26 auf framework-spezifische Empfehlungen.

Das neue mentale Modell

Swift 6.2 dreht die Logik komplett um. Statt „alles ist concurrent, es sei denn du sagst etwas anderes" gilt jetzt:

  • Standard: Dein Code läuft auf dem MainActor — sicher und sequenziell
  • Async/await: Für asynchrone Operationen, die aber auf dem gleichen Actor bleiben
  • @concurrent: Für explizite Hintergrund-Arbeit, wenn Performance es erfordert
  • Actors: Für gemeinsam genutzten, mutablen Zustand zwischen verschiedenen Kontexten

Meiner Meinung nach ist das im Grunde das, was Swift 6.0 hätte sein sollen. Für alle, die bisher mit der Migration gezögert haben: Jetzt ist der richtige Zeitpunkt. Die Einstiegshürde war nie niedriger.

Häufig gestellte Fragen (FAQ)

Muss ich mein bestehendes Projekt sofort auf Approachable Concurrency umstellen?

Nein, musst du nicht. Für bestehende Projekte sind alle neuen Settings standardmäßig deaktiviert. Du kannst weiterhin mit den bisherigen Concurrency-Regeln arbeiten. Die Migration wird jedoch empfohlen, weil sie die Komplexität deines Codes deutlich reduziert und viele Compiler-Warnungen eliminiert. Am besten migrierst du inkrementell — Feature für Feature.

Was ist der Unterschied zwischen @concurrent und nonisolated?

nonisolated bedeutet in Swift 6.2 mit NonisolatedNonsendingByDefault, dass die Funktion die Isolation des Aufrufers erbt. @concurrent hingegen erzwingt die Ausführung auf dem globalen Concurrent Executor — also einem Hintergrund-Thread. Verwende @concurrent nur für rechenintensive Arbeit, die den MainActor nicht blockieren soll.

Funktioniert @concurrent auch mit älteren iOS-Versionen?

Ja! @concurrent ist eine Compiler-Direktive und bis iOS 15 rückwärtskompatibel. Du brauchst allerdings Xcode 26 und Swift 6.2 zum Kompilieren. Zur Laufzeit gibt es keine Einschränkungen durch die iOS-Version.

Wie wirkt sich Approachable Concurrency auf die App-Performance aus?

Da mehr Code standardmäßig auf dem MainActor läuft, besteht das Risiko von Main-Thread-Blockierungen — aber nur, wenn rechenintensive Operationen nicht mit @concurrent markiert werden. Gleichzeitig eliminiert das neue Modell unnötige Thread-Wechsel, was die Performance bei typischem UI-Code sogar verbessern kann. Verwende Instruments, um Engpässe zu identifizieren.

Können Swift Packages und App-Target unterschiedliche Concurrency-Einstellungen haben?

Ja, und das ist sogar häufig der Fall. Swift Packages erhalten Approachable Concurrency nicht automatisch — du musst die Features in der Package.swift explizit aktivieren. Das kann zu subtilen Unterschieden im Verhalten zwischen App-Code und Package-Code führen, also achte bei der Migration darauf, dass alle Targets konsistente Settings verwenden.

Über den Autor Editorial Team

Our team of expert writers and editors.