Automatizar capturas de pantalla de App Store con SwiftUI sin Fastlane (2026)
XCUITest + XCUIScreenshot para capturar la app en ejecución real, la nueva API de adjuntos de Swift Testing para hacer lo mismo con menos código repetitivo, e ImageRenderer para renderizar una vista SwiftUI directamente a un PNG sin ejecutar el simulador. Proyectos de código abierto como swift-snapshot-testing y AppScreenshotKit envuelven esas piezas. Ninguno compone el carrusel de marketing — marcos, textos, fondos, copy en 50 idiomas. Eso es un paso independiente, y ahí es donde encaja Mokbi: compone y localiza las capturas de pantalla que ya has obtenido. No las captura.El módulo snapshot de Fastlane es la respuesta por defecto a «automatiza mis capturas de pantalla de App Store», y con razón — es maduro y funciona bien en CI. Pero también es un toolchain de Ruby, un Snapfile, un SnapshotHelper.swift que copias en tu target de pruebas, y una cantidad nada trivial de configuración para lo que, por debajo, es una capa fina alrededor de las mismas API de XCUITest que ya tienes. Si desarrollas en SwiftUI y prefieres no añadir una dependencia de Ruby a tu proyecto de iOS, puedes automatizar capturas de pantalla solo con Xcode y Swift. Este artículo es el mapa honesto de las opciones nativas en 2026, con código que puedes pegar directamente, además de una línea clara sobre qué herramienta hace qué trabajo. Para la comparación cara a cara, consulta Mokbi vs Fastlane.
Un primer planteamiento, porque evita mucha confusión: capturar un fotograma (llevar la app a un estado y guardar un PNG de lo que hay en pantalla) y componer una captura de pantalla para la tienda (ese fotograma dentro de un marco de dispositivo, con un titular, un fondo y textos traducidos, exportado en cada dimensión requerida) son dos problemas distintos. Todo lo que hay en este artículo resuelve el primero. Nada de ello resuelve el segundo.
¿Por qué prescindir de Fastlane?
No hay nada malo en Fastlane. Los motivos por los que los desarrolladores recurren a la vía nativa son prácticos, no ideológicos:
- Sin Ruby de por medio. Fastlane es un gem de Ruby con su propia rotación de versiones (Bundler, conflictos de gems, alguna que otra ruptura de CI tras una actualización de Ruby). Si tu proyecto es Swift puro por lo demás, eso es todo un runtime que mantienes para una sola tarea.
- Ya tienes el target de pruebas.
snapshotgenera pruebas de interfaz por debajo. Si de todas formas estás escribiendo pruebas de interfaz, capturar una pantalla son dos líneas más — no hace falta ninguna herramienta nueva. - SwiftUI cambió los cálculos. Con
ImageRendererpuedes renderizar una pantalla a un PNG sin lanzar la app en absoluto, algo que Fastlane no puede hacer — siempre lleva el simulador a cuestas. - Menos piezas móviles en CI. Un simple
xcodebuild testque produce capturas de pantalla es más fácil de razonar que un lane de Fastlane que ejecutaxcodebuildy luego postprocesa un bundle de resultados.
La otra cara: Fastlane empaqueta las partes molestas (recoger adjuntos del bundle .xcresult, organizarlos por configuración regional × dispositivo, generar una vista previa en HTML y subirlos vía deliver) en un solo comando. Al ir por la vía nativa, parte de ese trabajo de recogida se convierte en tu problema. Ese intercambio es toda la decisión, y merece la pena tomarla con conocimiento de causa.
Opción 1: XCUITest + XCUIScreenshot (captura la app en ejecución real)
Esta es la base sobre la que se construye el propio Fastlane. Un target de pruebas de interfaz lanza tu app real en el simulador, la lleva por sus pasos con la misma API de localizadores y toques que cualquier prueba de interfaz, y XCUIScreen.main captura toda la pantalla del dispositivo a resolución nativa. Envuelves esa captura en un XCTAttachment y estableces lifetime = .keepAlways para que Xcode la conserve incluso cuando la prueba pasa (por defecto, descarta los adjuntos cuando hay éxito).
// 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)
}
}Las capturas de pantalla acaban dentro del bundle .xcresult. Las extraes después de la ejecución — bien abriendo el bundle de resultados en Xcode y arrastrándolas hacia fuera, bien en CI con una herramienta como xcparse / xc-screenshot que recorre el bundle y escribe PNG con nombre en una carpeta. Este paso de extracción es exactamente lo que Fastlane automatiza por ti, y es lo principal que asumes al ir por la vía nativa.
Localización sin cambios de código. El truco elegante aquí es el plan de pruebas de Xcode: creas un plan de pruebas, añades una configuración por idioma/región (cada una fija el idioma y la región de la app), y la misma prueba se ejecuta una vez por configuración. Obtienes capturas de pantalla localizadas — la interfaz de la app renderizada en cada configuración regional — sin tocar el código de las pruebas. Ejecuta en un simulador de la clase iPhone de 6,9 pulgadas y uno de la clase iPad de 13 pulgadas para cubrir las resoluciones que exige App Store Connect.
Ventajas y desventajas:
- Píxeles reales de la app. Vistas en ejecución real — tipografías, tema, datos dinámicos, todo nativo — a la resolución exacta del simulador.
- Sin Fastlane, sin Ruby. Solo Xcode + Swift. Se ejecuta con
xcodebuild testen cualquier CI. - La configuración regional es una config del plan de pruebas, no código. Añade una configuración por idioma y vuelve a ejecutar; sin ramas por configuración regional en Swift.
- La extracción corre por tu cuenta. Sacar los PNG del bundle
.xcresultdepende de ti (un script de una sola vez, pero real). - Coste de mantenimiento. Los localizadores (
buttons["openLibrary"], toques) se rompen cuando cambia la interfaz, como en cualquier prueba de interfaz.
Opción 2: adjuntos de Swift Testing (la versión moderna y más ligera)
Swift Testing (el framework de Apple basado en macros que llega con Xcode 26) añadió una API Attachment que hace lo mismo que XCTAttachment, con menos formalismos y un punto de llamada más agradable. Sigues llevando la app con XCUIApplication y capturando con XCUIScreen.main.screenshot() — eso vive en XCUITest — pero registras la imagen con Attachment.record(...) desde una función @Test. La API de adjuntos no le importa cómo se produjo la imagen; acepta cualquier tipo de imagen conforme y la escribe en el bundle de resultados de la prueba.
// 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")
}
}Funcionalmente, esto captura los mismos píxeles de la app en ejecución real que la Opción 1 — es la misma captura de pantalla subyacente. Lo que ganas es la ergonomía más limpia de Swift Testing (pruebas parametrizadas, async por defecto, sin subclasificar XCTestCase) si tu proyecto ya ha dado el salto a Swift Testing. Lo que no te ahorras es el mismo paso de extracción: el PNG sigue viviendo en el bundle de resultados y sigue teniendo que sacarse para la subida. Usa esta opción cuando ya estés en Swift Testing; si no, la Opción 1 da un resultado idéntico.
Opción 3: ImageRenderer (sin ejecutar el simulador en absoluto)
Esta es la opción que Fastlane no puede ofrecer estructuralmente. ImageRenderer (SwiftUI, iOS 16+ / macOS 13+) toma una vista SwiftUI y renderiza su jerarquía directamente a una UIImage / CGImage — sin lanzar la app, sin automatización de interfaz en el simulador, sin localizadores que puedan romperse. Le das una vista con el tamaño exacto en píxeles que exige App Store y lees los bytes del PNG resultante.
// 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
}Dos cosas importan para la salida destinada a la tienda. Fija isOpaque = true — App Store Connect rechaza capturas de pantalla con canal alfa, y un renderizado opaco lo evita. Y presta atención a la scale: si dimensionas la vista en puntos en lugar de píxeles objetivo, fija renderer.scale a la escala de tu dispositivo para que la salida no salga borrosa; si dimensionas el frame en píxeles objetivo exactos (como arriba), mantén la escala en 1.
La gran salvedad es lo que ImageRenderer no puede renderizar. Renderiza SwiftUI, no vistas respaldadas por UIKit/AppKit: vistas web, vistas de mapa, reproductores multimedia, previsualizaciones de cámara y algunos controles del sistema salen como marcadores de posición. Para una pantalla SwiftUI limpia y determinista es fantástico — instantáneo, sin interfaz, con el tamaño perfecto, se ejecuta en cualquier sitio incluido en el propio dispositivo. Para una pantalla que incrusta vistas de la plataforma, vuelves a las Opciones 1–2 para capturar la salida real del compositor.
- El más rápido con diferencia. Milisegundos por imagen, sin arranque de simulador, sin manejar la interfaz. Estupendo en CI.
- Dimensiones exactas, sin esfuerzo. Dimensiona el frame a 1320 × 2868 (o cualquier tamaño requerido) y eso ya es tu salida — sin conjeturas de «redimensionar para App Store».
- Tú construyes el estado. Como no hay ninguna app en ejecución, pasas datos simulados directamente a la vista — determinista, sin argumentos de lanzamiento para sembrar demos.
- Solo SwiftUI. Las vistas respaldadas por la plataforma se renderizan como marcadores de posición. Verifica tu pantalla antes de confiar en ella.
Opción 4: proyectos de código abierto que envuelven lo anterior
Hay dos proyectos de código abierto que aparecen constantemente, resolviendo problemas adyacentes.
pointfree/swift-snapshot-testing es la biblioteca de snapshots de SwiftUI más popular, y merece la pena ser precisos sobre su propósito: está construida para pruebas de regresión, no para producir recursos de tienda. La primera ejecución registra un PNG de referencia y falla a propósito; las ejecuciones posteriores comparan contra esa referencia para detectar el día en que un cambio de padding o de tipografía desplaza tu interfaz.
// 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)))
}
}Puedes reutilizar las imágenes de referencia registradas como capturas de pantalla en bruto, y las estrategias .image(layout: .device(...)) renderizan a tamaños reales de dispositivo — pero eso pelea contra la rigurosidad de la herramienta (quiere coincidencias píxel a píxel; las diferencias de antialiasing entre máquinas provocan fallos inestables). Recurre a ella para proteger tu interfaz contra regresiones, no como tu pipeline principal de capturas de pantalla.
AppScreenshotKit (de shitamori1272) está construido específicamente para esta tarea. Declaras una vista SwiftUI con una macro @AppScreenshot que especifica clases de dispositivo y configuraciones regionales, envuelves tu pantalla real en DeviceView para que se renderice dentro de un marco de dispositivo de Apple, y exportas desde una función de Swift Testing. Organiza la salida en un árbol limpio 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 conocer otras herramientas relacionadas en este terreno: CLI como storescreens dirigen todo el pipeline de App Store Connect (captura con XCUITest en varios simuladores en paralelo, renderizados enmarcados con textos, subida de metadatos vía la API de Apple) desde un único archivo de configuración. Son genuinamente útiles si quieres una cadena única, idempotente y adecuada para CI. También traen sus propias opiniones sobre el diseño de textos — que es justo el punto de fricción donde un editor visual dedicado suele ganar para la superficie de marketing.
La parte que ninguna de las anteriores hace: composición y localización a 50 idiomas
Este es el párrafo acotado y honesto. Todas las opciones anteriores producen fotogramas sin procesar — la interfaz de tu app a una resolución de dispositivo (o, con AppScreenshotKit y similares, un fotograma dentro de un marco de dispositivo simple). Los carruseles de App Store que convierten no son fotogramas sin procesar: son el fotograma dentro de un marco de dispositivo con estilo, sobre un fondo, bajo un titular breve, a menudo como una secuencia de varios paneles, repetida en cada configuración regional que quieras cubrir. Ese paso de composición es para lo que sirve Mokbi. Traes los fotogramas que has capturado (desde XCUITest, ImageRenderer, el simulador, donde sea), los sueltas en un editor de navegador, añades marcos de dispositivo, textos y fondos, traduces los textos a hasta 50 idiomas de App Store en prácticamente un clic, y exportas por lotes en cada tamaño requerido. Diseñar es gratuito con una vista previa con marca de agua; la exportación y la publicación sin límites vienen con una suscripción: Solo a €29.99/mo (1 app) o Studio a €49.99/mo (hasta 5 apps), sin compra única. Para ser explícitos sobre el límite: Mokbi no captura las capturas de pantalla de origen — no ejecuta tu app ni renderiza tus vistas. La captura se queda en manos de las herramientas nativas de Swift descritas arriba; Mokbi compone y localiza lo que esas herramientas producen.
Un flujo de trabajo combinado realista para SwiftUI
- Captura los fotogramas de origen de forma nativa. Para pantallas SwiftUI deterministas, renderízalas con
ImageRenderera tamaños de píxel exactos (Opción 3) — es lo más rápido y no necesita simulador. Para pantallas con vistas de la plataforma o donde quieras el estado real en ejecución, escribe una captura con XCUITest (Opción 1) o Swift Testing (Opción 2) y ejecútala en un simulador de clase iPhone de 6,9 pulgadas y uno de clase iPad de 13 pulgadas. - Localiza la captura si lo necesitas. Añade una configuración de plan de pruebas por idioma para que la interfaz de la app se renderice en cada configuración regional, o pasa una configuración regional a la vista que le entregas a
ImageRenderer. Esto localiza los píxeles de la app — no tu titular de marketing. - Compón el carrusel de marketing. Suelta los fotogramas en Mokbi, añade marcos de dispositivo, textos y fondos con estilo, construye la secuencia de varios paneles. Este es el paso que las herramientas nativas no pueden hacer.
- Traduce los textos y exporta. Traduce con un clic el copy del titular a tus configuraciones regionales de App Store objetivo, y luego exporta por lotes cada dimensión requerida.
- En el siguiente cambio de interfaz, vuelve a capturar y vuelve a abrir. Vuelve a ejecutar el renderizador o la prueba de interfaz, reabre el proyecto guardado de Mokbi, sustituye los fotogramas y vuelve a exportar. La composición y las traducciones se conservan.
Cuándo prescindir de la automatización por completo
Si publicas entre uno y tres lanzamientos al año, construir cualquier andamiaje de capturas de pantalla — nativo o Fastlane — te costará más horas de las que jamás ahorrará. Ejecuta la app en el Simulador de iOS con la clase de dispositivo correcta, navega hasta cada pantalla, pulsa el comando de captura del simulador (captura a la resolución exacta del dispositivo) y tendrás PNG listos para App Store en diez minutos. Después compón y localiza una sola vez y sigue adelante. La automatización de arriba se amortiza precisamente cuando «volver a capturar a mano» deja de ser un trabajo de diez minutos — lanzamientos frecuentes, muchas configuraciones regionales o varias apps en un portfolio. Ajusta la maquinaria a tu ritmo de publicación, no a lo que parezca más riguroso. La misma lógica se aplica en las demás plataformas; consulta la versión de este problema en React Native y el análisis más amplio de Fastlane frente a sin código.
Sea cual sea la herramienta con la que captures, comprueba los objetivos antes de exportar: obtén las dimensiones exactas en píxeles y las reglas de formato (PNG/JPEG, RGB, sin canal alfa) en la guía de tamaños de capturas de pantalla de App Store. Apple rechaza dimensiones incorrectas en un solo píxel sin ninguna tolerancia, así que merece la pena esos treinta segundos.