| name | app-store-screenshots |
| description | End-to-end playbook for producing localized, caption-framed App Store screenshots (iPhone + iPad) from real simulator captures, then replacing them on App Store Connect. Use when asked to create, localize, redesign, or upload App Store screenshots for an iOS app. Covers capture automation via the accessibility tree, an HTML/CSS frame compositor, ASC upload mapping, and review-submission rebuild. |
App Store Screenshots — capture → frame → upload
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.py re-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/sizes-and-devices.md — simulators, exact pixel sizes, ASC
display types, device-mock CSS values, type scale tables
references/captions.md — caption anatomy, the narrative arc,
transcreation rules per locale (glossary, line breaks, CJK), captions.json
references/capture-locales.md — per-locale capture loops, iPhone vs iPad
differences, state discriminators, every automation gotcha
references/compositor.md — the HTML/CSS frame generator, auto-fit and
auto-push JS (with the bugs they prevent), render command, review checklist
references/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 shape
scripts/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. 5–7 shots, caption-led, arc: hook → core choice → retention →
secondary mode → social → monetization → trust. Background colors 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.
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)
- Frame-target every tap from
axe describe-ui output; 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.