Automatizando capturas de tela do App Store em SwiftUI sem fastlane (2026)
XCUITest + XCUIScreenshot para capturar o app em execução real, a nova API de anexos do Swift Testing para fazer o mesmo com menos boilerplate, e ImageRenderer para renderizar uma view SwiftUI direto para um PNG sem rodar o simulador. Projetos OSS como swift-snapshot-testing e AppScreenshotKit envolvem essas opções. Nenhum deles compõe o carrossel de marketing — molduras, legendas, fundos, texto em 50 localidades. Essa é uma etapa separada, e é aí que o Mokbi entra: ele compõe e localiza as capturas de tela que você já obteve. Ele não as captura.O snapshot do fastlane é a resposta padrão para "automatizar minhas capturas de tela do App Store", e por um bom motivo — é maduro e amigável para CI. Mas também é um toolchain Ruby, um Snapfile, um SnapshotHelper.swift que você copia para seu target de teste, e uma configuração nada trivial para o que é, por baixo, um invólucro fino sobre as mesmas APIs XCUITest que você já tem. Se você constrói em SwiftUI e prefere não adicionar uma dependência Ruby ao seu projeto iOS, dá para automatizar capturas de tela só com Xcode e Swift. Este post é o mapa honesto das opções nativas em 2026, com código que você pode colar, mais uma linha clara sobre qual ferramenta faz qual trabalho. Para a comparação direta, veja Mokbi vs fastlane.
Um enquadramento primeiro, porque evita muita confusão: capturar um frame (levar o app a um estado e salvar um PNG do que está na tela) e compor uma captura de tela para a loja (esse frame dentro de um mockup de dispositivo, com um título, um fundo e texto traduzido, exportado em todas as dimensões exigidas) são dois problemas diferentes. Tudo neste post resolve o primeiro. Nada dele resolve o segundo.
Por que pular o fastlane?
Não há nada de errado com o fastlane. Os motivos pelos quais desenvolvedores recorrem ao caminho nativo são práticos, não ideológicos:
- Sem Ruby no loop. O fastlane é uma gem Ruby com sua própria rotatividade de versões (Bundler, conflitos de gem, a ocasional quebra de CI depois de um upgrade do Ruby). Se seu projeto é Swift puro, isso é todo um runtime a mais para manter por causa de um único trabalho.
- Você já tem o target de teste. O
snapshotgera testes de UI por baixo dos panos. Se você já está escrevendo testes de UI de qualquer forma, capturar uma captura de tela é duas linhas a mais — nenhuma ferramenta nova é necessária. - O SwiftUI mudou a conta. Com
ImageRenderervocê renderiza uma tela para um PNG sem sequer iniciar o app, algo que o fastlane não consegue fazer — ele sempre conduz o simulador. - Menos peças móveis no CI. Um simples
xcodebuild testque gera capturas de tela é mais fácil de entender do que uma lane do fastlane que chamaxcodebuilde depois pós-processa um pacote de resultados.
O outro lado da moeda: o fastlane empacota as partes chatas (coletar anexos do pacote .xcresult, organizá-los por localidade × dispositivo, gerar uma prévia em HTML e subir via deliver) em um único comando. Indo pelo caminho nativo, parte dessa canalização de coleta vira seu problema. Essa troca é a decisão inteira, e vale a pena fazê-la conscientemente.
Opção 1: XCUITest + XCUIScreenshot (captura o app em execução real)
Essa é a base sobre a qual o próprio fastlane é construído. Um target de teste de UI inicia seu app real no simulador, o conduz com a mesma API de busca/toque de qualquer teste de UI, e XCUIScreen.main captura a tela inteira do dispositivo na resolução nativa. Você envolve essa captura em um XCTAttachment e define lifetime = .keepAlways para que o Xcode a mantenha mesmo quando o teste passa (o padrão descarta anexos em caso de sucesso).
// 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)
}
}As capturas de tela ficam dentro do pacote .xcresult. Você as extrai depois da execução — seja abrindo o pacote de resultados no Xcode e arrastando-as para fora, seja em CI com uma ferramenta como xcparse / xc-screenshot que percorre o pacote e grava PNGs nomeados numa pasta. Essa etapa de extração é exatamente a parte que o fastlane automatiza para você, e é a principal coisa que você assume ao ir pelo caminho nativo.
Localização sem mudanças de código. O truque interessante aqui é o test plan do Xcode: crie um test plan, adicione uma configuração por idioma/região (cada uma define o idioma + região do app), e o mesmo teste roda uma vez por configuração. Você obtém capturas de tela localizadas — a UI do app renderizada em cada localidade — sem tocar no código de teste. Rode em um simulador da classe iPhone de 6,9 polegadas e um simulador da classe iPad de 13 polegadas para atingir as resoluções que o App Store Connect exige.
Prós e contras:
- Pixels reais do app. Views realmente em execução — fontes, tema, dados dinâmicos, tudo nativo — na resolução exata do simulador.
- Sem fastlane, sem Ruby. Xcode + Swift puros. Roda via
xcodebuild testem qualquer CI. - Localidade é uma configuração de test plan, não código. Adicione uma configuração por idioma e rode de novo; sem branches por localidade no Swift.
- Você fica responsável pela extração. Tirar PNGs do pacote
.xcresulté por sua conta (um script único, mas real). - Custo de manutenção. Os localizadores (
buttons["openLibrary"], toques) quebram quando a UI muda, como em qualquer teste de UI.
Opção 2: Anexos do Swift Testing (a versão moderna e mais leve)
O Swift Testing (o framework baseado em macros da Apple que acompanha o Xcode 26) adicionou uma API Attachment que faz o que o XCTAttachment fazia, com menos cerimônia e um call site mais agradável. Você ainda conduz o app com XCUIApplication e captura com XCUIScreen.main.screenshot() — isso continua no XCUITest — mas registra a imagem com Attachment.record(...) a partir de uma função @Test. A API de anexo não se importa com como a imagem foi produzida; ela aceita qualquer tipo de imagem compatível e a grava no pacote de resultados do teste.
// 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 isso captura os mesmos pixels do app em execução real que a Opção 1 — é a mesma captura de tela por baixo dos panos. O que você ganha é a ergonomia mais limpa do Swift Testing (testes parametrizados, async por padrão, sem subclassear XCTestCase) se seu projeto já migrou para o Swift Testing. O que você não escapa é a mesma etapa de extração: o PNG ainda mora no pacote de resultados e ainda precisa ser retirado de lá para o upload. Use isso quando você já estiver no Swift Testing; do contrário, a Opção 1 é idêntica no resultado.
Opção 3: ImageRenderer (sem rodar o simulador)
Essa é a opção que o fastlane estruturalmente não pode oferecer. O ImageRenderer (SwiftUI, iOS 16+ / macOS 13+) pega uma view SwiftUI e renderiza sua hierarquia diretamente para uma UIImage / CGImage — sem iniciar o app, sem automação de UI no simulador, sem localizadores para quebrar. Você fornece uma view dimensionada exatamente nos pixels do App Store e lê de volta os bytes do 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
}Duas coisas importam para a saída da loja. Defina isOpaque = true — o App Store Connect rejeita capturas de tela com canal alfa, e uma renderização opaca evita isso. E preste atenção ao scale: se você dimensionar a view em pontos em vez de pixels-alvo, defina renderer.scale conforme a escala do dispositivo para a saída não ficar embaçada; se você dimensionar o frame exatamente nos pixels-alvo (como acima), mantenha a escala em 1.
A grande ressalva é o que o ImageRenderer não consegue renderizar. Ele renderiza SwiftUI, não views baseadas em UIKit/AppKit: web views, map views, players de mídia, previews de câmera e alguns controles de sistema aparecem como placeholders. Para uma tela SwiftUI limpa e determinística, é fantástico — instantâneo, headless, perfeitamente dimensionado, roda em qualquer lugar, inclusive no dispositivo real. Para uma tela que incorpora views de plataforma, você volta às Opções 1–2 para capturar a saída real do compositor.
- Muito mais rápido. Milissegundos por imagem, sem boot de simulador, sem conduzir UI. Ótimo em CI.
- Dimensões exatas, trivialmente. Dimensione o frame em 1320 × 2868 (ou qualquer tamanho exigido) e essa é sua saída — sem adivinhação de "redimensionar para o App Store".
- Você constrói o estado. Como não há app em execução, você passa dados fictícios diretamente para a view — determinístico, sem argumentos de inicialização para semear dados de demonstração.
- Apenas SwiftUI. Views baseadas na plataforma renderizam como placeholders. Verifique sua tela antes de confiar nela.
Opção 4: projetos OSS que envolvem o acima
Dois projetos open source aparecem o tempo todo, resolvendo problemas adjacentes.
O pointfree/swift-snapshot-testing é a biblioteca de snapshot para SwiftUI mais popular, e vale ser preciso sobre seu propósito: foi construída para teste de regressão, não para produzir ativos de loja. A primeira execução grava um PNG de referência e falha de propósito; execuções posteriores fazem diff contra essa referência para pegar o dia em que uma mudança de padding ou fonte deslocou sua 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)))
}
}Você pode reaproveitar as imagens de referência gravadas como capturas de tela brutas, e as estratégias .image(layout: .device(...)) renderizam nos tamanhos reais do dispositivo — mas isso vai contra o rigor da ferramenta (ela quer correspondências pixel-perfect; diferenças de antialiasing entre máquinas causam falhas instáveis). Recorra a ela para proteger sua UI contra regressões, não como seu pipeline principal de capturas de tela.
O AppScreenshotKit (de shitamori1272) foi construído especificamente para esse trabalho. Você declara uma view SwiftUI com uma macro @AppScreenshot especificando classes de dispositivo e localidades, envolve sua tela real em DeviceView para que ela renderize dentro de um mockup de dispositivo Apple, e exporta a partir de uma função do Swift Testing. Ele organiza a saída numa árvore limpa 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 conhecer também as ferramentas relacionadas a esse espaço: CLIs como storescreens conduzem todo o pipeline do App Store Connect (captura via XCUITest em vários simuladores em paralelo, renderizações com moldura e legendas, upload de metadados via API da Apple) a partir de um único arquivo de configuração. Elas são genuinamente úteis se você quer uma cadeia única, idempotente e amigável a CI. Também trazem suas próprias opiniões sobre layout de legenda — que é exatamente o ponto onde um editor visual dedicado tende a vencer para a superfície de marketing.
A parte que nada do acima faz: composição + localização em 50 idiomas
Aqui está o parágrafo limitado e honesto. Cada opção acima produz frames nus — a UI do seu app na resolução do dispositivo (ou, com AppScreenshotKit e ferramentas similares, um frame dentro de um mockup de dispositivo simples). Os carrosséis do App Store que convertem não são frames nus: são o frame dentro de um mockup de dispositivo estilizado, sobre um fundo, sob um título curto, muitas vezes como uma sequência multipainel, repetida em cada localidade que você atende. Essa etapa de composição é para o que o Mokbi serve. Você traz os frames que capturou (do XCUITest, do ImageRenderer, do simulador, de onde for), solta-os num editor no navegador, adiciona mockups de dispositivo, legendas e fundos, traduz as legendas em até 50 idiomas do App Store em basicamente um clique, e exporta em lote em todos os tamanhos exigidos. É gratuito para projetar com uma visualização com marca d'água; exportação e publicação ilimitadas vêm com uma assinatura — Solo a €29.99/mo (1 app) ou Studio a €49.99/mo (até 5 apps), sem compra avulsa. Para deixar o limite explícito: o Mokbi não captura capturas de tela de origem — ele não roda seu app nem renderiza suas views. A captura fica com as ferramentas nativas Swift acima; o Mokbi compõe e localiza o que essas ferramentas produzem.
Um fluxo de trabalho realista combinado em SwiftUI
- Capture os frames de origem nativamente. Para telas SwiftUI determinísticas, renderize-as com
ImageRendererem tamanhos exatos de pixel (Opção 3) — é o mais rápido e não precisa de simulador. Para telas com views de plataforma ou onde você quer o verdadeiro estado em execução, escreva uma captura via XCUITest (Opção 1) ou Swift Testing (Opção 2) e rode num simulador da classe iPhone de 6,9 polegadas e num da classe iPad de 13 polegadas. - Localize a captura se precisar. Adicione uma configuração de test plan por idioma para que a UI do app renderize em cada localidade, ou passe uma localidade para a view que você entrega ao
ImageRenderer. Isso localiza os pixels do app — não o título do seu marketing. - Componha o carrossel de marketing. Solte os frames no Mokbi, adicione mockups/legendas/fundos estilizados, construa a sequência multipainel. Essa é a etapa que as ferramentas nativas não conseguem fazer.
- Traduza as legendas e exporte. Traduza com um clique o texto do título nas localidades-alvo do App Store, depois exporte em lote todas as dimensões exigidas.
- Na próxima mudança de UI, recapture e reabra. Rode novamente o renderizador ou o teste de UI, reabra o projeto salvo no Mokbi, troque os frames, exporte de novo. A composição e as traduções são preservadas.
Quando pular a automação por completo
Se você lança de uma a três versões por ano, construir qualquer arcabouço de capturas de tela — nativo ou fastlane — vai custar mais horas do que jamais vai economizar. Rode o app no Simulador iOS na classe de dispositivo correta, navegue até cada tela, acione o comando de captura de tela do simulador (ele captura na resolução exata do dispositivo), e você tem PNGs prontos para o App Store em dez minutos. Depois componha e localize uma vez e siga em frente. A automação acima se paga precisamente quando "refazer manualmente" deixa de ser um trabalho de dez minutos — lançamentos frequentes, muitas localidades, ou vários apps num portfólio. Ajuste a maquinaria ao seu ritmo de lançamentos, não ao que parece rigoroso. A mesma lógica se aplica nas outras plataformas; veja a versão React Native desse problema e a comparação mais ampla fastlane vs no-code.
Seja qual for a forma de captura, confira os alvos antes de exportar: obtenha as dimensões exatas em pixels e as regras de formato (PNG/JPEG, RGB, sem canal alfa) no guia de tamanhos de captura de tela do App Store. A Apple rejeita dimensões erradas em um único pixel, sem tolerância, então isso vale trinta segundos do seu tempo.