Der Swift-6-Sprachmodus macht den Compiler zum Concurrency-Auditor: Statt @MainActor-Missbrauch oder Cross-Actor-Sharing im Review zu finden, bricht xcodebuild früh ab. Sie brauchen einen leistungsstabilen, versionsgepinnten Build-Knoten – volle Compiles ohne Nachbar-CPU-Jitter bei der Tail-Latency. Dieser Artikel zeigt die schrittweise Einführung strikter Swift-6-Concurrency-Checks auf NUKCLOUD-Dediziert-Remote-Mac-Knoten und deren Verankerung in PR-Merge-Gates.
00Warum jetzt migrieren – und warum Dediziert-Knoten
Swift-6-Strikte-Concurrency-Checks (-strict-concurrency=complete) skalieren mit der Codebasis: 200–800 Compile-Fehler beim ersten Aktivieren sind bei mittelgroßen iOS-Projekten üblich. Fixes dauern Wochen oder Monate; in dieser Phase muss CI garantieren:
- Xcode-Minor-Versionen pinnen: Actor-Isolation-Diagnosen ändern Formulierungen zwischen Xcode-Versionen; Pinnen macht CI-Ausgaben vergleichbar.
- Kein Nachbar-CPU-Streit: Volle Strikte-Concurrency-Compiles dauern 20–40 % länger als typische Debug-Builds; Dediziert-Knoten halten P95-Tail-Latency stabil.
- Isolierte DerivedData-Namespaces: Mehrere PRs mit Concurrency-Checks brauchen getrennte inkrementelle Caches, sonst sind Ergebnisse unzuverlässig.
01Drei Concurrency-Check-Stufen und Migrationspfad
Swift bietet drei Compiler-Flag-Stufen in OTHER_SWIFT_FLAGS (Xcode Build Settings) oder swiftSettings in Package.swift:
| Flag | Bedeutung | Empfohlene Phase |
|---|---|---|
-strict-concurrency=minimal | Prüft nur grundlegende async/await-Syntax, keine Actor-Grenzen | Legacy-Aufwärmphase (Woche 1–2) |
-strict-concurrency=targeted | Prüft Actor-Grenzen für explizit markierte Typen (Sendable, @MainActor usw.) | Modulweise Fixphase (Woche 3–6) |
-strict-concurrency=complete | Volle Prüfung wie Swift-6-Sprachmodus; unbehandeltes Concurrency-Sharing führt zu Fehlern | Nach 0 Fehlern ins Gate aufnehmen |
Praxis: modulweise migrieren – zuerst complete in Blattmodulen, dann Kernmodule. SwiftPM .enableUpcomingFeature("StrictConcurrency") steuert pro Target präzise.
02Actor-Isolation-Grenzen: drei häufige Fehlertypen
Etwa 90 % der Migrations-Compile-Fehler fallen in diese drei Kategorien; deren Behebung räumt die meisten roten Builds weg:
① Cross-Actor-Zugriff auf nicht-Sendable-Typen
// ❌ Fehler: @MainActor-isolierter Zustand außerhalb von @MainActor
actor DataFetcher {
func load() async {
let result = await networkClient.fetch()
// ❌ passing argument of non-sendable type 'UserModel' across actor boundary
await MainActor.run { viewModel.update(result) }
}
}
Fix: UserModel an Sendable anbinden (Werttypen erfüllen das oft automatisch) oder Cross-Boundary-Konvertierung innerhalb der Actor-Grenze durchführen.
② Fehlendes @MainActor
UIKit-/SwiftUI-ViewController- und ObservableObject-Subklassen liefen vor der Migration implizit auf dem Main Thread; Swift 6 verlangt ein explizites @MainActor. Xcode „Fix“ kann stapeln – jede Stelle manuell prüfen, damit keine Nicht-UI-Logik auf den Main Actor wandert.
③ Gleichzeitiger Zugriff auf globale Variablen
// ❌ In Swift 6 müssen Globals Sendable oder nonisolated sein
var sharedCache: NSCache<NSString, AnyObject> = .init()
// ✅ nonisolated(unsafe) als Übergang
nonisolated(unsafe) var sharedCache: NSCache<NSString, AnyObject> = .init()
// ✅ Besser: in einen Actor mit async API
actor CacheStore {
private let cache = NSCache<NSString, AnyObject>()
func object(forKey key: String) -> AnyObject? { cache.object(forKey: key as NSString) }
}
03CI-Gates auf Remote-Macs konfigurieren
Kernidee für PR-Gates: Concurrency-Checks in einem separaten Job von regulären Tests. Fehler verschmutzen keine Testberichte; in der Fixphase unterschiedliche Strategien (warn-only vs. block-merge). Auf NUKCLOUD zuerst Preisseite, dann Bestellung — keine temporären Minuten-Pool-Sprints während der Migration.
Fix-Fenster dauern oft Wochen: Concurrency-Fehlerzähler, Full-Build-Wall-Time und DerivedData-Größe strukturiert loggen und mit Spec-Änderungen verknüpfen.
name: Swift 6 Concurrency Gate
on:
pull_request:
branches: [main, release/*]
jobs:
concurrency-check:
runs-on: self-hosted # NUKCLOUD dedizierter Remote-Mac-Runner
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Xcode wählen (Minor-Version pinnen)
run: sudo xcode-select -s /Applications/Xcode_16.3.app
- name: Restore DerivedData cache
uses: actions/cache@v4
with:
path: ~/DerivedData/swift6-check
key: swift6-${{ runner.name }}-${{ hashFiles('**/*.xcodeproj/project.pbxproj', '**/Package.resolved') }}
restore-keys: swift6-${{ runner.name }}-
- name: Build with strict concurrency (complete)
run: |
xcodebuild build \
-project MyApp.xcodeproj \
-scheme MyApp \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath ~/DerivedData/swift6-check \
OTHER_SWIFT_FLAGS="-strict-concurrency=complete" \
| xcbeautify --renderer github-actions
- name: Upload build log on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: swift6-build-log-${{ github.sha }}
path: ~/DerivedData/swift6-check/Logs/Build/
-derivedDataPath ~/DerivedData/swift6-check getrennt von normalen Builds, damit Concurrency-Artefakte Debug-Caches nicht vergiften. Bei mehreren PRs zusätzlich runner.name oder PR-Nummer zur Isolation.04Sechsstufiges Migrations-Runbook
-
01
Baseline-Snapshot: Auf einem Dediziert-Remote-Mac einen Vollbuild mit
-strict-concurrency=minimalfahren, Fehlerzahl und Compile-Zeit als quantifizierten Startpunkt protokollieren. -
02
Blattmodule zuerst: Aus dem SwiftPM-Graphen Targets ohne Downstream-Abhängige wählen und
completeeinzeln aktivieren. Pro Target ein eigener PR für reviewbare Diffs. -
03
CI-Branch-Strategie: Auf einem lang lebenden
feature/swift6-migration-Branch Concurrency-Jobs alswarn-only(continue-on-error: true) laufen lassen, ohne tägliche PR-Merges zu blockieren. Erst bei 0 Fehlern pro Modul auf block-merge upgraden. -
04
Xcode-Version pinnen:
xcode-select-Version in Runbook und README festhalten, damit lokale Mac-Entwickler und CI-Knoten dieselbe Minor-Version nutzen – weniger „lokal grün, CI rot“. -
05
Fehlertrend-Dashboard: Swift-6-Fehlerzahlen pro CI-Lauf in strukturierte Logs oder Build Metrics schreiben; Fortschritt per Liniendiagramm und wöchentliche Teamziele („X Fehler weniger diese Woche“).
-
06
Vollständige Gate-Abnahme: Nach 0 Fehlern in allen Modulen auf
main„Require swift6-concurrency-check to pass“ in Branch Protection aktivieren undSWIFT_VERSION = 6für alle Targets setzen – offizieller Wechsel in den Swift-6-Sprachmodus.
05Lokaler Mac vs. Dediziert-Remote-Knoten
Swift-6-Migration braucht schnelle lokale Iteration und verlässliche Voll-CI-Validierung – die Anforderungen an Knoten unterscheiden sich:
| Dimension | Entwickler-Lokal-Mac | NUKCLOUD Dediziert-Remote-Mac (CI) |
|---|---|---|
| Xcode-Version | Vom Entwickler verwaltet, kann abweichen | Per Skript gepinnte Minor-Version, teamweit einheitlich |
| DerivedData | Mit Tagesentwicklung geteilt; inkrementeller Cache kann kippen | Getrennte Pfade, nach PR oder Runner gebucketed |
| CPU-Verfügbarkeit | Konkurrenz durch andere Apps; Compile-Zeit schwankt | Bare-Metal dediziert, planbare P95-Latency |
| Ergebnisvertrauen | Lokal grün heißt nicht CI grün | Finale Instanz in der Gate-Phase |
| Ideal für | Schnelle Einzelmodul-Versuche und Xcode-Diagnosen lesen | Volle Concurrency-Gate-Checks und tägliche Regression |
06FAQ
-strict-concurrency=complete ist ein Compiler-Flag, aktivierbar im Swift-5-Sprachmodus, um alle Fehler vor dem Wechsel zu sehen. Der formale Swift-6-Sprachmodus (SWIFT_VERSION = 6) entfernt zusätzlich Legacy-Syntax. Zuerst mit dem complete-Flag fixen, dann Sprachmodus wechseln..unsafeFlags(["-strict-concurrency=minimal"]) in Package.swift absenken oder SWIFT_STRICT_CONCURRENCY = minimal in einem Podfile-post_install-Hook setzen. Die meisten großen Libraries waren 2026 migriert – zuerst Release Notes prüfen.--concurrent-jobs N setzen und DerivedData nach PR bucketen (-derivedDataPath ~/DerivedData/pr-${{ github.event.number }}), damit Jobs unabhängig compilen. Dediziert-Bare-Metal-Knoten punkten hier: kein Nachbar-CPU-Streit, geringe Varianz bei parallelen Jobs.-strict-concurrency=complete über Wochen sind NUKCLOUD Multi-Region Bare-Metal Mac / Cloud-Mac-Knoten stabiler — mit dem Konsole-Runbook zur Abnahme.