Automatiser les captures d'écran App Store SwiftUI sans fastlane (2026)
XCUITest + XCUIScreenshot pour capturer l'app en cours d'exécution, la nouvelle API de pièces jointes Swift Testing pour faire la même chose avec moins de code répétitif, et ImageRenderer pour restituer une vue SwiftUI directement en PNG sans lancer de simulateur. Des projets open source comme swift-snapshot-testing et AppScreenshotKit les enveloppent. Aucun d'entre eux ne compose le carrousel marketing — cadres, légendes, fonds, textes en 50 locales. C'est une étape distincte, et c'est là que Mokbi s'insère : il compose et localise les captures d'écran que tu as capturées. Il ne les capture pas.Le module snapshot de fastlane est la réponse par défaut à « automatiser mes captures d'écran App Store », et pour de bonnes raisons — il est mature et adapté au CI. Mais c'est aussi une chaîne d'outils Ruby, un Snapfile, un SnapshotHelper.swift que tu copies dans ta cible de test, et une configuration non négligeable pour ce qui, en dessous, n'est qu'une fine surcouche des mêmes API XCUITest que tu as déjà. Si tu développes en SwiftUI et que tu préfères ne pas ajouter une dépendance Ruby à ton projet iOS, tu peux automatiser les captures d'écran avec rien d'autre que Xcode et Swift. Cet article dresse la carte honnête des options natives en 2026, avec du code que tu peux coller, plus une ligne claire sur quel outil fait quel travail. Pour le comparatif direct, voir Mokbi vs fastlane.
Un premier cadrage, car il évite pas mal de confusion : capturer une frame (amener l'app dans un état et enregistrer un PNG de ce qui est affiché) et composer une capture d'écran de store (cette frame dans une maquette d'appareil, avec un titre, un fond et un texte traduit, exportée à chaque dimension requise) sont deux problèmes différents. Tout ce qui suit résout le premier. Rien de tout cela ne résout le second.
Pourquoi se passer de fastlane ?
Il n'y a rien de mal avec fastlane. Les raisons pour lesquelles les développeurs se tournent vers la voie native sont pratiques, pas idéologiques :
- Pas de Ruby dans la boucle. fastlane est une gem Ruby avec son propre cycle de versions (Bundler, conflits de gems, la casse CI occasionnelle après une mise à jour de Ruby). Si ton projet est par ailleurs du Swift pur, c'est tout un runtime supplémentaire à maintenir pour une seule tâche.
- Tu as déjà la cible de test.
snapshotgénère des tests UI en coulisses. Si tu écris de toute façon des tests UI, capturer une capture d'écran, ce sont deux lignes de plus — aucun nouvel outil requis. - SwiftUI a changé la donne. Avec
ImageRenderer, tu peux restituer un écran en PNG sans lancer l'app du tout, ce que fastlane ne peut pas faire — il pilote toujours le simulateur. - Moins de rouages en CI. Un simple
xcodebuild testqui produit des captures d'écran est plus facile à raisonner qu'une lane fastlane qui appellexcodebuildpuis post-traite un bundle de résultats.
Le revers de la médaille : fastlane regroupe les parties fastidieuses (récupérer les pièces jointes dans le bundle .xcresult, les organiser par locale × appareil, générer un aperçu HTML, et uploader via deliver) en une seule commande. En passant au natif, une partie de cette plomberie de collecte devient ton problème. Ce compromis, c'est toute la décision, et il vaut la peine d'être fait consciemment.
Option 1 : XCUITest + XCUIScreenshot (capturer l'app en cours d'exécution)
C'est le socle sur lequel fastlane lui-même est construit. Une cible de test UI lance ta vraie app dans le simulateur, la pilote avec la même API de recherche/tap que n'importe quel test UI, et XCUIScreen.main capture l'écran entier de l'appareil à la résolution native. Tu enveloppes cette capture d'écran dans un XCTAttachment et tu définis lifetime = .keepAlways pour que Xcode la conserve même quand le test réussit (le comportement par défaut jette les pièces jointes en cas de succès).
// 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)
}
}Les captures d'écran atterrissent dans le bundle .xcresult. Tu les extrais après l'exécution — soit en ouvrant le bundle de résultats dans Xcode et en les glissant hors de là, soit en CI avec un outil comme xcparse / xc-screenshot qui parcourt le bundle et écrit des PNG nommés dans un dossier. Cette étape d'extraction est exactement la partie que fastlane automatise pour toi, et c'est la principale chose que tu prends en charge en passant au natif.
Localisation sans changement de code. L'astuce ici, c'est le test plan de Xcode : crée un test plan, ajoute une configuration par langue/région (chacune définit la langue + la région de l'app), et le même test s'exécute une fois par configuration. Tu obtiens des captures d'écran localisées — l'interface de l'app rendue dans chaque locale — sans toucher au code de test. Exécute sur un simulateur de classe iPhone 6,9 pouces et un simulateur de classe iPad 13 pouces pour atteindre les résolutions exigées par App Store Connect.
Avantages et inconvénients :
- De vrais pixels d'app. De vraies vues en cours d'exécution — polices, thème, données dynamiques, tout natif — à la résolution exacte du simulateur.
- Pas de fastlane, pas de Ruby. Xcode + Swift purs. Fonctionne sous
xcodebuild testdans n'importe quel CI. - La locale est une config de test plan, pas du code. Ajoute une configuration par langue et relance ; pas de branches par locale en Swift.
- L'extraction, c'est toi qui la gères. Sortir les PNG du bundle
.xcresultest à ta charge (un script à écrire une fois, mais réel). - Coût de maintenance. Les recherches d'éléments (
buttons["openLibrary"], taps) se cassent quand l'interface change, comme tout test UI.
Option 2 : pièces jointes Swift Testing (la version moderne et plus légère)
Swift Testing (le framework à base de macros d'Apple livré avec Xcode 26) a ajouté une API Attachment qui fait ce que faisait XCTAttachment, avec moins de cérémonie et un point d'appel plus agréable. Tu pilotes toujours l'app avec XCUIApplication et tu captures avec XCUIScreen.main.screenshot() — ça reste dans XCUITest — mais tu enregistres l'image avec Attachment.record(...) depuis une fonction @Test. L'API de pièce jointe ne se soucie pas de la façon dont l'image a été produite ; elle accepte tout type d'image conforme et l'écrit dans le bundle de résultats du 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")
}
}Fonctionnellement, ça capture les mêmes pixels d'app en direct que l'option 1 — c'est la même capture d'écran sous-jacente. Ce que tu gagnes, c'est l'ergonomie Swift Testing plus propre (tests paramétrés, async par défaut, pas de sous-classement XCTestCase) si ton projet est déjà passé à Swift Testing. Ce à quoi tu n'échappes pas, c'est la même étape d'extraction : le PNG vit toujours dans le bundle de résultats et doit toujours en être extrait pour l'upload. Utilise cette option si tu es déjà sur Swift Testing ; sinon, l'option 1 donne un résultat identique.
Option 3 : ImageRenderer (sans aucune exécution de simulateur)
C'est l'option que fastlane ne peut structurellement pas offrir. ImageRenderer (SwiftUI, iOS 16+ / macOS 13+) prend une vue SwiftUI et restitue sa hiérarchie directement en UIImage / CGImage — pas de lancement d'app, pas d'automatisation d'interface de simulateur, pas de recherches d'éléments à casser. Tu lui donnes une vue dimensionnée aux pixels exacts de l'App Store et tu récupères les octets PNG.
// 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
}Deux choses comptent pour la sortie destinée au store. Définis isOpaque = true — App Store Connect rejette les captures d'écran avec un canal alpha, et un rendu opaque évite ça. Et fais attention au scale : si tu dimensionnes la vue en points plutôt qu'en pixels cibles, définis renderer.scale à l'échelle de ton appareil pour que la sortie ne soit pas floue ; si tu dimensionnes la frame en pixels cibles exacts (comme ci-dessus), garde l'échelle à 1.
La grande réserve, c'est ce que ImageRenderer ne peut pas restituer. Il restitue du SwiftUI, pas des vues portées par UIKit/AppKit : les web views, les vues de carte, les lecteurs média, les aperçus caméra et certains contrôles système ressortent sous forme de placeholders. Pour un écran SwiftUI propre et déterministe, c'est fantastique — instantané, sans tête, parfaitement dimensionné, s'exécute partout y compris sur l'appareil. Pour un écran qui intègre des vues de plateforme, tu reviens aux options 1-2 pour capturer la vraie sortie du compositeur.
- De loin le plus rapide. Millisecondes par image, pas de démarrage de simulateur, pas de pilotage d'interface. Excellent en CI.
- Dimensions exactes, trivialement. Dimensionne la frame à 1320 × 2868 (ou toute taille requise) et c'est ta sortie — pas de tâtonnement « redimensionner pour l'App Store ».
- Tu construis l'état. Comme il n'y a pas d'app en cours d'exécution, tu passes des données factices directement dans la vue — déterministe, pas d'arguments de lancement pour préparer une démo.
- SwiftUI uniquement. Les vues portées par la plateforme se restituent en placeholders. Vérifie ton écran avant de lui faire confiance.
Option 4 : des projets open source qui enveloppent ce qui précède
Deux projets open source reviennent constamment, résolvant des problèmes adjacents.
pointfree/swift-snapshot-testing est la bibliothèque de snapshot SwiftUI la plus populaire, et il vaut la peine d'être précis sur son objectif : elle est conçue pour les tests de non-régression, pas pour produire des assets de store. La première exécution enregistre un PNG de référence et échoue exprès ; les exécutions suivantes le comparent à cette référence pour détecter le jour où un changement de marge ou de police décale ton interface.
// 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)))
}
}Tu peux réutiliser les images de référence enregistrées comme captures d'écran brutes, et les stratégies .image(layout: .device(...)) restituent aux vraies tailles d'appareil — mais ça va à l'encontre de la rigueur de l'outil (il veut des correspondances au pixel près, et les différences d'antialiasing d'une machine à l'autre provoquent des échecs instables). Utilise-le pour protéger ton interface contre les régressions, pas comme pipeline principal de captures d'écran.
AppScreenshotKit (par shitamori1272) est conçu spécifiquement pour cette tâche. Tu déclares une vue SwiftUI avec une macro @AppScreenshot spécifiant les classes d'appareils et les locales, tu enveloppes ton véritable écran dans DeviceView pour qu'il s'affiche dans un cadre d'appareil Apple, et tu exportes depuis une fonction Swift Testing. Il organise la sortie dans une arborescence propre Screenshots/<locale>/<device>/.
// 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/...
}Il vaut la peine de connaître d'autres outils apparentés dans ce domaine : des CLI comme storescreens pilotent tout le pipeline App Store Connect (capture XCUITest sur plusieurs simulateurs en parallèle, rendus encadrés avec légendes, upload des métadonnées via l'API d'Apple) depuis un seul fichier de configuration. Ils sont vraiment utiles si tu veux une chaîne unique, idempotente et adaptée au CI. Ils apportent aussi leurs propres partis pris sur la mise en page des légendes — c'est là que la couture où un éditeur visuel dédié tend à gagner pour la surface marketing.
Ce qu'aucune de ces options ne fait : la composition + la localisation en 50 langues
Voici le paragraphe honnête et cadré. Chaque option ci-dessus produit des frames brutes — l'interface de ton app à une résolution d'appareil (ou, avec AppScreenshotKit et consorts, une frame dans une simple maquette d'appareil). Les carrousels App Store qui convertissent ne sont pas des frames brutes : ce sont la frame dans une maquette d'appareil stylisée, sur un fond, sous un titre court, souvent sous forme de séquence multi-panneaux, répétée pour chaque locale que tu cibles. Cette étape de composition, c'est ce à quoi sert Mokbi. Tu apportes les frames que tu as capturées (depuis XCUITest, ImageRenderer, le simulateur, où que ce soit), tu les déposes dans un éditeur navigateur, tu ajoutes des cadres d'appareils, des légendes et des fonds, tu traduis les légendes dans jusqu'à 50 langues de l'App Store en à peu près un clic, et tu exportes en lot à chaque dimension requise. C'est gratuit pour concevoir avec un aperçu en filigrane ; l'export illimité et la publication sont inclus avec un abonnement — Solo à €29.99/mo (1 app) ou Studio à €49.99/mo (jusqu'à 5 apps), sans achat unique. Pour être explicite sur la frontière : Mokbi ne capture pas les captures d'écran sources — il ne fait pas tourner ton app et ne restitue pas tes vues. La capture reste du ressort des outils Swift natifs ci-dessus ; Mokbi compose et localise ce que ces outils produisent.
Un workflow SwiftUI combiné réaliste
- Capture les frames sources nativement. Pour des écrans SwiftUI déterministes, restitue-les avec
ImageRendereraux tailles de pixels exactes (option 3) — c'est le plus rapide et ça n'a besoin d'aucun simulateur. Pour des écrans avec des vues de plateforme ou quand tu veux le véritable état en direct, écris une capture XCUITest (option 1) ou Swift Testing (option 2) et exécute-la sur un simulateur de classe iPhone 6,9 pouces et de classe iPad 13 pouces. - Localise la capture si tu en as besoin. Ajoute une configuration de test plan par langue pour que l'interface de l'app se restitue dans chaque locale, ou passe une locale à la vue que tu donnes à
ImageRenderer. Ça localise les pixels de l'app — pas ton titre marketing. - Compose le carrousel marketing. Dépose les frames dans Mokbi, ajoute des cadres/légendes/fonds stylisés, construis la séquence multi-panneaux. C'est l'étape que les outils natifs ne peuvent pas faire.
- Traduis les légendes et exporte. Traduis en un clic le texte du titre dans toutes les locales App Store que tu cibles, puis exporte en lot chaque dimension requise.
- Au prochain changement d'interface, recapture et rouvre. Relance le renderer ou le test UI, rouvre le projet Mokbi sauvegardé, échange les frames, réexporte. La composition et les traductions sont conservées.
Quand se passer complètement de l'automatisation
Si tu livres une à trois releases par an, construire un harnais de captures d'écran — natif ou fastlane — te coûtera plus d'heures qu'il n'en fera jamais gagner. Lance l'app dans le simulateur iOS sur la bonne classe d'appareil, navigue vers chaque écran, utilise la commande de capture d'écran du simulateur (elle capture à la résolution exacte de l'appareil), et tu as des PNG prêts pour l'App Store en dix minutes. Compose et localise ensuite une fois, et passe à autre chose. L'automatisation ci-dessus se rentabilise précisément quand « reprendre les captures à la main » cesse d'être un travail de dix minutes — releases fréquentes, nombreuses locales, ou plusieurs apps dans un portefeuille. Adapte les outils à ton rythme de release, pas à ce qui a l'air rigoureux. La même logique s'applique sur les autres plateformes ; voir la version React Native de ce problème et le comparatif plus large fastlane vs sans code.
Quel que soit ton outil de capture, vérifie les cibles avant d'exporter : récupère les dimensions de pixels exactes et les règles de format (PNG/JPEG, RGB, pas de canal alpha) dans le guide des tailles de captures d'écran App Store. Apple rejette les dimensions décalées d'un pixel sans aucune tolérance, donc ça vaut trente secondes.