· Workflow · 9 Min. Lesezeit

SwiftUI-App-Store-Screenshots ohne Fastlane automatisieren (2026)

SwiftUI-App-Store-Screenshots ohne Fastlane automatisieren (2026)
TL;DR. Fastlane ist nicht der einzige Weg, iOS-Store-Screenshots zu automatisieren — nur der bekannteste. Apple liefert alles nativ mit, was du brauchst: XCUITest + XCUIScreenshot, um die laufende App aufzunehmen, die neue Swift Testing-Attachments-API, um dasselbe mit weniger Boilerplate zu tun, und ImageRenderer, um eine SwiftUI-View direkt in ein PNG zu rendern, ganz ohne Simulator-Lauf. Open Source wie swift-snapshot-testing und AppScreenshotKit kapselt das. Keines von ihnen komponiert das Marketing-Karussell — Geräterahmen, Untertitel, Hintergründe, Texte in 50 Locales. Das ist ein separater Schritt, und genau dort setzt Mokbi an: Es komponiert und lokalisiert die Screenshots, die du aufgenommen hast. Es nimmt sie nicht auf.

Fastlanes snapshot ist die Standardantwort auf „Automatisiere meine App-Store-Screenshots“, und das aus gutem Grund — es ist ausgereift und CI-freundlich. Aber es ist auch eine Ruby-Toolchain, ein Snapfile, eine SnapshotHelper.swift, die du in dein Test-Target kopierst, und ein nicht trivialer Einrichtungsaufwand für etwas, das im Kern ein dünner Wrapper um dieselben XCUITest-APIs ist, die du bereits hast. Wenn du in SwiftUI baust und lieber keine Ruby-Abhängigkeit zu deinem iOS-Projekt hinzufügen möchtest, kannst du Screenshots mit nichts als Xcode und Swift automatisieren. Dieser Beitrag ist die ehrliche Übersicht der nativen Optionen in 2026, mit Code zum Einfügen, plus einer klaren Linie, welches Tool welchen Job macht. Für den direkten Kompromiss-Vergleich siehe Mokbi vs. Fastlane.

Zuerst eine Einordnung, denn das erspart viel Verwirrung: einen Frame aufnehmen (die App in einen Zustand steuern und ein PNG von dem sichern, was auf dem Screen zu sehen ist) und einen Store-Screenshot komponieren (dieser Frame innerhalb eines Geräte-Mockups, mit einer Headline, einem Hintergrund und übersetztem Text, exportiert in jeder erforderlichen Abmessung) sind zwei verschiedene Probleme. Alles in diesem Beitrag löst das erste. Nichts davon löst das zweite.

Warum überhaupt Fastlane überspringen?

Mit Fastlane ist nichts falsch. Die Gründe, warum Entwickler zum nativen Weg greifen, sind praktisch, nicht ideologisch:

  • Kein Ruby im Spiel. Fastlane ist ein Ruby-Gem mit eigenem Versions-Wirrwarr (Bundler, Gem-Konflikte, gelegentliche CI-Brüche nach einem Ruby-Update). Wenn dein Projekt ansonsten reines Swift ist, ist das eine ganze Laufzeitumgebung, die du für eine einzige Aufgabe pflegst.
  • Du hast das Test-Target bereits. snapshot erzeugt unter der Haube UI-Tests. Wenn du ohnehin UI-Tests schreibst, ist das Aufnehmen eines Screenshots nur zwei zusätzliche Zeilen — kein neues Tool nötig.
  • SwiftUI hat die Rechnung verändert. Mit ImageRenderer kannst du einen Screen in ein PNG rendern, ohne die App überhaupt zu starten — das kann Fastlane nicht, es steuert immer den Simulator.
  • Weniger bewegliche Teile in CI. Ein einfacher xcodebuild test, der Screenshots ausgibt, ist einfacher zu durchschauen als eine Fastlane-Lane, die xcodebuild aufruft und dann ein Ergebnis-Bundle nachbearbeitet.

Die Kehrseite: Fastlane bündelt die lästigen Teile (Attachments aus dem .xcresult-Bundle sammeln, sie nach Locale × Gerät sortieren, eine HTML-Vorschau erzeugen und über deliver hochladen) in einem Befehl. Gehst du nativ, wird ein Teil dieser Sammel-Logik zu deinem Problem. Dieser Tausch ist die ganze Entscheidung, und es lohnt sich, ihn bewusst zu treffen.

Option 1: XCUITest + XCUIScreenshot (die laufende App aufnehmen)

Das ist das Fundament, auf dem Fastlane selbst aufbaut. Ein UI-Test-Target startet deine echte App im Simulator, steuert sie mit derselben Finder-/Tap-API wie jeder UI-Test, und XCUIScreen.main nimmt den gesamten Gerätebildschirm in nativer Auflösung auf. Du verpackst diesen Screenshot in ein XCTAttachment und setzt lifetime = .keepAlways, damit Xcode ihn behält, auch wenn der Test erfolgreich ist (standardmäßig verwirft Xcode Attachments bei Erfolg).

// ScreenshotUITests.swift  — runs in a UI test target
import XCTest

final class ScreenshotUITests: XCTestCase {
    func testCaptureCarouselScreens() {
        let app = XCUIApplication()
        // A launch argument your app reads to seed demo data + hide
        // anything that would make a noisy screenshot (debug banners, etc).
        app.launchArguments += ["-FASTLANE_SNAPSHOT", "NO", "-SCREENSHOT_MODE", "YES"]
        app.launch()

        // Drive the app to the state you want, then capture the WHOLE screen.
        snapshot("01_Home")

        app.buttons["openLibrary"].tap()
        snapshot("02_Library")
    }

    private func snapshot(_ name: String) {
        // XCUIScreen.main captures the full device screen at native
        // resolution — exactly what App Store Connect wants.
        let shot = XCUIScreen.main.screenshot()
        let attachment = XCTAttachment(screenshot: shot)
        attachment.name = name
        attachment.lifetime = .keepAlways // or Xcode discards it when the test passes
        add(attachment)
    }
}

Die Screenshots landen innerhalb des .xcresult-Bundles. Du extrahierst sie nach dem Lauf — entweder indem du das Ergebnis-Bundle in Xcode öffnest und sie herausziehst, oder in CI mit einem Tool wie xcparse / xc-screenshot, das durch das Bundle läuft und benannte PNGs in einen Ordner schreibt. Genau dieser Extraktionsschritt ist der Teil, den Fastlane für dich automatisiert, und es ist die Hauptsache, die du dir mit dem nativen Weg aufhalst.

Lokalisierung ohne Code-Änderungen. Der elegante Trick hier ist der Xcode-Testplan: Erstelle einen Testplan, füge eine Konfiguration pro Sprache/Region hinzu (jede legt App-Sprache + Region fest), und derselbe Test läuft einmal pro Konfiguration. Du bekommst lokalisierte Screenshots — die App-UI, gerendert in jeder Locale —, ohne den Testcode anzufassen. Führe es auf einem Simulator der 6.9-Zoll-iPhone-Klasse und einem der 13-Zoll-iPad-Klasse aus, um die Auflösungen zu treffen, die App Store Connect verlangt.

Vor- und Nachteile:

  • Echte App-Pixel. Tatsächlich laufende Views — Schriften, Theme, dynamische Daten, alles nativ — in der exakten Auflösung des Simulators.
  • Kein Fastlane, kein Ruby. Reines Xcode + Swift. Läuft unter xcodebuild test in jedem CI.
  • Locale ist eine Testplan-Konfiguration, kein Code. Füge eine Konfiguration pro Sprache hinzu und führe erneut aus — keine Locale-Verzweigungen in Swift.
  • Die Extraktion liegt bei dir. PNGs aus dem .xcresult-Bundle zu ziehen, ist deine Aufgabe (ein einmaliges Skript, aber real).
  • Wartungsaufwand. Die Finder (buttons["openLibrary"], Taps) brechen, wenn sich die UI ändert, wie bei jedem UI-Test.

Option 2: Swift-Testing-Attachments (die modernere, leichtere Version)

Swift Testing (Apples makrobasiertes Framework, das mit Xcode 26 ausgeliefert wird) hat eine Attachment-API hinzugefügt, die dasselbe tut wie XCTAttachment — mit weniger Ritual und einer schöneren Aufrufstelle. Du steuerst die App weiterhin mit XCUIApplication und nimmst sie mit XCUIScreen.main.screenshot() auf — das gehört zu XCUITest —, aber du zeichnest das Bild mit Attachment.record(...) aus einer @Test-Funktion auf. Der Attachment-API ist es egal, wie das Bild erzeugt wurde; sie akzeptiert jeden konformen Bildtyp und schreibt ihn in das Ergebnis-Bundle des Tests.

// ScreenshotTests.swift  — Swift Testing (Xcode 26+)
import Testing
import XCTest // still needed for XCUIApplication / XCUIScreen

@MainActor
struct ScreenshotTests {
    @Test func homeAndLibrary() async throws {
        let app = XCUIApplication()
        app.launchArguments += ["-SCREENSHOT_MODE", "YES"]
        app.launch()

        // Attachment.record attaches to THIS test's result bundle.
        let home = XCUIScreen.main.screenshot()
        Attachment.record(home.image, named: "01_Home.png")

        app.buttons["openLibrary"].tap()
        let library = XCUIScreen.main.screenshot()
        Attachment.record(library.image, named: "02_Library.png")
    }
}

Funktional nimmt das dieselben Live-App-Pixel auf wie Option 1 — es ist dieselbe zugrunde liegende Bildschirmaufnahme. Was du gewinnst, ist die saubere Swift-Testing-Ergonomie (parametrisierte Tests, async standardmäßig, kein Subclassing von XCTestCase), wenn dein Projekt bereits zu Swift Testing gewechselt ist. Was du dir nicht ersparst, ist derselbe Extraktionsschritt: Das PNG lebt weiterhin im Ergebnis-Bundle und muss weiterhin für den Upload herausgezogen werden. Nutze das, wenn du bereits auf Swift Testing bist; ansonsten ist Option 1 im Ergebnis identisch.

Option 3: ImageRenderer (ganz ohne Simulator-Lauf)

Das ist die Option, die Fastlane strukturell nicht bieten kann. ImageRenderer (SwiftUI, iOS 16+ / macOS 13+) nimmt eine SwiftUI-View und rendert ihre Hierarchie direkt in ein UIImage / CGImage — kein App-Start, keine Simulator-UI-Automatisierung, keine Finder, die brechen können. Du übergibst ihm eine View in den exakten App-Store-Pixel-Abmessungen und liest die PNG-Bytes zurück.

// Render a SwiftUI view straight to a PNG — no simulator UI run.
import SwiftUI

@MainActor
func renderScreen() -> Data? {
    let view = HomeScreen()                 // your real SwiftUI screen
        .frame(width: 1320, height: 2868)   // 6.9-inch iPhone, exact pixels at 1x

    let renderer = ImageRenderer(content: view)
    renderer.scale = 1                       // size is already in target pixels
    renderer.isOpaque = true                 // App Store rejects alpha channels

#if canImport(UIKit)
    return renderer.uiImage?.pngData()
#else
    guard let cg = renderer.cgImage else { return nil }
    let rep = NSBitmapImageRep(cgImage: cg)
    return rep.representation(using: .png, properties: [:])
#endif
}

Zwei Dinge sind für die Store-Ausgabe wichtig. Setze isOpaque = true — App Store Connect lehnt Screenshots mit Alphakanal ab, und ein opakes Rendering vermeidet das. Und achte auf die scale: Wenn du die View in Points statt in Ziel-Pixeln dimensionierst, setze renderer.scale auf deine Geräte-Skalierung, damit die Ausgabe nicht unscharf wird; wenn du den Frame in exakten Ziel-Pixeln dimensionierst (wie oben), belasse die Skalierung bei 1.

Der große Haken ist, was ImageRenderer nicht rendern kann. Es rendert SwiftUI, nicht UIKit-/AppKit-basierte Views: WebViews, MapViews, Media-Player, Kamera-Vorschauen und einige System-Controls kommen als Platzhalter heraus. Für einen sauberen, deterministischen SwiftUI-Screen ist das fantastisch — sofort, headless, perfekt dimensioniert, läuft überall, auch auf dem Gerät. Für einen Screen, der Platform-Views einbettet, bist du wieder bei Option 1–2, um die echte Compositor-Ausgabe aufzunehmen.

  • Mit Abstand am schnellsten. Millisekunden pro Bild, kein Simulator-Start, keine UI-Steuerung. Ideal in CI.
  • Exakte Abmessungen, ganz einfach. Dimensioniere den Frame auf 1320 × 2868 (oder jede erforderliche Größe), und das ist deine Ausgabe — kein Rätselraten beim „auf App-Store-Größe bringen“.
  • Du konstruierst den Zustand. Weil es keine laufende App gibt, übergibst du Mock-Daten direkt an die View — deterministisch, keine Demo-Seeding-Launch-Argumente.
  • Nur SwiftUI. Platform-basierte Views rendern als Platzhalter. Prüfe deinen Screen, bevor du ihm vertraust.

Option 4: Open Source, das das Obige kapselt

Zwei Open-Source-Projekte tauchen ständig auf und lösen angrenzende Probleme.

pointfree/swift-snapshot-testing ist die beliebteste SwiftUI-Snapshot-Bibliothek, und es lohnt sich, bei ihrem Zweck genau zu sein: Sie ist für Regressionstests gebaut, nicht für die Erzeugung von Store-Assets. Der erste Lauf zeichnet ein Referenz-PNG auf und schlägt absichtlich fehl; spätere Läufe vergleichen dagegen, um den Tag abzufangen, an dem eine Padding- oder Schriftänderung deine UI verschiebt.

// pointfree/swift-snapshot-testing — built for regression, not store assets.
import SnapshotTesting
import SwiftUI
import Testing

@MainActor
struct HomeSnapshotTests {
    @Test func home_iPhone16ProMax() {
        let view = HomeScreen()
        // First run records the reference PNG and FAILS on purpose;
        // later runs diff against it. To re-record: record: .all
        assertSnapshot(of: view, as: .image(layout: .device(config: .iPhone13ProMax)))
    }
}

Du kannst die aufgezeichneten Referenzbilder als rohe Screenshots zweckentfremden, und die .image(layout: .device(...))-Strategien rendern in echten Gerätegrößen — aber das widerspricht der Strenge des Tools (es will pixelgenaue Übereinstimmungen; Antialiasing-Unterschiede zwischen Rechnern verursachen instabile Fehlschläge). Greif darauf zurück, um deine UI gegen Regressionen abzusichern, nicht als primäre Screenshot-Pipeline.

AppScreenshotKit (von shitamori1272) ist genau für diesen Zweck gebaut. Du deklarierst eine SwiftUI-View mit einem @AppScreenshot-Makro, das Geräteklassen und Locales festlegt, umschließt deinen echten Screen mit DeviceView, sodass er innerhalb eines Apple-Geräterahmens gerendert wird, und exportierst aus einer Swift-Testing-Funktion. Es organisiert die Ausgabe in einer sauberen Screenshots/<locale>/<device>/-Struktur.

// AppScreenshotKit — declarative SwiftUI screenshots, runs under Swift Testing.
import AppScreenshotKit
import SwiftUI

@AppScreenshot(.iPhone69Inch(), options: .locale([Locale(identifier: "en_US")]))
struct HomeShot: View {
    var body: some View {
        DeviceView { HomeScreen() } // your real screen, wrapped in a device frame
    }
}

// In a test target:
import AppScreenshotKitTestTools
import Testing

@Test @MainActor
func exportScreens() throws {
    let out = URL(fileURLWithPath: "Screenshots")
    let exporter = AppScreenshotExporter(option: .file(outputURL: out))
    try exporter.export(HomeShot.self) // → Screenshots/en_US/iPhone_6_9_inch/...
}

Es lohnt sich, verwandtes Tooling in diesem Bereich zu kennen: CLIs wie storescreens steuern die gesamte App-Store-Connect-Pipeline (XCUITest-Aufnahme über Simulatoren parallel, gerahmte Renderings mit Untertiteln, Metadaten-Upload über Apples API) aus einer einzigen Konfigurationsdatei. Das ist wirklich nützlich, wenn du eine einzige, idempotente, CI-freundliche Kette willst. Sie bringen auch eigene Vorstellungen zum Untertitel-Layout mit — genau an dieser Nahtstelle gewinnt meist ein dedizierter visueller Editor für die Marketing-Oberfläche.

Der Teil, den nichts davon macht: Komposition + Lokalisierung in 50 Sprachen

Hier kommt der abgegrenzte, ehrliche Absatz. Jede Option oben erzeugt nackte Frames — deine App-UI in einer Geräteauflösung (oder, mit AppScreenshotKit und Ähnlichem, einen Frame innerhalb eines einfachen Geräte-Mockups). App-Store-Karussells, die konvertieren, sind keine nackten Frames: Sie sind der Frame in einem gestylten Geräte-Mockup, auf einem Hintergrund, unter einer kurzen Headline, oft als mehrteilige Sequenz, wiederholt über jede Locale, die du anvisierst. Genau für diesen Kompositionsschritt ist Mokbi da. Du bringst die Frames mit, die du aufgenommen hast (aus XCUITest, ImageRenderer, dem Simulator, egal woher), legst sie in einen Browser-Editor, fügst Geräterahmen, Untertitel und Hintergründe hinzu, übersetzt die Untertitel per Klick in bis zu 50 App-Store-Sprachen und exportierst per Batch in jeder erforderlichen Größe. Das Design ist kostenlos mit Vorschau mit Wasserzeichen; unbegrenzter Export und unbegrenztes Veröffentlichen sind im Abo enthalten — Solo für €29.99/mo (1 App) oder Studio für €49.99/mo (bis zu 5 Apps), kein einmaliger Kauf. Um die Grenze klar zu benennen: Mokbi nimmt keine Quell-Screenshots auf — es führt deine App nicht aus und rendert deine Views nicht. Die Aufnahme bleibt beim nativen Swift-Tooling oben; Mokbi komponiert und lokalisiert, was dieses Tooling produziert.

Ein realistischer kombinierter SwiftUI-Workflow

  1. Nimm die Quell-Frames nativ auf. Für deterministische SwiftUI-Screens rendere sie mit ImageRenderer in exakten Pixelgrößen (Option 3) — das ist am schnellsten und braucht keinen Simulator. Für Screens mit Platform-Views oder wenn du den echten Live-Zustand willst, schreibe eine XCUITest- (Option 1) oder Swift-Testing-Aufnahme (Option 2) und führe sie auf einem Simulator der 6.9-Zoll-iPhone-Klasse und der 13-Zoll-iPad-Klasse aus.
  2. Lokalisiere die Aufnahme, wenn du sie brauchst. Füge eine Testplan-Konfiguration pro Sprache hinzu, damit die App-UI in jeder Locale rendert, oder übergib eine Locale an die View, die du an ImageRenderer weitergibst. Das lokalisiert die App-Pixel — nicht deine Marketing-Headline.
  3. Komponiere das Marketing-Karussell. Lade die Frames in Mokbi, füge gestylte Rahmen/Untertitel/Hintergründe hinzu, baue die mehrteilige Sequenz. Das ist der Schritt, den das native Tooling nicht kann.
  4. Übersetze Untertitel und exportiere. Übersetze den Headline-Text per Klick über deine Ziel-App-Store-Locales hinweg und exportiere anschließend jede erforderliche Abmessung per Batch.
  5. Bei der nächsten UI-Änderung: neu aufnehmen und neu öffnen. Führe den Renderer oder den UI-Test erneut aus, öffne das gespeicherte Mokbi-Projekt wieder, tausche die Frames aus, exportiere erneut. Die Komposition und die Übersetzungen bleiben erhalten.

Wann du auf Automatisierung ganz verzichten solltest

Wenn du ein bis drei Releases im Jahr auslieferst, kostet dich jeder Screenshot-Harness — nativ oder Fastlane — mehr Stunden, als er je einspart. Starte die App im iOS-Simulator in der richtigen Geräteklasse, navigiere zu jedem Screen, löse den Screenshot-Befehl des Simulators aus (er nimmt in exakter Geräteauflösung auf), und du hast in zehn Minuten App-Store-fertige PNGs. Komponiere und lokalisiere dann einmal und mach weiter. Die obige Automatisierung zahlt sich genau dann aus, wenn „von Hand neu aufnehmen“ kein Zehn-Minuten-Job mehr ist — häufige Releases, viele Locales oder mehrere Apps in einem Portfolio. Richte die Maschinerie an deinem Release-Rhythmus aus, nicht daran, was rigoros aussieht. Dieselbe Logik gilt auf den anderen Plattformen; siehe die React-Native-Version dieses Problems und den umfassenderen Fastlane-vs.-no-code-Kompromiss.

Egal, womit du aufnimmst — prüfe die Zielwerte, bevor du exportierst: Die exakten Pixel-Abmessungen und Format-Regeln (PNG/JPEG, RGB, kein Alphakanal) findest du im Guide zu den App-Store-Screenshot-Größen. Apple lehnt Abmessungen ab, die auch nur um ein Pixel daneben liegen, ganz ohne Toleranz — das sind also dreißig Sekunden gut investiert.

Was du als Nächstes liest

Editor öffnen →