O que iOS Marketing Capture faz?
Visão Geral
Automatize a captura reproduzível de capturas de tela de marketing para um aplicativo SwiftUI iOS em vários locais, com dois fluxos de saída paralelos:
- Capturas de tela cheia — todas as telas relevantes para marketing, com dados determinísticos semeados, barra de status real e moldura de área segura
- Capturas de elementos — renderizações isoladas de componentes específicos (cartões, widgets, gráficos) em qualquer escala, com fundo natural dentro de cantos arredondados e transparência externa
Este skill é a etapa de captura. Se o usuário também quiser páginas de marketing no estilo Apple compostas ao redor das capturas (maquetes de dispositivos, manchetes, gradientes), combine com o skill app-store-screenshots como uma etapa de pós-processamento.
Abordagem Principal
Modo de captura dentro do aplicativo, não XCUITest. Esta é uma decisão difícil que compensa em relação às convenções do Fastlane snapshot / XCUITest e vence para quase todos os projetos reais.
Por que dentro do aplicativo em vez de XCUITest:
- Nenhum novo alvo de teste. Adicionar um alvo de teste de UI a um projeto Xcode existente é uma cirurgia frágil no pbxproj. Muitos projetos têm zero alvos de teste e nenhum xcodegen — adicionar um manualmente é propenso a erros.
- Iteração mais rápida. Um teste de UI leva 30s+ para iniciar por execução. A captura dentro do aplicativo é apenas uma reinicialização do binário instalado.
- Sem
xcodebuild test. Todo o fluxo éxcodebuild builduma vez, depoissimctl launchpor local. Sem sobrecarga de pacote de teste. - Acesso ao estado real do aplicativo. Você pode chamar ViewModels, SwiftData, ImageRenderer e
UIWindow.drawHierarchydiretamente. O XCUITest só pode tocar e ler elementos de acessibilidade. - Renderizações de elementos precisam estar no processo de qualquer forma.
ImageRendererem visualizações de widget ou componentes isolados deve ser executado dentro do processo do aplicativo — não há equivalente no XCUITest.
Como funciona:
- Um arquivo
MarketingCapture.swiftsomente DEBUG reside no alvo principal do aplicativo - Quando iniciado com
-MarketingCapture 1, o aplicativo semeia dados, então um coordenador percorre uma lista deCaptureStep— cada etapa navega, aguarda, captura e limpa - Os PNGs são gravados no diretório sandbox do aplicativo
Documents/marketing/<locale>/ - Um script de shell compila uma vez, instala, então percorre os locais reiniciando com
-AppleLanguages (xx) -AppleLocale xx, extraindo arquivos viasimctl get_app_container
Processo
Trabalhe nestas etapas em ordem. Não pule adiante.
Etapa 1: Levantar requisitos
Faça estas perguntas ao usuário uma de cada vez (não as agrupe — cada resposta pode invalidar perguntas posteriores):
- Telas para capturar — "Quais telas você deseja? Dê-me o caminho de navegação ou o nome da guia para cada uma." Obtenha uma lista concreta, não "os fluxos principais".
- Elementos isolados — "Algum componente que você deseja renderizar independentemente com fundos transparentes? (cartões de carrossel, widgets, blocos de heróis, gráficos, etc.)"
- Locais — "Quais locais? (a) todos os locais no seu
Localizable.xcstrings, (b) um subconjunto da App Store que eu especificarei, ou (c) deixe-me dar uma lista explícita." Se (a), use grep no arquivo.xcstringspara códigos de local:python3 -c "import json; d=json.load(open('<path>/Localizable.xcstrings')); langs=set(); [langs.update(v.get('localizations',{}).keys()) for v in d['strings'].values()]; print(sorted(langs))" - Dispositivo — "Qual simulador? (iPhone 17 de 6,1 polegadas recomendado para recursos de design do iOS 26)" — verifique se o dispositivo está disponível via
xcrun simctl list devices available. - Aparência — "Apenas claro, apenas escuro ou ambos?"
- Dados de demonstração — "Como os dados de demonstração são populados hoje? (a) a instalação limpa os semeia automaticamente, (b) há um botão de depuração 'Carregar Dados de Demonstração', (c) você os adiciona manualmente, (d) ainda não existem dados de demonstração." Então: "Os dados existentes são exaustivos o suficiente para que todas as telas que você listou pareçam populadas para marketing? Audite com o usuário."
Etapa 2: Exploração
Antes de escrever qualquer código, explore a base de código o suficiente para responder:
- O projeto usa grupos de pastas sincronizados do Xcode (Xcode 16+,
PBXFileSystemSynchronizedRootGroup)? Se sim, novos arquivos são automaticamente incluídos em seu alvo — sem edições no pbxproj necessárias. Verifique comgrep -c PBXFileSystemSynchronized <proj>.xcodeproj/project.pbxproj. - Qual é o padrão de navegação raiz?
TabView(selection:)— mais comum. Você precisa: da vinculação@State selectedTab, índices de guia e quais guias têmNavigationStackaninhado.NavigationStack(pilha única com um roteador) — você precisa: da vinculação de caminho ou objeto roteador, mais o conjunto de tiposNavigationLink(value:)/.navigationDestination.NavigationSplitView— você precisa: da vinculação de seleção da barra lateral, estado de navegação da coluna de detalhes.- Coordenador personalizado / host UIKit — você precisa: do método
navigate(to:)do coordenador ou equivalente.
- Como os links profundos são roteados? Encontre o manipulador
onOpenURLe o enum/switch que mapeia URLs para estado de navegação. - Onde os semeadores de dados de demonstração são definidos? Trace o caminho do código a partir do botão de depuração (se houver) até a função que realmente escreve em
ModelContext. Se não existir semeador, veja "Criando um semeador de dados de demonstração" abaixo. - Os widgets residem em um alvo separado? Os arquivos de visualização do widget e os tipos de entrada estão no alvo principal do aplicativo também? (Quase certamente não — eles precisam ser adicionados se você quiser renderizá-los via ImageRenderer.)
- O aplicativo usa Live Activities / ActivityKit? Se sim, sinalize isso como um problema conhecido (veja abaixo).
- O aplicativo usa SwiftData + CloudKit sync (
cloudKitDatabase: .automatic)? Se sim, sinalize como um problema conhecido. - Alguma visualização precisa ser capturada em um estado não padrão? (por exemplo, um temporizador no meio da contagem regressiva, um formulário parcialmente preenchido, um gráfico com valores específicos). Se sim, cada um precisa de um mecanismo de
static varde preparação (veja "Preparando estado da visualização" abaixo).
Etapa 3: Apresentar design ao usuário
Antes de escrever código, resuma seu plano nesta estrutura. Obtenha aprovação explícita antes de prosseguir:
- Arquitetura (modo de captura dentro do aplicativo, arquivo único, protegido por DEBUG)
- Lista de arquivos (caminhos exatos que você criará / modificará)
- Plano de captura tela por tela (como cada tela é alcançada — índice da guia, caminho de navegação, gatilho de folha)
- Justificativa da ordem de captura (quais telas devem vir antes de outras — veja problema nº 5)
- Abordagem de renderização de elementos (quais componentes, como serão encapsulados)
- Layout de saída (estrutura de pastas, convenção de nomenclatura)
- Problemas conhecidos relevantes para este projeto (sinalizados na Etapa 2)
- Estados preparados necessários (quais visualizações, quais variáveis estáticas)
Etapa 4: Implementar
Use os modelos em templates/ como pontos de partida. Eles são padrões de referência, não andaimes de copiar e colar — cada projeto tem navegação, modelos e visualizações diferentes. Os modelos mostram os blocos de construção; você os compõe para o aplicativo alvo.
Arquivos principais a serem produzidos:
<AppName>/Debug/MarketingCapture.swift— todo o sistema de captura, somente DEBUG. Contém:- Enum
MarketingCapture(análise de argumentos de lançamento, auxiliares de saída, captura de janela, variáveis de preparação) - Classe
MarketingCaptureCoordinator(percorre[CaptureStep]e captura cada um) - Enum
MarketingElementHarness(renderizações ImageRenderer de cartões, widgets, gráficos)
- Enum
<AppName>/ContentView.swift(ou onde quer que a visualização raiz viva) — hook DEBUG que semeia dados e executa o coordenador.- Quaisquer visualizações que precisem de estados preparados — hooks
.onAppearprotegidos por DEBUG e ouvintes de descarte.onReceive. scripts/capture-marketing.sh— compilar + instalar + loop por local..gitignore— adicionemarketing/.
Etapa 5: Verificar iterativamente
Não entregue o script ao usuário e espere. Execute-o você mesmo contra um simulador e verifique pelo menos um local antes de declarar concluído. Leia os PNGs de saída com a ferramenta de Leitura para verificar visualmente se cada tela mostra o que você espera. Problemas comuns de tempo de execução estão listados em "Problemas Conhecidos" abaixo.
Quando encontrar um problema, corrija-o, execute novamente todo o script (não apenas o local com falha — correções podem regredir locais anteriores) e verifique novamente visualmente.
Arquitetura: Captura Baseada em Etapas
O coordenador conduz a captura percorrendo uma lista de valores CaptureStep. Cada etapa é autocontida: ela sabe como navegar para sua tela, quanto tempo esperar e como limpar após.
struct CaptureStep {
let name: String // nome do arquivo de saída, ex. "01-home"
let navigate: @MainActor () -> Void // colocar o aplicativo no estado correto
let settle: Duration // aguardar animações/carregamentos
let cleanup: (@MainActor () -> Void)? // desmontar antes da próxima etapa
}
O coordenador é um loop simples:
for step in steps {
step.navigate()
try? await Task.sleep(for: step.settle)
if let image = MarketingCapture.snapshotKeyWindow() {
MarketingCapture.writePNG(image, name: step.name)
}
step.cleanup?()
try? await Task.sleep(for: .milliseconds(400)) // animação de limpeza
}
Construindo etapas para diferentes padrões de navegação
Aplicativo TabView (mais comum):
// Alternância simples de guia — basta definir o índice
CaptureStep(name: "01-home", navigate: { setTab(0) }, settle: .milliseconds(1800), cleanup: nil)
// Guia + folha apresentada
CaptureStep(
name: "05-timer-setup",
navigate: {
setTab(3)
pendingBrewRecipe = someRecipe
},
settle: .milliseconds(2000),
cleanup: {
NotificationCenter.default.post(name: MarketingCapture.dismissSheetNotification, object: nil)
pendingBrewRecipe = nil
}
)
Aplicativo NavigationStack + roteador:
// Empurrar uma rota na pilha
CaptureStep(
name: "02-detail",
navigate: { router.push(.itemDetail(item)) },
settle: .milliseconds(1800),
cleanup: { router.popToRoot() }
)
Aplicativo NavigationSplitView:
// Selecionar item da barra lateral, depois detalhe
CaptureStep(
name: "03-detail",
navigate: {
sidebarSelection = .recipes
detailSelection = recipes.first
},
settle: .milliseconds(1800),
cleanup: { detailSelection = nil }
)
Ordenação: a regra de empilhamento
Capture qualquer tela que precise de um estado de navegação "limpo" ANTES de telas que empurram para a mesma pilha. NavigationPath / @State aninhados dentro de visualizações filhas não podem ser desempilhados pelo coordenador. Então:
Bom: Prateleira (lista limpa) → Detalhe do Café (empurra na pilha da prateleira)
Ruim: Detalhe do Café → Prateleira (pilha ainda tem detalhe empurrado)
Se duas telas compartilham um NavigationStack, capture a visualização de nível raiz primeiro.
Preparando Estado da Visualização
Algumas telas precisam ser capturadas em um estado específico não padrão — um temporizador no meio da contagem regressiva, um gráfico com valores particulares, um formulário meio preenchido. O padrão:
-
Adicione um
static varaoMarketingCapturepara cada valor de preparação:/// Definido pelo coordenador antes de apresentar a visualização do temporizador. /// A visualização lê isso em .onAppear para saltar para um tempo decorrido específico. static var pendingElapsedSeconds: Int? /// Definido como true para mostrar a sobreposição de avaliação no temporizador. static var pendingShowAssessment: Bool = false -
Na visualização alvo, adicione um
.onAppearprotegido por DEBUG que lê o valor de preparação:.onAppear { #if DEBUG if MarketingCapture.isActive, let elapsed = MarketingCapture.pendingElapsedSeconds { phase = .active timerVM.elapsedTime = TimeInterval(elapsed) timerVM.start() DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { timerVM.pause() } } #endif } -
No coordenador, defina a var antes de navegar:
CaptureStep( name: "06-timer-midway", navigate: { MarketingCapture.pendingElapsedSeconds = 75 openTimerSheet(someRecipe) }, settle: .milliseconds(2400), cleanup: { MarketingCapture.pendingElapsedSeconds = nil NotificationCenter.default.post(name: MarketingCapture.dismissSheetNotification, object: nil) } )
Criando um Semeador de Dados de Demonstração
Se o aplicativo não tiver nenhum mecanismo de dados de demonstração existente, crie um. Coloque-o em <AppName>/Debug/DemoDataSeeder.swift, encapsulado em #if DEBUG.
Diretrizes:
- Semeie dados suficientes para que toda tela capturada pareça populada. Audite a lista de telas contra a semeadura.
- Use conteúdo realista: nomes de lugares reais, números plausíveis, estados variados (alguns itens "ficando baixos", alguns "frescos", alguns com imagens, alguns sem).
- Se o aplicativo usa SwiftData, escreva diretamente no
ModelContext. Se Core Data, use o contexto de objeto gerenciado. Se um backend REST, semeie através da camada de cache/armazenamento local. - Torne a semeadura idempotente — verifique se os dados já existem antes de inserir. O armazenamento persiste entre reinicializações do simulador, e a ressemeadura por local causa agitação de sincronização do CloudKit e falhas.
- Inclua variedade suficiente para preencher diferentes estados de UI: estados vazios NÃO devem aparecer a menos que sejam uma tela de marketing.
Forma mínima:
#if DEBUG
enum DemoDataSeeder {
static func seedIfEmpty(in context: ModelContext) {
let existing = (try? context.fetchCount(FetchDescriptor<Item>())) ?? 0
guard existing == 0 else { return }
// Itens com estados variados
let items = [
Item(nome: "...", status: .active, ...),
Item(nome: "...", status: .lowStock, ...),
// ...suficiente para preencher toda tela
]
items.forEach { context.insert($0) }
try? context.save()
}
}
#endif
Renderização de Elementos
Elementos são renderizados via ImageRenderer na escala 3x com transparência fora dos cantos arredondados.
Cartões / linhas de lista
@MainActor
static func renderCards(items: [Item], theme: AppTheme) {
let cardWidth: CGFloat = 380
for item in items {
let card = ItemCard(item: item, theme: theme)
.padding(.horizontal, 16)
.padding(.vertical, 12)
.frame(width: cardWidth)
.background(theme.background)
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
let renderer = ImageRenderer(content: card)
renderer.scale = 3
renderer.isOpaque = false
renderer.proposedSize = .init(width: cardWidth, height: nil as CGFloat?)
guard let uiImage = renderer.uiImage else { continue }
MarketingCapture.writeElementPNG(uiImage, name: "card-\(item.id)")
}
}
Widgets
Widgets precisam estar no alvo do aplicativo principal. Adicione os arquivos de visualização do widget ao alvo principal e use WidgetView().padding(16).background(...). O tempo de assentamento deve incluir um atraso de pelo menos 2 segundos para carregamento de dados do widget.
Script de Automação (capture-marketing.sh)
#!/bin/bash
# Exemplo: loop por locais
set -euo pipefail
PROJ="YourApp.xcodeproj"
SCHEME="YourApp"
DEVICE="iPhone 17"
BUILD_DIR="/tmp/marketing-build"
# Construir uma vez
xcodebuild build -project "$PROJ" -scheme "$SCHEME" -destination "platform=iOS Simulator,name=$DEVICE" -derivedDataPath "$BUILD_DIR" CODE_SIGNING_ALLOWED=NO
# Instalar
xcrun simctl install booted "$BUILD_DIR/Build/Products/Debug-iphonesimulator/YourApp.app"
LOCALES=("en" "pt" "de") # gerar da Etapa 1
for locale in "${LOCALES[@]}"; do
echo "=== Capturando para $locale ==="
xcrun simctl launch booted "com.yourapp.bundle" -AppleLanguages "($locale)" -AppleLocale "$locale" -MarketingCapture 1
sleep 10 # tempo para captura
APP_CONTAINER=$(xcrun simctl get_app_container booted com.yourapp.bundle)
cp -r "$APP_CONTAINER/Documents/marketing/$locale/" "output/$locale/"
done
Problemas Conhecidos
- SwiftData + CloudKit Sync: Se o aplicativo usar
cloudKitDatabase: .automatic, a semeadura de dados aciona sincronização do CloudKit, que pode falhar no simulador e travar o aplicativo. Corrigir: Use#if DEBUGcomcontainer = try ModelContainer(...)sem sincronização. - Live Activities / ActivityKit: Não é possível iniciar ou capturar Live Activities programaticamente do processo do aplicativo. Os participantes precisam ser fornecidos pelo provedor de atividades. Corrigir: Capture a visualização do widget da Live Activity via ImageRenderer como um elemento estático.
- Pastas Sincronizadas do Xcode: Em Xcode 16+, pastas sincronizadas adicionam arquivos automaticamente ao alvo. Verifique se
MarketingCapture.swiftestá em uma pasta sincronizada; caso contrário, adicione manualmente. - Status Bar vs. Moldura do Dispositivo: A captura de janela inclui a barra de status do sistema, que não está presente na moldura do dispositivo. Se o usuário planeja adicionar molduras de dispositivo mais tarde, considere ocultar a barra de status e usar configurações de status bar falsas.
- Ordem de Captura (a regra de empilhamento): Capturas de tela empurradas em uma NavigationStack devem vir depois da tela raiz na mesma pilha.
- Armazenamento Persistente e Ressemeadura: Dados semeados persistem entre execuções. Sempre verifique se os dados já existem antes de semear (
guard existing == 0 else { return }).