Battle-tested end to end on a shipping game (SAPPER, 2026): 7 shots × 10 languages × iPhone + iPad = 140 frames, replaced across 39 App Store Connect locales and resubmitted for review in one day.
The philosophy: App Store screenshots are marketing frames, not raw UI — a big localized headline over a device mock containing a real, localized, real-data capture. Keep three independent layers so each can change cheaply:
- Raw captures — slow to make, often not regenerable (live data, daily seeds). Archive them in git.
- Captions + design config — one JSON for all locales and shots.
- Compositor —
scripts/generate.pyre-renders every frame in minutes after any copy or design tweak. No recapture needed.
Prerequisites
- Xcode + iOS simulators for the target devices (iPhone 17 Pro Max class
for 6.9", iPad Pro 13" class) — captures use
xcrun simctl. - XcodeBuildMCP CLI
(
brew install cameroncooke/xcodebuildmcp/xcodebuildmcp) — provides element-ref UI automation (xcodebuildmcp ui-automation snapshot-ui/tap) and bundles the AXe binary the capture scripts depend on, at/opt/homebrew/Cellar/xcodebuildmcp/<ver>/libexec/bundled/axe. (Alternative: install AXe standalone —brew install cameroncooke/axe/axe.) - asc CLI, authenticated against the App Store Connect API — uploads, localizations, builds, review submissions.
- Google Chrome — headless renderer for the frame compositor.
- Python 3 — capture loops, compositor, verification (stdlib only).
- A DEBUG build of the app with a screenshot-mode launch flag (see Phase 1) installed on the simulators.
References (read the one matching your phase)
references/design-principles.md— read this first, before any capture: the 7 conversion principles, the composition pattern library, worked teardowns of 7 real listings (references/examples/), concept checklistreferences/sizes-and-devices.md— simulators, exact pixel sizes, ASC display types, device-mock CSS values, type scale tablesreferences/captions.md— caption anatomy, the narrative arc, transcreation rules per locale (glossary, line breaks, CJK), captions.jsonreferences/capture-locales.md— per-locale capture loops, iPhone vs iPad differences, state discriminators, every automation gotchareferences/compositor.md— the HTML/CSS frame generator, auto-fit and auto-push JS (with the bugs they prevent), render command, review checklistreferences/asc-upload.md— locale mapping table, the fan-out abort gotcha- mandatory verification, review-submission rebuild (incl. Game Center)
Bundled scripts (copy into the app's repo and adapt)
scripts/generate.py— the frame compositor (HTML/CSS → headless Chrome)scripts/captions.example.json— the copy/config data shapescripts/loop_locales_skeleton.py— per-locale capture loop with the battle-tested helpers (frame-targeted taps, state assertions, retries)scripts/verify_asc_screenshots.py— post-upload per-locale verification
Phase summary
0 — Plan the concept (references/design-principles.md). Panel 1 is the
value prop and comes from user research, not the design file. Sell the
outcome, not the feature. One message per shot. Find the visual hook before
writing the headline — cover the copy and the idea should still read.
Humanize where it makes sense. Put a true trust signal (rating, count, award,
press) in every panel. Shortest copy wins because it buys the biggest type.
Then: 5–7 shots, arc hook → core choice → retention → secondary mode →
social → monetization → trust. Backgrounds rotate through the app's own
palette, never repeating adjacently. No prices in screenshots (they vary by
storefront and drift). Captions transcreated per locale with explicit line
breaks.
1 — Prepare the app. Add a DEBUG-only launch flag (e.g.
-MarketingScreenshots YES) that: hides store prices behind a neutral
localized tag; forces the accessibility overlay on custom-drawn surfaces so
automation can read them. Make sure a debug menu can reset/replay any state
you need to capture.
2 — Capture iPhone. Use a sim whose native screenshot IS the store size
(no resizing, ever). Build each app state once in English, then per locale:
background the app (state saves on backgrounding!) → terminate → relaunch
with -AppleLanguages "(xx)" -AppleLocale xx_XX → re-shoot with
locale-independent navigation. Clean status bar via simctl status_bar.
3 — Capture iPad. Same flow, three traps: axe button home is a silent
no-op (use button lock to background); the status bar shows a date in the
SYSTEM language (switch .GlobalPreferences + reboot per locale); all layout
metrics differ from iPhone — re-probe everything.
4 — Frame. generate.py + captions.json → headless Chrome at exact
store resolution. Auto-fit shrinks long headlines; auto-push moves the device
below wrapping subs. Validate every PNG's dimensions; eyeball the longest
Latin locale and one CJK locale on every shot type. Auto-fit firing is a copy
smell — if a locale renders below its base size, shorten the headline instead
of accepting smaller type.
5 — Upload. Stage a hardlink tree keyed by ASC locale codes (app
languages fan out: en serves en-US/GB/AU/CA + all store-only locales).
Upload with --replace to the display types the listing ALREADY uses.
The fan-out aborts all remaining locales on the first delivery failure
while exiting 0 — run verify_asc_screenshots.py and re-run the gaps.
6 — Resubmit (if a review was pending). Save the pending submission's item IDs → cancel → new build + screenshots → attach build → recreate submission → re-add every item (the appStoreVersions item only attaches after screenshot assets finish processing — add it last) → audit the count → submit.
Hard-won rules (apply everywhere)
- Concept before capture: if the raw capture only makes sense once the headline is read, re-capture (different state, tighter crop) rather than writing a longer headline. Copy sharpens an idea; it can't rescue one.
- A trust signal must be true. Never invent ratings, counts, awards or press.
- Frame-target every tap from
axe describe-uioutput; fixed coordinates break across locales and states. - The accessibility tree contains ALL layers at once — find a geometric discriminator (e.g. a known card's y-position) to assert which screen you're on, and verify after every navigation.
- The first tap after a cold launch or transition is often eaten: retry loops with state assertions, never fire-and-forget.
- Long-press = separate
touch --down, real sleep,touch --up. The single-command delay form sometimes registers as a tap. - After any swipe, wait until two consecutive position reads agree before computing tap targets (content decelerates).
- Anything that reads localized a11y labels (solvers, content checks) runs in English only; per-locale passes navigate by geometry.
- Verification is part of every phase: look at the captures with your eyes, measure the frames, list the uploads. Every silent failure we hit was caught by checking, not by exit codes.
Final checklist (before submit)
Concept (references/design-principles.md)
- Panel 1 states an outcome a stranger would want, in one line
- Cover all copy — the app's purpose still reads
- One message per shot, nameable in one sentence, no duplicates
- Every panel carries one trust signal, and every one is TRUE
- Exactly one accent segment per headline, on the meaning-carrying words in each locale (not the same line number as English)
- Strip viewed at thumbnail size: layouts, device treatments and headline positions vary; every panel still reads at 200 px wide
Craft
- Shot narrative + captions reviewed in all locales (line breaks explicit)
- No prices, no system UI artifacts (charging bolt, app-switch breadcrumb, notification banners), clean 9:41 status bar
- Raw captures archived in git (NOT regenerable)
- All frames exactly store resolution; spot-check longest-copy locales (de, it) and CJK on every shot type
- No headline shrunk below its
baseSizeby auto-fit (shorten the copy instead of accepting smaller type)
Ship
- ASC per-locale verification after upload (count / order / COMPLETE)
- Submission items audited before submit