<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: kichiemon</title>
    <description>The latest articles on DEV Community by kichiemon (@kichiemondev).</description>
    <link>https://dev.to/kichiemondev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4176080%2Fdf5ef95b-c797-4872-abf3-9c0964437637.png</url>
      <title>DEV Community: kichiemon</title>
      <link>https://dev.to/kichiemondev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kichiemondev"/>
    <language>en</language>
    <item>
      <title>App Store screenshots from simctl, without an XCUITest target</title>
      <dc:creator>kichiemon</dc:creator>
      <pubDate>Sun, 11 Oct 2026 04:23:27 +0000</pubDate>
      <link>https://dev.to/kichiemondev/app-store-screenshots-from-simctl-without-an-xcuitest-target-322d</link>
      <guid>https://dev.to/kichiemondev/app-store-screenshots-from-simctl-without-an-xcuitest-target-322d</guid>
      <description>&lt;p&gt;Every release ends the same way: two device classes, two languages, six screens each. Screenshots take longer than they should, and they are the step that keeps breaking.&lt;/p&gt;

&lt;p&gt;I used &lt;code&gt;fastlane snapshot&lt;/code&gt; for a while. It works, but it pulls a UI test target, Ruby, fastlane and a pile of gems into the repo to press buttons in a simulator. And since it drives the UI through XCUITest, the failures are the annoying kind: &lt;code&gt;staticTexts["Foo"]&lt;/code&gt; that resolves locally and does not resolve on the CI machine, and a runner that hangs every so often until the job times out.&lt;/p&gt;

&lt;p&gt;So I wrote &lt;a href="https://github.com/kichiemon/simshot" rel="noopener noreferrer"&gt;simshot&lt;/a&gt;. Same job, &lt;code&gt;simctl&lt;/code&gt; only, no test runner.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;simshot shoot &lt;span class="nt"&gt;--project&lt;/span&gt; MyApp.xcodeproj &lt;span class="nt"&gt;--scheme&lt;/span&gt; MyApp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bundle-id&lt;/span&gt; com.example.myapp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--devices&lt;/span&gt; iphone-17-pro-max,ipad-pro-13 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--langs&lt;/span&gt; ja,en &lt;span class="nt"&gt;--scenes&lt;/span&gt; home,detail,settings &lt;span class="nt"&gt;--resize&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Running the UI is what makes it fragile
&lt;/h2&gt;

&lt;p&gt;Snapshot has to drive the app from the outside, because that is what a UI test runner does. Element queries, waits, taps, one failure per screen that happens to load slowly. What I actually want is much smaller than a test: show the app a specific screen, wait, take a picture.&lt;/p&gt;

&lt;p&gt;simshot inverts it. The app navigates itself, and the CLI only touches &lt;code&gt;simctl&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;xcodebuild&lt;/code&gt; builds for the simulator&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;simctl bootstatus&lt;/code&gt; waits for boot&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;simctl status_bar&lt;/code&gt; overrides the time and battery to 9:41 / 100%&lt;/li&gt;
&lt;li&gt;the app launches with launch arguments that name the screen&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;simctl io &amp;lt;udid&amp;gt; screenshot&lt;/code&gt; captures the display&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--resize&lt;/code&gt; writes App Store-sized copies with the alpha channel flattened&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The launch arguments are the whole interface between the tool and your app:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Argument&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--ui-testing&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;capture session (skip onboarding, for example)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--screenshot-scene &amp;lt;name&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;navigate to a named scene&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--screenshot-strokes &amp;lt;n&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;perform n gestures (drawing app strokes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--screenshot-scroll-bottom&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;scroll to the bottom before capture&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;-AppleLanguages&lt;/code&gt; and &lt;code&gt;-AppleLocale&lt;/code&gt; are always passed too, so &lt;code&gt;--langs ja,en&lt;/code&gt; captures both languages without any app-side localization code for the screenshots.&lt;/p&gt;

&lt;h2&gt;
  
  
  The app side is a few lines behind &lt;code&gt;#if DEBUG&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;HomeView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;@State&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;Character&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="kd"&gt;@State&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;showSettings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;some&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;NavigationStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;onAppear&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="cp"&gt;#if DEBUG&lt;/span&gt;
                    &lt;span class="nf"&gt;handleScreenshotScene&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="cp"&gt;#endif&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;isPresented&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;$showSettings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kt"&gt;SettingsView&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="cp"&gt;#if DEBUG&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;handleScreenshotScene&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;ProcessInfo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;processInfo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;
        &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;firstIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"--screenshot-scene"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="kt"&gt;DispatchQueue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asyncAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;characters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"settings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;showSettings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
            &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt; &lt;span class="c1"&gt;// "home" is the initial screen&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="cp"&gt;#endif&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scene names are yours. simshot never needs to know what &lt;code&gt;detail&lt;/code&gt; means, and because the handler sits behind &lt;code&gt;#if DEBUG&lt;/code&gt; it is not in the release build.&lt;/p&gt;

&lt;p&gt;Per-screen timing lives in &lt;code&gt;shots.json&lt;/code&gt; when the defaults are wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shots"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"01_home.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"scene"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"home"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"wait"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"02_trace.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"scene"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"trace"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"strokes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"wait"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"03_store.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"scene"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"store"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"scrollBottom"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The weak spot
&lt;/h2&gt;

&lt;p&gt;Without a runner there is nothing to query, so simshot cannot know when a screen is finished rendering. It waits a per-shot settle time (6 seconds by default) and captures. That is a guess, and a list that loads slowly can still get photographed mid-layout.&lt;/p&gt;

&lt;p&gt;Two things compensate. Every external command runs under a hard timeout with retries, so a hung &lt;code&gt;xcodebuild&lt;/code&gt; or a wedged simulator is killed instead of hanging CI. And &lt;code&gt;simshot verify appstore&lt;/code&gt; re-reads the output tree against Apple's rules (accepted dimensions, no alpha, no missing locale, no mixed sizes in one folder), exits non-zero for CI, and says what it thinks went wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❌ appstore/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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you can expose a readiness flag from the app, you can drop the wait instead of tuning it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it sits next to fastlane
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;simshot&lt;/th&gt;
&lt;th&gt;fastlane snapshot&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UI automation&lt;/td&gt;
&lt;td&gt;none, simctl only&lt;/td&gt;
&lt;td&gt;XCUITest runner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hangs&lt;/td&gt;
&lt;td&gt;hard timeout on every command&lt;/td&gt;
&lt;td&gt;can hang CI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ruby&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;Ruby + fastlane + gems&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test target&lt;/td&gt;
&lt;td&gt;a &lt;code&gt;#if DEBUG&lt;/code&gt; handler in your app&lt;/td&gt;
&lt;td&gt;dedicated UI test target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Status bar override, resize, multiple languages&lt;/td&gt;
&lt;td&gt;built in&lt;/td&gt;
&lt;td&gt;plugin or separate tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;fastlane is a good tool and I am not arguing against it. But carrying an XCUITest target and a Ruby toolchain purely to take pictures is a lot of surface area for the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/kichiemon/simshot/main/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or &lt;code&gt;brew tap kichiemon/homebrew-tap &amp;amp;&amp;amp; brew install simshot&lt;/code&gt;. The README also covers Nix, mise, Mint, npm, a prebuilt binary and building from source. macOS 13+, no third-party dependencies. &lt;code&gt;simshot doctor&lt;/code&gt; tells you if the Xcode and runtime setup is off before you waste a run on it.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/kichiemon/simshot" rel="noopener noreferrer"&gt;https://github.com/kichiemon/simshot&lt;/a&gt; — issues and questions are welcome.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>swift</category>
      <category>xcode</category>
      <category>tooling</category>
    </item>
  </channel>
</rss>
