· Workflow · 9 min lezen

SwiftUI App Store-screenshots automatiseren zonder Fastlane (2026)

SwiftUI App Store-screenshots automatiseren zonder Fastlane (2026)
TL;DR. Fastlane is niet de enige manier om iOS store-screenshots te automatiseren — het is alleen de bekendste. Apple levert alles wat je nodig hebt native mee: XCUITest + XCUIScreenshot om de live app vast te leggen, de nieuwe Swift Testing-attachments-API om hetzelfde te doen met minder boilerplate, en ImageRenderer om een SwiftUI-view rechtstreeks naar een PNG te renderen zonder simulator-run. Open source zoals swift-snapshot-testing en AppScreenshotKit verpakken dat. Geen van alle stelt de marketingcarrousel samen — frames, bijschriften, achtergronden, copy voor 50 locales. Dat is een aparte stap, en daar past Mokbi: het stelt de screenshots die je hebt vastgelegd samen en lokaliseert ze. Het legt ze niet vast.

Fastlane's snapshot is het standaardantwoord op "automatiseer mijn App Store-screenshots", en niet zonder reden — het is volwassen en CI-vriendelijk. Maar het is ook een Ruby-toolchain, een Snapfile, een SnapshotHelper.swift die je naar je testtarget kopieert, en een niet-triviale hoeveelheid installatiewerk voor iets wat onder de motorkap een dunne wrapper is om dezelfde XCUITest-API's die je al hebt. Als je in SwiftUI bouwt en liever geen Ruby-afhankelijkheid aan je iOS-project toevoegt, kun je screenshots automatiseren met niets anders dan Xcode en Swift. Dit artikel is de eerlijke kaart van de native opties in 2026, met code die je kunt plakken, plus een duidelijke lijn over welke tool welke taak doet. Voor de directe vergelijking, zie Mokbi vs Fastlane.

Eerst één kader, want dat scheelt veel verwarring: een frame vastleggen (de app naar een toestand sturen en een PNG opslaan van wat er op het scherm staat) en een store-screenshot samenstellen (dat frame in een device mockup, met een kop, een achtergrond, en vertaalde copy, geëxporteerd op elk vereist formaat) zijn twee verschillende problemen. Alles in dit artikel lost het eerste op. Niets ervan lost het tweede op.

Waarom Fastlane sowieso overslaan?

Er is niets mis met Fastlane. De redenen waarom developers naar het native pad grijpen zijn praktisch, niet ideologisch:

  • Geen Ruby in de mix. Fastlane is een Ruby-gem met zijn eigen versiewisselingen (Bundler, gem-conflicten, af en toe een kapotte CI na een Ruby-update). Als je project verder puur Swift is, is dat een hele runtime die je onderhoudt voor één taak.
  • Je hebt de test-target al. snapshot genereert onder de motorkap UI-tests. Als je toch al UI-tests schrijft, is een screenshot vastleggen twee extra regels — geen nieuwe tool nodig.
  • SwiftUI heeft de rekensom veranderd. Met ImageRenderer kun je een scherm naar een PNG renderen zonder de app helemaal te starten, wat Fastlane niet kan — het stuurt altijd de simulator aan.
  • Minder bewegende delen in CI. Een simpele xcodebuild test die screenshots oplevert is makkelijker te doorgronden dan een Fastlane-lane die xcodebuild aanroept en daarna een resultatenbundel naverwerkt.

De keerzijde: Fastlane bundelt de vervelende onderdelen (attachments uit de .xcresult-bundle halen, ze organiseren per locale × apparaat, een HTML-preview genereren, en uploaden via deliver) in één commando. Ga je native, dan wordt een deel van dat verzamelwerk jouw probleem. Die afweging is de hele beslissing, en die is de moeite waard om bewust te maken.

Optie 1: XCUITest + XCUIScreenshot (de live app vastleggen)

Dit is de basis waarop Fastlane zelf is gebouwd. Een UI-testtarget start je echte app in de simulator, stuurt hem aan met dezelfde finder/tap-API als elke UI-test, en XCUIScreen.main legt het hele apparaatscherm vast op native resolutie. Je verpakt die screenshot in een XCTAttachment en zet lifetime = .keepAlways zodat Xcode hem bewaart, ook als de test slaagt (standaard worden attachments bij een geslaagde test verwijderd).

// 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)
    }
}

De screenshots komen terecht in de .xcresult-bundle. Je haalt ze er na de run uit — hetzij door de resultatenbundle in Xcode te openen en ze eruit te slepen, hetzij in CI met een tool zoals xcparse / xc-screenshot die de bundle doorloopt en benoemde PNG's naar een map schrijft. Deze extractiestap is precies het onderdeel dat Fastlane voor je automatiseert, en is het belangrijkste dat je erbij krijgt als je native gaat.

Lokalisatie zonder codewijzigingen. De mooie truc hier is het Xcode-testplan: maak één testplan, voeg een configuratie toe per taal/regio (elk stelt de app-taal + regio in), en dezelfde test draait één keer per configuratie. Je krijgt gelokaliseerde screenshots — de app-UI weergegeven in elke locale — zonder de testcode aan te raken. Draai op een simulator van de 6,9-inch iPhone-klasse en een simulator van de 13-inch iPad-klasse om de resoluties te halen die App Store Connect vereist.

Voor- en nadelen:

  • Echte app-pixels. Daadwerkelijk draaiende views — lettertypen, thema, dynamische data, allemaal native — op de exacte resolutie van de simulator.
  • Geen Fastlane, geen Ruby. Puur Xcode + Swift. Draait onder xcodebuild test in elke CI.
  • Locale is een testplan-configuratie, geen code. Voeg een configuratie toe per taal en draai opnieuw; geen per-locale vertakkingen in Swift.
  • Het uitpakken is jouw verantwoordelijkheid. PNG's uit de .xcresult-bundle halen is voor jou (een eenmalig script, maar wel echt werk).
  • Onderhoudskosten. De finders (buttons["openLibrary"], taps) breken zodra de UI verandert, zoals elke UI-test.

Optie 2: Swift Testing-attachments (de moderne, lichtere versie)

Swift Testing (Apple's macro-gebaseerde framework dat met Xcode 26 wordt meegeleverd) voegde een Attachment-API toe die doet wat XCTAttachment deed, met minder poespas en een prettigere aanroep. Je stuurt de app nog steeds aan met XCUIApplication en legt vast met XCUIScreen.main.screenshot() — die horen bij XCUITest — maar je registreert de afbeelding met Attachment.record(...) vanuit een @Test-functie. De attachment-API maakt niet uit hoe de afbeelding is geproduceerd; hij accepteert elk conform afbeeldingstype en schrijft het naar de resultatenbundel van de test.

// 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")
    }
}

Functioneel legt dit dezelfde live-app-pixels vast als Optie 1 — het is dezelfde onderliggende schermopname. Wat je wint is de schonere Swift Testing-ergonomie (geparametriseerde tests, standaard async, geen XCTestCase-subclassing) als je project al is overgestapt op Swift Testing. Wat je niet ontloopt is dezelfde extractiestap: de PNG staat nog steeds in de resultatenbundel en moet er nog steeds uit gehaald worden voor upload. Gebruik dit als je al op Swift Testing zit; anders is Optie 1 identiek qua resultaat.

Optie 3: ImageRenderer (helemaal geen simulator nodig)

Dit is de optie die Fastlane structureel niet kan bieden. ImageRenderer (SwiftUI, iOS 16+ / macOS 13+) neemt een SwiftUI-view en rendert de hiërarchie ervan rechtstreeks naar een UIImage / CGImage — geen app-start, geen simulator-UI-automatisering, geen finders die kunnen breken. Je geeft het een view op precies de App Store-pixelafmetingen en leest PNG-bytes terug.

// 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
}

Twee dingen zijn belangrijk voor store-output. Zet isOpaque = true — App Store Connect wijst screenshots met een alphakanaal af, en een dekkende render voorkomt dat. En let op de scale: als je de view in punten in plaats van doelpixels sizet, zet dan renderer.scale op je apparaatschaal zodat de output niet wazig wordt; als je het frame op exacte doelpixels sizet (zoals hierboven), houd de scale dan op 1.

De grote kanttekening is wat ImageRenderer niet kan renderen. Het rendert SwiftUI, geen UIKit/AppKit-gebaseerde views: webviews, mapviews, mediaspelers, camerapreviews en sommige systeembesturingselementen komen eruit als placeholders. Voor een schoon, deterministisch SwiftUI-scherm is het fantastisch — direct, headless, perfect op maat, draait overal, ook op het apparaat zelf. Voor een scherm dat platform-views bevat, val je terug op Opties 1–2 om de echte compositor-output vast te leggen.

  • Verreweg het snelst. Milliseconden per afbeelding, geen simulator-boot, geen UI-aansturing. Uitstekend in CI.
  • Exacte afmetingen, moeiteloos. Zet het frame op 1320 × 2868 (of elk vereist formaat) en dat is je output — geen "resizen naar App Store"-giswerk.
  • Jij bouwt de toestand op. Omdat er geen draaiende app is, geef je mock-data direct door aan de view — deterministisch, geen demo-seeding via launch arguments.
  • Alleen SwiftUI. Platform-gebaseerde views renderen als placeholders. Controleer je scherm voordat je erop vertrouwt.

Optie 4: open source dat het bovenstaande verpakt

Twee open-sourceprojecten komen steeds weer terug, en lossen aangrenzende problemen op.

pointfree/swift-snapshot-testing is de populairste SwiftUI-snapshotbibliotheek, en het is de moeite waard om precies te zijn over het doel ervan: het is gebouwd voor regressietests, niet om store-assets te produceren. De eerste run legt een referentie-PNG vast en faalt met opzet; latere runs vergelijken met die referentie om de dag te vangen waarop een padding- of lettertypewijziging je UI verschuift.

// 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)))
    }
}

Je kunt de opgeslagen referentieafbeeldingen hergebruiken als ruwe screenshots, en de .image(layout: .device(...))-strategieën renderen op echte apparaatformaten — maar dat gaat in tegen de striktheid van de tool (hij wil pixelperfecte matches; antialiasing-verschillen tussen machines veroorzaken onstabiele fouten). Gebruik het om je UI te beschermen tegen regressies, niet als je primaire screenshot-pipeline.

AppScreenshotKit (van shitamori1272) is specifiek gebouwd voor deze taak. Je declareert een SwiftUI-view met een @AppScreenshot-macro die apparaatklassen en locales opgeeft, verpakt je echte scherm in DeviceView zodat het binnen een Apple-device frame rendert, en exporteert vanuit een Swift Testing-functie. Het organiseert output in een overzichtelijke Screenshots/<locale>/<device>/-boomstructuur.

// 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/...
}

Verwante tooling in dit gebied is de moeite waard om te kennen: CLI's zoals storescreens sturen de hele App Store Connect-pipeline aan (XCUITest-opname over simulators parallel, geframede renders met bijschriften, metadata-upload via Apple's API) vanuit één configuratiebestand. Deze zijn echt nuttig als je één idempotente, CI-vriendelijke keten wilt. Ze brengen ook hun eigen opvattingen mee over bijschrift-layout — precies de naad waar een specifieke visuele editor meestal wint voor het marketingoppervlak.

Het onderdeel dat niets van het bovenstaande doet: compositie + lokalisatie in 50 talen

Hier is de afgebakende, eerlijke paragraaf. Elke optie hierboven produceert kale frames — je app-UI op een apparaatresolutie (of, met AppScreenshotKit en vergelijkbare tools, een frame in een gewone device mockup). App Store-carrousels die converteren zijn geen kale frames: het zijn frames in een gestylede device mockup, op een achtergrond, onder een korte kop, vaak als een multi-paneel reeks, herhaald voor elke locale die je target. Die compositiestap is waar Mokbi voor is. Je brengt de frames die je hebt vastgelegd (uit XCUITest, ImageRenderer, de simulator, waar dan ook) naar een browser-editor, voegt device frames en bijschriften en achtergronden toe, vertaalt de bijschriften over maximaal 50 App Store-talen in ongeveer één klik, en exporteert in bulk op elk vereist formaat. Ontwerpen met een preview met watermerk is gratis; onbeperkt exporteren en publiceren zit bij een abonnement — Solo voor €29.99/mo (1 app) of Studio voor €49.99/mo (tot 5 apps), geen eenmalige aankoop. Om duidelijk te zijn over de grens: Mokbi legt geen bronscreenshots vast — het draait je app niet en rendert je views niet. Het vastleggen blijft bij de native Swift-tooling hierboven; Mokbi stelt samen en lokaliseert wat die tooling produceert.

Een realistische gecombineerde SwiftUI-workflow

  1. Leg de bronframes native vast. Voor deterministische SwiftUI-schermen render je ze met ImageRenderer op exacte pixelformaten (Optie 3) — het is het snelst en heeft geen simulator nodig. Voor schermen met platform-views, of waar je de echte live-toestand wilt, schrijf je een XCUITest- (Optie 1) of Swift Testing-opname (Optie 2) en draai je op een simulator van de 6,9-inch iPhone-klasse en de 13-inch iPad-klasse.
  2. Lokaliseer de opname als je dat nodig hebt. Voeg een testplan-configuratie toe per taal zodat de app-UI in elke locale rendert, of geef een locale door aan de view die je aan ImageRenderer geeft. Dit lokaliseert de app-pixels — niet je marketingkop.
  3. Stel de marketingcarrousel samen. Zet de frames in Mokbi, voeg gestylede frames/bijschriften/achtergronden toe, bouw de multi-paneel reeks. Dit is de stap die de native tooling niet kan.
  4. Vertaal bijschriften en exporteer. Vertaal de kop-copy met één klik over je App Store-doellocales, exporteer daarna in bulk op elk vereist formaat.
  5. Bij de volgende UI-wijziging: opnieuw vastleggen en heropenen. Draai de renderer of de UI-test opnieuw, open het opgeslagen Mokbi-project opnieuw, vervang de frames, exporteer opnieuw. De compositie en vertalingen blijven behouden.

Wanneer je automatisering helemaal kunt overslaan

Als je één tot drie releases per jaar uitbrengt, kost het bouwen van elk screenshot-harnas — native of Fastlane — je meer uren dan het ooit oplevert. Draai de app in de iOS Simulator op de juiste apparaatklasse, navigeer naar elk scherm, gebruik het screenshot-commando van de simulator (dat legt vast op exacte apparaatresolutie), en je hebt binnen tien minuten App Store-klare PNG's. Stel dan één keer samen en lokaliseer, en ga verder. De automatisering hierboven betaalt zich precies uit wanneer "opnieuw met de hand fotograferen" geen tien-minuten-klusje meer is — frequente releases, veel locales, of meerdere apps in een portfolio. Stem de machinerie af op je releasetempo, niet op wat rigoureus oogt. Dezelfde logica geldt op de andere platforms; zie de React Native-versie van dit probleem en de bredere Fastlane vs no-code-afweging.

Waarmee je ook vastlegt, controleer de doelwaarden voordat je exporteert: haal de exacte pixelafmetingen en formaatregels (PNG/JPEG, RGB, geen alphakanaal) uit de gids voor App Store-screenshotformaten. Apple wijst afmetingen die ook maar één pixel afwijken zonder pardon af, dus dit is de dertig seconden waard.

Verder lezen

Editor openen →