<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>twocentstudios</title>
    <description>A coding blog covering iOS, Swift, and other programming topics.</description>
    <link>https://twocentstudios.com/blog/tags/claudecode/index.html</link>
    <atom:link href="https://twocentstudios.com/blog/tags/claudecode/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Sun, 01 Feb 2026 00:12:37 -0600</pubDate>
    <lastBuildDate>Sun, 01 Feb 2026 00:12:37 -0600</lastBuildDate>
    <generator>Jekyll v3.9.3</generator>
    
      <item>
        <title>Closing the Loop on iOS with Claude Code</title>
        <description>&lt;p&gt;Closing the loop means giving Claude Code a way to view the output of its work. I’ll be focusing on iOS app development workflows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1&lt;/strong&gt; of closing the loop: &lt;strong&gt;building&lt;/strong&gt; a target so that Claude Code can see the errors and warnings. And doing so in a way that preserves the build cache (clean builds take a long time). This allows Claude Code to see its syntax errors and fix them before you review its work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2&lt;/strong&gt; of closing the loop: &lt;strong&gt;installing &amp;amp; launching&lt;/strong&gt; on the simulator. This saves you the step of opening Xcode and hitting build &amp;amp; run, letting you test each proposed code change right away.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3&lt;/strong&gt; of closing the loop: reading the &lt;strong&gt;console &amp;amp; log output&lt;/strong&gt;. This allows Claude Code to proactively verify codepaths and reactively do debugging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4&lt;/strong&gt; of closing the loop: &lt;strong&gt;controlling &amp;amp; viewing&lt;/strong&gt; the iOS simulator. This allows Claude Code to step through entire flows, evaluate visual designs, and generate its own logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5&lt;/strong&gt; of closing the loop: building, installing, launching, and logging &lt;strong&gt;on device&lt;/strong&gt;. This allows you and Claude Code to test Apple Frameworks that are absent or broken on the simulator.&lt;/p&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/closing-loop-cc-hero.jpg&quot; width=&quot;&quot; height=&quot;500&quot; alt=&quot;Building, installing, launching on the simulator from Claude Code&quot; title=&quot;Building, installing, launching on the simulator from Claude Code&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Building, installing, launching on the simulator from Claude Code&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;disclaimers-before-we-start&quot;&gt;Disclaimers before we start&lt;/h3&gt;

&lt;p&gt;Agentic tooling is changing rapidly with model and agent versions. I’ll cover each step as thoroughly as I can. The strategies in this post cover about a month of work in &lt;strong&gt;December 2025&lt;/strong&gt; with Claude &lt;strong&gt;Opus 4.5&lt;/strong&gt; inside Claude Code v2.0.76 (and several versions below). I used &lt;strong&gt;Xcode 26.1 and 26.2&lt;/strong&gt; on macOS 15.7.3 mostly developing for &lt;strong&gt;iOS 26&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This post is written for humans but can easily be adapted to a Skill or added to your CLAUDE.md file. The command structure will change based on how your project and schemes are set up. I outline a few different strategies that are useful in different situations, but you may only want to use one workflow as your default, or completely ignore certain steps altogether.&lt;/p&gt;

&lt;p&gt;If you’ve always used a manual Xcode-based flow, trying to both understand and incorporate these steps into your workflow can be intimidating. If you’ve never working with Xcode via the command line before, start with just the first step for a while. The best part about this workflow is you can seamlessly dip in and out of using Xcode and there’s no switching cost (not even needing to do clean builds).&lt;/p&gt;

&lt;p&gt;The below CLI commands also share a lot of coverage with &lt;a href=&quot;https://github.com/cameroncooke/XcodeBuildMCP&quot;&gt;XcodeBuildMCP&lt;/a&gt;, a more full-service MCP-based solution. I won’t get into the pros and cons of MCPs vs CLIs (its author has already &lt;a href=&quot;https://www.async-let.com/posts/my-take-on-the-mcp-verses-cli-debate/&quot;&gt;written about that&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;I’m specifically targeting this post to Claude Code and Opus 4.5 based on my first-hand knowledge of their combined capabilities. Other harness and model pairs will work with most of the commands in this post. The command backgrounding feature in step 3 is the only potential snag for some harnesses.&lt;/p&gt;

&lt;h2 id=&quot;step-1-building&quot;&gt;Step 1: Building&lt;/h2&gt;

&lt;p&gt;Allowing Claude Code to build after every proposed change is a requirement for agentic workflows. Like it does for human developers, the compiler catches dumb syntax errors and, with Swift concurrency, even data races. The alternative is tabbing back over to Xcode, hitting cmd+b, waiting, copying and pasting error messages into the terminal; a massive waste of human time.&lt;/p&gt;

&lt;h3 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h3&gt;

&lt;h4 id=&quot;move-deriveddata-location-to-your-project-folder-optional&quot;&gt;Move DerivedData location to your project folder (optional)&lt;/h4&gt;

&lt;p&gt;Moving DerivedData to a location inside your project folder is perhaps an unusual suggestion, but it has several benefits for an agentic workflow:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Permissions&lt;/strong&gt;: you’ll encounter fewer permissions dialogs when Claude is reading inside the project folder that you’re presumably running it in. Most devs expect DerivedData to be cleared regularly so it’s safe.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Git worktrees&lt;/strong&gt;: An advanced technique is to use &lt;a href=&quot;https://git-scm.com/docs/git-worktree&quot;&gt;Git worktrees&lt;/a&gt; to have independent copies of your repo. Colocating DerivedData ensures the separate repos don’t interfere with each others build artifacts.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Docs&lt;/strong&gt;: The DerivedData has a full copy of your Swift Packages, including any documentation. Claude Code can do fast greps to verify syntax or find examples. In my CLAUDE.md I have a direct link for each important package:&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-markdown highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;sb&quot;&gt;`DerivedData/train-timetable/SourcePackages/checkouts/swift-composable-architecture/Sources/ComposableArchitecture/Documentation.docc`&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; When using Search/Grep/etc. tools, ignore anything in the /DerivedData folder by default unless specifically looking for build artifacts or code/docs for Swift Packages used by this project
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Before making this change, add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DerivedData/&lt;/code&gt; as a line in your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.gitignore&lt;/code&gt; file if it’s not already there.&lt;/p&gt;

&lt;p&gt;Find the setting in Xcode Settings -&amp;gt; Locations. Set Derived Data to “Relative” and Build Location to “Unique”. It will report &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/DerivedData&lt;/code&gt; as the location.&lt;/p&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/closing-loop-cc-deriveddata-settings.jpg&quot; width=&quot;&quot; height=&quot;300&quot; alt=&quot;DerivedData settings in Xcode&quot; title=&quot;DerivedData settings in Xcode&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;DerivedData settings in Xcode&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;document-project-file--scheme&quot;&gt;Document project file &amp;amp; scheme&lt;/h4&gt;

&lt;p&gt;Build commands use your project/workspace file location and scheme name. Claude Code can find these pretty easily with tools but it’s faster to document them in CLAUDE.md.&lt;/p&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/closing-loop-cc-scheme-selection.jpg&quot; width=&quot;&quot; height=&quot;400&quot; alt=&quot;Finding scheme names from Xcode&quot; title=&quot;Finding scheme names from Xcode&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Finding scheme names from Xcode&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;get-simulators&quot;&gt;Get simulators&lt;/h4&gt;

&lt;p&gt;I’ll assume you’ve already downloaded the iOS simulators and iOS runtime versions you’d like to use in the Xcode interface.&lt;/p&gt;

&lt;p&gt;The usual command you’ll use to find the simulator you want produces a very long list, so it’s reasonable to cache your favorite simulator’s UDID so each new Claude Code session doesn’t need do this from scratch each time. “Cache” meaning note it in your CLAUDE.md file, add it as an environment variable, etc.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# Get all available simulators
xcrun simctl list devices available

     -- iOS 26.1 --
         iPhone 17 Pro (89F6D0BC-E855-4BF7-A400-9C19ED7A7350) (Shutdown)
         iPhone 17 Pro Max (F1FA81FA-ED32-40C4-BD78-753254D685AC) (Shutdown)
         iPhone Air (77702E5F-85F5-4997-BA14-BC8D8F639B84) (Shutdown)
		 ...
     -- iOS 26.2 --
         iPhone 17 Pro (DB0531E0-B47E-42AC-9AAB-FEB76D3D563A) (Booted)
         iPhone 17 Pro Max (0C54CF4B-8A45-450E-AB93-B800B97BD4DA) (Shutdown)
         iPhone Air (83BECA5F-7894-4705-B198-3DCAE0C4778E) (Shutdown)
         ...
     -- ...
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;At first, you’ll probably start with a single threaded workflow, having one preferred simulator booted and in use at a time. The below command will output just the UDID for the latest iPhone Pro with the latest installed iOS version.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# Get the UDID of the latest iPhone Pro (non-max) model with the latest available iOS version 
xcrun simctl list devices available | grep &quot;iPhone.*Pro (&quot; | tail -1 | grep -Eo &apos;[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Once you have more Claudes running in parallel, you’ll can ask each to find its own UDID by looking for a non-booted simulator before it starts building.&lt;/p&gt;

&lt;p&gt;Note that even though some commands can be run with more vague identifiers like name, os, or “booted”, it’s much more reliable to select a UDID and use it across commands. For example: specifying &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;platform=iOS Simulator,name=iPhone 17 Pro&quot;&lt;/code&gt; in the build command will pick any simulator that matches, which could be any iOS version. For the build command it’s not as big of an issue, but to ensure predictable runs I recommend using UDIDs only.&lt;/p&gt;

&lt;h4 id=&quot;install-xcsift&quot;&gt;Install xcsift&lt;/h4&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/ldomaradzki/xcsift&quot;&gt;xcsift&lt;/a&gt; is a companion parsing library for build output. You’ll be building &lt;em&gt;a lot&lt;/em&gt;, and you don’t want to fill up your context with hundreds of lines of “file.swift built”. xcsift solves this by producing just the actionable errors and warnings in json.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;brew install xcsift
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;I add the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-w&lt;/code&gt; flag to also include warnings in the output. I recommend browsing the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xcsift&lt;/code&gt; docs to find other flags you might find useful for your project.&lt;/p&gt;

&lt;h3 id=&quot;building&quot;&gt;Building&lt;/h3&gt;

&lt;p&gt;OK, after all that setup, we should have all the info we need to assemble the actual build command.&lt;/p&gt;

&lt;p&gt;Claude Code will be able to derive and customize the exact build command you need. I recommend doing a quick session with Claude - the goal being to produce a single, always-working command you can document somewhere and use automatically in each future session.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# Example: working build command for a specific simulator
xcodebuild -project train-timetable.xcodeproj -scheme &quot;train-timetable&quot; -destination &quot;platform=iphonesimulator,id=DB0531E0-B47E-42AC-9AAB-FEB76D3D563A&quot; -derivedDataPath DerivedData -configuration Debug build 2&amp;gt;&amp;amp;1 | xcsift -w
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-project&lt;/code&gt;&lt;/strong&gt;: path to your xcodeproj file. Use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-workspace&lt;/code&gt; if you use an xcworkspace file.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-scheme&lt;/code&gt;&lt;/strong&gt;: scheme name we found above.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-destination&lt;/code&gt;&lt;/strong&gt;: for simulator, we use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;platform=iphonesimulator,id=$UDID&quot;&lt;/code&gt; where &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id&lt;/code&gt; is the UDID of our favorite simulator instance. Note &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;platform=iOS Simulator&lt;/code&gt; has the same meaning and also works.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-derivedDataPath&lt;/code&gt;&lt;/strong&gt;: this is super important if you’ve moved the DerivedData to the project folder. Without this, the Xcode instance will be using a different directory and you’ll have super slow (clean) builds each time.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-configuration&lt;/code&gt;&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Debug&lt;/code&gt; is the default, so you don’t usually need this flag. It’s better to be explicit though because this affects the folder where your app binary will be copied to (see step 2).&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;build&lt;/strong&gt;: the actual build command&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&amp;gt;&amp;amp;1 | xcsift -w&lt;/code&gt;&lt;/strong&gt;: combines stdout and stderr and pipes them both into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xcsift&lt;/code&gt; so it has access to all output. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-w&lt;/code&gt; tells &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xcsift&lt;/code&gt; to also show build warnings, not just errors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s important to test your ideal build command to confirm:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;It doesn’t force a clean build each time.&lt;/li&gt;
  &lt;li&gt;It produces a concise set of errors and warnings.&lt;/li&gt;
  &lt;li&gt;It doesn’t interfere with builds via Xcode; you should be able to build/install/run from Xcode, use other SourceKit features, etc. and not clear the build cache.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In my understanding for builds, the simulator UDID (or at least simulator name) does not affect the build artifacts or app binary. However, there’s a lot going on behind the scenes so for simplicity I recommend using the same simulator UDID across all build, install, &amp;amp; launch steps. You need to be careful if running multiple simulators from the same project folder (i.e. without git worktrees) because each &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;build&lt;/code&gt; command will overwrite the app binary regardless of whether you run the build command with different simulator UDIDs.&lt;/p&gt;

&lt;h3 id=&quot;clearing-deriveddata&quot;&gt;Clearing DerivedData&lt;/h3&gt;

&lt;p&gt;When left to its own devices (literally), sometimes Claude will get frustrated when a build is failing continuously and it can’t figure out how to fix things. It will sometimes try to remove the entire DerivedData folder. This is a bad idea because 1. clearing DerivedData usually doesn’t fix the underlying problem and 2. it will temporarily break Xcode’s ability to read your Swift packages and you’ll need to restart Xcode to get everything working again.&lt;/p&gt;

&lt;p&gt;After I got my build commands more streamlined, Claude stopped doing this as much. But I still have decently strict permissions, so when it does happen, the session will usually block on any &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rm&lt;/code&gt; command and I’ll get a chance to step in and reprimand it. If you run into this problem, you can dig deeper into a configuration-based solution and modify your permissions, add hooks, or add more to your CLAUDE.md. Just something to look out for.&lt;/p&gt;

&lt;h2 id=&quot;step-2-installing--launching&quot;&gt;Step 2: Installing &amp;amp; Launching&lt;/h2&gt;

&lt;p&gt;Building should streamline a lot of your Claude Code workflow. But I slept on the automated install &amp;amp; launch step for too long.&lt;/p&gt;

&lt;p&gt;When properly set up with step 1, you should be able to wait for Claude Code to build its changes and return control to you. Then you can tab over to Xcode and hit “run without building” to handle the install &amp;amp; launch.&lt;/p&gt;

&lt;p&gt;But when you’re doing build-&amp;gt;install-&amp;gt;launch dozens of times a day, it’s way more streamlined to check Claude’s session output then tab over to the simulator and tap through screens to test out the changes.&lt;/p&gt;

&lt;h3 id=&quot;prerequisites-1&quot;&gt;Prerequisites&lt;/h3&gt;

&lt;h4 id=&quot;document-the-app-binary-location-for-your-scheme&quot;&gt;Document the app binary location for your scheme&lt;/h4&gt;

&lt;p&gt;Ask Claude to find the location of the app binary produced by the build command.&lt;/p&gt;

&lt;p&gt;Since my setup has DerivedData in the project folder and a build directory inside it, that’s where my app binary is: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DerivedData/Build/Products/Debug-iphonesimulator/Eki Bright.app&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You’ll notice the folder: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Debug-iphonesimulator&lt;/code&gt;, which corresponds to our build configuration of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Debug&lt;/code&gt; from earlier and the platform &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iphonesimulator&lt;/code&gt;. If you go off the beaten path and want to try out a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Release&lt;/code&gt; build for example, make sure to understand this relationship.&lt;/p&gt;

&lt;p&gt;Also be cautious because you want to make sure the most recent build is what you’re installing and launching and looking at. And there’s nothing in the file name that will indicate that.&lt;/p&gt;

&lt;h4 id=&quot;find-your-bundle-identifier&quot;&gt;Find your bundle identifier&lt;/h4&gt;

&lt;p&gt;This will be in your Target’s general settings pane beside Bundle Identifier: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;com.twocentstudios.train-timetable&lt;/code&gt;&lt;/p&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/closing-loop-cc-bundle-id.jpg&quot; width=&quot;&quot; height=&quot;400&quot; alt=&quot;Bundle identifier in Xcode target settings&quot; title=&quot;Bundle identifier in Xcode target settings&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Bundle identifier in Xcode target settings&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;installing-the-app-on-the-simulator&quot;&gt;Installing the app on the simulator&lt;/h3&gt;

&lt;p&gt;Installing is copying over the app binary into a specific simulator’s storage. &lt;strong&gt;This step depends on the build step having produced an app binary.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The parameters in the install command are:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;UDID&lt;/strong&gt;: for least headaches this should be the same simulator UDID you specified in the build command.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;path/to/My App.app&lt;/strong&gt;: the app binary location specified by your scheme.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# Example: install a previously built app binary on the simulator by UDID
xcrun simctl install DB0531E0-B47E-42AC-9AAB-FEB76D3D563A &quot;DerivedData/Build/Products/Debug-iphonesimulator/Eki Bright.app&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h3 id=&quot;launching-the-app-on-the-simulator&quot;&gt;Launching the app on the simulator&lt;/h3&gt;

&lt;p&gt;Launching the equivalent of tapping your app’s icon in Springboard. It of course depends on the install step having copied over the app binary.&lt;/p&gt;

&lt;p&gt;The parameters in the install command are:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;UDID&lt;/strong&gt;: the simulator UDID you specified in the build &amp;amp; install commands.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;bundle id&lt;/strong&gt;: apps are uniquely identified by bundle id after installation.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# Example: launch a previously installed app binary by bundle id
xcrun simctl launch DB0531E0-B47E-42AC-9AAB-FEB76D3D563A com.twocentstudios.train-timetable
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h3 id=&quot;navigating-by-url-bonus&quot;&gt;Navigating by URL (bonus)&lt;/h3&gt;

&lt;p&gt;Depending on your app’s navigation structure and pre-existing support for universal links or App Intents, you can save yourself even more time by having Claude automatically navigate the app to the tab, sheet, or navigation destination you’re currently testing.&lt;/p&gt;

&lt;p&gt;The full set of caveats is beyond the scope of this post. In my experience, adding Universal Links support without some caution can lead to giving Claude access to data or flows that are impossible for normal app users to see. It may also add maintenance burden for initializers that are only used during debug. Regardless, jumping through a dozen screens automatically can save you hours of unnecessary manual screen-clicking labor.&lt;/p&gt;

&lt;p&gt;The parameters for the openurl command are:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;UDID&lt;/strong&gt;: the simulator UDID you specified in the build &amp;amp; install commands.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;URL&lt;/strong&gt;: the deep link URL your app knows how to process.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;xcrun simctl openurl DB0531E0-B47E-42AC-9AAB-FEB76D3D563A &quot;train-timetable://tab?name=search&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;In my testing, it’s safe to have Claude to run the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;openurl&lt;/code&gt; command &lt;em&gt;immediately&lt;/em&gt; after the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;install&lt;/code&gt; command (or even in the same line) without needing a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sleep&lt;/code&gt; or otherwise waiting for the launch to complete.&lt;/p&gt;

&lt;h2 id=&quot;step-3-reading-console--log-output&quot;&gt;Step 3: Reading Console &amp;amp; Log Output&lt;/h2&gt;

&lt;p&gt;With Step 1, Claude has access to the compiler’s evaluation of its code changes. We can also give Claude access to the console and log outputs so it can evaluate the runtime results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There are two strategies&lt;/strong&gt;: console output via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;print&lt;/code&gt; statements and log output via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OSLog&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Logger&lt;/code&gt;. I use both depending on the situation.&lt;/p&gt;

&lt;p&gt;Depending on the strategy, we’ll either amend the launch command from step 2 or prepend a CLI command.&lt;/p&gt;

&lt;h3 id=&quot;blocking-vs-non-blocking&quot;&gt;Blocking vs. non-blocking&lt;/h3&gt;

&lt;p&gt;Claude can do &lt;em&gt;blocking&lt;/em&gt; and &lt;em&gt;non-blocking&lt;/em&gt; for the console variant, and &lt;em&gt;non-blocking&lt;/em&gt;-only for the log output.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Blocking&lt;/em&gt; means that the prompt input and Claude’s thinking will be suspended until you explicitly stop it or the default timeout (currently 10 minutes) triggers. It will print output from the command inline, but usually truncate portions.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Non-blocking&lt;/em&gt; means Claude will use its background capability to keep the command alive and retain access to its output but immediately move the command to the background so that the prompt input is available.&lt;/p&gt;

&lt;p&gt;I recommend the &lt;em&gt;blocking&lt;/em&gt; flow for when you want to add a few quick print statements to verify a limited (maybe less than 15 seconds) code execution flow that you, the human, are driving in the simulator and have Claude immediately evaluate the results inline. The amount of lines generated should be small, within 10s of lines.&lt;/p&gt;

&lt;p&gt;I recommend &lt;em&gt;non-blocking&lt;/em&gt; for all other scenarios, including:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;when you want Claude to drive the simulator (discussed in step 4) while monitoring the output.&lt;/li&gt;
  &lt;li&gt;when you want to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Logger&lt;/code&gt; instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;print&lt;/code&gt; logging, probably for more permanent logging code in your codebase.&lt;/li&gt;
  &lt;li&gt;when you’re expecting to generate dozens or hundreds of lines of logs in a single run. In order to be smart about preserving the session context, you’ll want to write to a file and allow either a subagent to extract meaning from it, or have the primary agent use parsing tools to read only the relevant portions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;--terminate-running-process&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--terminate-running-process&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;Adding the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--terminate-running-process&lt;/code&gt; flag to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;launch&lt;/code&gt; ensures idempotency by terminating any existing instance of your app and ensuring the app is always cold launched with the console output available.&lt;/p&gt;

&lt;p&gt;Adding the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--terminate-running-process&lt;/code&gt; is super important to the logging flow since you may not be rebuilding and reinstalling between launches.&lt;/p&gt;

&lt;p&gt;When you don’t terminate an existing process, the app instance will stay in memory on the simulator. By default, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;launch&lt;/code&gt; command will &lt;strong&gt;not&lt;/strong&gt; relaunch the app if it’s already launched. This will happen silently. Critically, it will also &lt;strong&gt;not&lt;/strong&gt; read any console output and Claude will get very confused about why nothing is being logged and it will start thrashing and making very dumb changes, ranging from adding more print commands to clearing DerivedData.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(This was my biggest roadblock in getting a reliable and robust debugging flow with Claude; please learn from my mistakes).&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id=&quot;launching-the-app-on-simulator-and-reading-the-output&quot;&gt;Launching the app on simulator and reading the output&lt;/h3&gt;

&lt;p&gt;Replace the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;launch&lt;/code&gt; commands from step 2 with any of the below variants depending on your use case.&lt;/p&gt;

&lt;h4 id=&quot;blocking-consoleprint-direct-when-you-know-output-volume-is-reasonable&quot;&gt;Blocking console/print direct (when you know output volume is reasonable)&lt;/h4&gt;

&lt;p&gt;Relevant flags and parameters:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--console-pty&lt;/code&gt;&lt;/strong&gt;: produce console print output.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--terminate-running-process&lt;/code&gt;&lt;/strong&gt;: as discussed above, ensure the command actually runs.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;UDID&lt;/strong&gt; - the simulator UDID you specified in the build &amp;amp; install commands.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;bundle id&lt;/strong&gt; - bundle id of the target that produces the app binary.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# Example with simulator UDID and bundle id
xcrun simctl launch --console-pty --terminate-running-process DB0531E0-B47E-42AC-9AAB-FEB76D3D563A com.twocentstudios.train-timetable
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Note: the flags &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--stdout --stderr&lt;/code&gt; do not work. Don’t use them. Use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--console-pty&lt;/code&gt; instead.&lt;/p&gt;

&lt;h4 id=&quot;blocking-consoleprint-to-file-safer-for-unknown-or-expected-heavy-output&quot;&gt;Blocking console/print to file (safer for unknown or expected heavy output)&lt;/h4&gt;

&lt;p&gt;Relevant flags and parameters:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--console-pty&lt;/code&gt;&lt;/strong&gt;: produce console print output.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--terminate-running-process&lt;/code&gt;&lt;/strong&gt;: as discussed above, ensure the command actually runs.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;UDID&lt;/strong&gt;: the simulator UDID you specified in the build &amp;amp; install commands.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;bundle id&lt;/strong&gt;: bundle id of the target that produces the app binary.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;output file path&lt;/strong&gt;: the plain text file console output will be written to. Note: I write to a tmp folder within DerivedData to ensure Claude has access to the result without triggering unnecessary permissions dialogs.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&amp;gt;&amp;amp;1&lt;/code&gt;&lt;/strong&gt;: ensure stdout &amp;amp; stderr both end up in the file.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;xcrun simctl launch --console-pty --terminate-running-process DB0531E0-B47E-42AC-9AAB-FEB76D3D563A com.twocentstudios.train-timetable &amp;gt; DerivedData/tmp/console.log 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h4 id=&quot;non-blocking-consoleprint&quot;&gt;Non-blocking console/print&lt;/h4&gt;

&lt;p&gt;Non-blocking requires using Claude Code’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run_in_background&lt;/code&gt; parameter on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Bash&lt;/code&gt; tool. This will produce a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;task_id&lt;/code&gt; that Claude can later use to get the output (from an implicitly created text file) and kill the task.&lt;/p&gt;

&lt;p&gt;After running the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Bash&lt;/code&gt; tool, the prompt will be unblocked and you can ask Claude to monitor the output or ask it do anything else you want.&lt;/p&gt;

&lt;p&gt;The non-blocking flow requires a bit more ceremony; you’ll need to tell Claude when you’re done working with the simulator and it should analyze the results. It usually leaves the background task running (potentially writing log data to the output), so you’ll need to specifically tell it to stop.&lt;/p&gt;

&lt;p&gt;The command itself is the same as the one from &lt;em&gt;Blocking console/print direct&lt;/em&gt;.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Bash(
  command: &quot;xcrun simctl launch --console-pty --terminate-running-process DB0531E0-B47E-42AC-9AAB-FEB76D3D563A com.twocentstudios.train-timetable&quot;,
  run_in_background: true
)

Command running in background with ID: b8e2ca5.

# *wait for next user prompt*

TaskOutput(task_id: &quot;b8e2ca5&quot;)
KillShell(shell_id: &quot;b8e2ca5&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h4 id=&quot;non-blocking-loggeroslog&quot;&gt;Non-blocking Logger/OSLog&lt;/h4&gt;

&lt;p&gt;With only its training data, Claude knows how to use &lt;a href=&quot;https://developer.apple.com/documentation/os/logging&quot;&gt;Logging&lt;/a&gt; by importing the OSLog framework. OSLog has strengths and weaknesses compared to console/print logging. You may already be using it in your app. I consider it more of a long term solution you’d add to your codebase alongside each feature and keep it up to date with any changes.&lt;/p&gt;

&lt;p&gt;Giving Claude access to these logs is different from the print/console flow we just discussed.&lt;/p&gt;

&lt;p&gt;The root command is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xcrun simctl spawn&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spawn log stream&lt;/code&gt; only captures logs emitted while it’s running (not before). If you want logs starting from launch, always run it before the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;launch&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;Blocking on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spawn log stream&lt;/code&gt; doesn’t make sense because you still need to launch the app. You should dispatch it directly to the background as non-blocking.&lt;/p&gt;

&lt;p&gt;Relevant flags and parameters (Claude knows how to adjust these freely):&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;UDID&lt;/strong&gt; - the simulator UDID you specified in the build &amp;amp; install commands.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--level&lt;/code&gt;&lt;/strong&gt;: matches the log level in your code; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;debug&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;info&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;warning&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;error&lt;/code&gt;, etc.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--predicate&lt;/code&gt;&lt;/strong&gt;: filters the firehose output the messages you’re interested in. Lots of options here depending on how you’ve defined &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Logger&lt;/code&gt;s and added log statements in your codebase.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spawn log stream&lt;/code&gt; is dispatched to the background, you’ll need to launch the app with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;launch&lt;/code&gt; command. You can choose a blocking or non-blocking &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;launch&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;Note that the raw &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spawn log stream&lt;/code&gt; command is not actually monitoring the specific app process. You can start this early in your session, cast a wide net, and keep this running through your whole session, asking Claude to filter the relevant time periods from the output. I personally haven’t needed this flow though.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Bash(
  command: &quot;xcrun simctl spawn DB0531E0-B47E-42AC-9AAB-FEB76D3D563A log stream --level=debug --predicate &apos;subsystem == &quot;com.twocentstudios.train-timetable&quot;&apos;)&quot;
  run_in_background: true
)

Command running in background with ID: b8e2ca5.

Bash(xcrun simctl launch --terminate-running-process DB0531E0-B47E-42AC-9AAB-FEB76D3D563A com.twocentstudios.train-timetable)

# *blocking prompt until user escapes*

TaskOutput(task_id: &quot;b8e2ca5&quot;)
KillShell(shell_id: &quot;b8e2ca5&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h4 id=&quot;non-blocking-consoleprint--loggeroslog&quot;&gt;Non-blocking console/print &amp;amp; Logger/OSLog&lt;/h4&gt;

&lt;p&gt;You can combine everything above and give Claude access to both console/print output and Logger/OSLog output. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;launch&lt;/code&gt; command can be blocking or non-blocking, but the below example is non-blocking.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Bash(
  command: &quot;xcrun simctl spawn DB0531E0-B47E-42AC-9AAB-FEB76D3D563A log stream --level=debug --predicate &apos;subsystem == &quot;com.twocentstudios.train-timetable&quot;&apos;)&quot;
  run_in_background: true
) 
Command running in background with ID: b8e2ca5.

Bash(
  command: &quot;xcrun simctl launch --console-pty --terminate-running-process DB0531E0-B47E-42AC-9AAB-FEB76D3D563A com.twocentstudios.train-timetable&quot;,
  run_in_background: true
)
Command running in background with ID: a792db1.

# *wait for next user prompt*
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h2 id=&quot;step-4-controlling--viewing-the-ios-simulator&quot;&gt;Step 4: Controlling &amp;amp; Viewing the iOS simulator&lt;/h2&gt;

&lt;p&gt;Giving Claude eyes and virtual fingers to see and control the iOS simulator is where we start to reach the avant-garde. At the current (end of 2025) model &amp;amp; harness capabilities things start to go off the rails pretty quickly. I wouldn’t expect great results from Claude at tasks related to manipulating the simulator like a human, but in certain scenarios, the benefits outweigh the costs.&lt;/p&gt;

&lt;h3 id=&quot;prerequisites-2&quot;&gt;Prerequisites&lt;/h3&gt;

&lt;h4 id=&quot;axe&quot;&gt;AXe&lt;/h4&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;a href=&quot;https://github.com/cameroncooke/AXe&quot;&gt;AXe&lt;/a&gt; is a comprehensive CLI tool for interacting with iOS Simulators using Apple’s Accessibility APIs and HID (Human Interface Device) functionality.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude can use AXe to manipulate the simulator through taps, swipes, button presses, and keyboard typing.&lt;/p&gt;

&lt;p&gt;Under the hood, AXe uses Facebook’s &lt;a href=&quot;https://github.com/facebook/idb&quot;&gt;idb&lt;/a&gt; CLI.&lt;/p&gt;

&lt;p&gt;Install AXe with Homebrew.&lt;/p&gt;

&lt;h4 id=&quot;image-magick-optional&quot;&gt;Image Magick (optional)&lt;/h4&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;a href=&quot;https://github.com/ImageMagick/ImageMagick&quot;&gt;ImageMagick&lt;/a&gt;® is a free and open-source software suite, used for editing and manipulating digital images.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude can use ImageMagick to do some post-processing on screenshots from the simulator.&lt;/p&gt;

&lt;p&gt;Install ImageMagick with Homebrew.&lt;/p&gt;

&lt;h4 id=&quot;ffmpeg-optional&quot;&gt;FFmpeg (optional)&lt;/h4&gt;

&lt;p&gt;Claude can use the venerable &lt;a href=&quot;https://www.ffmpeg.org/&quot;&gt;FFmpeg&lt;/a&gt; CLI for advanced video manipulation use cases. You may not need it but there’s a good chance you already have it.&lt;/p&gt;

&lt;h3 id=&quot;reading-from-the-simulator&quot;&gt;Reading from the simulator&lt;/h3&gt;

&lt;p&gt;In order to navigate the simulator beyond the universal links &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;openurl&lt;/code&gt; use case we detailed above, Claude needs to be able to see the current state of the simulator.&lt;/p&gt;

&lt;p&gt;There are 3 options for this:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Accessibility info&lt;/strong&gt; - Claude can read a hierarchical text description of the current screen using accessibility info.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Screenshots&lt;/strong&gt; - Claude can take a screenshot of the simulator and use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Read&lt;/code&gt; tool to access its multimodal capabilities.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Video&lt;/strong&gt; - Claude can record a short video capture of the simulator, slice it up into frames, and read a few to assess an animation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;accessibility-info-via-describe-ui&quot;&gt;Accessibility info via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;describe-ui&lt;/code&gt;&lt;/h4&gt;

&lt;p&gt;The AXe command for getting the accessibility trace is:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;axe describe-ui --udid SIMULATOR_UDID

  ...
      {
        &quot;frame&quot;: {&quot;y&quot;: 82, &quot;x&quot;: 346, &quot;width&quot;: 36, &quot;height&quot;: 36},
        &quot;AXLabel&quot;: &quot;閉じる&quot;,
        &quot;type&quot;: &quot;Button&quot;
      }
  ...
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;The output is a big JSON array.&lt;/p&gt;

&lt;p&gt;I thought Claude would be better at understanding and navigation with text information than image information, but in practice it almost always ignored my instructions in CLAUDE.md to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;describe-ui&lt;/code&gt; before the screenshot flow. Perhaps there’s something in the system prompt or it’s less efficient to hunt through all the text.&lt;/p&gt;

&lt;p&gt;I also immediately ran into a &lt;a href=&quot;https://github.com/cameroncooke/AXe/issues/8&quot;&gt;reported issue&lt;/a&gt; in AXe and idb where &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;describe-ui&lt;/code&gt; does not print tab or toolbar info, perhaps only from iOS 26. This makes it very difficult to deterministically do any sort of navigation from the root in many apps.&lt;/p&gt;

&lt;p&gt;All this is to say that, at the moment, it’s more reliable to use screenshots.&lt;/p&gt;

&lt;h4 id=&quot;screenshots&quot;&gt;Screenshots&lt;/h4&gt;

&lt;p&gt;Claude can use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;simctl&lt;/code&gt; to get screenshots.&lt;/p&gt;

&lt;p&gt;Like the other commands, I prefer to write to a tmp folder within DerivedData.&lt;/p&gt;

&lt;p&gt;Screenshots for most simulators are taken at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;3x&lt;/code&gt; scale, but input taps and swipes are at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1x&lt;/code&gt;. For the dual purposes of 1. reducing the amount of calculation required to translate screen position to next tap position and 2. reducing the amount of image data that needs to be sent to and processed by Claude, I automatically resize all screenshots to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1x&lt;/code&gt; via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;magick&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;xcrun simctl io DB0531E0-B47E-42AC-9AAB-FEB76D3D563A screenshot DerivedData/tmp/screen.png &amp;amp;&amp;amp; magick DerivedData/tmp/screen.png -resize 33.333% DerivedData/tmp/screen_1x.png
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h4 id=&quot;video&quot;&gt;Video&lt;/h4&gt;

&lt;p&gt;Reading live or even recorded video is currently beyond Opus 4.5’s capabilities. I’m guessing this will be a supported flow sometime in 2026, but until then analyzing video output of the simulator is still at proof-of-concept maturity.&lt;/p&gt;

&lt;p&gt;While I was debugging a tricky animation, I gave Claude some leash to test whether it could:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;start recording a short clip immediately before a tap.&lt;/li&gt;
  &lt;li&gt;stop the recording after 2 seconds.&lt;/li&gt;
  &lt;li&gt;use FFmpeg to grab 5 or 6 frames spaced out across the video.&lt;/li&gt;
  &lt;li&gt;read the frames and analyze the motion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It sort of worked? Not really? If you have a use case, you can try experimenting more with this flow. For now I’d consider the actual animation analysis a human-only endeavor. But Claude can still help get the simulator staged up to the start screen.&lt;/p&gt;

&lt;p&gt;I believe I used this AXe command as a Claude Code background task:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;axe record-video --udid DB0531E0-B47E-42AC-9AAB-FEB76D3D563A --fps 30 --output DerivedData/tmp/recording.mp4
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h3 id=&quot;manipulating-the-simulator-with-taps-and-swipes&quot;&gt;Manipulating the simulator with taps and swipes&lt;/h3&gt;

&lt;p&gt;AXe has a variety of tap and gesture commands. Claude can tap on points or accessibility labels.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Tap at logical coordinates (use frame center from describe-ui)&lt;/span&gt;
axe tap -x 201 -y 297 --udid DB0531E0-B47E-42AC-9AAB-FEB76D3D563A --post-delay 0.5
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Without additional guidance, &lt;strong&gt;Claude gets confused about which scroll command maps to what logical direction&lt;/strong&gt;.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Scroll (named by finger direction, not content direction)&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# scroll-up = finger UP = content UP = see content BELOW = triggers .onScrollDown&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# scroll-down = finger DOWN = content DOWN = see content ABOVE&lt;/span&gt;
axe gesture scroll-down --udid DB0531E0-B47E-42AC-9AAB-FEB76D3D563A --post-delay 0.5
axe gesture scroll-up --udid DB0531E0-B47E-42AC-9AAB-FEB76D3D563A --post-delay 0.5
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;It’s useful to note the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;swipe-from-left-edge&lt;/code&gt; gesture because it’s the quickest way for Claude to pop back a level in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NavigationStack&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Edge swipes (for back navigation, etc.)&lt;/span&gt;
axe gesture swipe-from-left-edge --udid DB0531E0-B47E-42AC-9AAB-FEB76D3D563A --post-delay 0.5
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h3 id=&quot;strategies-for-increasing-tap-accuracy&quot;&gt;Strategies for increasing tap accuracy&lt;/h3&gt;

&lt;p&gt;The most significant source of indeterministic behavior is in Claude’s ability to accurately measure of coordinates on screen. In other words, it can’t read an image and always find the center point of a button. This means there is plenty of opportunity for situations like:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Claude reads a screen and wants to tap a button.&lt;/li&gt;
  &lt;li&gt;Claude makes a bad guess and taps above the button.&lt;/li&gt;
  &lt;li&gt;Claude reads the screen again. There was no change.&lt;/li&gt;
  &lt;li&gt;Claude makes another bad guess and taps below the button.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Claude’s only feedback about whether its tap was successful is based on its next screenshot. This can lead to situations where it gets irrecoverably lost while navigating your app:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Claude reads a screen and wants to tap a button.&lt;/li&gt;
  &lt;li&gt;Claude makes a bad guess and taps above the button, hitting a completely different button.&lt;/li&gt;
  &lt;li&gt;Claude reads the screen again.&lt;/li&gt;
  &lt;li&gt;Claude sees it’s on a different screen than expected and becomes confused.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I came up with an experimental flow to try to improve Claude’s accuracy, but:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;It slows down the entire process by 2x.&lt;/li&gt;
  &lt;li&gt;By the time Claude realizes it needs to use the experimental flow, it’s already too far lost to recover.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Regardless, my flow, also using ImageMagick, is to make Claude draw a red circle on a screenshot in its targeted tap location before actually performing the tap:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Take screenshot and resize to 1x (so pixels = points):
    &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;xcrun simctl io DB0531E0-B47E-42AC-9AAB-FEB76D3D563A screenshot DerivedData/tmp/screen.png &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; magick DerivedData/tmp/screen.png -resize 33.333% DerivedData/tmp/screen_1x.png
&lt;/code&gt;&lt;/pre&gt;
    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;Read the 1x image and estimate target element center in points&lt;/li&gt;
  &lt;li&gt;Verify guess by drawing a red box at those coordinates:
    &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;magick DerivedData/tmp/screen_1x.png -fill none -stroke red -strokewidth 2 -draw &lt;span class=&quot;s2&quot;&gt;&quot;rectangle &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;$((&lt;/span&gt;X-30&lt;span class=&quot;k&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;$((&lt;/span&gt;Y-30&lt;span class=&quot;k&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;$((&lt;/span&gt;X+30&lt;span class=&quot;k&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;$((&lt;/span&gt;Y+30&lt;span class=&quot;k&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; DerivedData/tmp/screen_marked.png
&lt;/code&gt;&lt;/pre&gt;
    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;Read marked image to check if box is on target&lt;/li&gt;
  &lt;li&gt;If missed, adjust coordinates and repeat from step 3&lt;/li&gt;
  &lt;li&gt;If correct, tap at the verified coordinates&lt;/li&gt;
&lt;/ol&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/closing-loop-cc-tap-accuracy.jpg&quot; width=&quot;&quot; height=&quot;400&quot; alt=&quot;Tap accuracy verification flow - original screenshot, marked target, and result after tap&quot; title=&quot;Tap accuracy verification flow - original screenshot, marked target, and result after tap&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Tap accuracy verification flow - original screenshot, marked target, and result after tap&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;putting-it-all-together&quot;&gt;Putting it all together&lt;/h3&gt;

&lt;p&gt;So far in practice, I’ve used this capability alongside universal links to fix and verify simple visual bugs. I have Claude craft input to reproduce the error and find the screen where it occurs, take a “before” screenshot, implement the fix, build/install/launch, then find the same screen, verify the fix, and take an “after” screenshot.&lt;/p&gt;

&lt;p&gt;It takes way longer for Claude to do than me, but it’s mostly tedious work, and I’m usually in another tab working on a plan with another Claude. When I come back and see the before and after screenshots alongside the code change, I can feel confident in Claude’s work.&lt;/p&gt;

&lt;h2 id=&quot;step-5-building-installing-launching-reading-output-on-a-physical-device&quot;&gt;Step 5: Building, Installing, Launching, Reading Output on a Physical Device&lt;/h2&gt;

&lt;p&gt;Finally, for those Apple SDKs that only work on device, for ensuring observed buggy behavior isn’t just a simulator quirk, or just to get a more realistic look at our apps in context, we can implement steps 1, 2, and 3 on a physical device. Unfortunately, as far as I can tell, there’s no way to control a physical device via CLI tool, so step 4 is out reach for now.&lt;/p&gt;

&lt;p&gt;However, building, installing, launching, and logging can still save some time and annoyance during iterative debugging sessions. It’s especially useful to have Claude help analyze logs for (underdocumented) frameworks like Core Location that behave wildly different on a real device than on the simulator.&lt;/p&gt;

&lt;p&gt;Below is a collection of tested CLI commands for doing all the above tasks on a physical device.&lt;/p&gt;

&lt;p&gt;Note that in my testing all the relevant commands below work equally for devices on the same network and devices connected directly to your Mac via USB.&lt;/p&gt;

&lt;h3 id=&quot;prerequisites-3&quot;&gt;Prerequisites&lt;/h3&gt;

&lt;h4 id=&quot;get-devices&quot;&gt;Get devices&lt;/h4&gt;

&lt;p&gt;The device &lt;strong&gt;Name&lt;/strong&gt; and &lt;strong&gt;Identifier&lt;/strong&gt; are both important for on-device debugging. &lt;strong&gt;State&lt;/strong&gt; will be &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;available&lt;/code&gt; when Wi-Fi debugging is available, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;connected&lt;/code&gt; when directly connect via USB.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Get all connected devices&lt;/span&gt;
xcrun devicectl list devices

Name          Hostname                      Identifier                             State                Model                                
-----------   ---------------------------   ------------------------------------   ------------------   -------------------------------------
CT&lt;span class=&quot;s1&quot;&gt;&apos;s iPhone   CTs-iPhone.coredevice.local   ABCDEF01-1111-5555-AAAA-F7D81A900001   connected (no DDI)   iPhone 14 Pro (iPhone15,2)           
CT’s iPad     CTs-iPad.coredevice.local     ABCDEF01-2222-6666-BBBB-F44A19F00002   available            iPad Pro (11-inch) (iPad8,1)         
CT’s iPad     CTs-iPad-1.coredevice.local   ABCDEF01-3333-7777-CCCC-D33889000003   unavailable          iPad mini (5th generation) (iPad11,1)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Some example use cases for parsing out values in one go:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# Get the Identifier of the first accessible iPhone (WiFi or USB)
xcrun devicectl list devices | grep &quot;iPhone&quot; | grep -E &quot;(available|connected)&quot; | head -1 | grep -Eo &apos;[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}&apos;

ABCDEF01-1111-5555-AAAA-F7D81A900001
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# Get the name of the first accessible iPhone (WiFi or USB)
xcrun devicectl list devices | grep &quot;iPhone&quot; | grep -E &quot;(available|connected)&quot; | head -1 | awk -F&apos;  +&apos; &apos;{print $1}&apos;

CT&apos;s iPhone
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h3 id=&quot;build-for-device&quot;&gt;Build for device&lt;/h3&gt;

&lt;p&gt;Build commands are the same as those for the simulator, except &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;platform=iOS&lt;/code&gt; instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;platform=iphonesimulator&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;platform=iOS Simulator&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt; of your target device from the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;list devices&lt;/code&gt; command.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# Using device name
xcodebuild -project train-timetable.xcodeproj -scheme &quot;train-timetable&quot; -destination &quot;platform=iOS,name=CT&apos;s iPhone&quot; -derivedDataPath DerivedData build 2&amp;gt;&amp;amp;1 | xcsift -w
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Note: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;platform=iphoneos&lt;/code&gt; does not work. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id&lt;/code&gt; instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt; does not work.&lt;/p&gt;

&lt;h3 id=&quot;install-on-device&quot;&gt;Install on device&lt;/h3&gt;

&lt;p&gt;Install commands use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;devicectl&lt;/code&gt; but are the similar to those for the simulator, except &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--device&lt;/code&gt; should use the device ID, and the build product directory should use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Debug-iphoneos&lt;/code&gt; instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Debug-iphonesimulator&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;xcrun devicectl device install app --device E7E3E660-9E7A-5814-8BBB-F7D81A965CEB &quot;DerivedData/Build/Products/Debug-iphoneos/Eki Bright.app&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h3 id=&quot;launch-on-device&quot;&gt;Launch on device&lt;/h3&gt;

&lt;p&gt;The vanilla launch command is below. I again recommend using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--terminate-existing&lt;/code&gt;, the device equivalent of the simulator’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--terminate-running-process&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;xcrun devicectl device process launch --device E7E3E660-9E7A-5814-8BBB-F7D81A965CEB --console --terminate-existing com.twocentstudios.train-timetable
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h3 id=&quot;blocking-consoleprint-capture-on-device&quot;&gt;Blocking console/print capture on device&lt;/h3&gt;

&lt;p&gt;For console/print capture, use the launch command above with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--console&lt;/code&gt; flag. It works over USB and Wi-Fi.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;xcrun devicectl device process launch --device E7E3E660-9E7A-5814-8BBB-F7D81A965CEB --console --terminate-existing com.twocentstudios.train-timetable
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h3 id=&quot;non-blocking-consoleprint-capture-on-device&quot;&gt;Non-blocking console/print capture on device&lt;/h3&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# Use run_in_background: true on Bash tool
# Works over USB and WiFi
Bash(
  command: &quot;xcrun devicectl device process launch --device E7E3E660-9E7A-5814-8BBB-F7D81A965CEB --console --terminate-existing com.twocentstudios.train-timetable&quot;,
  run_in_background: true
)

Command running in background with ID: b8e2ca5.

# *wait for next user prompt*

TaskOutput(task_id: &quot;b8e2ca5&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h3 id=&quot;loggeroslog-capture-on-device-requires-manual-sudo&quot;&gt;Logger/OSLog capture on device (requires manual sudo)&lt;/h3&gt;

&lt;p&gt;A downside of OSLog is that it requires &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sudo&lt;/code&gt; and Claude Code can’t use sudo commands directly. There are presumably some ways to give Claude this capability in more a dangerous fashion. But a safer workaround for now is for you, the human, to run the below commands in another terminal tab. Claude can give you the full command to copy/paste into the other terminal.&lt;/p&gt;

&lt;p&gt;Another downside is that the process is slow and produces lots of logs.&lt;/p&gt;

&lt;p&gt;These commands will produce groups of files that Claude can read.&lt;/p&gt;

&lt;p&gt;Note that the log collect is of &lt;strong&gt;everything on the device&lt;/strong&gt; in the past, and can quickly balloon to gigabytes of storage. The command to collect even the last 2 minutes of logs can take about 30 seconds to complete on Wi-Fi.&lt;/p&gt;

&lt;p&gt;Content filtering with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--predicate&lt;/code&gt; is not supported: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Warning: --predicate is ignored when collecting from attached device&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Claude will handle filtering while reading/analyzing with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;log show --predicate ...&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The most logical way to use this is to:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;have Claude build, install, and launch the app on your device (ensure it’s unlocked), and have it note the start time.&lt;/li&gt;
  &lt;li&gt;tap around and do the testing you need in order to generate the logs you want.&lt;/li&gt;
  &lt;li&gt;have Claude give you the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sudo log collect&lt;/code&gt; command with a start time a little before the launch time.&lt;/li&gt;
  &lt;li&gt;run the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sudo log collect&lt;/code&gt; command in a separate terminal window, enter your password, wait ~1m for it to finish.&lt;/li&gt;
  &lt;li&gt;ask Claude to analyze the log archive.&lt;/li&gt;
&lt;/ol&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# Human user must run these commands in another terminal tab (Claude Code can&apos;t provide sudo password)

# `--last` collects from N minutes before the command was run
sudo log collect --device-name &quot;CT&apos;s iPhone&quot; --last 2m --output DerivedData/tmp/device-logs.logarchive

# `--start` collects from the specified start time until the command was run
sudo log collect --device-name &quot;CT&apos;s iPhone&quot; --start &quot;2025-12-30 16:11:00&quot; --output DerivedData/tmp/device-logs.logarchive
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# Then analyze with log show (Claude can do this)
log show DerivedData/tmp/device-logs.logarchive --predicate &apos;subsystem == &quot;com.twocentstudios.train-timetable&quot;&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Note &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--device-udid&lt;/code&gt; does not work, use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--device-name instead&lt;/code&gt; - &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;log: failed to create archive: Device not configured (6)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Note &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--size&lt;/code&gt; does not (seem to) work either.&lt;/p&gt;

&lt;h2 id=&quot;final-thoughts&quot;&gt;Final thoughts&lt;/h2&gt;

&lt;h3 id=&quot;how-to-parameterize-names-ids-etc-for-these-commands&quot;&gt;How to parameterize names, ids, etc. for these commands&lt;/h3&gt;

&lt;p&gt;So far, I’ve just been hardcoding these commands with my favorite simulator UDID and project path into my CLAUDE.md. When a new version of Xcode comes out I ask Claude to update all mentions of the UDID to the most recent simulator version and it only takes a minute. Hardcoding these values leaves the least room for hallucination. When running these commands dozens of times a day, you really want consistency.&lt;/p&gt;

&lt;p&gt;Other ways to handle this would be:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;set environment variables at some level.&lt;/li&gt;
  &lt;li&gt;add a start hook to have Claude fill in the environment variables fresh for each session.&lt;/li&gt;
  &lt;li&gt;set up another layer of orchestration that handles the pool of simulators and dispatches an ID to each new Claude instance that requests one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are all beyond the scope of this post. If you work primarily on one project with others, you probably already have some tooling for specifying the Xcode version, etc.&lt;/p&gt;

&lt;h3 id=&quot;why-not-include-traditional-testing&quot;&gt;Why not include traditional testing?&lt;/h3&gt;

&lt;p&gt;Arguably, TDD was the original “closing the loop” in software development. TDD has never caught on in the iOS world.&lt;/p&gt;

&lt;p&gt;I dabbled with an actual Swift Testing-based testing flow for another &lt;a href=&quot;/2025/12/25/shinkansen-live-developing-the-app-for-ios/#ocr-and-parsing-the-ticket-image&quot;&gt;recent project&lt;/a&gt;, and even wrote about another experimental system a few months ago in &lt;a href=&quot;/2025/07/13/giving-claude-code-eyes-to-see-your-swiftui-views/&quot;&gt;Giving Claude Code Eyes to See Your SwiftUI Views&lt;/a&gt; that used snapshot testing. What I found was although tests are great for verifying correct behavior over the long term, in the short term they are super slow on iOS:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Installing and launching requires instantiating a brand new simulator for each run (e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Clone 1&lt;/code&gt;), which takes a long time.&lt;/li&gt;
  &lt;li&gt;All builds are clean builds (this could have just been a fluke in my setup at the time though).&lt;/li&gt;
  &lt;li&gt;Swift Testing does not output failures in a way that Claude can read and iterate on (again, potentially solvable).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Admittedly, I didn’t spend as much time debugging these flows. Hopefully someone else will fill in the blanks for testing and write this guide.&lt;/p&gt;

&lt;h3 id=&quot;dont-sleep-on-simctl&quot;&gt;Don’t sleep on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;simctl&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;simctl&lt;/code&gt; CLI we’ve used throughout this post has a ton of other abilities that Claude can use to make our lives easier. This includes adding images to Photos, changing the system time, changing the system language, resetting privacy, resetting the keychain, and many more.&lt;/p&gt;

&lt;p&gt;Ask Claude to configure your simulator on the fly instead of clicking through settings with your mouse.&lt;/p&gt;

&lt;h3 id=&quot;what-does-the-future-hold&quot;&gt;What does the future hold?&lt;/h3&gt;

&lt;p&gt;I’m honestly not sure how long the hard-won knowledge in this post will be relevant, given the pace of model &amp;amp; harness capabilities. Peter Steinberger &lt;a href=&quot;https://steipete.me/posts/2025/shipping-at-inference-speed&quot;&gt;already says&lt;/a&gt; Codex is good enough and doesn’t need any additional guidance about build commands or working with the simulator.&lt;/p&gt;

&lt;p&gt;I can definitely see a world where Claude Code has a live feed of the simulator output it can process and react to at 60 fps, tapping and swiping with full accuracy. This is probably what’s missing in fully closing the development loop on iOS. Doing the same for a real device hopefully isn’t close behind.&lt;/p&gt;

&lt;p&gt;At that point though, I’m not sure what else about development will have changed.&lt;/p&gt;

&lt;h3 id=&quot;going-forward&quot;&gt;Going forward&lt;/h3&gt;

&lt;p&gt;Most of the material in this guide has been slowly compiled over the month in my various CLAUDE.md files. It was great getting a chance to formalize it even if I can’t make a quickly installable Plugin or Skill to share (hopefully you understand why after reading the post). I’m looking forward to seeing how far I can take each of these steps in the near future.&lt;/p&gt;

&lt;h3 id=&quot;corrections&quot;&gt;Corrections&lt;/h3&gt;

&lt;p&gt;Please reach out if you find any corrections or can contribute any additional knowledge or edge cases.&lt;/p&gt;

</description>
        <pubDate>Sat, 27 Dec 2025 15:37:01 -0600</pubDate>
        <link>https://twocentstudios.com/2025/12/27/closing-the-loop-on-ios-with-claude-code/</link>
        <guid isPermaLink="true">https://twocentstudios.com/2025/12/27/closing-the-loop-on-ios-with-claude-code/</guid>
        
        <category>apple</category>
        
        <category>ios</category>
        
        <category>claudecode</category>
        
        
      </item>
    
      <item>
        <title>An E-Ink Desk Companion With M5Paper</title>
        <description>&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/m5paper-desk-unit.jpg&quot; width=&quot;&quot; height=&quot;400&quot; alt=&quot;The M5Paper on my desk displaying my daily TODO list&quot; title=&quot;The M5Paper on my desk displaying my daily TODO list&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;The M5Paper on my desk displaying my daily TODO list&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I’ve had an &lt;a href=&quot;https://shop.m5stack.com/products/m5paper-esp32-development-kit-v1-1-960x540-4-7-eink-display-235-ppi&quot;&gt;M5Paper&lt;/a&gt; since my days in the R&amp;amp;D Department at Cookpad that I’ve never known what to do with. My friend Dave has been doing a lot of microcontroller projects in his spare time, and it inspired me to get out the M5Paper again.&lt;/p&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/m5paper-marketing.jpg&quot; width=&quot;&quot; height=&quot;400&quot; alt=&quot;M5Paper ESP32 development kit with E-Ink display marketing image&quot; title=&quot;M5Paper ESP32 development kit with E-Ink display marketing image&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;M5Paper ESP32 development kit with E-Ink display marketing image&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;M5Paper is an all-in-one development kit device that wraps an ESP32 microcontroller, a rechargeable battery, some environment sensors, and sticks on a 4.7” capacitive-touch E-Ink display. I bought it for about ¥8000 a couple years ago, but the &lt;a href=&quot;https://shop.m5stack.com/products/m5papers3-esp32s3-development-kit&quot;&gt;improved version S3&lt;/a&gt; goes for $59 USD at the time of this writing.&lt;/p&gt;

&lt;p&gt;My new idea was to have the M5Paper on my desk mirroring my current TODO list for the day.&lt;/p&gt;

&lt;h2 id=&quot;feasibility-research&quot;&gt;Feasibility research&lt;/h2&gt;

&lt;p&gt;I keep a daily markdown file with my TODOs at the top and then notes about the day’s work below it. I’ve been doing this for the last 2 years. Previously I’d use one markdown file for an entire year and use it as a rolling TODO list and scratch pad (this actually worked surprisingly well).&lt;/p&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/m5paper-obsidian-daily-notes.png&quot; width=&quot;&quot; height=&quot;400&quot; alt=&quot;Daily notes in Obsidian with TODO items at the top&quot; title=&quot;Daily notes in Obsidian with TODO items at the top&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Daily notes in Obsidian with TODO items at the top&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I’ve found that my daily TODO list often falls off my radar as soon as the Obsidian window gets buried behind all the others. I end up leaving TODOs unchecked or, even worse, completely forget to do a task. &lt;strong&gt;I realized the E-Ink display of the M5Paper was one way to keep my TODO list in my literal peripheral vision without requiring any additional TODO list management&lt;/strong&gt;, including manual duplication or otherwise altering my current workflow.&lt;/p&gt;

&lt;p&gt;The Obsidian vault I use for my daily notes and project notes is sourced from a folder synced with Dropbox. Therefore, I realized I could have the M5Paper run the following:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Wake up and connect to WiFi&lt;/li&gt;
  &lt;li&gt;Calculate the current date&lt;/li&gt;
  &lt;li&gt;Use the Dropbox API to fetch a file called 2025-10-10.md within a hardcoded folder path (i.e. the current date)&lt;/li&gt;
  &lt;li&gt;Display the contents of the file on the E-Ink display&lt;/li&gt;
  &lt;li&gt;Go to sleep for some amount of time (an hour?) to conserve battery&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/m5paper-workflow-illustration.png&quot; width=&quot;&quot; height=&quot;400&quot; alt=&quot;Workflow diagram showing the M5Paper fetch and display cycle&quot; title=&quot;Workflow diagram showing the M5Paper fetch and display cycle&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Workflow diagram showing the M5Paper fetch and display cycle&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I had Claude Code and Codex help me along the prototyping journey.&lt;/p&gt;

&lt;h2 id=&quot;first-prototype&quot;&gt;First prototype&lt;/h2&gt;

&lt;p&gt;The first big decision was what environment to use for programming the ESP32. There’s the beginner-friendly &lt;a href=&quot;https://docs.m5stack.com/en/uiflow2/uiflow_web&quot;&gt;UiFlow2&lt;/a&gt; web IDE, the &lt;a href=&quot;https://docs.m5stack.com/en/arduino/arduino_ide&quot;&gt;Arduino IDE&lt;/a&gt;, the &lt;a href=&quot;https://platformio.org/&quot;&gt;PlatformIO&lt;/a&gt; plugin for VSCode, and then the more command line heavy route.&lt;/p&gt;

&lt;p&gt;After flashing the firmware with UiFlow2 (this required downloading an out-of-date macOS app), I decided to use MicroPython with command line tools. I’ve used PlatformIO and Arduino IDE before but I wanted to see if MicroPython was more ergonomic than the embedded C++ variant.&lt;/p&gt;

&lt;p&gt;The command line tools I ended up using were &lt;a href=&quot;https://docs.espressif.com/projects/esptool/en/latest/&quot;&gt;esptool&lt;/a&gt; and &lt;a href=&quot;https://docs.micropython.org/en/latest/reference/mpremote.html&quot;&gt;mpremote&lt;/a&gt;. Another reason I picked these was so my coding agents could be more helpful in being part of the development loop, although maybe the models have more training data for Arduino code?&lt;/p&gt;

&lt;p&gt;The Dropbox part was a big unknown for me. I was banking on being able to fetch from my existing folder hierarchy with the Dropbox API. If it turned out to be impossible I probably would have given up since I’m not willing to change my entire personal knowledge-base setup.&lt;/p&gt;

&lt;p&gt;Luckily, it was really easy to create a Dropbox App that worked only for my account. I could even generate a hardcoded API token with the right permissions that lasted 4 hours for testing. With that in place, Claude Code generated the first attempt at the code needed to get something from Dropbox onto the screen.&lt;/p&gt;

&lt;p&gt;The next step was understanding how deployment to device was supposed to work and then later automating it. This was especially error-prone because I was intentionally putting the device into deep sleep, which would cause it to become unresponsive to the base commands. In the end, I have a Makefile with a deploy command that looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Makefile&quot;&gt;DEVICE_PORT = /dev/cu.usbserial-0214257D
ESPTOOL = python3 -m esptool --chip esp32 --port $(DEVICE_PORT) read_mac
MPREMOTE = python3 -m mpremote connect $(DEVICE_PORT)

.PHONY: deploy test list clean help reset rtc-check rtc-sync rtc-init repl-mode

deploy: ## Deploy main.py, secrets.py, and bm8563_rtc.py to M5Paper (full deployment)
	@echo &quot;🔄 Deploying to M5Paper...&quot;
	@echo &quot;⚡ Resetting device...&quot;
	@$(ESPTOOL) &amp;gt; /dev/null 2&amp;gt;&amp;amp;1
	@echo &quot;⏳ Waiting for boot...&quot;
	@sleep 4
	@echo &quot;📁 Copying files...&quot;
	@$(MPREMOTE) cp main.py :main.py &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; echo &quot;  ✓ main.py copied&quot; || echo &quot;  ⚠️ main.py failed&quot;
	@if [ -f secrets.py ]; then $(MPREMOTE) cp secrets.py :secrets.py &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; echo &quot;  ✓ secrets.py copied&quot; || echo &quot;  ⚠️ secrets.py failed&quot;; fi
	@if [ -f bm8563_rtc.py ]; then $(MPREMOTE) cp bm8563_rtc.py :bm8563_rtc.py &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; echo &quot;  ✓ bm8563_rtc.py copied&quot; || echo &quot;  ⚠️ bm8563_rtc.py failed&quot;; fi
	@echo &quot;🔄 Final reset...&quot;
	@$(ESPTOOL) &amp;gt; /dev/null 2&amp;gt;&amp;amp;1
	@echo &quot;✅ Deployment complete!&quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This essentially just copies over the Python files from my Mac to the M5Paper, but with a few extra steps to make sure the device is ready to be written to. Hard-won knowledge, but after getting this in place, the development loop is about as fast as it can be.&lt;/p&gt;

&lt;p&gt;Putting the device in deep sleep blocks the REPL from connecting normally. Therefore, I also have a Makefile command for temporarily replacing the deployed contents with a dummy file.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Makefile&quot;&gt;repl-mode: ## Replace main.py with a stub so the device boots straight to the REPL
	@echo &quot;🛠  Entering REPL mode...&quot;
	@echo &quot;⚡ Resetting device...&quot;
	@$(ESPTOOL) &amp;gt; /dev/null 2&amp;gt;&amp;amp;1
	@echo &quot;⏳ Waiting for boot...&quot;
	@sleep 4
	@echo &quot;🗑  Installing REPL stub...&quot;
	@$(MPREMOTE) cp repl_stub.py :main.py &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; echo &quot;  ✓ stub copied&quot; || echo &quot;  ⚠️ stub copy failed&quot;
	@echo &quot;🔄 Final reset...&quot;
	@$(ESPTOOL) &amp;gt; /dev/null 2&amp;gt;&amp;amp;1
	@echo &quot;✅ Device will now stay idle for REPL access.&quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&quot;going-beyond-the-prototype&quot;&gt;Going beyond the prototype&lt;/h2&gt;

&lt;p&gt;I had my first working prototype up and running after a few hours which was great. However, there were some lingering issues the next day:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The dev-use-only Dropbox API token I had used expired after 4 hours. I needed to use the full OAuth flow to get a refreshable token.&lt;/li&gt;
  &lt;li&gt;The real time clock (RTC) was often getting confused after wake-ups or power events and either returning to the unix epoch or defaulting to an incorrect hour during the correct day.&lt;/li&gt;
  &lt;li&gt;Japanese characters showed up as rectangular boxes.&lt;/li&gt;
  &lt;li&gt;I wanted a way to refresh the contents manually by pressing the side button.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;dropbox-api-token&quot;&gt;Dropbox API token&lt;/h3&gt;

&lt;p&gt;The Dropbox API token issue turned out to be the easiest solve of the above problems. Claude Code wrote me a quick script I could run on my Mac that used my Dropbox app secret credentials to return a refreshable token. I added that token to the secrets.py file on the M5Paper and now I had a long-term solution.&lt;/p&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/m5paper-dropbox-app-api.jpg&quot; width=&quot;&quot; height=&quot;400&quot; alt=&quot;Dropbox app settings showing API token configuration&quot; title=&quot;Dropbox app settings showing API token configuration&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Dropbox app settings showing API token configuration&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;clock-issues&quot;&gt;Clock issues&lt;/h3&gt;

&lt;p&gt;As usual, anything with clocks or calendars is the hardest problem. 1. My lack of knowledge about the M5Paper itself combined with 2. the difficulty of isolating a test environment that allowed me to observe the device without interrupting it – prolonged the debugging experience.&lt;/p&gt;

&lt;p&gt;Through a lot of agent churn, I finally learned that the M5Paper has two clocks: ESP’s on-chip RTC and a dedicated chip external RTC connected to the lithium battery. Setting the external RTC with a one-off script during dev time would save a lot of hassle and complexity in the main loop.&lt;/p&gt;

&lt;h3 id=&quot;japanese-characters&quot;&gt;Japanese characters&lt;/h3&gt;

&lt;p&gt;I sometimes leave notes in Japanese. I noticed these characters appeared on the display as unrenderable unicode blocks like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;▯▯▯&lt;/code&gt;. Leaving them this way was not a deal breaker, but through a quick exploration of the M5 API, I found there was already a pre-loaded Japanese font I could use that solved the problem. The only down side is that the granularity of font sizes with the Japanese font makes it so I have to choose between a-little-too-small and a-little-too-large (I chose large).&lt;/p&gt;

&lt;h3 id=&quot;contents-refresh-and-power-management&quot;&gt;Contents refresh and power management&lt;/h3&gt;

&lt;p&gt;I was originally hoping I could use the M5Paper for long stretches on battery power. Unfortunately, I think its age has left the battery in a vestigial state. Plus, I have a new charging port available for it, which makes keeping it plugged in all the time no longer an issue.&lt;/p&gt;

&lt;p&gt;I’ve therefore set the refresh logic going forward as:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;On battery power: refresh once an hour.&lt;/li&gt;
  &lt;li&gt;On USB power: refresh once every 5 minutes.&lt;/li&gt;
  &lt;li&gt;Also refresh on side button tap.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On redraws, the whole screen flickers black and white, then draws the text (quickly) line by line. This would be a little distracting to see in my field of view every 5 minutes, especially since the TODO list does not change that often (and I am the only one changing it). Therefore, I also added some diffing logic. Although the list is refreshed every 5 minutes, it will only redraw if the contents on screen have changed.&lt;/p&gt;

&lt;p&gt;The M5Paper does have a capacitive touch screen, but in my preliminary investigations, the deep sleep interrupt does not work the same way as it does for the hardware button. Since I’ve recently changed the refresh logic to account for unlimited USB power, I could go further and keep the ESP32 awake, add scrolling, and add other capacitive touch buttons to the display. But for now I’m happy with the light feature set.&lt;/p&gt;

&lt;p&gt;The final small task (mostly for ongoing debugging) was to add a battery level indicator to the bottom of the screen as a screen-width progress bar. I’m only using the battery level indicator to get a feel for how the internal battery handles daily usage going forward. This knowledge will inform future decisions about product direction (heavy air quotes on “product” since this is just for me).&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;If you’ve never done embedded systems development before, hopefully I’ve shown that building personal-use hardware is within reach for most software developers. There’s an infinite web of tangential devices and use cases out there (looking at you, Raspberry Pi 4 in a box in my closet). Hopefully this has given you, the reader, some ideas for fun weekend projects whose results you can enjoy every day.&lt;/p&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/m5paper-final-setup.jpg&quot; width=&quot;&quot; height=&quot;400&quot; alt=&quot;The completed M5Paper desk companion in action&quot; title=&quot;The completed M5Paper desk companion in action&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;The completed M5Paper desk companion in action&lt;/div&gt;&lt;/div&gt;
</description>
        <pubDate>Fri, 10 Oct 2025 13:39:28 -0500</pubDate>
        <link>https://twocentstudios.com/2025/10/10/an-e-ink-desk-companion-with-m5paper/</link>
        <guid isPermaLink="true">https://twocentstudios.com/2025/10/10/an-e-ink-desk-companion-with-m5paper/</guid>
        
        <category>hardware</category>
        
        <category>micropython</category>
        
        <category>claudecode</category>
        
        <category>m5paper</category>
        
        <category>esp32</category>
        
        
      </item>
    
      <item>
        <title>Giving Claude Code Eyes to See Your SwiftUI Views</title>
        <description>&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Update 2025/12/30&lt;/strong&gt;: I recommend using a simpler simulator-based flow instead of the techniques discussed in this post. See my newer post &lt;a href=&quot;/2025/12/27/closing-the-loop-on-ios-with-claude-code/&quot;&gt;Closing the Loop on iOS with Claude Code&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href=&quot;https://claude.ai/claude-code&quot;&gt;Claude Code&lt;/a&gt; works best as a multi-shot agent, iterating on a task by making changes and checking whether its attempts match the target.&lt;/p&gt;

&lt;p&gt;Let’s explore one way of giving Claude Code (henceforth “CC”) a way to use its multimodal capabilities to view the results of the SwiftUI code: &lt;a href=&quot;https://github.com/pointfreeco/swift-snapshot-testing&quot;&gt;Swift Snapshot Testing&lt;/a&gt;. We’ll look into ways to enhance its image analysis capabilities with tool calling. And finally we’ll see how well it does with the challenge of recreating a SwiftUI View from a reference image.&lt;/p&gt;

&lt;p&gt;The strategy in this post is optimized for “unit testing” SwiftUI Views in isolation (i.e. without the status bar, with flexible dimensions, etc.). We’ll briefly review other visualization strategies at the end of this post.&lt;/p&gt;

&lt;h2 id=&quot;setting-up-swift-snapshot-testing&quot;&gt;Setting up Swift Snapshot Testing&lt;/h2&gt;

&lt;p&gt;This is not a full tutorial, so here are some other walkthroughs to get you started:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.delasign.com/blog/how-to-setup-swift-testing-in-swift-or-swiftui-project-in-xcode/&quot;&gt;How to setup Swift Testing in a Swift or SwiftUI project in Xcode&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/pointfreeco/swift-snapshot-testing?tab=readme-ov-file#installation&quot;&gt;pointfreeco/swift-snapshot-testing: Installation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I created a separate target &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ViewSnapshotTests&lt;/code&gt; to isolate these kinds of tests and disabled it from running with Cmd+U alongside my main iOS target.&lt;/p&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/cc-eyes-xcode-scheme-test-panel.png&quot; width=&quot;600&quot; height=&quot;&quot; alt=&quot;Xcode scheme test panel configuration ignoring ViewSnapshotTests&quot; title=&quot;Xcode scheme test panel configuration ignoring ViewSnapshotTests&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Xcode scheme test panel configuration ignoring ViewSnapshotTests&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Our goal with this setup is to give CC a way to visually reference its work, &lt;em&gt;not&lt;/em&gt; create long-lived snapshot tests that will be maintained.&lt;/p&gt;

&lt;p&gt;Let’s add a file we’ll have CC use a template. We’ll instruct it to modify this test, run it, then reset it once the verification is complete.&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;/// ViewVerificationTests.swift&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;SnapshotTesting&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;SwiftUI&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;@testable&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mytarget&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Testing&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;@Suite(&quot;ViewVerificationTests&quot;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;@MainActor&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ViewVerificationTests&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;@Test(&quot;ViewVerificationTest&quot;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;viewVerification&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    	&lt;span class=&quot;c1&quot;&gt;// Replace with the view under test&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;view&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;EmptyView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;nf&quot;&gt;assertSnapshot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;nv&quot;&gt;of&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;view&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;layout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fixed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)),&lt;/span&gt;
            &lt;span class=&quot;nv&quot;&gt;record&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Then we can try exercising this template:&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;/// HelloWorldView.swift&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;HelloWorldView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Hello, World!&quot;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;some&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;VStack&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;spacing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;kt&quot;&gt;Text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;font&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

            &lt;span class=&quot;kt&quot;&gt;Rectangle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fill&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Color&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;blue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;frame&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;50&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;cornerRadius&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;secondary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Modify the template test&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;viewVerification&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;view&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;HelloWorldView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;nf&quot;&gt;assertSnapshot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;of&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;view&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;layout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fixed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;150&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)),&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;record&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;If you run the suite manually, the test will (as expected) fail, and a new folder and image will be created in the test directory:&lt;/p&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/cc-eyes-snapshots-file-hierarchy.png&quot; width=&quot;400&quot; height=&quot;&quot; alt=&quot;Snapshot testing file hierarchy&quot; title=&quot;Snapshot testing file hierarchy&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Snapshot testing file hierarchy&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/cc-eyes-view-verification-output.png&quot; width=&quot;200&quot; height=&quot;&quot; alt=&quot;ViewVerification test output&quot; title=&quot;ViewVerification test output&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;ViewVerification test output&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;instructions-for-claude-codes-iteration-loop&quot;&gt;Instructions for Claude Code’s iteration loop&lt;/h2&gt;

&lt;p&gt;Exactly &lt;em&gt;when&lt;/em&gt; you, the developer, decide to use Claude’s visualization depends highly on your design -&amp;gt; code workflow.&lt;/p&gt;

&lt;p&gt;For the sake of argument, let’s start by adding this instruction to CLAUDE.md:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Any time you create or modify a SwiftUI View, use the workflow defined in the &lt;strong&gt;SwiftUI View Verification Workflow&lt;/strong&gt; section below to check your work. Iterate &lt;strong&gt;at least 2 times&lt;/strong&gt; and &lt;strong&gt;up to 5 times&lt;/strong&gt; before considering your SwiftUI code complete.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The instruction is heavy handed, but will give us a baseline requirements to relax. Now let’s describe the ideal workflow to CC in detail:&lt;/p&gt;

&lt;div class=&quot;language-markdown highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;gu&quot;&gt;## SwiftUI View Verification Workflow&lt;/span&gt;

&lt;span class=&quot;gu&quot;&gt;### View Creation Workflow&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;
1.&lt;/span&gt; Create a SwiftUI View based on the developer-provided written specifications or reference image.
&lt;span class=&quot;p&quot;&gt;2.&lt;/span&gt; Run xcodegen to add the &lt;span class=&quot;sb&quot;&gt;`.swift` file to the `.xcodeproj`&lt;/span&gt;.
&lt;span class=&quot;p&quot;&gt;3.&lt;/span&gt; Modify the &lt;span class=&quot;sb&quot;&gt;`viewVerification` test in `ViewVerificationTests.swift`&lt;/span&gt; to use the new View and set the expected layout.
&lt;span class=&quot;p&quot;&gt;4.&lt;/span&gt; Run &lt;span class=&quot;sb&quot;&gt;`xcodebuild test -only-testing:&quot;ViewSnapshotTests/ViewVerificationTests&quot; -quiet`&lt;/span&gt; and ignore the expected test failure.
&lt;span class=&quot;p&quot;&gt;5.&lt;/span&gt; Read the output image &lt;span class=&quot;sb&quot;&gt;`ViewSnapshotTests/__Snapshots__/ViewVerificationTests/viewVerification.1.png`&lt;/span&gt; and compare it to the written specifications or reference image. Use any image analysis tools or techniques listed in the &quot;Image Analysis Strategies&quot; section.
&lt;span class=&quot;p&quot;&gt;6.&lt;/span&gt; Plan a list of changes to the SwiftUI View code that will bring &lt;span class=&quot;sb&quot;&gt;`viewVerification.1.png`&lt;/span&gt; closer to the written spec or reference image.
&lt;span class=&quot;p&quot;&gt;7.&lt;/span&gt; Implement the changes in the plan.
&lt;span class=&quot;p&quot;&gt;8.&lt;/span&gt; Run the command in (4) to replace the snapshot image.
&lt;span class=&quot;p&quot;&gt;9.&lt;/span&gt; Repeat steps (5) to (9) as many times as specified in previous instructions.
&lt;span class=&quot;p&quot;&gt;10.&lt;/span&gt; Once I have approved, please reset the test files and image to their original state.
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;You’ll need to heavily modify that prompt to fit with your ideal workflow and use the proper command line commands for your project. For example, if you’re going off a written spec with no particular design in mind, you could to add “make the View more beautiful” after each iteration.&lt;/p&gt;

&lt;p&gt;It’s important to note that, when using Swift Testing instead of XCTest, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xcodebuild test&lt;/code&gt; can &lt;strong&gt;only&lt;/strong&gt; target &lt;em&gt;suites&lt;/em&gt; via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-only-testing&lt;/code&gt;, &lt;strong&gt;not&lt;/strong&gt; individual tests like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;swift test&lt;/code&gt; can. For the root cause and workarounds, see &lt;a href=&quot;https://trinhngocthuyen.com/posts/tech/swift-testing-and-xcodebuild/&quot;&gt;this post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The View Modification Workflow would be subset of the View Creation Workflow. In that prompt, we tell CC to reference another section for image analysis. Below are some ImageMagick commands that could be useful.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Extract exact RGB values from specific coordinates&lt;/span&gt;
magick image.png -crop 1x1+200+300 txt:
&lt;span class=&quot;c&quot;&gt;# Output: (240,240,240,255) #F0F0F0FF grey94&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Check image dimensions and properties&lt;/span&gt;
magick identify image.png
&lt;span class=&quot;c&quot;&gt;# Output: image.png PNG 1206x2622 1206x2622+0+0 8-bit sRGB&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Get Root Mean Square Error between images&lt;/span&gt;
magick compare -verbose -metric RMSE reference.png snapshot.png null:
&lt;span class=&quot;c&quot;&gt;# Provides per-channel distortion percentages&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Generate visual difference overlay&lt;/span&gt;
magick compare reference.png snapshot.png diff_output.png
&lt;span class=&quot;c&quot;&gt;# Red areas show differences, black areas show matches&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h2 id=&quot;weak-points-of-snapshot-testing&quot;&gt;Weak points of snapshot testing&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;As far as I can tell, it’s not possible to get the full system UI wrapper with snapshot testing (e.g. the status bar).&lt;/li&gt;
  &lt;li&gt;There are built in device sizes, but they aren’t frequently updated to include new devices.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;challenge-create-a-swiftui-view-from-a-reference-image&quot;&gt;Challenge: create a SwiftUI View from a reference image&lt;/h2&gt;

&lt;p&gt;I gave CC a challenge as a way to develop the above setup and strategies. I gave it the simple users list screen from my recently re-released app &lt;a href=&quot;/2025/06/22/vinylogue-swift-rewrite/&quot;&gt;Vinylogue&lt;/a&gt;, captured directly from the simulator.&lt;/p&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/cc-eyes-vinylogue-reference.png&quot; width=&quot;300&quot; height=&quot;&quot; alt=&quot;Reference image of Vinylogue users list directly from the simulator&quot; title=&quot;Reference image of Vinylogue users list directly from the simulator&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Reference image of Vinylogue users list directly from the simulator&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Alongside the reference image, I gave a variant of the above instructions flow. I gave it some upfront hints: the font is AvenirNext; please ignore the dynamic island. Then had it run unguided for 5 iterations before stepping in and giving it more hints and tools to see how close it could get to pixel perfect.&lt;/p&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/cc-eyes-swiftui-evolution.png&quot; width=&quot;800&quot; height=&quot;&quot; alt=&quot;Evolution of SwiftUI view across 9 iterations (please view full)&quot; title=&quot;Evolution of SwiftUI view across 9 iterations (please view full)&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Evolution of SwiftUI view across 9 iterations (please view full)&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/cc-eyes-comparison-1.png&quot; width=&quot;600&quot; height=&quot;&quot; alt=&quot;Iteration 1: First blind attempt&quot; title=&quot;Iteration 1: First blind attempt&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Iteration 1: First blind attempt&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/cc-eyes-comparison-4.png&quot; width=&quot;600&quot; height=&quot;&quot; alt=&quot;Iteration 4: Improved spacing but worse background color&quot; title=&quot;Iteration 4: Improved spacing but worse background color&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Iteration 4: Improved spacing but worse background color&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/cc-eyes-comparison-6.png&quot; width=&quot;600&quot; height=&quot;&quot; alt=&quot;Iteration 6: Font weights are still wrong&quot; title=&quot;Iteration 6: Font weights are still wrong&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Iteration 6: Font weights are still wrong&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;caption-wrapper&quot;&gt;&lt;img class=&quot;caption&quot; src=&quot;/images/cc-eyes-comparison-9.png&quot; width=&quot;600&quot; height=&quot;&quot; alt=&quot;Iteration 9: After asking Gemini&apos;s help, for some reason the titles are now uppercased&quot; title=&quot;Iteration 9: After asking Gemini&apos;s help, for some reason the titles are now uppercased&quot; /&gt;&lt;div class=&quot;caption-text&quot;&gt;Iteration 9: After asking Gemini&apos;s help, for some reason the titles are now uppercased&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;challenge-results-analysis&quot;&gt;Challenge Results Analysis&lt;/h2&gt;

&lt;p&gt;Without direct prompting, even with a reference image, CC will default to system fonts and colors (to be fair, this is usually the best route if you have no specific design spec). I had to give it pretty specific instructions to “notice” things about the image like the colors not being black and white, or the font weights being incorrect. Even using the ImageMagick techniques, CC got confused more often than not. I felt like CC had limited ability to see either absolute or relative differences in padding or sizing. After almost every step, CC thought the output was close enough and it praised itself and wanted to stop.&lt;/p&gt;

&lt;p&gt;With CC’s current image analysis capabilities, using snapshot testing isn’t a useful strategy for getting to a pixel perfect result. If your development flow involves reproducing mocks from Figma, it’d be better to provide the mock and generated web code and colors and fonts directly to CC. If your development flow involves giving CC general vibe reference shots, snapshot testing may give it a few more shots at getting it right.&lt;/p&gt;

&lt;p&gt;This technique is in research phase for me. Without putting it through its full paces, I’m guessing it doesn’t make sense at the moment to give it more than 3 iterations before putting a human in the loop.&lt;/p&gt;

&lt;h2 id=&quot;other-ways-to-give-claude-code-eyes&quot;&gt;Other ways to give Claude Code eyes&lt;/h2&gt;

&lt;p&gt;Below are a few other techniques, although I have not tried any of them enough to say how well they work for any particular workflow.&lt;/p&gt;

&lt;h3 id=&quot;full-xctest-uiautomation&quot;&gt;Full XCTest UIAutomation&lt;/h3&gt;

&lt;p&gt;It’s possible to get the full simulator output with XCTest, but more complicated to get access to the raw image. You can also simulate taps and perform navigation. But the tradeoff is that you have to set up the whole app environment even if you just want to see one view in isolation.&lt;/p&gt;

&lt;h3 id=&quot;simulator&quot;&gt;Simulator&lt;/h3&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Update 2025/12/30&lt;/strong&gt;: See my newer post &lt;a href=&quot;/2025/12/27/closing-the-loop-on-ios-with-claude-code/&quot;&gt;Closing the Loop on iOS with Claude Code&lt;/a&gt; for how to set up a simulator-based flow with CLI commands.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/cameroncooke/XcodeBuildMCP&quot;&gt;XcodeBuildMCP&lt;/a&gt; advertises simulator automation features:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;ul&gt;
    &lt;li&gt;UI Automation: Interact with simulator UI elements&lt;/li&gt;
    &lt;li&gt;Screenshot: Capture screenshots from a simulator&lt;/li&gt;
  &lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h3 id=&quot;macos-system-viewing-with-peekaboomcp&quot;&gt;macOS system viewing with PeekabooMCP&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://peekaboo.dev/&quot;&gt;PeekabooMCP&lt;/a&gt; is a macOS system-wide tool for accessing screen contents. This is more useful for developing macOS apps.&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;For anyone looking to take the next steps in CC automation in the realm of the view layer, I hope this was somewhat helpful in understanding the current landscape an capabilities.&lt;/p&gt;

&lt;p&gt;The expected shelf-life of this post is short. This post references Claude Code v1.0.51, Xcode 16.4, Swift Snapshot Testing 1.18.4.&lt;/p&gt;
</description>
        <pubDate>Sun, 13 Jul 2025 05:30:00 -0500</pubDate>
        <link>https://twocentstudios.com/2025/07/13/giving-claude-code-eyes-to-see-your-swiftui-views/</link>
        <guid isPermaLink="true">https://twocentstudios.com/2025/07/13/giving-claude-code-eyes-to-see-your-swiftui-views/</guid>
        
        <category>claudecode</category>
        
        <category>swiftui</category>
        
        <category>apple</category>
        
        <category>ios</category>
        
        
      </item>
    
  </channel>
</rss>
