Xcode Cloud Deploy — brain-ios
Projekt-Setup
| Feld | Wert |
|---|---|
| Bundle ID | $(BRAIN_BUNDLE_ID_BASE) aus Config/Local.xcconfig (Platzhalter im Repo: com.example.brain-ios) |
| Team ID | $(BRAIN_TEAM_ID) aus Config/Local.xcconfig (Platzhalter: TEAM_ID_HERE) |
| Scheme | BrainApp |
| Branch | main |
| Min iOS | 17.0 |
| Geraete | iPhone + iPad |
Private Identifier (xcconfig)
Das oeffentliche Repo enthaelt nur Platzhalter. Alle deploy-relevanten IDs
(Bundle-ID-Basis, Team-ID, Google-Client-ID; daraus abgeleitet App Group,
iCloud-Container, BGTask-IDs, Extension-Bundle-IDs, IAP-Produkt-ID) kommen aus
Config/Base.xcconfig, das optional Config/Local.xcconfig (gitignored) inkludiert.
- Lokal:
Config/Local.xcconfig.examplenachConfig/Local.xcconfigkopieren und befuellen. Nie committen. - Xcode Cloud: Im Workflow die Environment-Variablen
BRAIN_BUNDLE_ID_BASE,BRAIN_TEAM_ID,BRAIN_GOOGLE_CLIENT_IDsetzen —ci_post_clone.shschreibt darausConfig/Local.xcconfigvor dem Build. - Zur Laufzeit liest
AppIdentifiers.swiftdie Werte aus Info.plist bzw. der Bundle-ID; App, Share Extension und Widgets teilen sich so dieselbe App Group. - Der Keychain-Service-Name bleibt bewusst fix (
com.example.brain-ios), damit gespeicherte Keys einen Identifier-Wechsel ueberleben.
Deployment ausloesen
Ein git push origin main triggert automatisch einen Xcode Cloud Build.
Der Build archiviert, signiert und laedt zu TestFlight hoch.
# Build-Nummer erhoehen vor Push:
# In BrainApp.xcodeproj/project.pbxproj: CURRENT_PROJECT_VERSION erhoehen
git add -A && git commit -m "..." && git push origin main
Build-Status pruefen
Xcode Cloud Builds sind unter App Store Connect einsehbar: https://appstoreconnect.apple.com → Brain → Xcode Cloud → Builds
Bekannte Fehler und Loesungen
Exit Code 65 — Kompilierungsfehler
- Artifacts → Logs →
xcodebuild-archive.logherunterladen - Nach
error:suchen fuer die eigentlichen Fehlermeldungen - Xcode Cloud kompiliert STRENGER als
swift testauf Linux/VPS!
Swift 6 Strict Concurrency (haeufigster Fehler!)
static varin AppIntent: Mussstatic letsein- Betrifft:
title,description,openAppWhenRunin AppIntent Structs - Fehler: "static property is not concurrency-safe because it is nonisolated global shared mutable state"
- Betrifft:
[String: Any]in Sendable Struct:@unchecked Sendableverwenden- Fehler: "stored property of Sendable-conforming struct contains non-Sendable type"
- Closure in AsyncThrowingStream:
Task { @Sendable in ... }und Parameter@Sendablemarkieren- Fehler: "Passing closure as a 'sending' parameter risks causing data races"
- AVFoundation Captures:
nonisolated(unsafe) letfuer AVAudioEngine, SFSpeechAudioBufferRecognitionRequest- Fehler: "Capture of non-Sendable type in a '@Sendable' closure"
- Missing
await: GRDBpool.readist async in Xcode-Kontext- Fehler: "expression is 'async' but is not marked with 'await'"
.accentist kein ShapeStyle-Member:Color.accentColorstatt.accentverwenden- Fehler: "Type 'ShapeStyle' has no member 'accent'"
[weak self]in Task mit @MainActor:Task { @MainActor [weak self] in }funktioniert nicht mit Swift 6- Fehler: "sending 'self' risks causing data races"
- Loesung:
[weak self]entfernen, Klasse selbst@MainActormachen
- Captured var in async Closure mutiert: Variable wird in GRDB
pool.writeetc. mutiert- Fehler: "mutation of captured var in concurrently-executing code"
- Loesung: By-value capturen
[assistantMsg], lokale Kopie mutieren
Extension-Target Fehler
import GRDBfehlt: Widgets/Extensions die GRDB-Types nutzen (z.B.Row) brauchen explizitesimport GRDB- Property-Name-Kollision:
@State private var bodykollidiert mitvar body: some View→ umbenennen! - Unused
[weak self]: Wenn self nicht im Closure genutzt wird →[weak self]entfernen- Fehler: "Variable 'self' was written to, but never read"
#if canImport(FoundationModels) richtig verwenden
- Das
importStatement muss AUSSERHALB der Funktion stehen:#if canImport(FoundationModels) import FoundationModels #endif - Methoden die FoundationModels-Types nutzen muessen KOMPLETT in
#if canImporteingewickelt sein - NICHT nur den Body, sondern die ganze Methode einwickeln
LanguageModelSession()Init wirft nicht — kein do/catch noetig
pbxproj — Dateien werden nicht kompiliert (KRITISCH!)
Symptom: "Cannot find 'TypeName' in scope" obwohl die Datei existiert.
Eine Datei braucht DREI Eintraege im pbxproj um kompiliert zu werden:
- PBXFileReference — Datei existiert im Projekt
B1020019 /* OpenAIProvider.swift */ = {isa = PBXFileReference; ...}; - PBXBuildFile — Verknuepfung FileReference → Build-Phase
B1010019 /* OpenAIProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1020019; }; - PBXSourcesBuildPhase — Datei in der Compile-Liste des Targets
B1010019 /* OpenAIProvider.swift in Sources */,
Wenn EINES davon fehlt, wird die Datei STILL ignoriert! Xcode zeigt keinen Fehler — die Datei erscheint im Projekt, wird aber nie kompiliert. Alle Typen darin sind dann "not in scope" in allen anderen Dateien.
Checkliste bei "Cannot find in scope":
# Pruefe ob alle 3 Eintraege existieren:
grep "MyFile.swift" BrainApp.xcodeproj/project.pbxproj
# Erwartetes Ergebnis (3 Zeilen):
# XXXX /* MyFile.swift */ = {isa = PBXFileReference; ...} ← FileReference
# YYYY /* MyFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = XXXX; } ← BuildFile
# YYYY /* MyFile.swift in Sources */, ← In BuildPhase
Haeufige Fallen:
- Datei in PBXGroup + PBXFileReference, aber KEIN PBXBuildFile → wird nicht kompiliert
- Datei hat PBXBuildFile, ist aber nicht in PBXSourcesBuildPhase → wird nicht kompiliert
- Datei in Unterordner (z.B. Onboarding/) aber PBXGroup hat falschen
path→ Xcode findet Datei nicht
Unterordner im pbxproj (Onboarding/, LLMProviders/, Bridges/)
Dateien in Unterordnern brauchen eine eigene PBXGroup mit path:
OB030001 /* Onboarding */ = {
isa = PBXGroup;
children = (
2E2ECF4C /* OnboardingStaticPages.swift */,
);
path = Onboarding; ← Xcode sucht in Sources/BrainApp/Onboarding/
sourceTree = "<group>";
};
Wenn die Dateien stattdessen direkt in der BrainApp-Gruppe liegen (ohne eigene
Untergruppe mit path), sucht Xcode in Sources/BrainApp/ und findet sie nicht.
Exit Code 70 (Export fehlgeschlagen)
- Paid Apps Agreement in App Store Connect → Business pruefen
- Status muss "Active" sein, nicht "New"
- Kann bis zu 30–60 Min dauern bis Agreement propagiert
- Alle Felder muessen ausgefuellt sein (auch fuer Privatpersonen)
- Auch pruefen: developer.apple.com/account → Agreements
Exit Code 75 — Timeout (30 Min kein Output)
Build hat 30 Min kein Output produziert. Zwei Hauptursachen:
Ursache 1: some View mit komplexen @ViewBuilder Switches
- Exponentieller Type-Checker-Aufwand bei verschachtelten
if/else/switchin@ViewBuilder - Compile-Zeit: 31 Min → unter 2 Min nach Fix!
- Loesung: AnyView Type Erasure
// FALSCH — exponentieller Type-Check: @ViewBuilder func render(_ node: ScreenNode) -> some View { switch node.type { case "text": Text(node.value) case "stack": VStack { ... } // 20+ cases → Compiler explodiert } } // RICHTIG — linearer Type-Check: func render(_ node: ScreenNode) -> AnyView { switch node.type { case "text": return AnyView(Text(node.value)) case "stack": return AnyView(VStack { ... }) } } - Kein Performance-Nachteil zur Laufzeit (AnyView-Overhead ist vernachlaessigbar)
@ViewBuilderentfernen, stattdessenreturn AnyView(...)
Ursache 2: ci_post_clone.sh ruft resolve auf
xcodebuild -resolvePackageDependenciesin ci_post_clone.sh kann Timeout verursachen- Xcode Cloud resolved selbst — das Script soll das NICHT nochmal machen
List(selection:content:) nicht auf iOS verfuegbar
// FALSCH — macOS-only API:
List(items, selection: $selected) { item in ... }
// RICHTIG — iOS-kompatibel:
List { ForEach(items) { item in ... } }
"The archive contains nothing that can be signed"
XCLocalSwiftPackageReferencefehlt in project.pbxproj- Muss auf
relativePath = .zeigen (lokales Package.swift) - Symptom: Build dauert nur 3-5 Sek, kein Code wird kompiliert
"Could not resolve package dependencies" / "a resolved file is required"
- Package.resolved MUSS committed sein unter:
BrainApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved - NIEMALS in .gitignore packen! Xcode Cloud deaktiviert automatische Resolution
(
CLONED_SOURCE_PACKAGES_PATHgesetzt) und verlangt die Datei zwingend - Auch
xcodebuild -resolvePackageDependenciesin ci_post_clone.sh kann das NICHT umgehen - ci_post_clone.sh mit resolve als Sicherheitsnetz behalten, aber nie als Ersatz fuer committed Package.resolved
- Wenn Package.resolved auf VPS nicht generierbar (z.B. wegen Swift-Version-Mismatch): Die Datei manuell von einem erfolgreichen Xcode Cloud Build herunterladen und committen
"Container file path is invalid"
- Workflow wurde aus Package.swift statt BrainApp.xcodeproj erstellt
- Loesung: Projekt ueber .xcodeproj oeffnen, Workflow neu erstellen
"Create Workflow" ausgegraut
- GitHub-Account in Xcode → Settings → Source Control hinzufuegen
- Projekt ueber .xcodeproj oeffnen (nicht Package.swift)
- Signing muss funktionieren (Geraet registriert, Team gesetzt)
SWIFT_TREAT_WARNINGS_AS_ERRORS — Versteckte Fehler
SWIFT_TREAT_WARNINGS_AS_ERRORS = YES in Release-Config bedeutet: Jede Warning wird
zum Error. Xcode Cloud baut in Release-Mode.
Problem: Wenn eine Datei (z.B. OpenAIProvider.swift) eine Warning-turned-Error hat, zeigt Xcode manchmal NUR die Folge-Fehler ("Cannot find 'OpenAIProvider' in scope") statt den eigentlichen Fehler in der Quelldatei.
Diagnose: Wenn "Cannot find X in scope" erscheint, aber die Datei korrekt registriert ist (alle 3 pbxproj-Eintraege vorhanden), dann hat die Datei vermutlich einen internen Kompilierfehler (oft eine Sendable-Warning).
Extension-Targets in pbxproj (Share Extension, Widgets)
Struktur pro Extension-Target
Jedes Extension-Target braucht diese Sektionen in project.pbxproj:
PBXNativeTarget(product type:com.apple.product-type.app-extension)PBXBuildFilefuer jede Source-DateiPBXFileReferencefuer jede Source-DateiPBXGroupfuer den OrdnerPBXSourcesBuildPhasemit allen Source-FilesPBXFrameworksBuildPhasemit BrainCoreXCBuildConfiguration(Debug + Release)XCConfigurationListPBXContainerItemProxy(Dependency auf Main-App)PBXTargetDependencyPBXCopyFilesBuildPhase("Embed Foundation Extensions")
Wichtige Build-Settings fuer Extensions
SKIP_INSTALL = YES
INFOPLIST_FILE = Sources/[Target]/Info.plist (plus GENERATE_INFOPLIST_FILE = YES)
CODE_SIGN_STYLE = Automatic
CODE_SIGN_ENTITLEMENTS = Sources/[Target]/[Target].entitlements (App Group!)
PRODUCT_BUNDLE_IDENTIFIER = "$(BRAIN_BUNDLE_ID_BASE).[extension-name]"
SWIFT_EMIT_LOC_STRINGS = YES
GENERATE_INFOPLIST_FILE = YES
SharedContainer fuer App Group DB
- Alle Targets die DB-Zugriff brauchen muessen
SharedContainer.swiftUNDAppIdentifiers.swiftkompilieren - App Group ID:
$(BRAIN_APP_GROUP)(=group.$(BRAIN_BUNDLE_ID_BASE), aus Base.xcconfig) — in den Entitlements jedes Targets und als Info.plist-KeyBrainAppGroupID - SharedContainer.swift + AppIdentifiers.swift in PBXBuildFile jedes Targets eintragen
Kritische Dateien
BrainApp.xcodeproj/project.pbxproj ← Projekt-Config, Signing, Build-Nummer
Config/Base.xcconfig ← Identifier-Platzhalter (committed), inkludiert Local.xcconfig
Config/Local.xcconfig ← Echte IDs (gitignored; Vorlage: Local.xcconfig.example)
BrainApp.xcodeproj/project.xcworkspace/
xcshareddata/swiftpm/Package.resolved ← SPM Lock-File (MUSS committed sein, NICHT in .gitignore!)
BrainApp.xcodeproj/xcshareddata/
xcschemes/BrainApp.xcscheme ← Shared Scheme
ci_scripts/ci_post_clone.sh ← Post-Clone Hook: schreibt Local.xcconfig aus Env-Vars, Docs-only-Skip
Voraussetzungen-Checkliste
- Apple Developer Program aktiv ($99/Jahr)
-
Config/Local.xcconfigbefuellt (lokal) bzw. Xcode-Cloud-Env-Vars gesetzt - App IDs
<base>,<base>.share-extension,<base>.widgetsregistriert; App Groupgroup.<base>und iCloud-ContaineriCloud.<base>zugewiesen (developer.apple.com → Identifiers) - Geraet UDID registriert (developer.apple.com → Devices)
- App in App Store Connect angelegt (Name: Brain, SKU: brain-ios)
- Paid Apps Agreement akzeptiert (App Store Connect → Business)
- GitHub in Xcode Source Control verbunden
- Package.resolved committed (NICHT in .gitignore!)
- ci_post_clone.sh vorhanden und executable (Sicherheitsnetz)
Wenn $ARGUMENTS "rebuild" ist
Erhoehe die Build-Nummer in project.pbxproj, committe und pushe.
Wenn $ARGUMENTS "status" ist
Weise den User auf App Store Connect → Brain → Xcode Cloud → Builds hin.
Wenn $ARGUMENTS "fix" ist
Lies die Build-Logs und vergleiche mit den bekannten Fehlern oben.
Pruefungsreihenfolge:
- pbxproj-Eintraege: Fehlen PBXBuildFile-Eintraege fuer neue Dateien?
- PBXSourcesBuildPhase: Sind alle PBXBuildFiles in der Build-Phase gelistet?
- Unterordner-Gruppen: Haben Dateien in Unterordnern eine PBXGroup mit korrektem
path? - Package.resolved: Committed und aktuell?
- Signing-Config: Automatic, Team ID gesetzt?
- Agreements: Paid Apps Agreement aktiv?