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/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 checklist
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 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-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.
Final checklist (before submit)
Concept (references/design-principles.md)
Craft
Ship
1---2name: app-store-screenshots3description: 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.4---56# App Store Screenshots — capture → frame → upload78Battle-tested end to end on a shipping game (SAPPER, 2026): 7 shots × 109languages × iPhone + iPad = 140 frames, replaced across 39 App Store Connect10locales and resubmitted for review in one day.1112The philosophy: App Store screenshots are **marketing frames, not raw UI** —13a big localized headline over a device mock containing a *real, localized,14real-data* capture. Keep three independent layers so each can change cheaply:15161. **Raw captures** — slow to make, often not regenerable (live data, daily17 seeds). Archive them in git.182. **Captions + design config** — one JSON for all locales and shots.193. **Compositor** — `scripts/generate.py` re-renders every frame in minutes20 after any copy or design tweak. No recapture needed.2122## Prerequisites2324- **Xcode + iOS simulators** for the target devices (iPhone 17 Pro Max class25 for 6.9", iPad Pro 13" class) — captures use `xcrun simctl`.26- **[XcodeBuildMCP](https://github.com/cameroncooke/XcodeBuildMCP) CLI**27 (`brew install cameroncooke/xcodebuildmcp/xcodebuildmcp`) — provides28 element-ref UI automation (`xcodebuildmcp ui-automation snapshot-ui/tap`)29 and **bundles the AXe binary** the capture scripts depend on, at30 `/opt/homebrew/Cellar/xcodebuildmcp/<ver>/libexec/bundled/axe`.31 (Alternative: install AXe standalone — `brew install cameroncooke/axe/axe`.)32- **[asc CLI](https://github.com/rorkai/App-Store-Connect-CLI)**, authenticated33 against the App Store Connect API — uploads, localizations, builds, review34 submissions.35- **Google Chrome** — headless renderer for the frame compositor.36- **Python 3** — capture loops, compositor, verification (stdlib only).37- A **DEBUG build of the app** with a screenshot-mode launch flag (see38 Phase 1) installed on the simulators.3940## References (read the one matching your phase)4142- `references/design-principles.md` — **read this first, before any capture**:43 the 7 conversion principles, the composition pattern library, worked44 teardowns of 7 real listings (`references/examples/`), concept checklist45- `references/sizes-and-devices.md` — simulators, exact pixel sizes, ASC46 display types, device-mock CSS values, type scale tables47- `references/captions.md` — caption anatomy, the narrative arc,48 transcreation rules per locale (glossary, line breaks, CJK), captions.json49- `references/capture-locales.md` — per-locale capture loops, iPhone vs iPad50 differences, state discriminators, every automation gotcha51- `references/compositor.md` — the HTML/CSS frame generator, auto-fit and52 auto-push JS (with the bugs they prevent), render command, review checklist53- `references/asc-upload.md` — locale mapping table, the fan-out abort gotcha54 + mandatory verification, review-submission rebuild (incl. Game Center)5556## Bundled scripts (copy into the app's repo and adapt)5758- `scripts/generate.py` — the frame compositor (HTML/CSS → headless Chrome)59- `scripts/captions.example.json` — the copy/config data shape60- `scripts/loop_locales_skeleton.py` — per-locale capture loop with the61 battle-tested helpers (frame-targeted taps, state assertions, retries)62- `scripts/verify_asc_screenshots.py` — post-upload per-locale verification6364## Phase summary6566**0 — Plan the concept** (`references/design-principles.md`). Panel 1 is the67value prop and comes from user research, not the design file. Sell the68outcome, not the feature. One message per shot. Find the visual hook before69writing the headline — cover the copy and the idea should still read.70Humanize where it makes sense. Put a true trust signal (rating, count, award,71press) in every panel. Shortest copy wins because it buys the biggest type.72Then: 5–7 shots, arc hook → core choice → retention → secondary mode →73social → monetization → trust. Backgrounds rotate through the app's own74palette, never repeating adjacently. No prices in screenshots (they vary by75storefront and drift). Captions transcreated per locale with explicit line76breaks.7778**1 — Prepare the app.** Add a DEBUG-only launch flag (e.g.79`-MarketingScreenshots YES`) that: hides store prices behind a neutral80localized tag; forces the accessibility overlay on custom-drawn surfaces so81automation can read them. Make sure a debug menu can reset/replay any state82you need to capture.8384**2 — Capture iPhone.** Use a sim whose native screenshot IS the store size85(no resizing, ever). Build each app state once in English, then per locale:86background the app (state saves on backgrounding!) → terminate → relaunch87with `-AppleLanguages "(xx)" -AppleLocale xx_XX` → re-shoot with88locale-independent navigation. Clean status bar via `simctl status_bar`.8990**3 — Capture iPad.** Same flow, three traps: `axe button home` is a silent91no-op (use `button lock` to background); the status bar shows a date in the92SYSTEM language (switch `.GlobalPreferences` + reboot per locale); all layout93metrics differ from iPhone — re-probe everything.9495**4 — Frame.** `generate.py` + `captions.json` → headless Chrome at exact96store resolution. Auto-fit shrinks long headlines; auto-push moves the device97below wrapping subs. Validate every PNG's dimensions; eyeball the longest98Latin locale and one CJK locale on every shot type. Auto-fit firing is a copy99smell — if a locale renders below its base size, shorten the headline instead100of accepting smaller type.101102**5 — Upload.** Stage a hardlink tree keyed by ASC locale codes (app103languages fan out: en serves en-US/GB/AU/CA + all store-only locales).104Upload with `--replace` to the display types the listing ALREADY uses.105**The fan-out aborts all remaining locales on the first delivery failure106while exiting 0** — run `verify_asc_screenshots.py` and re-run the gaps.107108**6 — Resubmit (if a review was pending).** Save the pending submission's109item IDs → cancel → new build + screenshots → attach build → recreate110submission → re-add every item (the appStoreVersions item only attaches111after screenshot assets finish processing — add it last) → audit the count →112submit.113114## Hard-won rules (apply everywhere)115116- Concept before capture: if the raw capture only makes sense once the117 headline is read, re-capture (different state, tighter crop) rather than118 writing a longer headline. Copy sharpens an idea; it can't rescue one.119- A trust signal must be true. Never invent ratings, counts, awards or press.120- Frame-target every tap from `axe describe-ui` output; fixed coordinates121 break across locales and states.122- The accessibility tree contains ALL layers at once — find a geometric123 discriminator (e.g. a known card's y-position) to assert which screen124 you're on, and verify after every navigation.125- The first tap after a cold launch or transition is often eaten: retry loops126 with state assertions, never fire-and-forget.127- Long-press = separate `touch --down`, real sleep, `touch --up`. The128 single-command delay form sometimes registers as a tap.129- After any swipe, wait until two consecutive position reads agree before130 computing tap targets (content decelerates).131- Anything that reads localized a11y labels (solvers, content checks) runs in132 English only; per-locale passes navigate by geometry.133- Verification is part of every phase: look at the captures with your eyes,134 measure the frames, list the uploads. Every silent failure we hit was135 caught by checking, not by exit codes.136137## Final checklist (before submit)138139**Concept** (`references/design-principles.md`)140- [ ] Panel 1 states an outcome a stranger would want, in one line141- [ ] Cover all copy — the app's purpose still reads142- [ ] One message per shot, nameable in one sentence, no duplicates143- [ ] Every panel carries one trust signal, and every one is TRUE144- [ ] Exactly one accent segment per headline, on the meaning-carrying words145 in each locale (not the same line number as English)146- [ ] Strip viewed at thumbnail size: layouts, device treatments and headline147 positions vary; every panel still reads at 200 px wide148149**Craft**150- [ ] Shot narrative + captions reviewed in all locales (line breaks explicit)151- [ ] No prices, no system UI artifacts (charging bolt, app-switch breadcrumb,152 notification banners), clean 9:41 status bar153- [ ] Raw captures archived in git (NOT regenerable)154- [ ] All frames exactly store resolution; spot-check longest-copy locales155 (de, it) and CJK on every shot type156- [ ] No headline shrunk below its `baseSize` by auto-fit (shorten the copy157 instead of accepting smaller type)158159**Ship**160- [ ] ASC per-locale verification after upload (count / order / COMPLETE)161- [ ] Submission items audited before submit