Ein Gate für die Swift-6-Concurrency-Migration aufbauen

Ein Gate für die Swift-6-Concurrency-Migration aufbauen

Wenn ein seit vielen Jahren gepflegtes iOS-Projekt auf Swift 6 umgestellt wird, besteht das größte Risiko nicht in einem langsamen Vorgehen. Gefährlich wird es vielmehr, wenn der neue Sprachmodus auf einmal aktiviert wird und anschließend Hunderte Concurrency-Diagnosen mit laufenden fachlichen Änderungen vermischt sind. Robuster ist es, zunächst auf einem Cloud-Mac einen separaten Prüfjob einzurichten: mit festgelegter Toolchain, isoliertem Build-Verzeichnis und einer dokumentierten Ausgangsbasis der aktuellen Warnungen. Danach lassen sich die Regeln Modul für Modul verschärfen.

Die Migration zunächst in zwei Phasen aufteilen

In der ersten Phase bleibt der Sprachmodus auf Swift 5, während SWIFT_STRICT_CONCURRENCY auf complete gesetzt wird. Dadurch werden unter anderem nicht Sendable-konforme Typen sichtbar, die Isolationsgrenzen überschreiten, ebenso Aufrufe über die Main-Actor-Grenze hinweg und unsichere globale veränderliche Zustände. Die vollständigen semantischen Änderungen von Swift 6 greifen dabei jedoch noch nicht sofort.

Erst in der zweiten Phase werden bereinigte Module auf Swift 6 umgestellt. Sinnvoll ist eine Reihenfolge, die bei Basispaketen mit wenigen Abhängigkeiten beginnt und anschließend Netzwerk-, Daten- und UI-Schichten umfasst. Sobald die Blattmodule stabil sind, nimmt meist auch die Zahl der Diagnosen in den darüberliegenden Modulen ab.

Phase Sprachmodus Prüfeinstellung Merge-Bedingung
Probleme erfassen Swift 5 complete Die Zahl der Concurrency-Warnungen darf nicht steigen
Modul bereinigen Swift 5 complete Keine Warnungen mehr im Zielmodul
Endgültig umstellen Swift 6 Strenge Standardregeln Build und Tests vollständig erfolgreich

Das Ziel eines Gates für die Concurrency-Migration besteht nicht darin, sämtliche Warnungen am ersten Tag zu beseitigen. Entscheidend ist, dass ab dem ersten Tag keine neuen Probleme mehr in den Hauptbranch gelangen.

Einen reproduzierbaren Prüfjob festlegen

Der Job auf dem Cloud-Mac muss die verwendeten Versionen von Xcode und des Swift-Compilers sowie den jeweiligen Commit ausdrücklich protokollieren. Von manuellen Entwickler-Builds erzeugte DerivedData dürfen nicht wiederverwendet werden, da zwischengespeicherte alte Module tatsächliche Abhängigkeiten verdecken können. Jeder Commit erhält ein eigenes Verzeichnis, das nach Abschluss des Jobs gemäß der Aufbewahrungsrichtlinie bereinigt wird.

#!/bin/zsh
set -o pipefail
export LC_ALL=C
root="$PWD/.ci"
derived="$root/DerivedData"
result="$root/ConcurrencyCheck.xcresult"
log="$root/concurrency.log"

rm -rf "$derived" "$result"
mkdir -p "$root"

xcodebuild -version
xcrun swiftc --version
git rev-parse HEAD

xcodebuild \
  -project Example.xcodeproj \
  -scheme Example \
  -configuration Debug \
  -destination 'generic/platform=iOS Simulator' \
  -derivedDataPath "$derived" \
  -resultBundlePath "$result" \
  SWIFT_VERSION=5.0 \
  SWIFT_STRICT_CONCURRENCY=complete \
  build 2>&1 | tee "$log"

build_status=${pipestatus[1]}
exit "$build_status"

project, scheme und die Zielplattform sollten aus der Repository-Konfiguration stammen, statt in mehreren Skripten separat gepflegt zu werden. Verwendet das Projekt einen Workspace, wird -project durch -workspace ersetzt. Gleichzeitig ist sicherzustellen, dass das gemeinsam verwendete Scheme in die Versionsverwaltung eingecheckt wurde.

Den bestehenden Hauptbranch über ein Warnungsbudget anbinden

Bei älteren Projekten lassen sich in der Regel nicht sofort alle Warnungen beseitigen. Nach dem ersten Durchlauf wird daher die Zahl der Concurrency-Diagnosen gespeichert und als anfängliches Budget festgelegt. Jeder nachfolgende Commit schlägt fehl, sobald er dieses Budget überschreitet. Wird eine Gruppe von Problemen behoben, sollte das Budget im selben Merge Request entsprechend gesenkt werden.

budget=${CONCURRENCY_WARNING_BUDGET:-0}
count=$(grep -Ec \
  'warning:.*(Sendable|actor-isolated|concurrency-safe)' \
  .ci/concurrency.log || true)

printf 'concurrency warnings: %s, budget: %s
' "$count" "$budget"
test "$count" -le "$budget"

Textbasierte Filter eignen sich als vorübergehendes Gate. Dafür muss jedoch LC_ALL=C festgelegt sein, und bei jedem Xcode-Upgrade ist die Formulierung der Diagnosemeldungen erneut zu prüfen. Das Build-Ergebnis-Bundle sollte bei Fehlern als Artefakt aufbewahrt werden, damit nicht nur ein abgeschnittenes Terminalprotokoll übrig bleibt. Sobald das Budget auf null gesunken ist, können sämtliche Concurrency-Warnungen direkt untersagt werden.

Verunreinigungen durch generierten Code vermeiden

Werden API-Clients oder Modelle von Tools generiert, muss die Generatorversion in der Repository-Konfiguration festgeschrieben werden. Externer generierter Code, der sich nicht sofort anpassen lässt, kann in ein eigenes Zielmodul ausgelagert werden. Das gesamte fachliche Modul darf jedoch nicht von den strengen Prüfungen ausgenommen werden. Andernfalls würde auch neuer handgeschriebener Code das Gate umgehen.

Probleme nach Kategorie beheben statt Warnungen pauschal zu unterdrücken

Bei einer Sendable-Diagnose ist zunächst zu klären, ob die betreffenden Daten tatsächlich zwischen Tasks übertragen werden müssen. Für schreibgeschützte Konfigurationen sind Werttyp-Snapshots vorzuziehen. Referenztypen mit veränderlichem Cache können in einem Actor gekapselt werden. Müssen synchronisierte Objekte gemeinsam verwendet werden, sind Sperren und Zugriffsgrenzen ausdrücklich zu kapseln.

UI-Zustand sollte mit @MainActor auf konkrete Typen oder Methoden isoliert werden. Nur um die Prüfung schnell zu bestehen, dürfen nicht die gesamte Datenschicht oder sämtliche Protokolle mit @MainActor versehen werden. Dadurch würde Hintergrundarbeit implizit auf den Hauptthread verlagert, was weitere Aufrufe über Isolationsgrenzen hinweg erzeugt.

@unchecked Sendable eignet sich ausschließlich für Typen, deren Sicherheit bereits durch interne Sperren, ein unveränderliches Design oder eine serielle Queue gewährleistet ist. Ein Code-Review muss mindestens drei Fragen beantworten: Welche Felder werden geschützt? Laufen sämtliche Lese- und Schreibzugriffe über dieselbe Grenze? Wie bleiben künftig hinzugefügte Felder ebenfalls geschützt? Lassen sich diese Fragen nicht beantworten, sollte die Deklaration nicht verwendet werden.

Beim Überführen von Callbacks nach async muss außerdem geprüft werden, dass die Continuation genau einmal fortgesetzt wird. Sowohl „kein Callback“ als auch „mehrfacher Callback“ gehören in die Tests. Es genügt nicht, lediglich die Compilerdiagnose zu beseitigen.

Module schrittweise umstellen und Abnahmenachweise aufbewahren

Bevor ein Modul umgestellt wird, müssen zunächst im Modus Swift 5 mit complete alle eigenen Warnungen beseitigt werden. Erst danach wird Swift 6 aktiviert. Die Abnahme sollte mindestens einen Debug-Build, einen Release-Build, Unit-Tests und wichtige Integrationstests umfassen. Ein reiner Debug-Build für den Simulator reicht nicht aus, um Probleme in optimierten Konfigurationen oder bedingt kompilierten Codepfaden aufzudecken.

Für jede Migration sollten Modulname, verantwortliche Person, Umstellungs-Commit, verbleibende Ausnahmen und der Pfad zum Ergebnis-Bundle dokumentiert werden. Bei einem Xcode-Upgrade ist zunächst in einem separaten Branch eine neue Ausgangsbasis zu erstellen. Toolchain-Upgrade, Concurrency-Korrekturen und fachliche Funktionen gehören nicht in dieselbe Änderung.

Der strenge Prüfjob bleibt auch nach Abschluss der Migration bestehen. Swift 6 verschärft zwar die Standardvorgaben, doch aktualisierte Abhängigkeiten, Objective-C-Grenzen und generierter Code können erneut Lücken in der Isolation verursachen. Ein stabiler Endzustand bedeutet daher nicht, dass die Warnungszahl irgendwann einmal null war, sondern dass jeder Merge nachweist, dass es keinen Rückschritt gegeben hat.

Häufig gestellte Fragen

Sollte das gesamte Projekt sofort auf Swift 6 wechseln?

Nein. Zuerst wird die complete-Prüfung im Swift-5-Modus aktiviert. Danach werden Basismodule bereinigt und einzelne Targets nacheinander umgestellt.

Ist @unchecked Sendable als schnelle Lösung geeignet?

Nur wenn Unveränderlichkeit, Sperren oder serielle Isolation die Sicherheit bereits gewährleisten. Die Begründung muss dokumentiert und geprüft werden.

Wie lässt sich das Gate in ein älteres Projekt einführen?

Die aktuelle Warnungszahl wird als Budget festgelegt. Zunächst sind nur Erhöhungen verboten; nach jeder Bereinigung wird das Budget weiter gesenkt.

Dedizierter physischer Mac mini

Cloud-Mac-Mietdauer passend zur Aufgabendauer wählen

Zwei M4-Konfigurationen können tageweise, wöchentlich, monatlich oder vierteljährlich gemietet werden. Maßgeblich sind die im Dashboard in Echtzeit angezeigten Knoten und Verfügbarkeitsinformationen.

Konfiguration auswählen und bestellen