Skip to content
Launch offer: Mac Manager £9.99 first year

Shotlist guide

Capturing

What shotlist capture does step by step, the simulators it manages, change review, the manifest and run log, every flag, exit codes, and how to recover from failures.

Capturing

shotlist capture

One command builds your app, captures every cell (each scene on each slot, in each locale and appearance) and records how each compares with the last run. In the Mac app, the same run starts with Capture (⌘R) in the Matrix.

Capturing needs a subscription. See Subscription.

What you see

Progress goes to the terminal as the run goes, one line per event, and to Shotlist/Output/run.log:

[00:20:54.619] 16 cells: 2 slots × 2 locales, 4 scenes
[00:20:54.620] app app.shotlist.example.tca 1.0 (1) at …/.shotlist/DerivedData/Build/Products/Debug-iphonesimulator/TCAExample.app
[00:20:54.626] control channel on 127.0.0.1:62994; 2 simulators, 2 at a time
[00:21:28.361] iphone-6.3: Shotlist · iphone-6.3 · iOS-27-0 ready
[00:21:28.830] iphone-6.9: Shotlist · iphone-6.9 · iOS-27-0 ready
[00:21:54.594] en-GB/iphone-6.3/01-table-light unchanged
[00:21:54.621] en-GB/iphone-6.9/01-table-light unchanged
…
[00:22:23.188] done in 88 s: 16 captured (0 new, 0 changed, 16 unchanged), 0 failed

At the end, a summary:

16 captured · 0 new · 0 changed · 16 unchanged · 0 failed · 88 s
Manifest: …/Shotlist/Output/manifest.json
Log: …/Shotlist/Output/run.log

New and changed cells are listed under the summary line, and failed cells with their reasons:

1 captured · 1 new · 0 changed · 0 unchanged · 2 failed · 34 s
  new      en-GB/iphone-6.3/01-shop-light
  ✗ failed  en-GB/iphone-6.3/02-missing-fixture-light: scene 'missing-fixture' failed: No fixture for GET /v1/products/no-such-product (…); looked in Fixtures/en-GB/GET/v1/products/no-such-product and Fixtures/shared/GET/v1/products/no-such-product
  ✗ failed  en-GB/iphone-6.3/03-stuck-hold-light: scene 'stuck-hold' failed: not ready after 8 s: open hold 'a stock check that never finishes'

A cell is named <locale>/<slot>/<NN>-<scene>-<appearance>, where NN is the scene’s position in sequence.

Times in the log are UTC.

What a run does

  1. Reads shotlist.json and works out the cells, applying any --slots, --locales and --scenes.
  2. Takes the capture lock. One capture runs at a time on a Mac, because every project shares Shotlist’s simulators. A second capture stops at once: “another shotlist capture is running on this Mac; captures share simulators, so wait for it to finish”.
  3. Builds the app once: xcodebuild build for your scheme and configuration (Debug by default), for generic/platform=iOS Simulator, into .shotlist/DerivedData. --skip-build and --app skip this.
  4. Keeps the last run’s captures of the cells it’s about to capture, in Output/previous/raw/, for before-and-after review.
  5. Opens a control channel on a loopback port. Each launch of your app connects to it with a one-off token, and the engine accepts only the process it launched.
  6. Prepares one simulator per device, up to capture.concurrency at a time (2 by default). For each:
    • picks the newest installed iOS runtime that supports the device;
    • creates Shotlist · <slot> · <runtime> if it doesn’t exist, or reuses it;
    • boots it headless;
    • sets its region to en_US once, so the status bar reads 9:41 (your app’s locale comes from the run, not the region);
    • checks its framebuffer against the slot table;
    • overrides the status bar: the time, full battery, full Wi-Fi and signal, no carrier;
    • uninstalls and reinstalls your app, so the run starts with no app data.
  7. Works through each locale and appearance on that simulator, one after another:
    • sets the simulator’s appearance;
    • launches the app with the locale’s language and region, the run’s date, the slot, the scene timeout, SWIFT_DETERMINISTIC_HASHING=1 and your timeZone;
    • waits for the app’s hello;
    • shows each scene in sequence order, relaunching first for a scene with isolation: .relaunch;
    • waits for the scene to be ready (see Readiness), answering its HTTP requests from fixtures as it goes;
    • captures until two consecutive frames match;
    • checks the size is one the slot accepts and the image isn’t blank;
    • flattens it onto opaque black as sRGB with no alpha channel, and writes Output/raw/<locale>/<slot>/NN-<scene>-<appearance>.png;
    • compares it with the last run’s capture of the same cell, and records the result in the manifest.
  8. For the iPhone Duo, captures every outer-display cell folded, then unfolds once and captures every inner-display cell. See iPhone Duo.
  9. Shuts each simulator down when its cells are done, unless you pass --keep-warm.
  10. Writes the summary, and exits non-zero if any cell failed.

The manifest is written after every cell, so a run that stops early still leaves an accurate record.

Simulators

Shotlist only ever uses simulators it created, named Shotlist · <slot> · <runtime>:

Shotlist · iphone-6.9 · iOS-27-0
Shotlist · iphone-6.3 · iOS-27-0
Shotlist · ipad-13 · iOS-27-0
Shotlist · iphone-duo · iOS-27-1

Every simulator command addresses a device by its UDID, never as “booted”, so your own simulators and the apps on them are never touched, even while they’re running.

  • They’re shared across projects. One set of simulators serves every app on the Mac, which is why one capture runs at a time.
  • They’re reused. The first run creates them, which takes longer. Later runs boot the existing ones.
  • A new runtime means new simulators. When a newer runtime is installed, the next run creates simulators for it and logs that the old ones are “for an older runtime and no longer used”. It doesn’t delete them; see Cleaning up.
  • They run headless. Device Hub doesn’t need to be open, except to unfold the iPhone Duo.

Don’t quit Device Hub during a run. Quitting Device Hub shuts down every booted simulator on the Mac, yours included. Mid-run, every remaining cell on those simulators fails or has to recover.

Output

Path What it is
Shotlist/Output/raw/<locale>/<slot>/NN-<scene>-<appearance>.png The flattened capture: the slot’s exact size, opaque sRGB, no alpha.
Shotlist/Output/previous/raw/… The same cell’s file from the run before, for before and after.
Shotlist/Output/manifest.json One entry per cell: what was captured, its hashes and how it compares.
Shotlist/Output/run.log This run’s log. Each capture replaces it.
.shotlist/DerivedData/ The build.
.shotlist/capture/ Scratch frames while capturing.

Output/ isn’t committed. Nothing in it needs to be: a capture recreates it.

Change review

Every captured cell is marked against the last run:

State In the CLI In the Mac app Meaning
New new New There’s no earlier capture of this cell.
Changed changed Changed Different from the last run. The manifest gives the perceptual distance (0 to 64).
Unchanged unchanged Unchanged Byte-identical to the last run, or different only by Liquid Glass rendering noise, in which case the last run’s file is kept.
Failed failed Failed The cell couldn’t be captured. Its error says why.
Skipped counted as not applicable Not used here The scene doesn’t list this slot’s posture (iPhone Duo).

When a cell is unchanged because of rendering noise, Shotlist puts the last run’s file back in place of the new one, so the bytes don’t change, and records the number of noisy pixels as renderingNoise. See Making runs identical.

To review changes:

  • In the Mac app, the Matrix’s Needs Attention filter shows only new, changed and failed cells. Selecting one shows before and after, side by side or with a swipe divider.
  • In the terminal, the summary lists new and changed cells. Compare Output/previous/raw/<cell>.png with Output/raw/<cell>.png.

There’s nothing to accept. The next run compares against whatever this run captured.

The manifest

Shotlist/Output/manifest.json has one entry per cell:

Field Meaning
key The cell: en-GB/iphone-6.3/01-shop-light.
scene, index, slot, locale, appearance, posture What the cell is. index is its position in sequence, from 1.
rawPath The capture, relative to Output/.
width, height Its size in pixels.
sha256 The SHA-256 of the PNG file.
perceptualHash A 64-bit difference hash, in hex.
change new, changed, unchanged, failed or skipped.
perceptualDistance For a changed cell: how many of the 64 hash bits differ from last run.
renderingNoise For a cell unchanged by noise alone: how many pixels differed.
error For a failed or skipped cell: why.
buildNumber The app’s CFBundleVersion.
capturedAt When it was captured.
composedPath, composedInput, captionReview Added by compose: the composed file, a hash of everything it was made from, and its caption’s review state.

The manifest also records the bundle ID, the build number and when the run started and finished. With --slots, --locales or --scenes, entries for cells outside the filter are kept from earlier runs, so the manifest always describes the whole matrix.

Options

Option Effect
--slots <list> Only these slots, comma-separated: --slots iphone-6.3,ipad-13.
--locales <list> Only these locales.
--scenes <list> Only these scenes.
--skip-build Use the last build in .shotlist/DerivedData (or app.derivedData) instead of building again.
--app <path> Use this built .app instead of building. It must be a simulator build containing ShotlistKit.
--keep-warm Leave the simulators booted, for a faster next run.
--concurrency <n> Simulators to run at once, overriding capture.concurrency.
--json Print the summary as JSON on stdout; progress goes to stderr as JSON lines. See CI and automation.
--project-dir <folder> The folder that holds Shotlist/, if it isn’t the current one.

Each filter must name values shotlist.json lists, or the run stops: “–slots names ipad-11, which shotlist.json doesn’t list”. If the filters together match nothing, it stops with “the filters match no cells”.

Typical runs

# Everything.
shotlist capture

# One scene you're working on, on one size, reusing the build.
shotlist capture --skip-build --slots iphone-6.3 --scenes library

# German only, after fixing a translation.
shotlist capture --locales de-DE

# Iterating quickly: keep simulators booted between runs.
shotlist capture --skip-build --keep-warm --slots iphone-6.3

--skip-build only helps when the app hasn’t changed. After changing your scenes or app code, build again.

Exit codes

Code Meaning
0 Every cell was captured or skipped.
1 The run finished, but at least one cell failed; or shotlist.json isn’t valid; or there’s no subscription.
2 The run couldn’t proceed: the build failed, another capture holds the lock, the filters matched nothing, or the built app couldn’t be found.
64 The command line was wrong: an unknown option, say.

Recovering from failures

A failure stays with its cell. The rest of the run carries on, and the summary lists every failure with its reason.

A cell failed

Read its reason (in the summary, the run log, the manifest’s error, or by selecting it in the Mac app). The reason names the cause: an open hold, a pending URL, a missing fixture, a scene id the app doesn’t have. See Troubleshooting.

Fix it, then capture just the gap:

shotlist capture --locales de-DE --slots iphone-6.3 --scenes orders

Entries for every other cell are kept, so the manifest stays complete.

A simulator quit or crashed mid-run

The cell it was on fails. Shotlist notices the simulator has gone, boots it again, reinstalls the app and carries on with the rest of its cells. The run exits 1. Capture the failed cells again with filters.

If a simulator can’t be prepared at all (no runtime, a framebuffer that doesn’t match, a boot that never finishes), every cell on it fails with that reason, and the other simulators carry on.

The app crashed

The cell fails with “the app (pid …) closed the control channel while waiting for ready from scene ‘…’; it may have crashed”. Shotlist launches the app again for the next cell. Run the scene from Xcode, or look in the simulator’s crash logs, to find the crash.

The build failed

The run stops with “the build failed:”, the exact xcodebuild command, and up to 20 lines containing error:. Run that command yourself to see the full output.

Another capture is running

Wait for it to finish. The lock is released when the other run ends, however it ends. If you’re sure nothing is running and it still says so, a shotlist process is still alive: find it with pgrep -fl shotlist.

Stopping a run

Press Control-C, or Stop (⌘.) in the Mac app. Stopping terminates the commands in progress. The manifest records every cell captured so far. Simulators may be left booted; the next run reuses them.

How long a run takes

Rough figures from real projects on an Apple silicon Mac:

  • 16 cells (2 slots × 2 locales × 4 scenes), reusing a build and booted simulators: about 90 seconds.
  • 192 cells (3 slots × 8 locales × 8 scenes): about 6 minutes.

The first run is slower: it creates the simulators (boots of new simulators can take 30 seconds) and does a full build. Captures are slowest just after a simulator boots. Relaunching for isolation: .relaunch scenes adds a few seconds each.

Cleaning up

  • Disk: each simulator takes 2 to 6 GB, and each project’s build about 1 GB in .shotlist/. Delete .shotlist/ to reclaim the build; the next capture rebuilds.
  • Old simulators: after a runtime update, simulators for the old runtime stay. List Shotlist’s with xcrun simctl list devices | grep "Shotlist ·" and remove one with xcrun simctl delete <udid>. Delete only simulators named Shotlist · ….
  • Previous captures: Output/previous/ holds one earlier copy of each cell. It’s replaced as cells are recaptured.