simshot:仅用 simctl 从 iOS 模拟器抓取 App Store 截图
kichiemon/simshot
simshot 是一款仅依赖 simctl 的 iOS 模拟器截图工具,无需 XCUITest 或测试运行器,即可完成构建、启动模拟器、覆盖状态栏、逐场景截图并缩放到 App Store 尺寸。
Capture App Store-ready screenshots from the iOS Simulator with simctl only — no XCUITest, no flaky test runners, no hangs.
⭐ If simshot makes your next release easier, consider starring the repo. Questions or ideas? Join the Discussions.
Your app implements a tiny #if DEBUG launch-argument handler; simshot does the rest: build, boot simulators, override the status bar, launch each scene, capture, and resize to App Store sizes.
simshot shoot --project Kanapp.xcodeproj --scheme Kanapp \ --bundle-id dev.kichiemon.kanapp \ --devices iphone-17-pro-max,ipad-pro-13 \ --langs ja,en --shots shots.json --resize
Why simshot?
fastlane snapshot drives the UI through XCUITest, which means maintaining test targets, fighting flaky element queries, and dealing with runner crashes. simshot inverts the model: the app itself navigates to each scene (reading a documented launch-argument protocol), and simshot just drives simctl with a hard timeout on every step.
| simshot | fastlane snapshot | |
|---|---|---|
| UI automation | None — simctl only |
XCUITest runner |
| Hangs | Hard timeout on every command | Can hang CI |
| Ruby | No | Ruby + fastlane + gems |
| Test target | #if DEBUG handler in your app |
Dedicated UI test target |
| Status bar override | Built in | Plugin |
| Resize to App Store sizes | Built in (--resize) |
Separate tooling |
| Multi-language | --langs ja,en |
Per-locale setup |
| Dependency footprint | Zero | Dozens of gems |
How it works
xcodebuildbuilds the app for the generic simulator (or use an existing.appwith--app-path).- For each device × language:
simctl bootstatus <udid> -bboots (and waits).simctl status_bar <udid> override --time "9:41" --batteryState charged --batteryLevel 100 ...prettifies the status bar.- The app is installed (previous install removed first for a clean state).
- For each shot, simshot launches the app with the scene protocol args, waits for the scene to settle, then takes
simctl io <udid> screenshot.
- Raw captures land in
<output>/raw/<device>/<lang>/, and (with--resize) App Store-ready copies — alpha flattened, resized to the matching size — in<output>/<device>/<lang>/.
Every external command runs through a timeout + retry wrapper, so a crashed app or stuck simulator can never hang CI.
Install
Eight ways to get simshot — pick one:
| # | Path | Command | Needs |
|---|---|---|---|
| 1 | Install script (curl) | curl -fsSL …/install.sh | bash |
macOS |
| 2 | Homebrew (tap) | brew tap kichiemon/homebrew-tap && brew install simshot |
Homebrew |
| 3 | Single binary (curl) | download simshot-macos-{arm64,x86_64} |
macOS |
| 4 | Nix | nix profile install github:kichiemon/simshot |
Nix |
| 5 | mise | mise use -g github:kichiemon/simshot |
mise |
| 6 | npm | npm i -g simshot |
Node.js 14+ |
| 7 | Mint | mint install kichiemon/simshot |
Xcode |
| 8 | Build from source | swift build -c release |
Xcode |
| + | Agent skill | npx skills add kichiemon/simshot |
npx |
Install script (curl, macOS)
One-liner that detects your architecture, downloads the matching prebuilt binary from GitHub Releases, verifies its sha256, and installs it to /usr/local/bin (or ~/.local/bin):
curl -fsSL https://raw.githubusercontent.com/kichiemon/simshot/main/install.sh | bashCustomize with SIMSHOT_VERSION (release tag) and SIMSHOT_INSTALL_DIR:
SIMSHOT_VERSION=v0.2.1 SIMSHOT_INSTALL_DIR=~/.local/bin \ curl -fsSL https://raw.githubusercontent.com/kichiemon/simshot/main/install.sh | bash
Install as an agent skill (npx)
npx skills add kichiemon/simshot
Installs simshot as a reusable agent skill (for Claude Code / opencode and other skill-aware agents). Once installed, skill-aware agents can use simshot automatically — just ask it to capture App Store screenshots, and the agent builds and runs simshot shoot for you (see skills/simshot/SKILL.md).
Target a specific agent, for example OpenAI Codex:
npx skills add kichiemon/simshot -a codex
This installs the skill into .agents/skills/ (Codex's project skill directory). The skill is kept in sync across three locations — skills/simshot/SKILL.md (canonical), .claude/skills/simshot/SKILL.md (Claude Code), and .agents/skills/simshot/SKILL.md (Codex and other .agents/skills/ agents) — so all skill-aware agents get identical instructions.
Build from source
git clone https://github.com/kichiemon/simshot.git cd simshot swift build -c release # binary at .build/release/simshot — symlink or copy it into your PATH ln -s "$(pwd)/.build/release/simshot" /usr/local/bin/simshot
Single binary (curl, macOS)
Grab the prebuilt binary for your architecture directly from GitHub Releases — no Swift toolchain required:
# Apple Silicon (arm64) curl -sL https://github.com/kichiemon/simshot/releases/download/v0.2.1/simshot-macos-arm64 -o /usr/local/bin/simshot && chmod +x /usr/local/bin/simshot # Intel (x86_64) curl -sL https://github.com/kichiemon/simshot/releases/download/v0.2.1/simshot-macos-x86_64 -o /usr/local/bin/simshot && chmod +x /usr/local/bin/simshot
Nix
The repo ships a flake.nix that fetches the prebuilt single binary (no source build):
nix run github:kichiemon/simshot # run without installing nix profile install github:kichiemon/simshot # install into your profile
mise
mise installs simshot from the GitHub Releases assets. The github backend is recommended (it also verifies SLSA provenance / artifact attestations):
mise use -g github:kichiemon/simshot # install into your global config simshot --version # v0.2.1
The legacy ubi backend (mise use ubi:kichiemon/simshot) resolves the same simshot-macos-{os}-{arch} release assets and also works, but it is deprecated in mise and will be removed in mise 2027.1.0.
Homebrew (tap, recommended)
simshot ships a tap formula that installs the prebuilt release binary. First time:
brew tap kichiemon/homebrew-tap brew install simshot
Formula source:
kichiemon/homebrew-tap(Formula/simshot.rb):
class Simshot < Formula desc "App Store screenshot capture CLI (simctl only, no XCUITest)" homepage "https://github.com/kichiemon/simshot" version "0.2.1" license "MIT" on_macos do on_arm do url "https://github.com/kichiemon/simshot/releases/download/v0.2.1/simshot-macos-arm64.tar.gz" sha256 "6819ad59d5999254085126c582dea5c31274646578b7931799419cc90cc7c62d" end on_intel do url "https://github.com/kichiemon/simshot/releases/download/v0.2.1/simshot-macos-x86_64.tar.gz" sha256 "5a489aa2aa84190c12a4ae4c9cfb6309e1ea1037b26260d48bd9197dc730ca3f" end end def install bin.install "simshot" end end
Mint
Mint builds and installs simshot from source via the Mintfile (requires Xcode):
brew install mint mint install kichiemon/simshot
npm (npmjs.com)
npm i -g simshot npx simshot shoot
Requires Node.js 14+ on macOS (arm64/x86_64). The package downloads the matching prebuilt single binary from GitHub Releases in postinstall (see npm/) and verifies its sha256. Published on npm as simshot (0.2.1).
Quick start
-
Add the scene protocol handler to your app (5 minutes).
-
Verify the environment and list simulators:
simshot doctor # diagnose xcode-select / Xcode / simctl / runtimes simshot devices # list available simulators
-
Create a
shots.json(seeexamples/shots.json) or use--scenes:simshot shoot --project MyApp.xcodeproj --scheme MyApp \ --bundle-id com.example.myapp \ --devices iphone-17-pro-max,ipad-pro-13 \ --langs ja,en --scenes home,detail,settings \ --output appstore --resize
Screenshots land in appstore/<device>/<lang>/NN_name.png.
Commands
| Command | Description |
|---|---|
simshot shoot <options> |
Build, boot, capture, and resize screenshots. |
simshot devices |
List available simulators (name + UDID). |
simshot doctor |
Diagnose the local Xcode / simulator environment (exit 1 on problems). |
simshot init |
Generate a commented simshot.yml config scaffold. |
simshot verify [dir] |
Check captured screenshots against App Store requirements (exit 1 on problems). |
simshot version |
Print the version. |
simshot help |
Show help. |
simshot doctor
Checks xcode-select, Xcode, simctl, iOS runtimes, and available simulators, and exits 1 if anything is broken. Run it when setting up a new machine or before filing a bug.
$ simshot doctor
✅ xcode-select: /Applications/Xcode.app/Contents/Developer
✅ Xcode: Xcode 26.5
✅ simctl: found
✅ iOS runtimes: 7 installed
✅ Available simulators: 36 available
✅ All checks passed.
simshot init
Generates a commented simshot.yml scaffold that documents the options you'd pass to simshot shoot. Interactive by default; --yes writes it with placeholder values.
simshot init --yes # → simshot.yml (commented template)simshot verify
Checks the tree simshot shoot produced against Apple's screenshot rules; anything App Store Connect would reject fails here first (exit 1), so it works as a CI gate.
simshot shoot --project MyApp.xcodeproj --scheme MyApp \ --bundle-id com.example.myapp --devices iphone-17-pro-max \ --langs ja,en --scenes home,detail --resize simshot verify appstore --devices iphone-17-pro-max --langs ja,en
simshot verify — appstore
✅ iphone-17-pro-max/ja 3 files 1290×2796 (6.7inch)
❌ iphone-17-pro-max/en 2 files 1290×2796 (6.7inch)
❌ iphone-17-pro-max/en/02_detail.png
image has an alpha channel — App Store Connect rejects these
→ run shoot with `--resize` to write flattened App Store copies
It flags dimensions that aren't an accepted size, an alpha channel, mixed sizes inside one folder, and empty or missing device/language folders. raw/ is skipped, --devices/--langs require those folders to exist, and --json gives the same report for tooling.
simshot shoot reference
BUILD
--project <path.xcodeproj> Xcode project to build (or --workspace)
--workspace <path.xcworkspace> Xcode workspace to build
--scheme <name> Scheme to build
--app-path <path.app> Skip building; use an existing .app bundle
REQUIRED
--bundle-id <id> App bundle identifier
--devices <list> Simulator names or UDIDs (comma-separated)
SHOTS
--shots <config.json> Shot config file ({ "shots": [...] })
--scenes <list> Shortcut: one shot per scene, e.g. home,trace
--wait <secs> Default settle time per shot for --scenes (default: 6)
LOCALIZATION
--langs <list> Languages (default: en), e.g. ja,en
--locales <map> lang=locale overrides, e.g. ja=ja_JP,en=en_US
OUTPUT
--output <dir> Output directory (default: appstore)
--resize Write App Store-ready copies (alpha removed)
--derived-data <dir> DerivedData path (default: ~/.simshot/DerivedData)
SIMULATOR
--timeout <secs> Timeout for external commands (default: 300)
--status-bar-time <t> Status bar clock (default: 9:41)
--status-bar-battery <n> Status bar battery % (default: 100)
--no-ui-testing Do not pass --ui-testing to the app
--no-clean Do not uninstall before installing
--keep-running Do not shut down simulators afterwards
MISC
--verbose, -v Verbose output
--help, -h Show help
Devices
--devices accepts either device names or UDIDs:
simshot shoot ... --devices iphone-17-pro-max,ipad-pro-13 simshot shoot ... --devices 67DF6727-31BC-4246-9FC0-313A22FB2A6C
Names are matched case-insensitively and hyphen/dash-insensitively. When the same name exists on several iOS runtimes, the newest runtime wins. Output subdirectories use the name you passed (or the device slug when using a UDID).
Shot config
shots.json gives full control over names, scenes, and settle times. Every field except name and scene is optional.
{
"shots": [
{ "name": "04_home.png", "scene": "home", "wait": 8 },
{ "name": "01_trace.png", "scene": "trace", "strokes": 1, "wait": 6 },
{ "name": "06_store.png", "scene": "store", "wait": 8 },
{ "name": "07_store_tip.png", "scene": "store", "scrollBottom": true, "wait": 8 }
]
}| Field | Type | Default | Meaning |
|---|---|---|---|
name |
string | — | Output filename, e.g. 04_home.png |
scene |
string | — | Scene name passed as --screenshot-scene |
strokes |
int | nil |
Passed as --screenshot-strokes |
scrollBottom |
bool | false |
Passes --screenshot-scroll-bottom |
wait |
int | 6 |
Seconds to wait after launch before capturing |
uiTesting |
bool | true |
Whether to pass --ui-testing |
The file may be either a bare array of shots or { "shots": [...] }.
Screenshot scene protocol
simshot launches your app with a fixed argument set. Your app owns the scene names and what they do — simshot only passes them through.
| Argument | Meaning |
|---|---|
--ui-testing |
Tell the app this is an automated session (skip onboarding etc.). |
--screenshot-scene <scene> |
Navigate to the named scene on launch. |
--screenshot-strokes <N> |
Perform N interactions (e.g. draw N strokes). Optional. |
--screenshot-scroll-bottom |
Scroll the scene to the bottom before capturing. Optional. |
In addition, simshot always passes -AppleLanguages (lang) and -AppleLocale locale so the app renders in the requested language.
App-side implementation (SwiftUI)
import SwiftUI struct HomeView: View { @State private var path: [Character] = [] @State private var showSettings = false var body: some View { NavigationStack(path: $path) { content .onAppear { #if DEBUG handleScreenshotScene() #endif } } .sheet(isPresented: $showSettings) { SettingsView() } } #if DEBUG /// Screenshot-capture support: interpret simshot's launch arguments. private func handleScreenshotScene() { let args = ProcessInfo.processInfo.arguments guard let index = args.firstIndex(of: "--screenshot-scene"), index + 1 < args.count else { return } let scene = args[index + 1] DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { switch scene { case "detail": if let first = characters.first { path = [first] } case "settings": showSettings = true default: break // "home" is the default screen } } } #endif }
Because the handler lives behind #if DEBUG, it never ships in a release build.
Resizing
--resize writes App Store-ready copies to <output>/<device>/<lang>/. simshot:
- Matches the raw capture against Apple's published screenshot sizes — exact dimensions first, then aspect ratio within 1%. A capture that is already an accepted size keeps its own resolution instead of being upscaled.
- Flattens any alpha channel onto white.
- Resizes with high-quality interpolation (LANCZOS-equivalent) and writes PNG.
Sizes accepted for current devices, in either orientation (verify also accepts Apple's older sizes, down to the 3.5-inch iPhone):
| Size | Device |
|---|---|
| 1320×2868 | iPhone 16 Pro Max / 17 Pro Max (6.9") |
| 1290×2796 | iPhone 14 Pro Max / 15 Plus / 16 Plus (6.7") |
| 1260×2736 | iPhone Air (6.5") |
| 1284×2778 | iPhone 12 Pro Max / 13 Pro Max |
| 1242×2688 | iPhone 11 Pro Max / XS Max (6.5") |
| 1206×2622 | iPhone 16 Pro / 17 Pro (6.3") |
| 1179×2556 | iPhone 15 / 16 / 17 (6.1") |
| 1170×2532 | iPhone 12 / 13 / 14 |
| 1125×2436 | iPhone X / XS / 11 Pro |
| 1080×2340 | iPhone 12 mini |
| 1242×2208 | iPhone 6 Plus / 8 Plus |
| 750×1334 | iPhone 8 / SE (2nd, 3rd gen) |
| 2064×2752 | iPad Pro 13-inch |
| 2048×2732 | iPad Pro 12.9-inch |
| 1668×2420 | iPad Pro 11-inch (M4, M5) |
| 1668×2388 | iPad Pro 11-inch (2018-2022) |
| 2266×1488 | iPad mini (6th gen, A17 Pro) |
| 1640×2360 | iPad Air 11-inch / iPad (10th gen, A16) |
| 1668×2224 | iPad Pro 10.5-inch / iPad (9th gen) |
| 1536×2048 | iPad 9.7-inch |
| 768×1024 | iPad mini |
A 10.2-inch iPad screen captures at 2160×1620, which App Store Connect does not accept (that display class submits at 1668×2224), so the raw capture is resized to the iPad 13-inch size rather than passed through.
If no target matches (within 1% aspect ratio), simshot warns and keeps the raw capture.
Output layout
appstore/
├── raw/
│ └── iphone-17-pro-max/
│ ├── ja/
│ │ ├── 04_home.png
│ │ └── 01_trace.png
│ └── en/
│ └── 04_home.png
└── iphone-17-pro-max/ # --resize: App Store-ready copies
└── ja/
└── 04_home.png
Reliability
- Timeout: every external command (xcodebuild, simctl, …) is killed if it exceeds
--timeout. A hung simulator can never stall a CI job. - Retries:
launch,install, andscreenshotretry on failure. - Per-device isolation: a failing device is reported and skipped without aborting the rest of the matrix.
- Clean state: the app is uninstalled before install, and
--keep-running/--no-cleanare opt-outs if you want to preserve state.
Development
swift build # build swift test # run tests swift format lint --recursive Sources Tests # code style swift run simshot shoot --help
License
来源:GitHub · github.com
