Capturing Screenshots
Capture App Store screenshots directly from iOS/iPadOS simulators using UI tests. Replaces fastlane snapshot.
Quick start
ascelerate screenshot init # Create config and helper in ascelerate/
ascelerate screenshot # Capture screenshots
Set up with an AI coding agent
Capturing takes a UI test target, the screenshot helper, a screenshot mode with demo content in the app, the tests themselves and the configuration. A coding agent can set all of it up:
ascelerate screenshot setup-prompt # Print the setup instructions
ascelerate screenshot setup-prompt --copy # Copy them to the clipboard
Paste the instructions into a coding agent working in the project (Claude Code, Codex, Cursor, Xcode's coding assistant…). They start from what the project already has (projects, schemes, UI test targets, localizations, configuration, helper version, installed simulators), so running the command again shows what's left. They also cover device frames, which you download from Apple's design resources (Product Bezels).
--design prints the instructions for a design generator instead: a small web app in app-store-screenshots/ that turns the framed screenshots into App Store images with headlines in every language, exported for ascelerate apps media upload (see Media).
Commands
Run
ascelerate screenshot # Capture screenshots
ascelerate screenshot run # Same as above
ascelerate screenshot --json-events # Progress as JSON Lines events on stdout
Always uses ascelerate/screenshot.yml in the current directory.
--json-events is for other tools following a run: stdout carries one JSON object per line (start, build, language, device with steps such as testing, passed, failed and collected, and finished with every result), and the readable progress moves to stderr.
Init
ascelerate screenshot init
Creates both ascelerate/screenshot.yml and ascelerate/ScreenshotHelper.swift in the ascelerate/ directory. Prompts for confirmation before writing (-y skips it). Won't overwrite existing files.
Create helper
ascelerate screenshot create-helper # Generates ScreenshotHelper.swift
ascelerate screenshot create-helper -o CustomHelper.swift
ascelerate screenshot create-helper -o UITests/ScreenshotHelper.swift -y # Overwrite without asking
Frame
ascelerate screenshot frame # Frame screenshots with device bezels
Frames captured screenshots using device bezel images. Uses the frameDevice and deviceBezel settings from the config. Can be run independently after capturing screenshots.
Doctor
ascelerate screenshot doctor # Check config and environment
ascelerate screenshot doctor --json # The same as JSON
Validates the screenshot configuration and environment: checks config file, project/workspace existence, xcodebuild and simctl availability, simulator devices, helper file version, device bezel files, and output directories. Shows a checklist with pass/fail/warning indicators. With --json it prints the configuration (scheme, devices, languages, output directory) and each check with its status (pass, warn or fail).
Config (ascelerate/screenshot.yml)
workspace: MyApp.xcworkspace
# project: MyApp.xcodeproj # Use project instead of workspace
scheme: AppUITests
devices:
- simulator: iPhone 17 Pro Max
# frameDevice: true
# deviceBezel: ./bezels/iPhone 17 Pro Max.png
- simulator: iPad Pro 13-inch (M5)
# frameDevice: true
# deviceBezel: ./bezels/iPad Pro 13-inch (M5).png
languages:
- en-US
- de-DE
outputDirectory: ./screenshots
# framedOutputDirectory: ./screenshots/framed
clearPreviousScreenshots: true
eraseSimulator: false
localizeSimulator: true
overrideStatusBar: true
darkMode: false
disableAnimations: true
waitAfterBoot: 0
# waitAfterEraseAndReboot: 30 # Extra wait for first-run system alerts
# statusBarArguments: "--time '9:41' --dataNetwork wifi"
# testWithoutBuilding: true # Skip build, use existing xctestrun
# cleanBuild: false
# headless: false # Don't open simulator windows (DeviceHub / Simulator.app)
# helperPath: AppUITests/ScreenshotHelper.swift
# launchArguments:
# - -ui_testing
# configuration: Debug # Build configuration
# testplan: MyTestPlan # Xcode test plan name
# numberOfRetries: 0 # Retry failed languages (erase + reboot simulator)
# stopAfterFirstError: false # Stop all devices on first failure
# reinstallApp: false # Delete and reinstall app before tests
# disableAssetDownloads: false # Disable mobileassetd (no Siri/keyboard/ML asset downloads; also blocks on-device ML assets)
# xcargs: SWIFT_ACTIVE_COMPILATION_CONDITIONS=SCREENSHOTS
UITest usage
Add ScreenshotHelper.swift to your UITest target:
override func setUp() {
setupScreenshots(app)
app.launch()
}
func testScreenshots() {
screenshot("01-home")
app.buttons["Settings"].tap()
screenshot("02-settings")
}
Your app can detect screenshot mode via:
if ProcessInfo.processInfo.arguments.contains("-ASC_SCREENSHOT") {
// Show demo data, hide debug UI, etc.
}
The helper also provides disableAnimationsIfNeeded() to turn off animations when disableAnimations is enabled in the config:
override func setUp() {
setupScreenshots(app)
disableAnimationsIfNeeded()
app.launch()
}
iPhone Duo
On the iPhone Duo simulator (Xcode 27.1+), setHinge(.open) unfolds the device and setHinge(.closed) folds it, then waits for the system to move the app to the matching display, so the next screenshot() captures that display. The simulator always boots folded.
func testScreenshots() {
screenshot("01-folded")
setHinge(.open)
screenshot("02-unfolded")
}
Unfolded, many apps lay out as they do on iPad, but UIDevice.current.userInterfaceIdiom in the test runner still reports .phone. To pick the right path in your tests, check ProcessInfo.processInfo.environment["SIMULATOR_MODEL_IDENTIFIER"] == "iPhone19,4".
There is no official API for the hinge. The helper sends the same input event as Device Hub's hinge slider, so a future Xcode release may break it.
How it works
- Builds once with
build-for-testing(or skips iftestWithoutBuilding: true) - For each language: boots all simulators, silences the iOS 26 "Ready for Apple Intelligence" banner (and
mobileassetdifdisableAssetDownloadsis set), localizes, overrides status bar - Runs tests concurrently across devices
- If
numberOfRetriesis set and any device fails: erases failed simulators, re-localizes, reboots, and retries - Collects screenshots from per-device cache to output directory
- Frames screenshots with device bezels (if
frameDeviceis enabled) - Errors skip and continue — error logs saved to output
Output
screenshots/
├── en-US/
│ ├── iPhone 17 Pro Max-01-home.png
│ ├── iPhone 17 Pro Max-02-settings.png
│ └── iPad Pro 13-inch (M5)-01-home.png
└── de-DE/
└── ...
Device framing
Frame captured screenshots with Apple device bezels.
Device bezels are not included with ascelerate — download them from Apple Product Bezels (requires an Apple Developer account). The download is a DMG file containing PNG bezels for all current devices.
Setup
- Download the Product Bezels DMG from Apple Design Resources
- Extract the bezel PNG files to a folder in your project (e.g.
./bezels/) - Enable framing per device in the config:
devices:
- simulator: iPhone 17 Pro Max
frameDevice: true
deviceBezel: ./bezels/iPhone 17 Pro Max.png
- simulator: iPad Pro 13-inch (M5)
frameDevice: false
A device can list several bezels. Each screenshot is framed with the bezel whose screen area matches its size, so an iPhone Duo's folded screenshots get the closed frame and its unfolded ones get the open frame:
devices:
- simulator: iPhone Duo
frameDevice: true
deviceBezel:
- ./bezels/iPhone Duo - Outer Closed Portrait.png
- ./bezels/iPhone Duo - Inner Open Landscape.png
Landscape screenshots use a landscape bezel if one is listed, otherwise the portrait bezel rotated.
Output
Framed screenshots are saved to framedOutputDirectory (defaults to {outputDirectory}/framed):
screenshots/framed/
├── en-US/
│ └── iPhone 17 Pro Max-01-home.png
└── de-DE/
└── ...
Only devices with frameDevice: true are framed. Framing runs automatically after each language during screenshot run, or standalone via screenshot frame.
Options
| Option | Description |
|---|---|
clearPreviousScreenshots | Clear language folder before collecting (only if all devices succeed) |
eraseSimulator | Erase simulator before each language |
localizeSimulator | Set simulator language/locale per language |
overrideStatusBar | Override status bar (9:41, full bars, Wi-Fi) |
statusBarArguments | Custom xcrun simctl status_bar arguments |
darkMode | Enable dark mode on simulators |
disableAnimations | Disable animations during tests |
waitAfterBoot | Seconds to wait after simulator boot (default: 0) |
waitAfterEraseAndReboot | Extra seconds to wait when the simulator is in a fresh state — first language of the run, or any time the simulator was erased (via eraseSimulator: true or a retry). Gives first-run system alerts time to appear before screenshots. (The iOS 26 "Ready for Apple Intelligence" banner is suppressed automatically and needs no wait.) |
testWithoutBuilding | Skip build, use existing xctestrun file |
cleanBuild | Run clean before building |
headless | Don't open the simulator windows (DeviceHub in Xcode 27, Simulator.app before) |
helperPath | Path to ScreenshotHelper.swift for version checking |
launchArguments | Extra launch arguments passed to the app |
configuration | Build configuration (e.g. Debug, Release) |
testplan | Xcode test plan name |
numberOfRetries | Number of times to retry failed languages — erases the simulator, re-localizes, reboots, and reruns tests. Only retries failed devices. Retried results are marked in the summary table. |
stopAfterFirstError | Stop all devices after the first failure |
reinstallApp | Delete and reinstall the app before running tests |
disableAssetDownloads | Disable the simulator's mobileassetd so it stops downloading gigabytes of Siri, keyboard, and ML assets into freshly erased simulators. Also blocks on-device ML assets some apps need (e.g. text recognition), so leave it off if your screenshots depend on those. Applied on every prep, since an erase resets it. |
xcargs | Extra arguments passed to xcodebuild |
frameDevice | Enable device bezel framing for this device (per-device) |
deviceBezel | Path to the device bezel PNG file, or a list of paths; each screenshot uses the bezel that fits its size (per-device) |
framedOutputDirectory | Output directory for framed screenshots (default: {outputDirectory}/framed) |