Mastering the iOS Simulator with `simctl`: Push Testing, Screenshots, and CI
You have probably clicked through the Simulator menu to toggle dark mode, or maybe you manually typed in a push
notification payload and wished there was a faster way. There is. The simctl command-line tool ships with every Xcode
installation and gives you programmatic control over every simulator on your machine — from sending push notifications
to recording pixel-perfect App Store screenshots, all without touching the GUI.
This guide covers the simctl subcommands that matter most in day-to-day iOS development and CI/CD pipelines. We will
not cover Simulator internals or the CoreSimulator private framework — the focus here is practical, production-ready
usage.
Contents
- The Problem
- Getting Started with
simctl - Push Notification Testing
- App Store Screenshots with Status Bar Overrides
- Recording Video
- Adding Media and Managing Privacy
- Advanced Usage: CI/CD Scripting
- Performance Considerations
- When to Use (and When Not To)
- Summary
The Problem
Imagine you are building the notification system for a Pixar movie streaming app. Every time a new short drops — say Lava or Bao — your app fires a push notification with a custom image and deep link. Testing this flow typically means provisioning a real device, configuring APNs certificates, running a backend script, and waiting for the push to arrive. That cycle is slow.
And it is not just push. Consider this common screenshot workflow:
# The painful manual approach
# 1. Launch Simulator
# 2. Navigate to the right screen
# 3. Toggle dark mode in Settings
# 4. Change the clock to 9:41 manually... wait, you can't
# 5. Take a screenshot with Cmd+S
# 6. Repeat for 6 device sizes x 2 color schemes = 12 screenshots
Twelve manual screenshots, each requiring you to navigate to the correct screen and hope the status bar looks clean. For a production app with localization, multiply that by every supported language. This does not scale.
simctl solves both problems — and a dozen others — from a single terminal command.
Getting Started with simctl
Every simctl command runs through xcrun, which resolves the active Xcode toolchain. The basic invocation pattern is:
xcrun simctl <subcommand> <device> [arguments]
The <device> argument can be a device UDID, a device name, or the keyword booted to target whichever simulator is
currently running. Start by listing available devices:
xcrun simctl list devices available
== Devices ==
-- iOS 18.0 --
iPhone 16 Pro (A1B2C3D4-...) (Shutdown)
iPhone 16 Pro Max (E5F6G7H8-...) (Booted)
-- iOS 17.5 --
iPhone 15 (I9J0K1L2-...) (Shutdown)
You can filter the output to find exactly what you need:
# List only booted devices
xcrun simctl list devices booted
# Get a specific device UDID by name
xcrun simctl list devices available | grep "iPhone 16 Pro Max"
Tip: Pass
-jto anysimctl listcommand to get JSON output. This is invaluable for CI scripts that need to parse device information programmatically:xcrun simctl list devices available -j
To create, boot, and shut down simulators programmatically:
# Create a new simulator
xcrun simctl create "PixarScreenshots" \
"com.apple.CoreSimulator.SimDeviceType.iPhone-16-Pro" \
"com.apple.CoreSimulator.SimRuntime.iOS-18-0"
# Boot it
xcrun simctl boot "PixarScreenshots"
# Shut it down when done
xcrun simctl shutdown "PixarScreenshots"
# Delete it entirely
xcrun simctl delete "PixarScreenshots"
Apple Docs:
xcrun simctl— Xcode documentation on Simulator runtimes
Push Notification Testing
This is the subcommand that saves the most time. Instead of configuring APNs and running a backend, you can send a push notification directly to a booted simulator with a JSON payload file.
Create a file called pixar-notification.apns:
{
"Simulator Target Bundle": "com.cocoabytes.PixarTracker",
"aps": {
"alert": {
"title": "New Pixar Short Available!",
"subtitle": "Inside Out: Riley's First Date?",
"body": "Watch the latest short now. Joy and Disgust have opinions."
},
"badge": 3,
"sound": "default",
"category": "NEW_CONTENT"
},
"deepLink": "pixartracker://shorts/rileys-first-date"
}
Now send it:
xcrun simctl push booted \
com.cocoabytes.PixarTracker pixar-notification.apns
The notification appears immediately on the simulator — no APNs certificate, no backend, no device provisioning. If you
include the Simulator Target Bundle key inside the JSON (as shown above), you can omit the bundle identifier from the
command:
# Bundle ID read from the payload itself
xcrun simctl push booted pixar-notification.apns
Tip: You can test different notification categories, custom actions, and even interruption levels by modifying the payload JSON. This is the fastest way to iterate on your
UNNotificationContentExtensionorUNNotificationServiceExtensionbefore involving real APNs infrastructure.
Testing Background Push
For silent push notifications that trigger background processing, use the content-available flag:
{
"Simulator Target Bundle": "com.cocoabytes.PixarTracker",
"aps": {
"content-available": 1
},
"updateType": "new-movie-added",
"movieId": "inside-out-3"
}
xcrun simctl push booted pixar-notification.apns
This triggers your application(_:didReceiveRemoteNotification:fetchCompletionHandler:) delegate method, letting you
verify that background data sync works without deploying to a device.
App Store Screenshots with Status Bar Overrides
Clean status bar presentation matters for App Store screenshots. No one wants to see “Carrier” at 47% battery with a
random time. simctl status_bar override lets you set every status bar element to pixel-perfect values.
xcrun simctl status_bar booted override \
--time "9:41" \
--batteryState charged \
--batteryLevel 100 \
--wifiBars 3 \
--cellularBars 4 \
--cellularMode active \
--dataNetwork wifi \
--operatorName ""
After running this command, the simulator status bar shows 9:41, full battery, full signal, and no carrier name — exactly what Apple uses in its own marketing materials.
To capture the screenshot:
xcrun simctl io booted screenshot ~/Desktop/PixarTracker-Home.png
When you are done, clear the override:
xcrun simctl status_bar booted clear
Automating Multi-Device Screenshots
Here is where simctl really shines. Combine device creation, status bar overrides, and screenshot capture into a
single script that runs across every required device size:
#!/bin/bash
# screenshot-all-devices.sh
# Captures App Store screenshots for all required device sizes
DEVICES=(
"iPhone 16 Pro Max"
"iPhone 16 Pro"
"iPhone SE (3rd generation)"
"iPad Pro (13-inch) (M4)"
)
BUNDLE_ID="com.cocoabytes.PixarTracker"
OUTPUT_DIR="$HOME/Desktop/AppStoreScreenshots"
mkdir -p "$OUTPUT_DIR"
for DEVICE in "${DEVICES[@]}"; do
echo "Capturing screenshots for $DEVICE..."
# Boot the device
xcrun simctl boot "$DEVICE" 2>/dev/null
# Wait for boot to complete
xcrun simctl bootstatus "$DEVICE"
# Override status bar
xcrun simctl status_bar "$DEVICE" override \
--time "9:41" \
--batteryState charged \
--batteryLevel 100 \
--wifiBars 3 \
--cellularBars 4
# Launch the app
xcrun simctl launch "$DEVICE" "$BUNDLE_ID"
# Give the app time to render
sleep 3
# Capture screenshot
SAFE_NAME=$(echo "$DEVICE" | tr ' ' '-')
xcrun simctl io "$DEVICE" screenshot \
"$OUTPUT_DIR/${SAFE_NAME}-home.png"
# Clean up
xcrun simctl status_bar "$DEVICE" clear
xcrun simctl shutdown "$DEVICE"
done
echo "Screenshots saved to $OUTPUT_DIR"
Warning:
xcrun simctl bootstatusblocks until the device finishes booting, but the app itself may need additional time to render its first frame. Thesleep 3is a pragmatic workaround. For CI, consider usingxcrun simctl spawnwith a health-check script instead of a fixed delay.
Recording Video
Need to record a demo for your PR description or a bug reproduction? simctl can record the simulator screen to a video
file:
# Start recording (press Ctrl+C to stop)
xcrun simctl io booted recordVideo \
~/Desktop/pixar-tracker-demo.mov
The recording captures at the simulator’s native resolution. You can specify the codec and display:
xcrun simctl io booted recordVideo \
--codec h264 \
--display internal \
--force \
~/Desktop/demo.mov
The --force flag overwrites any existing file at that path. For CI pipelines that capture video of test runs, this is
essential to avoid failures from file-already-exists errors.
Tip: Combine video recording with
xcrun simctl launchand UI test execution to automatically capture video evidence of test failures. Some teams pipe this into their PR review workflow so reviewers can watch the reproduction without setting up the project locally.
Adding Media and Managing Privacy
Adding Photos and Videos
Testing a photo picker or camera roll integration? Instead of dragging files into the simulator window, use addmedia:
# Add a single image
xcrun simctl addmedia booted ~/Assets/woody-portrait.jpg
# Add multiple files at once
xcrun simctl addmedia booted \
~/Assets/buzz-lightyear.png \
~/Assets/monsters-inc-trailer.mp4 \
~/Assets/nemo-album.jpg
The files appear in the Photos app immediately. This is particularly useful for testing image pickers, share extensions, or any feature that reads from the photo library.
Resetting Privacy Permissions
When testing first-launch flows — permission dialogs for camera, location, photos, notifications — you need the app to
behave as if it has never been opened. simctl privacy handles this:
# Reset all permissions for your app
xcrun simctl privacy booted reset all \
com.cocoabytes.PixarTracker
# Reset a specific permission
xcrun simctl privacy booted reset photos \
com.cocoabytes.PixarTracker
xcrun simctl privacy booted reset location \
com.cocoabytes.PixarTracker
xcrun simctl privacy booted reset camera \
com.cocoabytes.PixarTracker
You can also grant or deny permissions without the dialog:
# Grant photo library access silently
xcrun simctl privacy booted grant photos \
com.cocoabytes.PixarTracker
# Deny location access
xcrun simctl privacy booted revoke location \
com.cocoabytes.PixarTracker
This is critical for UI tests. Instead of writing fragile code to tap “Allow” on system dialogs, pre-grant the permissions before the test suite runs.
Toggling Appearance
Switch between light and dark mode without opening Settings:
# Switch to dark mode
xcrun simctl ui booted appearance dark
# Switch back to light mode
xcrun simctl ui booted appearance light
Pair this with the screenshot script above to capture both color schemes automatically.
Opening URLs
Test deep links and universal links by opening a URL directly:
xcrun simctl openurl booted \
"pixartracker://movies/inside-out-2"
xcrun simctl openurl booted \
"https://cocoabytes.io/movie/coco"
This triggers your app’s URL handling code — onOpenURL in SwiftUI or application(_:open:options:) in UIKit —
without needing to tap a link in Safari.
Advanced Usage: CI/CD Scripting
The real power of simctl emerges when you integrate it into CI/CD pipelines. Here are patterns that work in
production.
Creating Ephemeral Simulators
Instead of relying on pre-existing simulators (which may have stale state), create fresh ones per CI run:
#!/bin/bash
# ci-test-runner.sh
# Creates a clean simulator, runs tests, captures artifacts
DEVICE_NAME="CI-PixarTracker-$(date +%s)"
RUNTIME="com.apple.CoreSimulator.SimRuntime.iOS-18-0"
DEVICE_TYPE="com.apple.CoreSimulator.SimDeviceType.iPhone-16-Pro"
# Create and boot
DEVICE_UDID=$(xcrun simctl create \
"$DEVICE_NAME" "$DEVICE_TYPE" "$RUNTIME")
xcrun simctl boot "$DEVICE_UDID"
xcrun simctl bootstatus "$DEVICE_UDID"
# Pre-grant permissions to avoid dialog interruptions
xcrun simctl privacy "$DEVICE_UDID" grant all \
com.cocoabytes.PixarTracker
# Run tests
xcodebuild test \
-scheme PixarTracker \
-destination "platform=iOS Simulator,id=$DEVICE_UDID" \
-resultBundlePath ./test-results.xcresult
# Capture a final screenshot for the test report
xcrun simctl io "$DEVICE_UDID" screenshot \
./test-final-state.png
# Tear down
xcrun simctl shutdown "$DEVICE_UDID"
xcrun simctl delete "$DEVICE_UDID"
Keychain and App Data Management
Clear app data without reinstalling:
# Uninstall and reinstall for a clean slate
xcrun simctl uninstall booted com.cocoabytes.PixarTracker
xcrun simctl install booted \
./Build/Products/Debug-iphonesimulator/PixarTracker.app
# Or terminate and relaunch
xcrun simctl terminate booted com.cocoabytes.PixarTracker
xcrun simctl launch booted com.cocoabytes.PixarTracker
Passing Launch Arguments and Environment Variables
You can pass launch arguments and environment variables when spawning an app, which is useful for toggling feature flags or pointing to staging servers:
xcrun simctl launch booted com.cocoabytes.PixarTracker \
-EnableDebugOverlay YES \
-APIEnvironment staging
Your app reads these through ProcessInfo:
// In your app's launch configuration
let isDebugOverlay = ProcessInfo.processInfo
.arguments.contains("-EnableDebugOverlay")
let apiEnvironment = ProcessInfo.processInfo
.environment["APIEnvironment"] ?? "production"
Erasing a Simulator
When you need a truly clean slate — no apps, no data, no settings:
xcrun simctl erase booted
Warning:
eraseresets the simulator to factory defaults. Every app, photo, and setting is deleted. This is irreversible. Use it deliberately in CI teardown scripts, not on your daily-driver simulator.
Performance Considerations
simctl commands are generally fast — most complete in under a second. However, there are a few operations where
performance matters:
Boot time is the biggest bottleneck. A cold boot takes 10-30 seconds depending on the runtime version and host machine. On CI, prefer keeping a simulator booted across test suites rather than creating and destroying one per test target.
bootstatus blocks until SpringBoard is ready. Always use it after boot in scripts — without it, subsequent
commands may fail silently because the runtime has not finished initializing.
Screenshot capture is near-instant, but video recording adds CPU overhead. On CI machines with limited resources, recording video during test execution can slow down tests by 5-15%. Record only when investigating failures, not as a default.
Parallel simulators are supported but resource-intensive. Each booted simulator consumes 1-2 GB of RAM. On a 16 GB CI machine, running more than four simulators concurrently risks memory pressure and flaky tests. The sweet spot for most teams is 2-3 parallel simulators.
Tip: Use
xcrun simctl list devices bootedat the start of your CI script to detect leftover simulators from previous failed runs. Shut them down before starting fresh to avoid resource contention.
When to Use (and When Not To)
| Scenario | Recommendation |
|---|---|
| Push notifications | Use simctl push. No APNs config needed. |
| App Store screenshots | status_bar override + io screenshot. |
| CI/CD test pipelines | Ephemeral simulators via simctl create. |
| Bug reproductions | simctl io recordVideo for .mov capture. |
| Deep links | simctl openurl. Faster than Safari. |
| On-device performance | Do not use the simulator. Use a device. |
| Hardware sensors | Do not use the simulator. Not emulated. |
| Load testing | Do not use simctl. Single-user only. |
Summary
xcrun simctl pushsends push notifications to the simulator instantly, bypassing APNs entirely — use it for every notification-related feature.xcrun simctl status_bar overridegives you pixel-perfect control over the status bar for App Store screenshots. Combine it withio screenshotand a shell loop to automate every required device size and color scheme.xcrun simctl privacygrants, revokes, and resets permissions programmatically, eliminating flaky system dialogs in UI tests.xcrun simctl io recordVideocaptures screen recordings for bug reproductions and PR reviews.- In CI/CD, create ephemeral simulators with
simctl create, pre-grant permissions, run tests, capture artifacts, and delete the simulator. Clean state on every run eliminates an entire category of flaky failures.
The simulator is a development tool, not a performance benchmark. For anything involving real-world hardware
characteristics — CPU thermals, GPU performance, sensor data — you still need a physical device. But for the 90% of
development workflow that involves building, testing, and capturing screenshots, simctl is the most underused tool in
your Xcode installation.
Want to go deeper into Xcode tooling? Check out Xcode 26: The Features That Change How You Work for the latest IDE capabilities, or TestFlight Beyond the Basics to automate your beta distribution pipeline.