Skip to content

Blog

How to take App Store screenshots from the Simulator

The capture step decides how good your listing can be, and it is the step everyone rushes. Permissions, seeded data, first-run overlays, driving to each screen without synthetic taps, and the TCC flag that quietly ruins a run.

Three panes of smoked glass emerging from a larger frame

The failure mode here is never "the script errored." It is a technically successful screenshot of an empty list sitting behind a permission dialog, captured at the wrong device size, at 14:37 with 23% battery.

Everything downstream — design, captions, art direction — is limited by what you capture. This is the step to slow down on.

Pick the right device first

An iPhone 16 Pro capture is 1206 × 2622. An iPhone 16 Pro Max is 1290 × 2796. Both are valid App Store sizes, for different display classes. Capture on the wrong one and you are either upscaling later — which looks soft — or getting an upload rejected.

xcrun simctl list devices available | grep iPhone
xcrun simctl boot <UDID>
open -a Simulator

Capture at the largest class you intend to ship. Apple derives the smaller ones, and downscaling is lossless in a way upscaling never is.

Install, then grant permissions — then verify them

xcrun simctl install <UDID> "<path>.app"
xcrun simctl privacy <UDID> grant all <bundle-id>

Terminals rarely hold Accessibility permission, so you usually cannot just tap the dialog away — which is why verifying beats retrying.

Kill the first-run furniture

Coach marks, tooltips, "what's new" sheets, rating prompts. Every one of them will appear in exactly the frame you wanted. Most apps already have flags for these; set them through the simulator's defaults:

xcrun simctl spawn <UDID> defaults write <bundle-id> coachMarksEnabled -bool NO

Seed believable data — this is the whole game

An honest screenshot of an empty app sells nothing. A task app with three tasks, a finance app with one transaction, a photo grid of grey placeholders: each tells a browsing user that nobody uses your product.

Fill it with content that looks like a real account several months in. For media, push files straight into the simulator's library:

find <staging-dir> -type f -print0 | xargs -0 xcrun simctl addmedia <UDID>

This is the single biggest quality lever in the entire process, and it is the one most guides skip, because it is work rather than a command to paste.

Standardise the frame

One status bar state, held across every panel:

xcrun simctl status_bar <UDID> override \
  --time "9:41" --dataNetwork wifi --wifiBars 3 \
  --cellularBars 4 --batteryState charged --batteryLevel 100 \
  --operatorName ''

Note the ranges differ: --wifiBars takes 0–3, --cellularBars takes 0–4. There is a whole post on getting this right, including the QuickTime trick for real devices.

Drive to each screen without tapping

simctl has no tap verb, and automating the UI by guessing at coordinates is how capture runs become things you babysit. Two approaches actually work.

Launch-argument hooks. Add a tiny test-only hook to your app that reads an environment variable and jumps straight to a screen. simctl forwards anything prefixed with SIMCTL_CHILD_:

SIMCTL_CHILD_APP_SCREEN=clean xcrun simctl launch <UDID> <bundle-id>

This is worth the twenty minutes it takes to add. It makes the run deterministic and repeatable, which matters the third time you re-capture because the design changed.

An accessibility-tree agent. Where you cannot add hooks, idb ui describe-all dumps elements with labels and tap frames, so you can target semantically instead of by pixel. It can also dismiss system dialogs that the launch-argument route cannot reach.

The loop

A row of smoked glass panels of decreasing size, each lit from above
Terminate, launch into a known state, wait, capture. Repeatable beats fast — you will run this more than once.
shot () {   # shot <name> <env>
  xcrun simctl terminate $UDID $BUNDLE 2>/dev/null
  env $2 xcrun simctl launch $UDID $BUNDLE >/dev/null
  sleep 2
  xcrun simctl io $UDID screenshot "$OUT/$1.png"
}

shot 01-home   SIMCTL_CHILD_APP_SCREEN=home
shot 02-search SIMCTL_CHILD_APP_SCREEN=search
shot 03-detail SIMCTL_CHILD_APP_SCREEN=detail

Then look at every single frame

Not the file list. The images. Open them and check, one by one:

  1. Is there real content, or an empty state?
  2. Is any dialog, toast, coach mark or keyboard visible that should not be?
  3. Is the status bar identical across every frame?
  4. Are all frames the same pixel dimensions?
  5. Is anything on screen that is not true of the shipped app?

Verification is not optional here, because every failure in this list produces a file that exists, opens fine, and is unusable. A capture run that "succeeded" tells you nothing.

What happens next

Once you have clean frames at a known size, the design step has something worth designing around. That is covered in how to make App Store screenshots, and if App Store Connect has already rejected a set, start with the dimensions error.

Guides

App previews vs screenshots: which actually earns the slot

An app preview autoplays before your first screenshot, which makes it the most valuable slot on your listing and the easiest one to waste. When video helps, when it costs you, and how to decide without guessing.

Read
Guides

Apple creative assets beyond screenshots: the surfaces most listings ignore

Your screenshots are not the only images Apple shows. Search results, the product page header and editorial placements each have their own treatment — and most listings leave all of them on default.

Read
Guides

Get a clean 9:41 status bar in every App Store screenshot

Apple does not require 9:41, but it does notice a set where the clock and battery change between panels. Here is the exact simctl command, verified flag by flag, plus the two things it cannot fix.

Read