Capture iOS simulator screenshots from the command line.
Build, install, launch with deterministic data, and capture — one command. Designed for App Store screenshots, regression testing, and CI pipelines.
# Run directly with npx (no install needed)
npx screenshots-ios --context screenshots/home.json
# Or install globally
npm install -g screenshots-ios
screenshots-ios --context screenshots/home.jsonRun from your app's repo root. The tool handles the full pipeline:
- Resolve simulator device
- Boot simulator
- Build app (
xcodebuild) - Install on simulator
- Override status bar (9:41, full signal, 100% battery)
- Launch app with environment variables
- Wait for UI to settle
- Capture screenshot
- macOS with Xcode installed
xcrun,xcodebuild, andsimctlavailable in PATH- Node.js >= 18
Create a JSON context file that describes what to capture:
{
"workspace": "MyApp.xcworkspace",
"scheme": "MyApp",
"screenshotName": "home",
"outputDir": "docs/screenshots",
"launchEnv": {
"APP_USE_PREVIEW_DATA": "1",
"APP_INITIAL_SCREEN": "home"
}
}npx screenshots-ios --context screenshots/home.jsonBuild once, then --skip-build the rest:
npx screenshots-ios --context screenshots/home.json
npx screenshots-ios --context screenshots/settings.json --skip-build
npx screenshots-ios --context screenshots/profile.json --skip-buildnpx screenshots-ios --list-devices
npx screenshots-ios --list-devices --simulator "iPhone" --os "26"Flags override values from the context file:
npx screenshots-ios --context screenshots/home.json \
--skip-build \
--simulator "iPhone 17 Pro Max" \
--wait 5 \
--name home-dark \
--launch-env THEME=dark \
--openAll fields except scheme are optional.
| Field | Type | Default | Description |
|---|---|---|---|
workspace |
string | auto-detected | .xcworkspace path |
project |
string | auto-detected | .xcodeproj path (if no workspace) |
scheme |
string | required | Xcode build scheme |
configuration |
string | "Debug" |
Build configuration |
appName |
string | auto-detected | Product name (to find .app bundle) |
bundleId |
string | from Info.plist | Bundle identifier |
simulator |
string | "iPhone 17 Pro" |
Simulator device name |
simulatorOS |
string | any | Runtime filter (e.g. "26.2") |
simulatorUdid |
string | — | Exact UDID (bypasses name lookup) |
derivedDataPath |
string | "Derived/Screenshots" |
xcodebuild derived data |
outputDir |
string | "docs/screenshots" |
Where screenshots are saved |
screenshotName |
string | "app" |
File prefix: {name}-{timestamp}.png |
waitSeconds |
number | 2 |
Seconds to wait before capture |
statusBar.enabled |
boolean | true |
Override status bar |
statusBar.time |
string | "09:41" |
Status bar clock time |
skipBuild |
boolean | false |
Skip xcodebuild |
cleanBuild |
boolean | false |
Run clean build |
openAfterCapture |
boolean | false |
Open screenshot in Preview |
testBeforeCapture |
boolean | false |
Run tests first |
testScheme |
string | same as scheme |
Test scheme |
launchEnv |
object | {} |
Env vars passed to the app |
launchArgs |
string[] | [] |
Launch arguments |
Usage: screenshots-ios [options]
Required:
--scheme <name> Build scheme (or set in context file)
Container:
--workspace <path> .xcworkspace path
--project <path> .xcodeproj path
--context <path> JSON context file
Build:
--configuration <name> Build configuration (default: Debug)
--derived-data <path> DerivedData path
--skip-build Reuse existing build
--clean-build Clean before building
Simulator:
--simulator <name> Device name (default: iPhone 17 Pro)
--os <value> Runtime filter (e.g. "26.2")
--udid <uuid> Exact simulator UDID
Output:
--name <value> Screenshot file prefix
--output-dir <path> Output directory
--wait <seconds> Delay before capture
--open Open after capture
App context:
--launch-env KEY=VALUE Environment variable (repeatable)
--launch-arg <arg> Launch argument (repeatable)
--app-name <name> Product name
--bundle-id <id> Bundle identifier
Status bar:
--status-bar-time <HH:MM> Clock time (default: 09:41)
--no-status-bar Disable override
Testing:
--test-before-capture Run tests first
--test-scheme <name> Test scheme
General:
--list-devices List available simulators
-h, --help Show help
The tool handles build/install/launch/capture. Your app handles showing the right screen with the right data. Here's the contract:
Read an environment variable at launch to switch to deterministic data:
let usePreview = ProcessInfo.processInfo.environment["APP_USE_PREVIEW_DATA"] == "1"Read an environment variable to jump to a specific screen:
@State private var selectedTab: Tab = {
if let tab = ProcessInfo.processInfo.environment["APP_INITIAL_TAB"] {
return Tab(rawValue: tab) ?? .home
}
return .home
}()Replace real data sources with seeded in-memory implementations:
actor PreviewRepository: SomeRepository {
private var items: [Item]
init(seed: [Item]) { self.items = seed }
func fetchAll() async -> [Item] { items }
}Use hardcoded data with fixed IDs — no random values, no Date():
enum PreviewData {
static let items: [Item] = [
Item(id: "fixed-uuid-1", name: "Groceries", amount: 25000),
Item(id: "fixed-uuid-2", name: "Transport", amount: 37000),
]
}For screens triggered by user actions, add env var hooks:
.onAppear {
if ProcessInfo.processInfo.environment["APP_OPEN_REVIEW_FLOW"] == "1" {
showReviewFlow = true
}
}context.json → launchEnv → SIMCTL_CHILD_ prefix → simulator → ProcessInfo.processInfo.environment
The tool automatically adds the SIMCTL_CHILD_ prefix. Your app reads the original key name.
your-app/
├── screenshots/ # Context files (one per screenshot)
│ ├── capture-all.sh # Batch script
│ ├── home.json
│ ├── settings.json
│ └── profile.json
├── docs/screenshots/ # Output
├── MyApp.xcworkspace
└── ...
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$REPO_ROOT"
CONTEXTS=(home settings profile)
for i in "${!CONTEXTS[@]}"; do
ctx="${CONTEXTS[$i]}"
skip=""
[[ "$i" -gt 0 ]] && skip="--skip-build"
npx screenshots-ios --context "$SCRIPT_DIR/${ctx}.json" $skip
doneMIT