CommunityBildgithub.com

ParthJadhav/ios-marketing-capture

Verwenden Sie dieses Skill, um Marketing-Screenshots einer SwiftUI iOS App automatisiert über mehrere Locales, Geräte oder Darstellungen hinweg zu erfassen. Es deckt Vollbildaufnahmen, isolierte Element-Renderings (Karussellkarten, Widgets) und reproduzierbare Ausgabebenennung ab.

Was ist ios-marketing-capture?

Dieses Skill automatisiert die reproduzierbare Erstellung von Marketing-Screenshots für SwiftUI iOS Apps über mehrere Locales hinweg. Es bietet zwei parallele Ausgabeströme: Vollbildaufnahmen aller marketingrelevanten Bildschirme mit deterministischen Seed-Daten und echten Statusleisten/Safe-Area-Chrome sowie isolierte Element-Renderings spezifischer Komponenten (Karten, Widgets, Diagramme) in beliebiger Skalierung mit transparentem Hintergrund und abgerundeten Ecken. Die Kernentscheidung dieses Skills ist der Einsatz eines In-App-Capture-Modus anstelle von XCUITest, was eine schnellere Iteration, direkten Zugriff auf echte App-Zustände und keine Abhängigkeit von Test-Targets ermöglicht. Es unterstützt gängige Navigationsmuster wie TabView, NavigationStack und NavigationSplitView und bietet Mechanismen zur Vorbereitung spezifischer View-Zustände (Priming) für Aufnahmen in Nicht-Standard-Zuständen. Dieses Skill ist primär für die Verwendung mit KI-Agenten wie Claude Code, Cursor oder Codex konzipiert, die SwiftUI und das iOS-Ökosystem nutzen.

Funktioniert mit~Claude Code~Codex CLI~Cursor
npx skills add ParthJadhav/ios-marketing-capture

Installed? Explore more Bild skills: steipete/songsee, affaan-m/frontend-design, mattpocock/improve-codebase-architecture · View all 6 →

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

iOS Marketing Capture

Übersicht

Automatisieren Sie die reproduzierbare Erstellung von Marketing-Screenshots für eine SwiftUI iOS App über mehrere Locales hinweg, mit zwei parallelen Ausgabeströmen:

  1. Vollbildaufnahmen — jeder marketingrelevante Bildschirm mit deterministischen Seed-Daten, echter Statusleiste / Safe-Area-Chrome
  2. Elementaufnahmen — isolierte Renderings bestimmter Komponenten (Karten, Widgets, Diagramme) in beliebiger Skalierung, mit natürlichem Hintergrund innerhalb abgerundeter Ecken und Transparenz außerhalb

Dieses Skill ist der Capture-Schritt. Wenn der Benutzer auch Apple-ähnliche Marketing-Seiten wünscht, die um die Aufnahmen herum zusammengesetzt werden (Gerätemockups, Überschriften, Farbverläufe), kombinieren Sie dies mit dem app-store-screenshots-Skill als Nachbearbeitungsschritt.

Kernansatz

In-App-Capture-Modus, nicht XCUITest. Dies ist eine bewusste Entscheidung, die gegen Fastlane-Snapshot / XCUITest-Konventionen abgewogen wurde, und sie gewinnt für fast jedes reale Projekt.

Warum In-App gegenüber XCUITest:

  • Kein neues Test-Target. Das Hinzufügen eines UI-Test-Targets zu einem bestehenden Xcode-Projekt ist eine fragile pbxproj-Operation. Viele Projekte haben keine Test-Targets und kein xcodegen – das manuelle Hinzufügen ist fehleranfällig.
  • Schnellere Iteration. Ein UI-Test benötigt 30s+ pro Start. In-App-Capture ist lediglich ein Neustart der installierten Binärdatei.
  • Kein xcodebuild test. Der gesamte Ablauf ist einmal xcodebuild build, dann simctl launch pro Locale. Kein Test-Bundle-Overhead.
  • Zugriff auf echten App-Zustand. Sie können ViewModels, SwiftData, ImageRenderer und UIWindow.drawHierarchy direkt aufrufen. XCUITest kann nur tippen und Barrierefreiheitselemente lesen.
  • Element-Renderings benötigen ohnehin In-Process-Zugriff. ImageRenderer für Widget-Ansichten oder isolierte Komponenten muss innerhalb des App-Prozesses ausgeführt werden – es gibt kein XCUITest-Äquivalent.

So funktioniert es:

  1. Eine DEBUG-exklusive Datei MarketingCapture.swift befindet sich im Haupt-App-Target
  2. Wenn die App mit -MarketingCapture 1 gestartet wird, werden Seed-Daten gesetzt, dann durchläuft ein Koordinator eine Liste von CaptureSteps – jeder Schritt navigiert, wartet auf Beruhigung, erstellt einen Screenshot und räumt auf
  3. PNGs werden in das Sandbox-Verzeichnis Documents/marketing/<locale>/ der App geschrieben
  4. Ein Shell-Skript baut einmal, installiert, durchläuft dann die Locales durch Neustart mit -AppleLanguages (xx) -AppleLocale xx und extrahiert die Dateien über simctl get_app_container

Ablauf

Arbeiten Sie diese Schritte in der angegebenen Reihenfolge ab. Überspringen Sie keine Schritte.

Schritt 1: Anforderungen sammeln

Stellen Sie dem Benutzer diese Fragen einzeln (nicht gebündelt – jede Antwort kann spätere Fragen ungültig machen):

  1. Aufzunehmende Bildschirme — "Welche Bildschirme möchten Sie? Geben Sie für jeden den Navigationspfad oder den Tab-Namen an." Holen Sie eine konkrete Liste ein, nicht "die Hauptabläufe".
  2. Isolierte Elemente — "Gibt es Komponenten, die Sie unabhängig mit transparentem Hintergrund rendern möchten? (Karussellkarten, Widgets, Hero-Kacheln, Diagramme usw.)"
  3. Locales — "Welche Locales? (a) alle Locales in Ihrer Localizable.xcstrings, (b) eine von mir zu spezifizierende App Store-Teilmenge, oder (c) ich gebe Ihnen eine explizite Liste." Wenn (a), durchsuchen Sie die .xcstrings-Datei nach Locale-Codes:
    python3 -c "import json; d=json.load(open('<path>/Localizable.xcstrings')); langs=set(); [langs.update(v.get('localizations',{}).keys()) for v in d['strings'].values()]; print(sorted(langs))"
    
  4. Gerät — "Welcher Simulator? (6,1" iPhone 17 empfohlen für iOS 26 Design-Features)" – Überprüfen Sie, ob das Gerät über xcrun simctl list devices available verfügbar ist.
  5. Darstellung — "Nur hell, nur dunkel oder beides?"
  6. Seed-Daten — "Wie werden Demodaten derzeit befüllt? (a) Frischinstallation seedet automatisch, (b) es gibt einen Debug-Button 'Demo-Daten laden', (c) Sie fügen sie manuell hinzu, (d) es existieren noch keine Demo-Daten." Dann: "Sind die vorhandenen Daten umfassend genug, dass jeder von Ihnen aufgelistete Bildschirm für Marketing befüllt aussieht? Prüfen Sie dies mit dem Benutzer."

Schritt 2: Erkundung

Bevor Sie Code schreiben, erkunden Sie die Codebasis ausreichend, um Folgendes zu beantworten:

  • Verwendet das Projekt Xcode synchronisierte Ordner-Gruppen (Xcode 16+, PBXFileSystemSynchronizedRootGroup)? Wenn ja, werden neue Dateien automatisch in ihr Target aufgenommen – keine pbxproj-Bearbeitung erforderlich. Überprüfen Sie mit grep -c PBXFileSystemSynchronized <proj>.xcodeproj/project.pbxproj.
  • Was ist das grundlegende Navigationsmuster?
    • TabView(selection:) – am häufigsten. Sie benötigen: die @State selectedTab-Bindung, Tab-Indizes und welche Tabs verschachtelte NavigationStacks haben.
    • NavigationStack (einzelner Stack mit einem Router) – Sie benötigen: die Pfadbindung oder das Router-Objekt sowie die Menge der NavigationLink(value:) / .navigationDestination-Typen.
    • NavigationSplitView – Sie benötigen: die Seitenleisten-Auswahlbindung, den Navigationsstatus der Detailspalte.
    • Benutzerdefinierter Koordinator / UIKit-Host – Sie benötigen: die navigate(to:)-Methode des Koordinators oder ein Äquivalent.
  • Wie werden Deep Links weitergeleitet? Finden Sie den onOpenURL-Handler und das Enum/Switch, das URLs mit Navigationszuständen verknüpft.
  • Wo sind Demodaten-Seeder definiert? Verfolgen Sie den Codepfad vom Debug-Button (falls vorhanden) zur Funktion, die tatsächlich in den ModelContext schreibt. Wenn kein Seeder existiert, siehe "Erstellen eines Demo-Daten-Seeders" unten.
  • Leben Widgets in einem separaten Target? Sind die Widget-View-Dateien und Entry-Typen auch im Haupt-App-Target? (Höchstwahrscheinlich nein – sie müssen hinzugefügt werden, wenn Sie sie über ImageRenderer rendern möchten.)
  • Verwendet die App Live Activities / ActivityKit? Wenn ja, markieren Sie dies als bekannte Hürde (siehe unten).
  • Verwendet die App SwiftData + CloudKit Sync (cloudKitDatabase: .automatic)? Wenn ja, markieren Sie dies als bekannte Hürde.
  • Muss eine Ansicht in einem vom Standard abweichenden Zustand erfasst werden? (z. B. ein Timer während des Countdowns, ein teilweise ausgefülltes Formular, ein Diagramm mit bestimmten Werten). Wenn ja, benötigt jede einen static var-Mechanismus zum Vorbereiten (siehe "View-Zustand vorbereiten" unten).

Schritt 3: Design dem Benutzer vorstellen

Fassen Sie Ihren Plan in dieser Struktur zusammen. Holen Sie vor der Implementierung explizite Zustimmung ein:

  1. Architektur (In-App-Capture-Modus, einzelne Datei, DEBUG-gesteuert)
  2. Dateiliste (genaue Pfade, die Sie erstellen / ändern werden)
  3. Bildschirm-für-Bildschirm-Capture-Plan (wie jeder Bildschirm erreicht wird – Tab-Index, Navigationspfad, Sheet-Trigger)
  4. Begründung der Capture-Reihenfolge (welche Bildschirme müssen vor anderen kommen – siehe Hürde Nr. 5)
  5. Ansatz zum Rendern von Elementen (welche Komponenten, wie sie eingebettet werden)
  6. Ausgabelayout (Ordnerstruktur, Namenskonvention)
  7. Bekannte Hürden, die für dieses Projekt relevant sind (aus Schritt 2 markiert)
  8. Vorbereitete Zustände (welche Ansichten, welche statischen Variablen)

Schritt 4: Implementieren

Verwenden Sie die Vorlagen in templates/ als Ausgangspunkte. Sie sind Referenzmuster, kein Kopier-Einfüge-Gerüst – jedes Projekt hat andere Navigation, Modelle und Ansichten. Die Vorlagen zeigen die Bausteine; Sie setzen sie für die Ziel-App zusammen.

Wichtige zu erstellende Dateien:

  • <AppName>/Debug/MarketingCapture.swift – das gesamte Capture-System, DEBUG-exklusiv. Enthält:
    • MarketingCapture-Enum (Launch-Argument-Parsing, Ausgabe-Helfer, Fenster-Screenshot, Vorbereitungsvariablen)
    • MarketingCaptureCoordinator-Klasse (durchläuft [CaptureStep] und erstellt für jeden einen Screenshot)
    • MarketingElementHarness-Enum (ImageRenderer-Renderings von Karten, Widgets, Diagrammen)
  • <AppName>/ContentView.swift (oder wo die Root-View lebt) – DEBUG-Hook, der Daten seedet und den Koordinator ausführt.
  • Alle Ansichten, die vorbereitete Zustände benötigen – DEBUG-gesteuerte .onAppear-Hooks und .onReceive-Dismiss-Listener.
  • scripts/capture-marketing.sh – Build + Install + Locale-Schleife.
  • .gitignore – marketing/ hinzufügen.

Schritt 5: Iterativ verifizieren

Übergeben Sie das Skript nicht an den Benutzer und warten Sie. Führen Sie es selbst gegen einen Simulator aus und verifizieren Sie mindestens ein Locale, bevor Sie es als fertig erklären. Lesen Sie die ausgegebenen PNGs mit dem Lese-Tool, um visuell zu überprüfen, ob jeder Bildschirm Ihren Erwartungen entspricht. Häufige Laufzeitprobleme sind in "Bekannte Hürden" unten aufgeführt.

Wenn Sie ein Problem finden, beheben Sie es, führen Sie das gesamte Skript erneut aus (nicht nur das fehlschlagende Locale – Korrekturen können frühere Locales beeinträchtigen) und überprüfen Sie visuell erneut.

Architektur: Schrittbasierte Aufnahme

Der Koordinator steuert die Aufnahme, indem er eine Liste von CaptureStep-Werten durchläuft. Jeder Schritt ist in sich abgeschlossen: Er weiß, wie er zu seinem Bildschirm navigiert, wie lange er warten muss und wie er danach aufräumt.

struct CaptureStep {
    let name: String                        // Ausgabedateiname, z.B. "01-home"
    let navigate: @MainActor () -> Void     // App in den richtigen Zustand versetzen
    let settle: Duration                    // Warten auf Animationen/Ladevorgänge
    let cleanup: (@MainActor () -> Void)?   // Aufräumen vor dem nächsten Schritt
}

Der Koordinator ist eine einfache Schleife:

for step in steps {
    step.navigate()
    try? await Task.sleep(for: step.settle)
    if let image = MarketingCapture.snapshotKeyWindow() {
        MarketingCapture.writePNG(image, name: step.name)
    }
    step.cleanup?()
    try? await Task.sleep(for: .milliseconds(400))  // Aufräum-Animation
}

Schritte für verschiedene Navigationsmuster erstellen

TabView-App (am häufigsten):

// Einfacher Tab-Wechsel – nur den Index setzen
CaptureStep(name: "01-home", navigate: { setTab(0) }, settle: .milliseconds(1800), cleanup: nil)

// Tab + angezeigtes Sheet
CaptureStep(
    name: "05-timer-setup",
    navigate: {
        setTab(3)
        pendingBrewRecipe = someRecipe
    },
    settle: .milliseconds(2000),
    cleanup: {
        NotificationCenter.default.post(name: MarketingCapture.dismissSheetNotification, object: nil)
        pendingBrewRecipe = nil
    }
)

NavigationStack + Router-App:

// Eine Route auf den Stack schieben
CaptureStep(
    name: "02-detail",
    navigate: { router.push(.itemDetail(item)) },
    settle: .milliseconds(1800),
    cleanup: { router.popToRoot() }
)

NavigationSplitView-App:

// Seitenleisten-Element auswählen, dann Detail
CaptureStep(
    name: "03-detail",
    navigate: {
        sidebarSelection = .recipes
        detailSelection = recipes.first
    },
    settle: .milliseconds(1800),
    cleanup: { detailSelection = nil }
)

Reihenfolge: Die Stapelregel

Erfassen Sie Bildschirme, die einen "sauberen" Navigationszustand benötigen, VOR Bildschirmen, die auf denselben Stapel schieben. Verschachtelte NavigationPath / @State innerhalb von Kindansichten können vom Koordinator nicht zurückgesetzt werden. Also:

Gut:  Regal (saubere Liste) → Kaffee-Detail (schiebt auf das Stapel des Regals)
Schlecht: Kaffee-Detail → Regal (Stapel hat immer noch das Detail drauf)

Wenn zwei Bildschirme einen gemeinsamen NavigationStack verwenden, erfassen Sie zuerst die Ansicht auf der Stammebene.

View-Zustand vorbereiten

Einige Bildschirme müssen in einem bestimmten, vom Standard abweichenden Zustand erfasst werden – ein Timer während des Countdowns, ein Diagramm mit bestimmten Werten, ein halb ausgefülltes Formular. Das Muster:

  1. Fügen Sie eine static var zu MarketingCapture für jeden vorzubereitenden Wert hinzu:

    /// Vom Koordinator gesetzt, bevor die Timer-Ansicht angezeigt wird.
    /// Die Ansicht liest dies in .onAppear, um zu einer bestimmten verstrichenen Zeit zu springen.
    static var pendingElapsedSeconds: Int?
    
    /// Auf true gesetzt, um den Bewertungs-Overlay auf dem Timer anzuzeigen.
    static var pendingShowAssessment: Bool = false
    
  2. Fügen Sie in der Zielansicht einen DEBUG-gesteuerten .onAppear-Hook hinzu, der den Vorbereitungswert liest:

    .onAppear {
        #if DEBUG
        if MarketingCapture.isActive, let elapsed = MarketingCapture.pendingElapsedSeconds {
            phase = .active
            timerVM.elapsedTime = TimeInterval(elapsed)
            timerVM.start()
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { timerVM.pause() }
        }
        #endif
    }
    
  3. Setzen Sie im Koordinator die Variable vor der Navigation:

    CaptureStep(
        name: "06-timer-midway",
        navigate: {
            MarketingCapture.pendingElapsedSeconds = 75
            openTimerSheet(someRecipe)
        },
        settle: .milliseconds(2400),
        cleanup: {
            MarketingCapture.pendingElapsedSeconds = nil
            NotificationCenter.default.post(name: MarketingCapture.dismissSheetNotification, object: nil)
        }
    )
    

Erstellen eines Demo-Daten-Seeders

Wenn die App keinen bestehenden Mechanismus für Demo-Daten hat, erstellen Sie einen. Platzieren Sie ihn in <AppName>/Debug/DemoDataSeeder.swift, eingeschlossen in #if DEBUG.

Richtlinien:

  • Seedet genügend Daten, dass jeder erfasste Bildschirm befüllt aussieht. Prüfen Sie die Liste der Bildschirme gegen den Seed.
  • Verwenden Sie realistische Inhalte: echte Ortsnamen, plausible Zahlen, unterschiedliche Zustände (einige Artikel "zur Neige gehend", einige "frisch", einige mit Bildern, einige ohne).
  • Wenn die App SwiftData verwendet, schreiben Sie direkt in den ModelContext. Bei Core Data verwenden Sie den Managed-Object-Context. Bei einem REST-Backend seeden Sie über die lokale Cache/Speicherschicht.
  • Machen Sie das Seeden idempotent – prüfen Sie, ob bereits Daten vorhanden sind, bevor Sie einfügen. Der Speicher bleibt über Simulator-Neustarts hinweg erhalten und das erneute Seed pro Locale verursacht CloudKit-Synchronisationschurn und Abstürze.
  • Fügen Sie genug Vielfalt hinzu, um verschiedene UI-Zustände zu füllen: Leere Zustände sollten NICHT erscheinen, es sei denn, sie sind ein Marketing-Bildschirm.

Minimales Beispiel:

#if DEBUG
enum DemoDataSeeder {
    static func seedIfEmpty(in context: ModelContext) {
        let existing = (try? context.fetchCount(FetchDescriptor<Item>())) ?? 0
        guard existing == 0 else { return }

        // Artikel mit verschiedenen Zuständen
        let items = [
            Item(name: "...", status: .active, ...),
            Item(name: "...", status: .lowStock, ...),
            // ...genug, um jeden Bildschirm zu füllen
        ]
        items.forEach { context.insert($0) }
        try? context.save()
    }
}
#endif

Element-Rendering

Elemente werden über ImageRenderer im 3-fachen Maßstab mit Transparenz außerhalb abgerundeter Ecken gerendert.

Karten / Listenzeilen

@MainActor
static func renderCards(items: [Item], theme: AppTheme) {
    let cardWidth: CGFloat = 380

    for item in items {
        let card = ItemCard(item: item, theme: theme)
            .padding(.horizontal, 16)
            .padding(.vertical, 12)
            .frame(width: cardWidth)
            .background(theme.background)
            .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))

        let renderer = ImageRenderer(content: card)
        renderer.scale = 3
        renderer.isOpaque = false
        renderer.proposedSize = .init(width: cardWidth, height: ...)
        // ...weitere Logik
    }
}

Widgets

Standard-Widget-Background (benötigt App-Erweiterung im Haupt-Target):

// Widget in einer View einbetten, die den ContainerBackground API verwendet
// (Nur iOS 26+ für Drittanbieter-Widgets)

Bildschirmaufnahme

Vollbildaufnahmen werden mit UIWindow.drawHierarchy erstellt:

static func snapshotKeyWindow() -> UIImage? {
    guard let window = UIApplication.shared.connectedScenes
        .compactMap({ $0 as? UIWindowScene })
        .first?.windows
        .first(where: \.isKeyWindow) else { return nil }
    
    let format = UIGraphicsImageRendererFormat()
    format.scale = 1
    format.opaque = true
    let renderer = UIGraphicsImageRenderer(bounds: window.bounds, format: format)
    return renderer.image { ctx in
        window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
    }
}

Ausgabe-Pipeline

Der Ausgabeordner ist Documents/marketing/<locale>/ im Sandbox der App. Dateien heißen <name>.png. Nach der Aufnahme extrahiert das Shell-Skript sie:

xcrun simctl get_app_container <device> <bundle-id> data

Dann kopiert es den gesamten Ordner Documents/marketing an einem definierten Ausgabeort.

Shell-Skript

Das Skript scripts/capture-marketing.sh:

  1. Baut das App-Target mit xcodebuild build (Debug-Schema)
  2. Installiert den Build auf dem Simulator (xcrun simctl install)
  3. Für jedes Locale:
    • Startet die App mit -AppleLanguages (xx) -AppleLocale xx -MarketingCapture 1
    • Wartet auf Abschluss des Capture-Durchlaufs
    • Extrahiert Ausgabedateien aus dem Sandbox
  4. Kombiniert alle Locales im Ausgabeordner

Git-Ignorieren

Fügen Sie marketing/ zu Ihrer .gitignore hinzu, um generierte Screenshots aus der Versionskontrolle auszuschließen.

Bekannte Hürden

1. SwiftData + CloudKit Sync

Wenn die App cloudKitDatabase: .automatic verwendet, verursacht jede ModelContext.save()-Operation einen Netzwerk- und Synchronisationsaufwand. Bei mehreren Locales kann dies die App verlangsamen oder einfrieren (CloudKit-Warteschlange wächst). Lösung: Vor dem Capture-Durchlauf die CloudKit-Synchronisation deaktivieren:

// In der App-Umgebung, falls verfügbar
UserDefaults.standard.set(false, forKey: "CloudKitSyncEnabled")

Alternativ Modell-Container nur für die Sitzung local-only konfigurieren.

2. ActivityKit / Live Activities

Live Activities können nicht in einem Simulator ausgeführt werden. Wenn die App eine aktive Live Activity benötigt, um den Bildschirm korrekt darzustellen, muss der Benutzer auf einem physischen Gerät testen. Das Skript kann dies nicht automatisieren.

3. Widget-Rendering erfordert zusätzliches Target

Wenn Widgets in einem separaten Widget-Extension-Target leben, sind die SwiftUI-Views und Entry-Strukturen für das Haupt-App-Target nicht sichtbar. Lösung: Verschieben Sie die Widget-View-Dateien und Entry-Modelle in das Haupt-Target (wählen Sie in Xcode das Haupt-Target in der Dateiinspektor aus). Dies bricht das Widget nicht, solange die Dateien in beiden Targets bleiben.

4. Core Data + CloudKit Sync

Ähnlich wie SwiftData+CloudKit. Verwenden Sie die Option NSPersistentCloudKitContainer.Options zum Deaktivieren während der Aufnahmesitzung.

5. Stapelverschmutzung bei nicht bereinigten Ansichten

Jeder CaptureStep sollte seinen eigenen Zustand bereinigen. Wenn ein Schritt vergisst, einen Sheet zu schließen (cleanup: { dismissSheet() }) oder eine Route nicht poppt, wird der nächste Schritt in einem verschmutzten Navigationszustand starten. Lösung: Erfassen Sie immer die übergeordnete Ansicht vor untergeordneten Ansichten auf demselben NavigationStack. Verwenden Sie DispatchQueue.main.async für Bereinigungen, die nach Abschluss der Animation ausgeführt werden müssen.

6. Statusleistenüberlagerung

Für Aufnahmen in voller Bildschirmgröße (ohne Safe Area) sollten Sie die Statusleiste im Simulator ausblenden (xcrun simctl status_bar <device> override --time "9:41" --batteryState charged --batteryLevel 100), um konsistente Aufnahmen zu gewährleisten. Fügen Sie dies als optionalen Schritt im Shell-Skript hinzu.

Fehlerbehebung

ProblemMögliche UrsacheLösung
Screenshot ist schwarzFenster nicht bereit nach Navigationsettle-Dauer erhöhen (≥2s)
Screenshot zeigt leere ListeSeed-Daten wurden nicht gefülltPrüfen, ob seedIfEmpty() in .task aufgerufen wird
App stürzt ab beim LaunchLaunch-Argument falsch geparst#if DEBUG um Capture-Logik prüfen
PNG-Datei beschädigtSimulator ausgelastetRender.afterScreenUpdates auf true setzen
simctl kann Container nicht findenBundle-ID nicht korrekt oder Gerät nicht verfügbarÜberprüfen mit xcrun simctl list devices

Siehe auch SwiftUI Rendering Guide für ImageRenderer-spezifische Einschränkungen.

Verwandte Skills