storescreens
storescreens-cli runs XCUITest-based UI tests across simulators, extracts named XCTAttachment screenshots from the .xcresult bundle, and organizes them by device size. It also renders the raw captures into captioned, framed App Store Connect-ready screenshots (backgrounds, device bezels, markdown captions, per-slide highlights).
Follow the steps below in order. Each step checks state before acting - skip steps that are already complete.
The full workflow is:
- Install CLI + MCP server (Steps 1-1b)
- Detect project and generate
storescreens.yml(Steps 2-3) - Set up UI test target +
ScreenshotTests.swift(Steps 4-6) - Verify the build (Step 7)
- Capture raw screenshots (Step 8)
- (optional) Render captioned, framed App Store-ready screenshots (Step 9)
- (optional) Upload screenshots + metadata to App Store Connect (Step 10)
- (optional) Archive + upload the app binary to App Store Connect / TestFlight (Step 10b)
- (optional) Upload to storescreens.app for visual editing (Step 11)
Step 1: Check / Install storescreens-cli
Run:
storescreens --help 2>&1
Print the full output - it shows the available subcommands and options so the user knows what's available. If the command is not found, install it:
brew tap ciscoriordan/tap && brew install storescreens
# or, with mise (prebuilt, no Swift toolchain - installs storescreens + storescreens-mcp):
mise use -g github:ciscoriordan/storescreens-cli
# or, the install script (CLI only):
curl -fsSL https://raw.githubusercontent.com/ciscoriordan/storescreens-cli/main/install.sh | sh
Prefer Homebrew or mise for an automated install - both are prebuilt and need no toolchain. storescreens is also installable with Mint (mint install ciscoriordan/storescreens-cli storescreens), but Mint builds from source and needs a Swift toolchain, so reach for it only if the user asks.
Step 1b: MCP Server Setup (REQUIRED - do this before anything else)
The storescreens MCP server is the preferred way to run captures. It gives you structured tools (capture, get_capture_status, list_screenshots, get_screenshot, etc.) instead of parsing CLI output in Bash. Always set this up.
Check if already connected: Look at your available MCP tools. If you see capture, get_capture_status, list_screenshots, etc. from a storescreens MCP server, it's already working - skip to Step 2.
If MCP is NOT connected, you MUST set it up now:
Check if
.mcp.jsonexists in the project root. If it does, read it.If
.mcp.jsondoesn't exist, create it:
{
"mcpServers": {
"storescreens": {
"command": "storescreens-mcp",
"args": []
}
}
}
If
.mcp.jsonalready exists but doesn't have astorescreensentry, add thestorescreensserver to the existingmcpServersobject. Do not overwrite other MCP servers.After creating or updating
.mcp.json, STOP and tell the user:
Action required: Please exit and relaunch Claude Code from this project directory.
I just created/updated
.mcp.jsonto add the storescreens MCP server. Claude Code only reads project-level.mcp.jsonfiles on startup - running/mcpinside the current session won't pick it up.After relaunching, run
/storescreensagain and I'll continue from where we left off.
Do not continue with further steps until MCP is confirmed working. The MCP server provides capture, get_capture_status, check, list_screenshots, get_screenshot, take_screenshot, read_config, write_config, and other tools that make the entire workflow dramatically better. Without it, you're stuck parsing raw xcodebuild output in Bash.
Step 2: Detect project
Find the .xcworkspace or .xcodeproj in the working directory. Identify the scheme name (usually matches the app name). You'll need both for storescreens.yml and the xcodebuild verify command later.
Also check for XcodeGen: if project.yml exists at the repo root, this project is XcodeGen-managed. The .xcodeproj is a generated artifact, so any target changes must go in project.yml and then xcodegen generate regenerates the project. Note this state (keep a note or variable like usesXcodeGen = true) and branch accordingly in Steps 4 and 7.
Step 3: Check for existing storescreens.yml
If storescreens.yml already exists, skip storescreens init and proceed directly to Step 4. Do not re-run init or ask config questions unless the user explicitly asks to change devices, locales, or appearances.
Only run storescreens init when:
storescreens.ymldoes not exist yet (fresh project), or- the user explicitly asks to reconfigure or reinitialise
When you do run init:
storescreens init
Print the full output. If .mcp.json was created or updated, tell the user: exit and relaunch Claude Code from this project directory to pick up the new MCP config. Running /mcp inside the current session is not enough - project-level .mcp.json files are only read on startup.
After running storescreens init, read storescreens.yml and review the config with the user. Show the current values and ask:
- Devices - Are these the right simulators? (Show the current list)
- Appearances - Currently set to
[current value]. Light only, or both light and dark? - Locales - Currently
[current value or "not set"]. Do you need screenshots in multiple languages?
Make any requested changes by editing storescreens.yml directly, then continue to Step 4.
If storescreens.yml does not exist yet (fresh project), ask the user these questions before running storescreens init:
iPhone devices - Present these options and ask the user to choose:
- 6.9" only (recommended) -
iPhone 18 Pro Max. iOS 27 runtimes (Xcode 27 or later) create it; iOS 26 runtimes createiPhone 17 Pro Maxinstead, which has the same screen and still works from Xcode 27 when an iOS 26 runtime is installed. Checkstorescreens listfor which one is installed. App Store Connect auto-fills the 6.5" slot from 6.9" screenshots, so this single device covers both. This is all most apps need. - 6.9" + 6.5" - Add
iPhone 11 Pro MaxoriPhone Xs Maxif they want distinct screenshots in the 6.5" slot rather than the auto-scaled 6.9" ones. Note: no current model produces a 6.5" size (1242×2688 or 1284×2778) - only older simulators do. - More sizes - App Store Connect also has slots for 6.3", 6.1", 5.5", 4.7", 4", and 3.5". Ask if they want any of these. Corresponding simulators:
iPhone 18 Pro(6.3";iPhone 17 Proon iOS 26 runtimes),iPhone 17e(6.1"),iPhone 8 Plus(5.5"),iPhone SE (3rd generation)(4.7"). Sizes smaller than 4.7" are very old and rarely needed. - Skip iPhone for now - valid choice; they can add iPhone later.
Note: App Store Connect has no 6.7" slot. Its
APP_IPHONE_67display type is the 6.9" slot, andiPhone 16 Plus(1290x2796) screenshots land there too, so do not suggest it next to the Pro Max. Likewise do not suggestiPhone Airas an extra size: its 1260x2736 screenshots also go to the 6.9" slot (storescreens still labels its filesiPhone 6.3"). Two devices in one slot meanssubmituploads only one device's screenshots there (the one with the largest screen). Do not putiPhone Duoin the main config; it needs its own config (see Step 3c).- 6.9" only (recommended) -
Appearances - Light mode only, or also dark mode?
Once you have the iPhone answers, write storescreens.yml with the iPhone devices. See references/config-reference.md for the full schema. The test_target and test_class should be <AppName>UITests and ScreenshotTests - you'll confirm these in Step 4.
IMPORTANT: Always include derived_data_path in the config. Without it, every capture run recompiles everything from scratch (including all SPM packages), which can add 3-5+ minutes to each run. Set it to ~/.storescreens-cache/<AppName>:
derived_data_path: ~/.storescreens-cache/<AppName>
If storescreens.yml already exists but is missing derived_data_path, add it now.
Step 3b: iPad devices (separate question)
Ask the user separately whether they want iPad screenshots. Don't push it - it's fine to skip and add later.
App Store Connect iPad slots:
- 13" (required when iPad is supported) →
iPad Pro 13-inch (M5)- recommend this as the starting point - 11" →
iPad Pro 11-inch (M5) - 12.9", 10.5", 9.7" - legacy slots requiring older simulator runtimes; rarely needed
If they want iPad, add the chosen simulators to devices in storescreens.yml. Also note: if their app has iPad support disabled in Xcode (Supported Destinations), they'll need to re-enable it there first (General tab → Supported Destinations → add iPad).
Step 3c: iPhone Duo (optional, only if the user asks)
The iPhone Duo is a foldable with an outer display (1398x2034, folded) and an inner display (2007x2853, open). Only set it up when the user asks for it:
Its simulator needs Xcode 27.1 beta or later plus the iOS 27.1 simulator runtime, which creates the
iPhone Duosimulator. If they aren't installed, the user can runxcodes install 27.1 Beta --experimental-unxipin Terminal (it asks for their Apple ID and 2FA code, which you can't enter for them), then you can fetch the runtime withDEVELOPER_DIR=/Applications/Xcode-27.1.0-Beta.app/Contents/Developer xcodebuild -downloadPlatform iOS(no sign-in). storescreens uses whichever Xcodexcode-selectorDEVELOPER_DIRselects, so keep the main config on the release Xcode and write a second config, e.g.storescreens-duo.yml, that copiesstorescreens.ymlbut lists only- simulator: "iPhone Duo"and sets its ownoutput_dir(andrender.output_dir, if rendering), because a successful capture replaces the previous output in its directory. Also remove thesearch_previewblock from the copy: search previews never use Duo screenshots, so a Duo-only run would overwrite the main previews with empty tiles. Capture it with--no-search-preview, which guards against that even if the block is left in:DEVELOPER_DIR=/Applications/Xcode-27.1.0-Beta.app/Contents/Developer storescreens capture --config storescreens-duo.yml --no-search-previewThe first launch of the Duo simulator can take several minutes (a known issue in Apple's beta). Don't treat a slow first boot as a hang.
The pose (folded or open) can't be set from the command line or from a UI test; Apple exposes it only in Xcode's Device Hub. The simulator boots folded, so captures come out as outer-display screenshots (1398x2034) unless the user opens it in Device Hub. Screenshots come from whichever display is active, and storescreens labels them
iPhone Duo outeroriPhone Duo innerby pixel size (filesiPhone_Duo_outer_<name>.png/iPhone_Duo_inner_<name>.png).Tell the user App Store Connect doesn't accept iPhone Duo screenshots yet (Apple says later this year):
submitskips them with a notice. The captures are still useful for checking the layout on both displays.
Step 4: Check for a UI test target
Look for an existing UI test target by searching for *UITests directories or .swift files containing XCTestCase in a test target folder.
If a UI test target already exists:
Check whether ScreenshotTests.swift exists inside it. If it does, skip to Step 6. If it doesn't, proceed to Step 5 to write the test file.
If no UI test target exists and the project is XcodeGen-managed (project.yml exists):
Don't ask the user to create the target in Xcode. Editing the generated .xcodeproj directly is pointless because xcodegen generate wipes those changes. Add the target to project.yml yourself.
Append to the targets: block:
<AppName>UITests:
type: bundle.ui-testing
platform: iOS
deploymentTarget: "<same as the app target>"
sources:
- path: <AppName>UITests
dependencies:
- target: <AppName>
settings:
PRODUCT_BUNDLE_IDENTIFIER: <app bundle id>.uitests
GENERATE_INFOPLIST_FILE: YES
Also add the test target to the app scheme's test: block so xcodebuild test can discover it:
schemes:
<AppName>:
build:
targets:
<AppName>: all
test:
targets:
- <AppName>UITests
Then run xcodegen generate to regenerate the .xcodeproj. After that, proceed to Step 5.
If no UI test target exists and the project is not XcodeGen-managed:
Tell the user they need to create one manually in Xcode (this cannot be done from code):
Please add a UI test target in Xcode:
- File → New → Target → UI Testing Bundle
- Name it
<AppName>UITests- Set "Target to be Tested" to your app
- Click Finish
Xcode will generate a placeholder
<AppName>UITestsLaunchTests.swift- delete that file. Then come back and I'll writeScreenshotTests.swiftfor you.
Wait for the user to confirm before continuing.
Step 4b: Preflight check (run by the setup wizard)
The storescreens setup wizard automatically runs storescreens check before generating the test file. If it finds issues:
- Errors (e.g. unguarded CloudKit, missing
.toolbarVisibilityiPad guard): the wizard asksContinue setup anyway? [y/N]. Default is No - fix errors first, then re-runstorescreens setup. - Warnings (e.g. missing accessibility identifiers, unguarded review prompts): the wizard asks
Continue setup with warnings? [Y/n]. Default is Yes - warnings are informational and don't block setup, but should be addressed before capture.
The missing-accessibility-identifier warning fires when a UI test file references an identifier (e.g. waitForElement(id: "saveButton") or app.buttons["saveButton"]) that has no corresponding .accessibilityIdentifier("saveButton") in the app source. Help the user add the missing identifier to their view before continuing.
The localized-nav-button warning fires when a test uses navigationBars.buttons["Back"] (or any title-cased label) to find a navigation bar button. Localized titles change per locale - "Back" becomes "Atrás" in Spanish, "Zurück" in German, etc. - causing test failures when running multi-locale screenshot capture. Fix: use app.navigationBars.buttons.element(boundBy: 0) for the system back button (it is always the first/leftmost button), or add an .accessibilityIdentifier to a custom button and query by that instead.
When reporting findings, handle each warning/error category separately - present one category, ask if the user wants to fix it now, fix it if yes, then move to the next category. Do not bundle multiple categories into a single question.
Step 5: Check / configure screenshot mode in the app
Before writing the test file, check whether the app already handles a screenshot mode launch argument (search for --uitesting or --uitesting in the Swift source).
If it does not, explain what needs to be added and help the user add it to the appropriate place (app init, @main struct, or root view .task):
if ProcessInfo.processInfo.arguments.contains("--uitesting") {
// Force premium/subscriber access - skip StoreKit verification
// subscriptionService.isSubscribed = true
// Disable animations for fast, deterministic captures
UIView.setAnimationsEnabled(false)
// Reset persisted UI state if needed
// UserDefaults.standard.removeObject(forKey: "onboardingDone")
}
Also guard any checkEntitlements() or StoreKit calls so they don't run in screenshot mode. Wrap the specific call - do not add a guard...return at the top of the function. An early return silently skips everything below it, including state setup like isInitialized = true, which can leave the app in a broken state during tests.
// Good - wraps just the entitlement call:
if !ProcessInfo.processInfo.arguments.contains("--uitesting") {
checkEntitlements()
}
// Bad - skips ALL code below this line, including isInitialized = true:
// guard !ProcessInfo.processInfo.arguments.contains("--uitesting") else { return }
Confirm with the user which (if any) subscription/entitlement service needs to be bypassed.
Step 6: Write ScreenshotTests.swift
Ask the user to describe the screens they want to capture and the navigation flow between them. Use their description to write a tailored test file.
Key requirements for the test file:
app.launchArguments = ["--uitesting"]- Wait for the app to fully load before the first screenshot:
XCTAssertTrue(element.waitForExistence(timeout: 20)) - Name screenshots with their meaningful identifier only:
"Home","Detail","MealPlan". No numeric prefixes. The capture pipeline stamps each output PNG's mtime and creationDate in the order of thescreenshots:list instorescreens.yml, sols -tand Finder's "Date Created" sort match the configured display order without needing01_/02_prefixes. - Use accessibility identifiers for all element lookups - ask the user what identifiers exist or help them add them. Avoid fragile text/position-based queries.
- Always
waitForExistence(timeout:)before tapping any element - Clean up any state changed during the test (e.g. delete test permits/data added during the flow) at the end
After you decide the screens, write the same list under top-level screenshots: in storescreens.yml. That list is the single source of truth for display order: it drives capture filtering, HTML preview ordering, render order, and mtime stamping.
Use assets/ScreenshotTests.swift.template as a starting point. Place the file inside the <AppName>UITests/ folder.
For XcodeGen-managed projects (project.yml exists): the file goes in <AppName>UITests/ matching the sources: path in project.yml. No "Add Files to target" step is needed because XcodeGen picks up every .swift file under that directory on the next xcodegen generate. Run xcodegen generate after writing the file, then continue to Step 7.
For projects without XcodeGen, tell the user:
Add
ScreenshotTests.swiftto the Xcode target: Right-click the<AppName>UITestsgroup → Add Files to "[project]" → selectScreenshotTests.swift→ confirm the target membership checkbox is checked.
Step 7: Verify the build
If the project is XcodeGen-managed, run xcodegen generate first so the .xcodeproj reflects the new test target and any ScreenshotTests.swift additions. Any time you edit project.yml or add/remove test files, regenerate before invoking xcodebuild.
Pipe xcodebuild output to a log file so you can inspect errors:
xcodebuild build-for-testing \
-workspace MyApp.xcworkspace \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 18 Pro' \
2>&1 | tee build.log
Use a simulator name from the config, and check storescreens list for the names installed: iOS 27 runtimes create iPhone 18 Pro, iOS 26 runtimes iPhone 17 Pro. The Xcode version alone doesn't decide it, since Xcode 27 can still use an installed iOS 26 runtime.
If this fails, check build.log for the full error output and fix before running capture.
Step 8: Capture
Targeted screenshot (quick visual check)
When you need to verify a UI change visually, do NOT run the full screenshot suite. Use the take_screenshot MCP tool instead. It captures the current simulator screen in under a second - no build, no tests.
When to use take_screenshot:
- You edited a SwiftUI view and want to see the result
- You want to confirm a layout change before committing
- The simulator is already running your app
When to use full capture instead:
- You need final App Store screenshots
- You need multiple devices or locale variants
- The app is not running and you need it built from source
Using the MCP tool:
If the simulator is already running your app:
take_screenshot(simulator: "iPhone 18 Pro")
The image renders inline immediately. If no simulator name is given, it uses the first booted simulator.
If the simulator is not booted:
take_screenshot(simulator: "iPhone 18 Pro", boot: true)
Using the CLI (if MCP is not available):
storescreens screenshot --simulator "iPhone 18 Pro" --output screenshot.png
# Boot variant:
storescreens screenshot --simulator "iPhone 18 Pro" --boot --output screenshot.png
If you need to navigate to a specific screen that requires UI test interaction, the old approach still works: temporarily disable the main test, write a focused testQuickVisual() method, run capture, then clean up. But for a simple "what does the current screen look like" check, take_screenshot is much faster.
Choosing the right screenshot tool
If Xcode 26.3+ is running and its MCP server is available (you'll have tools like RenderPreview, BuildProject, etc.), you have three options for visual checks:
| Tool | What it captures | When to use |
|---|---|---|
Xcode RenderPreview |
A single SwiftUI #Preview |
Checking an individual view's layout. No simulator needed. |
storescreens take_screenshot |
The full running app in a simulator | Checking the app with real data, navigation state, and system chrome. |
storescreens capture |
Full App Store screenshots across devices | Final screenshots for App Store Connect. Multiple devices, locales, appearances. |
Use RenderPreview when you want to check a single view in isolation (it renders SwiftUI previews, not the running app). Use take_screenshot when you need to see the actual app running with real state. Use capture for the final App Store screenshot suite.
Full capture
Use the MCP capture tool. If the storescreens MCP server is not connected, STOP - go back to Step 1b and set it up. Do not proceed with Bash capture unless the user explicitly says they don't want MCP. The MCP tools provide structured progress, inline screenshot previews, and eliminate the need to parse raw xcodebuild output.
Check whether the storescreens MCP server is connected (you'll have tools like capture, get_capture_status, list_screenshots available). If it is, call the capture tool. Only fall back to Bash if the user explicitly declines MCP setup.
If using MCP:
- Call the
capturetool - it returns ataskIdimmediately and starts capture in the background. - Poll interval: follow what
get_capture_statustells you. Each response ends with either "Wait 30 seconds" (build phase) or "Wait 5 seconds" (test phase). Always use the interval the server specifies - never switch on your own based on which devices have finished or what you see in the output.
Then callsleep 30 # when server says "Wait 30 seconds" sleep 5 # when server says "Wait 5 seconds"get_capture_status(task_id: "<taskId>"). Repeat untilstatus: completedorstatus: failed. Never callget_capture_statusback-to-back without the sleep in between. - What to show the user while polling:
- After each poll, output any new
✓ [Device] [Slot] screenshot_namelines that weren't in the previous poll response. This is the primary progress signal - output them as-is so the user can see screenshots being captured in real time. - Also surface these meaningful transitions when they first appear:
Testing started,** TEST SUCCEEDED **,** BUILD FAILED **, locale changes (● Locale: ...), and device completion lines (✓ DeviceName: N screenshots). - Do not repeat lines that were already shown in a previous poll. Track what you've output and only emit new lines.
- "last activity Xs ago" does NOT mean the run is stuck. xcodebuild writes build output to stdout (which the MCP tracks), but once the build phase completes and tests start running, xcodebuild writes results directly to the
.xcresultbundle on disk - no stdout output is produced during test execution. A "last activity 300s ago" status is normal and expected during the test phase. Keep polling - do not report a stall to the user unless the run exceeds 20 minutes total. If the run HAS exceeded 20 minutes and all device tests have already passed (you sawTest case '...' passedfor every device in the status output), the MCP server is stuck in a post-test deadlock. Tell the user: "The capture process is stuck. Please runpkill -f storescreens-mcpin a terminal, then exit and relaunch Claude Code so the MCP server restarts, then re-run capture." - Detect the stuck-buffer problem. If the output is identical across any 2 consecutive polls and
last activityhas not advanced, immediately calllist_screenshotsAND check the file timestamps (vials -laon the output directory) to verify the screenshots are from the current run - not a previous one. Compare the file modification times against when the current capture started. Only report screenshots as complete if they are newer than the capture start time. If timestamps are stale, the run is still in progress - keep polling. - "0 screenshots" for a completed device is a red flag. If a poll response shows a device reporting "Tests completed" or "0 screenshots", treat this as a likely failure and immediately inspect the per-device log. Do NOT continue polling silently. Read the log:
If errors are found, report them to the user immediately.cat <output_dir>/logs/test-<UDID-prefix>.log | grep -E "error:|FAILED|fatalError|Could not resolve" - Device failures in logs during polling:
get_capture_statuswill include a "Device failures detected in logs" section if it finds error patterns in any per-device log file. When this appears, immediately report the failure to the user - do not wait for the run to complete. If some devices have failed and others are still running, tell the user: "Device X failed (see error below). Other devices are still running. Do you want to abort or wait for them to finish?" Then wait for the user's answer before taking action.
- After each poll, output any new
- On completion, report results: how many screenshots per device. Output a clickable preview link using the absolute path:
file://<absolute-path-to-project>/<output_dir>/preview.html. On failure, read the log.
If using Bash (MCP unavailable):
storescreens capture
The CLI streams full xcodebuild output to stdout (auto-detected in non-TTY mode). The command output will be large (compile steps + test output).
IMPORTANT - after the command finishes, ALWAYS print these two messages as your own text output (NOT inside a bash block):
- Tell the user they can expand the output block above to see the full xcodebuild build and test log.
- Show the tail command for the log files using the absolute path to the project's output dir:
To follow logs in real time on future runs, open another terminal:
tail -f <absolute-path-to-project>/<output_dir>/logs/test-*.log
Then report the results: how many screenshots captured, whether tests passed/failed. Output a clickable preview link using the absolute path: file://<absolute-path-to-project>/<output_dir>/preview.html.
If tests failed, read the log file to diagnose:
cat <absolute-path-to-project>/<output_dir>/logs/test-*.log
Output lands in storescreens-output/ with device name as a filename prefix. Light and dark mode get separate directories:
storescreens-output/
├── preview.html ← open: file://<absolute-path-to-project>/<output_dir>/preview.html
├── manifest.json
├── logs/
│ └── test-<device>.log ← one per device
├── light/
│ ├── iPhone_6.9_Home.png
│ ├── iPhone_6.9_Detail.png
│ ├── iPhone_6.3_Home.png
│ └── ...
└── dark/
├── iPhone_6.9_Home.png
├── iPhone_6.9_Detail.png
└── ...
Screenshot names are the meaningful identifier only (Home, Detail, Search). The capture pipeline stamps each output PNG's mtime + creationDate in the order of the top-level screenshots: list in storescreens.yml, so ls -t storescreens-output/light/ and Finder's "Date Created" sort match the configured App Store display order without numeric prefixes.
Step 9: Render captioned, framed screenshots (optional)
Raw captures from Step 8 are functional but plain - just the app UI with no marketing chrome. The render pipeline turns them into framed, captioned images ready to upload to App Store Connect: background, device bezel, images (logos, badges), laurel award overlays, marketing captions with markdown, per-slide highlights.
When to offer this: any time the user wants App Store Connect-ready screenshots with captions or device frames. The render is opt-in (off by default) but most users want it before shipping.
Rendering runs automatically after storescreens capture when render.enabled: true is in storescreens.yml. It can also be invoked standalone with storescreens render, which skips recapture and is the right loop for iterating on caption text, colors, fonts, etc. Pass --no-render to storescreens capture to skip rendering on one run.
9a. Design the narrative first
Before touching YAML, ask the user:
- What's the hero story? Typically the first 2-3 slides in App Store Connect do 90% of the conversion work. What's the single most important thing to communicate? Often this becomes slide 1's title.
- What's the slide order? Get a numbered list. This list goes verbatim into the top-level
screenshots:key instorescreens.yml- the render pipeline walks it in this exact order (no alphabetical reordering anywhere). - Which frame look?
chrome.style: bezelis the polished App Store look: Apple's real bezel artwork once the DMGs are imported, and until then it automatically falls back todevice, a procedurally drawn generic frame (band, bezel ring, Dynamic Island/notch, buttons) that needs zero external assets.chrome.style: deviceuses the drawn frame permanently;strokeis a minimal rounded-rect outline. All of these work out of the box - only real Apple artwork requires the bezel install below. - Light, dark, or both? Most render fields (
background.image,background.color,images[].path,laurels[].color) accept{ light:, dark: }variants. - Any brand fonts? Four tiers available:
system(SF Pro), installed family name, local.otf/.ttfpath,{ google: "Inter" }(auto-downloaded), or{ regular:, bold:, italic:, bold_italic: }bundle for correct markdown bold/italic.
9b. Write the screenshots list
Add the authoritative order at the top level of storescreens.yml:
screenshots:
- Home
- Search
- Detail
# ...
This list is the single source of truth for display order. It drives capture filtering, render order, HTML preview order, and mtime stamping on the output PNGs (first in list = most recent mtime, so ls -t and Finder's "Date Created" both match). A panoramic background's left edge pins to the first entry here. An image with placement: first_only (also the default for above_title) draws on the first entry here.
9c. Install bezels (optional - upgrades bezel chrome to Apple's real artwork)
Without this install, chrome.style: bezel still renders fine using the drawn device frame (a warning in the render output says which slides used the fallback). Install the real bezels when the user wants Apple's own artwork.
Apple licenses the bezel PSDs for use with their products. StoreScreens does not redistribute them - the user downloads once, then the importer extracts what it needs.
- Open https://developer.apple.com/design/resources/ in a browser. Scroll to "Product Bezels". Download the DMG for each device family needed (iPhone, iPad, MacBook).
- Double-click each downloaded DMG to mount it. They appear under
/Volumes/. - Run:
storescreens bezels import
This auto-scans /Volumes/ for Apple Design Resource DMGs, classifies PSDs by screen pixel dimensions, and writes one transparent-screen PNG + JSON sidecar per screen size and orientation to ~/Library/Application Support/storescreens/bezels/ (user-global, shared by every project). When several PSDs fit one size, it ranks them with fixed defaults: a file name that states the orientation first, then model (Pro Max, Pro, Air, mini, any other), then the newer iPhone generation, then colorway (Space Black, Black, Night Sky, Natural Titanium, Silver, Space Gray, Deep Blue, any other), then file name. It does not read render.chrome.model_preference / colorway_preference. For a different finish, copy that PSD into an empty folder and import with --volume <folder>; only the sizes it covers are replaced.
- The iPhone 18 DMG (18 Pro and Pro Max; Black, Silver, Glacier, Burgundy) imports like the iPhone 17 one. The screens are the same size, so when both DMGs are mounted the iPhone 18 artwork wins.
- The iPhone Duo DMG imports
Inner Open Portrait/Landscape(inner display) andOuter Closed Portrait/Landscape(outer display). ItsOuter Openfile is ignored; the outer display usesOuter Closed Portrait. Colorways are Night Sky and Star White.
Flags:
--volume PATH- scan this one path (a mounted DMG or any folder of PSDs) instead of auto-scanning/Volumes/.--yes- skip the "about to write N files, ok?" confirmation.
Inspect afterwards:
storescreens bezels check # list installed bezels + canonical keys
storescreens bezels path # print the install directory
Per-project override: drop PNG + JSON sidecar files into ./bezels/ next to storescreens.yml. Project-local bezels take precedence over user-global ones.
9d. Add a render: block - build it up incrementally
Fast path - start from a named template. If the user wants a sensible default look instead of hand-tuning every field, pick a built-in template. List them:
storescreens templates
Then seed the config:
render:
enabled: true
template: sahara # or: ascent, all_the_wiser, ethereal, midnight, pinecrest, blueprint, sunset_blvd
output_dir: ./storescreens-framed
The template fills in background, caption, and chrome defaults (curated palette + typography + a procedural background pattern where appropriate). Anything the user writes explicitly in the same render: block still wins over the template - treat the template as "defaults I can override," not a lock-in. The sections below still apply when the user wants to tune individual fields.
Once picked, continue with per-slide captions (Step 9d's "slides:" block below). Most users won't need the background / scrim / chrome sections when using a template.
Fast path - derive colors from the app itself. When the user wants the store artwork to match their app's branding rather than a preset, run:
storescreens themes suggest # analyzes the last capture; add --json for structured output
It returns up to three themes derived from the screenshots' dominant background and most vivid accent color (App Match, Brand Gradient, Soft Tint), each as a ready-to-paste render: snippet: background.color (solid or gradient), caption.title.color, and chrome.device_colorway, with legible contrast built in. The same analysis is available as the suggest_themes MCP tool. Present the options to the user, and always offer custom colors as an alternative. Apps with no saturated accent (grayscale UIs) get only App Match.
Don't dump a huge render block on the user and hope it renders well. Add fields one at a time, run storescreens render after each, and open preview.html to inspect.
Start with the minimum:
render:
enabled: true
output_dir: ./storescreens-framed
chrome:
style: bezel # drawn device frame out of the box; real Apple bezels once imported
Add a background (solid color or image, with optional light/dark variants):
background:
color: "#1a1a2e"
# or a vertical gradient, top → bottom:
# color: ["#1a1a2e", "#4a1e5c"]
# or an image (single wide image = panoramic, sliced across all slides):
# image: ./marketing/panorama.jpg
# fit: cover # cover | contain | tile
# align: center # top | center | bottom
# with appearance variants:
# image:
# light: ./bg-light.png
# dark: ./bg-dark.png
Panoramic background: if background.image is a single image wider than one slide, the renderer slices it across all slides in screenshots: order. Left edge of image pins to left edge of the first slide. Slides concatenate side-by-side in the App Store Connect gallery with a continuous image behind them.
Add a scrim to tame a busy background image:
scrim:
color: "#000000"
opacity: 0.35
# or a vertical opacity gradient instead of flat opacity:
# gradient:
# top_opacity: 0.0
# bottom_opacity: 0.6
Add image overlays (logos, badges - up to 2 per slide, dropped near the caption block):
images:
- path: ./marketing/logo-wordmark.svg
position: above_title # above_title | below_title | above_subtitle | below_subtitle
align: center # left | center (default) | right
max_height_pct: 6 # % of canvas height; default 8
placement: first_only # first_only | all | none
position defaults to above_title (matches the legacy logo placement). placement defaults to first_only for above_title and all for every other slot. Two entries in the same slot stack horizontally if they share an align, or place independently if they don't.
Optionally add laurel "award badge" overlays - left/right laurel SVGs around centered title/subtitle text:
laurels:
- title: "Editors' Choice"
subtitle: "App Store"
color: "#FFD66B"
position: below_subtitle
max_height_pct: 11
title is bold by default and subtitle is regular; override with title_style: / subtitle_style: which accept the same fields as caption.title. Up to 2 per slide, same slot rules as images.
Backwards compatibility: the legacy logo: block (single path + placement) still renders, treated as a single image at above_title. New configs should prefer images:. Setting images: [] explicitly suppresses the legacy fallback.
Add captions. Each role (title, subtitle) is optional. min_font_size_pct lets the renderer auto-shrink a long title to fit before it wraps:
caption:
title:
font: system # or "Helvetica Neue" / path / bundle / google
weight: bold # thin|light|regular|medium|semibold|bold|heavy
italic: false
font_size_pct: 5.5
min_font_size_pct: 3.0
color: "#ffffff"
align: center # left | center | right
subtitle:
font: system
weight: regular
font_size_pct: 3.2
min_font_size_pct: 2.0
color: "#ffffff"
align: center
spacing_pct: 1.0
min_height_pct: 22 # reserved area at top of canvas for captions
padding_pct: 5
Then fill in per-slide caption text:
slides:
"Home":
caption: "Your recipes, organized." # shorthand: title only
"Search":
caption: # array: strict line breaks
- "Find anything"
- "in *seconds*."
"Detail":
caption: # full object
title: "Every **detail**, at a glance."
subtitle: "Powered by AI"
highlights:
- { match: "detail", color: "#feb909", weight: heavy, italic: true }
Caption shorthand:
| Form | Meaning |
|---|---|
caption: "text" |
title only, auto-wraps at canvas width |
…(truncated)