Automatizzare gli screenshot App Store SwiftUI senza fastlane (2026)
XCUITest + XCUIScreenshot per catturare l'app in esecuzione, la nuova API di attachment di Swift Testing per fare lo stesso con meno boilerplate, e ImageRenderer per renderizzare una vista SwiftUI direttamente in un PNG senza eseguire il simulatore. OSS come swift-snapshot-testing e AppScreenshotKit avvolgono queste API. Nessuno di questi strumenti compone il carosello marketing — cornici, didascalie, sfondi, testi in 50 locale. Quella è una fase separata, ed è lì che si inserisce Mokbi: compone e localizza gli screenshot che hai catturato. Non li cattura.Il modulo snapshot di fastlane è la risposta predefinita a "automatizza i miei screenshot App Store", e per una buona ragione: è maturo e adatto al CI. Ma è anche una toolchain Ruby, uno Snapfile, uno SnapshotHelper.swift che copi nel tuo test target e una quantità non trascurabile di setup per quella che, sotto il cofano, è solo una sottile interfaccia sopra le stesse API XCUITest che hai già. Se sviluppi in SwiftUI e preferiresti non aggiungere una dipendenza Ruby al tuo progetto iOS, puoi automatizzare gli screenshot con nient'altro che Xcode e Swift. Questo articolo è la mappa onesta delle opzioni native nel 2026, con codice che puoi incollare, più una linea chiara su quale strumento fa cosa. Per il confronto diretto, vedi Mokbi vs fastlane.
Un primo inquadramento, perché evita molta confusione: catturare un frame (portare l'app a uno stato e salvare un PNG di ciò che è a schermo) e comporre uno screenshot per lo store (quel frame dentro un mockup del dispositivo, con un titolo, uno sfondo e testi tradotti, esportato in ogni dimensione richiesta) sono due problemi diversi. Tutto in questo articolo risolve il primo. Niente risolve il secondo.
Perché saltare fastlane del tutto?
Non c'è nulla di sbagliato in fastlane. Le ragioni per cui gli sviluppatori scelgono il percorso nativo sono pratiche, non ideologiche:
- Niente Ruby nel ciclo. fastlane è una gem Ruby con il proprio ricambio di versioni (Bundler, conflitti tra gem, l'occasionale rottura del CI dopo un aggiornamento di Ruby). Se il resto del tuo progetto è puro Swift, è un intero runtime in più da mantenere per un solo compito.
- Hai già il test target.
snapshotgenera UI test dietro le quinte. Se scrivi comunque UI test, catturare uno screenshot sono due righe in più — nessun nuovo strumento richiesto. - SwiftUI ha cambiato i conti. Con
ImageRendererpuoi renderizzare una schermata in un PNG senza avviare l'app, cosa che fastlane non può fare — guida sempre il simulatore. - Meno parti in movimento nel CI. Un semplice
xcodebuild testche produce screenshot è più facile da ragionare rispetto a una lane fastlane che invocaxcodebuilde poi post-elabora un bundle di risultati.
Il rovescio della medaglia: fastlane racchiude le parti noiose (raccogliere gli attachment dal bundle .xcresult, organizzarli per locale × dispositivo, generare un'anteprima HTML e caricarli via deliver) in un solo comando. Andando nativo, parte di quell'idraulica di raccolta diventa un tuo problema. Questo scambio è l'intera decisione, e vale la pena farla consapevolmente.
Opzione 1: XCUITest + XCUIScreenshot (catturare l'app in esecuzione)
Questa è la base su cui fastlane stesso è costruito. Un test target UI avvia la tua app reale nel simulatore, la guida con la stessa API finder/tap di qualsiasi UI test, e XCUIScreen.main cattura l'intero schermo del dispositivo alla risoluzione nativa. Avvolgi quello screenshot in un XCTAttachment e imposti lifetime = .keepAlways perché Xcode lo mantenga anche quando il test passa (il comportamento predefinito scarta gli attachment in caso di successo).
// 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)
}
}Gli screenshot finiscono dentro il bundle .xcresult. Li estrai dopo l'esecuzione — aprendo il bundle dei risultati in Xcode e trascinandoli fuori, oppure in CI con uno strumento come xcparse / xc-screenshot che percorre il bundle e scrive PNG con nome in una cartella. Questo passaggio di estrazione è esattamente la parte che fastlane automatizza per te, ed è la cosa principale che ti assumi andando nativo.
Localizzazione senza modifiche al codice. Il trucco elegante qui è il test plan di Xcode: crei un test plan, aggiungi una configurazione per lingua/regione (ognuna imposta la lingua + regione dell'app), e lo stesso test gira una volta per configurazione. Ottieni screenshot localizzati — la UI dell'app renderizzata in ogni locale — senza toccare il codice del test. Esegui su un simulatore classe iPhone da 6,9 pollici e uno classe iPad da 13 pollici per coprire le risoluzioni richieste da App Store Connect.
Pro e contro:
- Pixel reali dell'app. Viste effettivamente in esecuzione — font, tema, dati dinamici, tutto nativo — alla risoluzione esatta del simulatore.
- Niente fastlane, niente Ruby. Puro Xcode + Swift. Gira sotto
xcodebuild testin qualsiasi CI. - La locale è una configurazione del test plan, non codice. Aggiungi una configurazione per lingua e riesegui; nessun branch per locale in Swift.
- L'estrazione è tua. Tirare fuori i PNG dal bundle
.xcresultè a tuo carico (uno script una tantum, ma reale). - Costo di manutenzione. I finder (
buttons["openLibrary"], i tap) si rompono quando la UI cambia, come in qualsiasi UI test.
Opzione 2: gli attachment di Swift Testing (la versione moderna e più leggera)
Swift Testing (il framework di Apple basato su macro incluso in Xcode 26) ha aggiunto un'API Attachment che fa ciò che faceva XCTAttachment, con meno cerimonie e un call site più pulito. Guidi ancora l'app con XCUIApplication e catturi con XCUIScreen.main.screenshot() — quelle API vivono in XCUITest — ma registri l'immagine con Attachment.record(...) da una funzione @Test. L'API di attachment non si preoccupa di come è stata prodotta l'immagine; accetta qualsiasi tipo immagine conforme e la scrive nel result bundle del 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")
}
}Funzionalmente questo cattura gli stessi pixel dell'app in esecuzione dell'Opzione 1 — è la stessa cattura di schermo sottostante. Ciò che guadagni è l'ergonomia più pulita di Swift Testing (test parametrizzati, async di default, nessuna sottoclasse XCTestCase) se il tuo progetto è già passato a Swift Testing. Ciò che non eviti è lo stesso passaggio di estrazione: il PNG vive comunque nel result bundle e deve comunque essere tirato fuori per il caricamento. Usala quando sei già su Swift Testing; altrimenti l'Opzione 1 è identica come risultato.
Opzione 3: ImageRenderer (nessuna esecuzione del simulatore)
Questa è l'opzione che fastlane strutturalmente non può offrire. ImageRenderer (SwiftUI, iOS 16+ / macOS 13+) prende una vista SwiftUI e ne renderizza la gerarchia direttamente in un UIImage / CGImage — nessun avvio dell'app, nessuna automazione UI del simulatore, nessun finder da rompere. Gli dai una vista dimensionata esattamente ai pixel richiesti da App Store e leggi i byte del PNG risultante.
// 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
}Due cose contano per l'output store. Imposta isOpaque = true — App Store Connect rifiuta gli screenshot con un canale alfa, e un render opaco lo evita. E fai attenzione alla scale: se dimensioni la vista in punti anziché nei pixel target, imposta renderer.scale alla scala del tuo dispositivo perché l'output non sia sfocato; se dimensioni il frame nei pixel target esatti (come sopra), mantieni la scala a 1.
L'avvertenza importante è ciò che ImageRenderer non può renderizzare. Renderizza SwiftUI, non viste basate su UIKit/AppKit: web view, map view, lettori multimediali, anteprime della fotocamera e alcuni controlli di sistema escono come placeholder. Per una schermata SwiftUI pulita e deterministica è fantastico — istantaneo, headless, dimensionato perfettamente, gira ovunque incluso sul dispositivo. Per una schermata che integra viste native della piattaforma, torni alle Opzioni 1-2 per catturare l'output reale del compositor.
- Di gran lunga il più veloce. Millisecondi per immagine, nessun avvio del simulatore, nessuna guida della UI. Ottimo nel CI.
- Dimensioni esatte, banalmente. Dimensiona il frame a 1320 × 2868 (o qualsiasi dimensione richiesta) ed è già il tuo output — nessun "ridimensiona per App Store" a occhio.
- Costruisci tu lo stato. Poiché non c'è un'app in esecuzione, passi direttamente dati mock alla vista — deterministico, nessun launch argument per seminare dati demo.
- Solo SwiftUI. Le viste native della piattaforma vengono renderizzate come placeholder. Verifica la tua schermata prima di fidartene.
Opzione 4: OSS che avvolge quanto sopra
Due progetti open source ricorrono costantemente, risolvendo problemi adiacenti.
pointfree/swift-snapshot-testing è la libreria di snapshot testing per SwiftUI più popolare, e vale la pena essere precisi sul suo scopo: è costruita per il test di regressione, non per produrre asset per lo store. La prima esecuzione registra un PNG di riferimento e fallisce di proposito; le esecuzioni successive confrontano con quel riferimento per intercettare il giorno in cui un cambio di padding o font sposta la tua UI.
// 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)))
}
}Puoi riutilizzare le immagini di riferimento registrate come screenshot grezzi, e le strategie .image(layout: .device(...)) renderizzano alle dimensioni reali del dispositivo — ma questo va contro la rigidità dello strumento (vuole corrispondenze pixel-perfette, e le differenze di antialiasing tra macchine causano fallimenti intermittenti). Usala per proteggere la tua UI dalle regressioni, non come pipeline primaria per gli screenshot.
AppScreenshotKit (di shitamori1272) è costruita appositamente per questo compito. Dichiari una vista SwiftUI con una macro @AppScreenshot specificando classi di dispositivo e locale, avvolgi la tua schermata reale in DeviceView perché venga renderizzata dentro una cornice del dispositivo Apple, ed esporti da una funzione Swift Testing. Organizza l'output in un albero pulito 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/...
}Vale la pena conoscere anche il tooling correlato in questo spazio: CLI come storescreens guidano l'intera pipeline di App Store Connect (cattura XCUITest su più simulatori in parallelo, render con cornice e didascalie, caricamento dei metadati via API Apple) da un unico file di configurazione. Sono genuinamente utili se vuoi una catena unica, idempotente e adatta al CI. Portano anche le proprie opinioni sul layout delle didascalie — che è il punto in cui un editor visivo dedicato tende a vincere per la superficie marketing.
La parte che nessuna delle opzioni sopra fa: composizione + localizzazione in 50 lingue
Ecco il paragrafo circoscritto e onesto. Ogni opzione sopra produce frame grezzi — la UI della tua app a una risoluzione del dispositivo (o, con AppScreenshotKit e simili, un frame dentro un semplice mockup del dispositivo). I caroselli App Store che convertono non sono frame grezzi: sono il frame dentro un mockup del dispositivo curato, su uno sfondo, sotto un titolo breve, spesso come sequenza multi-pannello, ripetuta per ogni locale che targetizzi. Quella fase di composizione è a cosa serve Mokbi. Porti i frame che hai catturato (da XCUITest, ImageRenderer, il simulatore, ovunque), li trascini in un editor nel browser, aggiungi cornici del dispositivo, didascalie e sfondi, traduci le didascalie fino a 50 lingue App Store in circa un clic, ed esporti in batch a ogni dimensione richiesta. Progettare è gratuito con un'anteprima con watermark; l'esportazione e la pubblicazione illimitate arrivano con un abbonamento — Solo a €29.99/mo (1 app) o Studio a €49.99/mo (fino a 5 app), nessun acquisto una tantum. Per essere espliciti sul confine: Mokbi non cattura gli screenshot sorgente — non esegue la tua app né renderizza le tue viste. La cattura resta con il tooling nativo Swift descritto sopra; Mokbi compone e localizza ciò che quel tooling produce.
Un workflow SwiftUI combinato realistico
- Cattura i frame sorgente nativamente. Per schermate SwiftUI deterministiche, renderizzale con
ImageRenderera dimensioni pixel esatte (Opzione 3) — è la più veloce e non serve il simulatore. Per schermate con viste native o dove vuoi lo stato reale in esecuzione, scrivi una cattura XCUITest (Opzione 1) o Swift Testing (Opzione 2) ed esegui su un simulatore classe iPhone da 6,9 pollici e classe iPad da 13 pollici. - Localizza la cattura se ti serve. Aggiungi una configurazione del test plan per lingua perché la UI dell'app venga renderizzata in ogni locale, oppure passa una locale alla vista che dai a
ImageRenderer. Questo localizza i pixel dell'app — non il titolo marketing. - Componi il carosello marketing. Trascina i frame in Mokbi, aggiungi cornici/didascalie/sfondi curati, costruisci la sequenza multi-pannello. Questo è il passaggio che il tooling nativo non può fare.
- Traduci le didascalie ed esporta. Traduci con un clic il testo del titolo in tutte le locale App Store target, poi esporta in batch ogni dimensione richiesta.
- Al prossimo cambio di UI, ricattura e riapri. Riesegui il renderer o l'UI test, riapri il progetto Mokbi salvato, sostituisci i frame, riesporta. La composizione e le traduzioni sono conservate.
Quando saltare del tutto l'automazione
Se pubblichi da una a tre release all'anno, costruire qualsiasi impalcatura per gli screenshot — nativa o fastlane — ti costerà più ore di quante ne farà mai risparmiare. Esegui l'app nel simulatore iOS nella classe di dispositivo giusta, naviga a ogni schermata, usa il comando screenshot del simulatore (cattura alla risoluzione esatta del dispositivo), e in dieci minuti hai PNG pronti per App Store. Poi componi e localizza una volta sola e vai avanti. L'automazione descritta sopra si ripaga esattamente quando "rifare a mano" smette di essere un lavoro da dieci minuti — release frequenti, molte locale, o più app in un portfolio. Adatta la macchina alla tua cadenza di release, non a ciò che sembra rigoroso. La stessa logica vale sulle altre piattaforme; vedi la versione React Native di questo problema e il più ampio confronto fastlane vs no-code.
Qualunque strumento tu usi per catturare, verifica gli obiettivi prima di esportare: ottieni le dimensioni pixel esatte e le regole di formato (PNG/JPEG, RGB, nessun canale alfa) dalla guida alle dimensioni degli screenshot App Store. Apple rifiuta dimensioni sbagliate anche di un solo pixel senza tolleranza, quindi vale trenta secondi del tuo tempo.