Automating your App Store screenshots

App Store Connect asks for screenshots per language and per device size. For Foodproof, that’s 9 screens to capture, in French and then in English. Redoing them by hand every time the UI changes is exactly the kind of repetitive task that makes you want to skip a release. Here’s how we automated it.

The idea: let the app drive itself

Instead of writing a script that simulates taps on the screen (fragile, breaks with every layout change), we put the navigation logic directly inside the app. When the app starts in screenshot mode, AppNavigator.onReady triggers a function called runScreenshotTour(), which switches to the target language and then navigates through the 9 screens on its own, taking a capture at each step with captureScreen() from react-native-view-shot.

The screen order is fixed:

  1. 01_scanner
  2. 02_result_compatible
  3. 03_result_not_compatible
  4. 04_result_uncertain
  5. 05_product_detail
  6. 06_home
  7. 07_history
  8. 08_profile
  9. 09_profile_edit

The script

What's the iOS simulator? A virtual iPhone that runs on your Mac, bundled with Xcode. `xcrun simctl` is the command-line tool to control it: list available simulators, launch an app on one, or dig through its files.

The take-screenshots.sh script at the root of the repo is used like this:

./take-screenshots.sh fr
./take-screenshots.sh en

It doesn’t do much on its own, which is almost the point: it restarts the app on whichever simulator is currently booted (xcrun simctl terminate then launch), waits for the screenshot tour to finish (35 seconds, the time it takes the app to walk through its 9 screens), then goes looking for the PNGs written by react-native-view-shot in the simulator’s app container temp folder, and copies them, renamed, into website/screenshots/<language>/.

# excerpt of the script's logic
xcrun simctl get_app_container <device_id> com.yabaa.foodproof data
# then looks for PNGs newer than a marker created right before launch

No fragility tied to screen coordinates, no dependency on an end-to-end testing tool. Just the app knowing how to put itself in the right state.

The gotcha: a flag that resets itself

Screenshot mode relies on a flag, _isScreenshotMode, in screenshotMode.ts. The real gotcha: this flag needs to be set to true in two places, the module-level declaration and inside the initScreenshotMode() function. If you only set it in the first place, the call to initScreenshotMode() from index.js overwrites it with initialProps?.screenshotMode === true, which defaults to false. The result: the mode quietly turns itself off right after startup, and you spend a while wondering why nothing is happening.

What needs to be in place before running the script

  • A booted simulator (the script looks for whichever one is currently “booted” via xcrun simctl list devices booted)
  • Metro running (npx react-native start)
  • The app installed with the right terminal encoding:
LANG=en_US.UTF-8 npx react-native run-ios --simulator "iPhone 16 Pro Max"

Small honest note: the comment at the top of the script still mentions “iPhone 14 Plus” while the internal docs now recommend the iPhone 16 Pro Max. A leftover we haven’t cleaned up, as often happens with scripts you only skim once they work.

The result

The images land directly in website/screenshots/{fr,en}/, which is also the folder the website serves without any copy step in between. One command, two languages, nine screens, zero manual clicks.