<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
	<channel>
		<title>Elegant Chaos</title>
		<description>News and updates from Elegant Chaos</description>
		<link>https://elegantchaos.com</link>
		<atom:link href="https://elegantchaos.com/rss.xml" rel="self" type="application/rss+xml" />
		
        
			<item>
				<title>Fixing The ActionBuilder Plugin</title>
				
				<description>&lt;p&gt;In a &lt;a href=&quot;https://elegantchaos.com/2025/09/02/more-workflow-generation.html&quot;&gt;previous post&lt;/a&gt;, I mentioned a command line tool that I built which you can point at a Swift package, and which will generate a GitHub Actions workflow file for it.&lt;/p&gt;

&lt;p&gt;I also built a SwiftPM plugin which invokes the tool, but at some point it stopped working. Instead of running, it would just hang, for reasons unknown.&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;the-deadlock&quot;&gt;The Deadlock&lt;/h2&gt;

&lt;p&gt;The root cause turned out to be nested SwiftPM invocations.&lt;/p&gt;

&lt;p&gt;The plugin launches &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ActionBuilderTool&lt;/code&gt;, and the tool calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;swift package dump-package&lt;/code&gt; to inspect metadata and infer sensible workflow defaults.&lt;/p&gt;

&lt;p&gt;I &lt;em&gt;think&lt;/em&gt; that did work, at some point in the past, but changes in Swift Package Manager appear to have broken it. When the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;swift&lt;/code&gt; command line tool runs the plugin, which runs the tool, which runs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;swift&lt;/code&gt;, you end up with lock contention around &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index.lock&lt;/code&gt;, and everything grinds to a halt.&lt;/p&gt;

&lt;p&gt;The fix I found was to make the tool’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;swift package dump-package&lt;/code&gt; use an isolated scratch path inside a subfolder of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.build&lt;/code&gt; folder. This means that the inner invocation doesn’t fight with the outer one, and it’s still in a place that we can write to.&lt;/p&gt;

&lt;p&gt;The workaround isn’t ideal, since building to a different scratch path means doing a lot of work again, but this isn’t a command that you run that often.&lt;/p&gt;

&lt;p&gt;I’d like to find a better fix, but for now, it works.&lt;/p&gt;

&lt;p&gt;To make the behavior explicit, I added a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--called-from-plugin&lt;/code&gt; flag. The plugin uses this, but if you just want to use the tool manually, you don’t need to.&lt;/p&gt;

&lt;h2 id=&quot;unexpected-lack-of-items-in-the-bagging-area&quot;&gt;Unexpected Lack Of Items In The Bagging Area…&lt;/h2&gt;

&lt;p&gt;I thought I was done at this point, and made a new release, only to discover that it was broken and not doing anything at all.&lt;/p&gt;

&lt;p&gt;🤔&lt;/p&gt;

&lt;p&gt;I eventually tracked that down to an issue with nested sandboxes. The tool invocation was silently failing when the plugin called it, but the plugin wasn’t reporting anything. The failure was because it was trying to sandbox inside a sandbox. Invoking the inner &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;swift package dump-package&lt;/code&gt; without sandboxing seems to sort that out - and hopefully it’s still safe since the plugin itself is running sandboxed.&lt;/p&gt;

&lt;h2 id=&quot;smoke-em-if-you-got-em&quot;&gt;Smoke Em If You Got Em&lt;/h2&gt;

&lt;p&gt;I fixed the problem, eventually, after a lot of head scratching.&lt;/p&gt;

&lt;p&gt;In doing so I also improved the plugin diagnostics so it now reports a proper error if the tool exits non-zero, and explicitly checks that the expected workflow file was actually produced.&lt;/p&gt;

&lt;p&gt;I then added smoke tests for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;direct tool workflow generation&lt;/li&gt;
  &lt;li&gt;tool generation in plugin-mode&lt;/li&gt;
  &lt;li&gt;actual &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;swift package&lt;/code&gt; plugin invocation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Obviously I then used the tool on the plugin source code itself, to generate a workflow file which runs these tests in an action.&lt;/p&gt;

&lt;p&gt;Hopefully that will stop me from releasing a broken plugin next time…&lt;/p&gt;

&lt;h2 id=&quot;other-fixes-in-passing&quot;&gt;Other Fixes In Passing&lt;/h2&gt;

&lt;p&gt;While I was in there, I also tidied up a few other things in &lt;a href=&quot;https://github.com/elegantchaos/ActionBuilderCore&quot;&gt;ActionBuilderCore&lt;/a&gt;, which is the underlying library that the tool uses to do the generation:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;bumped the supported Swift versions and moved the minimum up to 5.10&lt;/li&gt;
  &lt;li&gt;updated the GH images that the workflow uses; some of them had become obsolete&lt;/li&gt;
  &lt;li&gt;reduced generated workflow noise and improved log handling&lt;/li&gt;
  &lt;li&gt;removed some stale dependencies and legacy code paths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There’s a bunch more polishing I’d like to do, but at least the “plugin hangs forever” issue is no longer one of the mysteries of the universe.&lt;/p&gt;

&lt;p&gt;Part of the reason I started doing all of this in the first place is that I decided to revive and update &lt;a href=&quot;https://actionstatus.elegantchaos.com&quot;&gt;Action Status&lt;/a&gt;, and I needed some actual running workflows to point it at. Which led me to my discovery of the broken plugin. And thus another rabbit hole was entered.&lt;/p&gt;

&lt;p&gt;More about the Action Status refresh anon…&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;Oh, if you’re interested, you can &lt;a href=&quot;https://github.com/elegantchaos/ActionBuilderPlugin&quot;&gt;find the plugin here&lt;/a&gt;.&lt;/p&gt;
</description>
				<pubDate>Fri, 27 Feb 2026 00:00:00 +0000</pubDate>
				<link>https://elegantchaos.com/2026/02/27/actionbuilderplugin-deadlock.html</link>
				<guid isPermaLink="true">https://elegantchaos.com/2026/02/27/actionbuilderplugin-deadlock.html</guid>
			</item>
        
		
        
			<item>
				<title>Localizable String Catalogues in Swift Packages</title>
				
				<description>&lt;p&gt;I’ve been slowly splitting &lt;a href=&quot;https://thestack.elegantchaos.com&quot;&gt;The Stack&lt;/a&gt; up into small internal Swift packages.&lt;/p&gt;

&lt;p&gt;Unfortunately, if you use &lt;a href=&quot;https://developer.apple.com/documentation/xcode/localizing-and-varying-text-with-a-string-catalog&quot;&gt;localizable string catalogues&lt;/a&gt; in Xcode, there are a few wrinkles.&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;lots-of-small-targets&quot;&gt;Lots Of Small Targets&lt;/h2&gt;

&lt;p&gt;I’ve been splitting Xcode app projects into multiple packages for a few years now.&lt;/p&gt;

&lt;p&gt;Mihaela Mihaljevic &lt;a href=&quot;https://aleahim.com/blog/extreme-packaging/&quot;&gt;wrote about something similar a while ago&lt;/a&gt;. Some of the details in that post are different, but the basic idea is similar to what I do.&lt;/p&gt;

&lt;p&gt;I find it a good way to minimize entangled dependencies, and it has other advantages.&lt;/p&gt;

&lt;p&gt;It gives you an obvious place to put a small targeted set of unit tests, it can help with SwiftUI preview performance, and it lets you build and test a lot of code without the need for Xcode.&lt;/p&gt;

&lt;h2 id=&quot;using-catalogues-in-packages&quot;&gt;Using Catalogues In Packages&lt;/h2&gt;

&lt;p&gt;If you just want localised strings in a package, the basics are straightforward:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;defaultLocalization&lt;/code&gt; in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Package.swift&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;put your catalogue in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Sources/MyTarget/Resources/Localizable.xcstrings&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;tell Package.swift to process the resource file&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Something like this:&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;package&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Package&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;StackCore&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;defaultLocalization&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;en&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;targets&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;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;StackCommands&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;nv&quot;&gt;resources&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;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Resources&quot;&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;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you’re only using Xcode, this mostly works.&lt;/p&gt;

&lt;h2 id=&quot;which-catalogue-was-it-again&quot;&gt;Which Catalogue Was It Again?&lt;/h2&gt;

&lt;p&gt;By default, localization lookup assumes that the strings are in your main bundle.&lt;/p&gt;

&lt;p&gt;That is fine for apps, but not so fine for strings that actually live inside a package target.&lt;/p&gt;

&lt;p&gt;You &lt;em&gt;can&lt;/em&gt; force this to work in a library target by being explicit:&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kt&quot;&gt;LocalizedStringResource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;show.active&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;bundle&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;module&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That works, but it is noisy, and is particularly clunky in SwiftUI views where the visual clutter detracts from otherwise clean code. It is also easy to forget to add the extra bundle parameter.&lt;/p&gt;

&lt;p&gt;A nicer option is to get Swift to generate symbols for your localization keys, and to use these directly from code:&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;localized&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;actionNew&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;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;settingsHotkeyHelp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;appName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;One benefit of this is that if the key changes, the generated symbol name will change, and your code won’t build. No more mismatched keys.&lt;/p&gt;

&lt;p&gt;However, another benefit specifically useful for libraries is that those generated constants carry the right context. If you use the generated symbol in a place that expects a localizable string, it will automatically pull it from the correct bundle.&lt;/p&gt;

&lt;p&gt;This is nice. Less boilerplate, fewer mistakes with keys, and localizations close to the code that uses them.&lt;/p&gt;

&lt;h2 id=&quot;building-outside-xcode&quot;&gt;Building Outside Xcode&lt;/h2&gt;

&lt;p&gt;There is another wrinkle if you build packages from the command line.&lt;/p&gt;

&lt;p&gt;If you build within Xcode, or with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xcodebuild&lt;/code&gt; command line tool, symbol generation 
for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.xcstrings&lt;/code&gt; happens automatically, even for SwiftPM library targets.&lt;/p&gt;

&lt;p&gt;If you build with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;swift build&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;swift test&lt;/code&gt; however, it doesn’t. This is because Swift Package Manager does not support string catalogs out of the box.&lt;/p&gt;

&lt;p&gt;Luckily, there is a command line tool to do the required work:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;xcrun xcstringstool generate-symbols ...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It’s easy enough to write a small SwiftPM plugin to call that tool for each of your targets.&lt;/p&gt;

&lt;p&gt;That solves command-line builds, but introduces a new problem: when Xcode does its own generation, it will run your plugin too, so you end up with duplicate symbols.&lt;/p&gt;

&lt;p&gt;The way around this is to only apply the plugin when the build is &lt;em&gt;not&lt;/em&gt; being driven by Xcode.&lt;/p&gt;

&lt;p&gt;In your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Package.swift&lt;/code&gt; you can work around this with a small environment check&lt;sup id=&quot;fnref:1&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;#if canImport(Darwin)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Darwin&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;buildingInXcode&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Context&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;__CFBundleIdentifier&quot;&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;contains&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Xcode&quot;&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;kc&quot;&gt;false&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;#else&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;buildingInXcode&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;#endif&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;localizationPlugins&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;Target&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;PluginUsage&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;buildingInXcode&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;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;plugin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;StackStringCatalogSymbols&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then targets that own catalogues just use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;plugins: localizationPlugins&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It’s a little bit of plumbing, but it keeps both worlds happy: Xcode builds and command-line SwiftPM builds.&lt;/p&gt;

&lt;h2 id=&quot;other-possible-patterns&quot;&gt;Other Possible Patterns&lt;/h2&gt;

&lt;p&gt;I did wonder whether having all the localizations split up like this is actually a good idea in the first place? You do end up with a lot of different catalogues, which could be hard to maintain.&lt;/p&gt;

&lt;p&gt;Maybe, instead, localization should be a layer that you only add at the application level? That way they’re all in one place, and different apps could even use the same library but overlay different localizations.&lt;/p&gt;

&lt;p&gt;Unfortunately, if you want to use the generated string symbols in your code, your code needs to be able to see them. If the code is in a library, the symbol has to also be generated by that library, or visible to it.&lt;/p&gt;

&lt;p&gt;Following this line of reasoning a bit further, could I make a library target with all the localizations in it, and then import that from the libraries that want to use the symbols?&lt;/p&gt;

&lt;p&gt;This would still tie a library to a specific set of localizations for its keys, but at least all the strings would be in a single catalogue, which might be easier to manage.&lt;/p&gt;

&lt;p&gt;Sadly this doesn’t work either, because the code generator, that creates the symbols, declares them as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;internal&lt;/code&gt;. You don’t get a free, public, cross-package symbol API that every other target can just import and use 😢.&lt;/p&gt;

&lt;p&gt;For now, I’ve stuck with per-target catalogues in the Stack, each local to the package that owns the UI or command surface.&lt;/p&gt;

&lt;p&gt;This feels consistent with the rest of the architecture. Generally the Stack code is factored into services that are responsible for one aspect of the application. Each service target owns its commands, views, tests, logic and resources - including strings.&lt;/p&gt;

&lt;p&gt;Currently, I like this setup. Your Mileage May Vary™.&lt;/p&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:1&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;Thanks to &lt;a href=&quot;https://github.com/gwynne&quot;&gt;Gwynne&lt;/a&gt; for pointing me in the right direction with this check. I originally tried importing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Foundation&lt;/code&gt; and looking at the environment with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProcessInfo&lt;/code&gt;, but I couldn’t find something that was reliably set by Xcode and not by SwiftPM. Using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Context&lt;/code&gt; works, and also removes the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Foundation&lt;/code&gt; import, which is apparently best avoided. &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</description>
				<pubDate>Thu, 12 Feb 2026 00:00:00 +0000</pubDate>
				<link>https://elegantchaos.com/2026/02/12/string-catalogues.html</link>
				<guid isPermaLink="true">https://elegantchaos.com/2026/02/12/string-catalogues.html</guid>
			</item>
        
		
        
			<item>
				<title>SwiftUI Navigation Pain</title>
				
				<description>&lt;p&gt;One of the things I’ve wanted to do with &lt;a href=&quot;https://elegantchaos.com/2025/12/04/the-stack.html&quot;&gt;The Stack&lt;/a&gt; is to handle all of the user’s interactions with some sort of &lt;a href=&quot;https://elegantchaos.com/2025/09/09/swiftui-action-abstraction.html&quot;&gt;action abstraction&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It turns out that this is harder than it ought to be with the modern SwiftUI navigation mechanisms.&lt;/p&gt;

&lt;!--more--&gt;

&lt;p&gt;SwiftUI’s modern navigation uses &lt;a href=&quot;https://developer.apple.com/documentation/swiftui/understanding-the-composition-of-navigation-stack&quot;&gt;NavigationStack&lt;/a&gt; instead of the older NavigationView, and it’s definitely an improvement on what came before.&lt;/p&gt;

&lt;p&gt;The basic idea is:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;you bind the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NavigationStack&lt;/code&gt; view to some sort of model representing the route/path that is currently visible&lt;/li&gt;
  &lt;li&gt;ideally you use the type-erased &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NavigationPath&lt;/code&gt; for this purpose&lt;/li&gt;
  &lt;li&gt;whenever possible, you let SwiftUI manipulate the route for you using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NavigationLink&lt;/code&gt;, the default back button, and/or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Environment(\.dismiss)&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;this pushes items onto the route and pops them off (you can also do this explicitly yourself)&lt;/li&gt;
  &lt;li&gt;you use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.navigationDestination&lt;/code&gt; to teach SwiftUI which view to use to back each kind of item&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Doing this has some nice benefits.&lt;/p&gt;

&lt;p&gt;The view and models stay loosely coupled, and there is no one place in the code where you have to tell the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NavigationStack&lt;/code&gt; about everything that might get pushed onto it.&lt;/p&gt;

&lt;p&gt;The places where you do the pushing/popping can work in terms of the model or some other abstract notion of what is being presented, without needing to know anything about that actual views that will be used.&lt;/p&gt;

&lt;p&gt;You can also put your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NavigationPath&lt;/code&gt; instance into a router class and inject that into the environment. This lets you fish it out from anywhere and manipulate the route explicitly. For example you can implement deep links by splitting them up into items and pushing those items into the route. SwiftUI will see that the route has changed, and sort out the details of animating from where you were to where you’re going.&lt;/p&gt;

&lt;h2 id=&quot;actions&quot;&gt;Actions&lt;/h2&gt;

&lt;p&gt;For &lt;a href=&quot;https://elegantchaos.com/2025/12/04/the-stack.html&quot;&gt;The Stack&lt;/a&gt; (and my other apps), I want to define all the user’s interactions as abstract actions. This includes navigation actions.&lt;/p&gt;

&lt;p&gt;I want each thing that the user can do to be represented by something that implements a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Command&lt;/code&gt; protocol. The user will tap or click something, which will cause a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Command&lt;/code&gt; to be performed, and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;perform&lt;/code&gt; method of the command will do the actual mutations of the model or view-model. This will probably result in one or more updates to the UI, which the user will then interact with, and the cycle will repeat.&lt;/p&gt;

&lt;p&gt;One reason for doing things this way is to allow the same actions to be triggered from multiple places without duplicating code. I can define a single &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Command&lt;/code&gt; for something like making a new note, and have a menu item, a button, a toolbar item, an app intent, or a siri command all invoke it.&lt;/p&gt;

&lt;p&gt;This is especially helpful when targetting an application on multiple platforms that support different interaction mechanisms.&lt;/p&gt;

&lt;p&gt;Another reason is that I can write common code once to take any &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Command&lt;/code&gt; and have it build the UI for me. If my command abstraction has enough information (for example a label and icon to use to represent the command, maybe an optional shortcut or tooltip), then some generic code can create a button for the command, implement the app intent for the command, insert the menu item for the command, and so on. This reduces boilerplate, and enhances consistency.&lt;/p&gt;

&lt;p&gt;A third reason is that I can use this abstraction to support undo/redo at a level that makes sense to me. Foundation’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UndoManager&lt;/code&gt; has been around a long time, and is integrated into a lot of systems, including SwiftData, but it works at too granular a level for me. On this topic, I very much agree with what Drew McCormack says &lt;a href=&quot;https://forums.swift.org/t/undomanager-custom-actor-and-callbacks-oh-my/70722/8&quot;&gt;in this comment&lt;/a&gt;. I would prefer to define undo at a high level.&lt;/p&gt;

&lt;p&gt;Finally, funnelling everything that the user does through a single mechanism allows me to support analytics and auditing.&lt;/p&gt;

&lt;p&gt;I don’t want to know what any specific user is doing, but I would like some general analytics on usage patterns. What features to people use? What do they miss? After using feature A, do they usually choose B, or C (or rage quit in disgust!)?&lt;/p&gt;

&lt;p&gt;Also, there is some need to monetize the applications I make! I have always been a proponent of what I would call “true micropayments”, by which I mean really charging tiny amounts for small actions, such that the user genuinely pays nothing if they don’t launch your app for weeks or months.&lt;/p&gt;

&lt;p&gt;I’d like to try to do monetization this way. In order to do this, I need a reliable mechanism to record not what actions my users are taking, but perhaps just how many actions they’ve taken. I can then come up with a formula which ties this to real money in some way.&lt;/p&gt;

&lt;h3 id=&quot;swiftui-navigation-vs-actions&quot;&gt;SwiftUI Navigation vs Actions&lt;/h3&gt;

&lt;p&gt;Unfortunately, using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NavigationLink&lt;/code&gt;, the default back button, and/or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Environment(\.dismiss)&lt;/code&gt; does not lend itself particularly well to the action-based approach I’ve outlined above.&lt;/p&gt;

&lt;p&gt;If I use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NavigationLink&lt;/code&gt;, there is no way to ask it to invoke one of my actions when it is selected. Similarly when the user taps the default back button, or a button that calls the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dismiss()&lt;/code&gt; method supplied by the environment.&lt;/p&gt;

&lt;p&gt;The view-model manipulation of the route is just done for you in these cases. That means that if you want all of your navigation to be done through actions, you’re out of luck unless you make your own UI elements. This isn’t &lt;em&gt;hard&lt;/em&gt; to do, but it is a bit annoying, and you can lose behaviour that you’d otherwise get for free. Given the fragility of SwiftUI, I prefer to give it as much semantic information as I can. Using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NavigationLink&lt;/code&gt; tells SwiftUI more about what’s really going on, compared to just using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Button&lt;/code&gt;. I don’t like the fact that I might be losing some clever behaviour (possibly &lt;em&gt;future&lt;/em&gt; behaviour) if I don’t do that.&lt;/p&gt;

&lt;h3 id=&quot;watching-the-path&quot;&gt;Watching The Path&lt;/h3&gt;

&lt;p&gt;A possibly solution to this problem is to allow SwiftUI to manipulate the route, but bind to the path variable and watch it for changes - for example using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.onChange&lt;/code&gt; in my root content view.&lt;/p&gt;

&lt;p&gt;When a change to the path happens, we can work out what it is and issue a ‘fake’ command to our action system, so that the interaction is recorded and can be audited, undone, etc. This isn’t great, but it might be better than the alternative of abandoning the recommended UI mechanisms.&lt;/p&gt;

&lt;p&gt;Unfortunately this is pretty much impossible to achieve if you use SwiftUI’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NavigationPath&lt;/code&gt; to represent your route!&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NavigationPath&lt;/code&gt; is a type-erased list of the items in your stack. The type erasure is good in that it works regardless of what you throw into it, which in turn means that you can use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.navigationDestination&lt;/code&gt; the way that it is intended to be used, which is to declare an individual destination for each type that you place on the stack. This is important for keeping things loosely coupled - and so using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NavigationPath&lt;/code&gt; is recommended.&lt;/p&gt;

&lt;p&gt;Unfortunately, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NavigationPath&lt;/code&gt; only lets you manipulate the path by adding or removing items from it, and the only information you can obtain about the current state is how many items are on it. At the point that a user taps on a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NavigationLink&lt;/code&gt;, SwiftUI still has enough information to supply the linked value to the closure that you gave to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.navigationDestination&lt;/code&gt; as an actual typed instance.&lt;/p&gt;

&lt;p&gt;Sadly, it doesn’t seem to allow you retrieve a value from the path later. You can’t say, for example, “what’s the value on the top of the stack?”.&lt;/p&gt;

&lt;p&gt;The upshot of this is that if you watch for changes to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NavigationPath&lt;/code&gt; instance, all you can tell is that its length has changed!&lt;/p&gt;

&lt;p&gt;If the length reduced, you know that the user popped, but if the length increased, you know that the user pushed &lt;em&gt;something&lt;/em&gt;, but not what it was. This is not much use for generating fake navigation commands!&lt;/p&gt;

&lt;h3 id=&quot;not-using-navigationpath&quot;&gt;Not Using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NavigationPath&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;There is an alternative approach.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NavigationStack&lt;/code&gt; allows you to supply a binding to a collection of anything hashable, and so you don’t strictly have to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NavigationPath&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You could, instead, define an enum with all the various kinds of things you want to push, and bind to an array of your enum type.&lt;/p&gt;

&lt;p&gt;This works, and when you watch your array, you know exactly what kinds of things it contains, and so you can issue the relevant fake commands.&lt;/p&gt;

&lt;p&gt;The downside of this approach is that there’s more coupling. You have a single enum that has to know about all of the things that you are going to push, and you will end up with a single &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.navigationDestination&lt;/code&gt; call for the enum type.&lt;/p&gt;

&lt;h2 id=&quot;imperfect-solutions&quot;&gt;Imperfect Solutions&lt;/h2&gt;

&lt;p&gt;Binding to a path which is an array of enum (or some other custom type) values isn’t &lt;em&gt;wrong&lt;/em&gt;, per-se. It is probably what I’ll need to do if I want to fully support recording commands for all of the user’s navigations.&lt;/p&gt;

&lt;p&gt;It seems to me that it defeats much of the point of the way &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NavigationStack&lt;/code&gt; has been designed though.&lt;/p&gt;

&lt;p&gt;In any case, personally I really wish I didn’t have to take the approach of watching mutations to the path in the first place 😢.&lt;/p&gt;

&lt;p&gt;It would be far better if I could register a single hook with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NavigationStack&lt;/code&gt;. This would be called with a path manipulation to perform (push/pop/reset), and allowed to do it however it wanted to. I could use this hook to dispatch commands through my action system, and they would change the path.&lt;/p&gt;

&lt;p&gt;This would allow me use all of the other navigation mechanisms that SwiftUI has supplied me.&lt;/p&gt;

&lt;p&gt;Currently I’ve adopted a hybrid approach:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;I don’t watch the path.&lt;/li&gt;
  &lt;li&gt;I use custom buttons instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NavigationLink&lt;/code&gt;, and they issue navigation commands to change the route&lt;/li&gt;
  &lt;li&gt;I use the default SwiftUI back button, and accept that I’m not currently capturing the user popping views off the stack&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;tune-in-next-time&quot;&gt;Tune In Next Time…&lt;/h2&gt;

&lt;p&gt;Another possible approach to this problem might be to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.onAppear&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.onDisappear&lt;/code&gt; on the views themselves, and somehow have them generate the navigation events that way.&lt;/p&gt;

&lt;p&gt;This would also solve another problem that I have, which is that I’ve some global UI components that I would like to always be present, but to react differently depending on what’s currently at the top of the navigation stack.&lt;/p&gt;

&lt;p&gt;Unfortunately, relying on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.onAppear&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.onDisappear&lt;/code&gt; also has a number of problems!&lt;/p&gt;

&lt;p&gt;Originally I’d intended to cover those in this post too, but it’s getting a bit wordy, so I think I will leave that one for another day…&lt;/p&gt;

&lt;h2 id=&quot;am-i-holding-it-wrong&quot;&gt;Am I Holding It Wrong?&lt;/h2&gt;

&lt;p&gt;As always, this post reflects my imperfect understanding of SwiftUI. Have I missed something? If so, let me know. I’d be delighted!&lt;/p&gt;
</description>
				<pubDate>Fri, 12 Dec 2025 00:00:00 +0000</pubDate>
				<link>https://elegantchaos.com/2025/12/12/navigation-pain.html</link>
				<guid isPermaLink="true">https://elegantchaos.com/2025/12/12/navigation-pain.html</guid>
			</item>
        
		
        
			<item>
				<title>All Quiet In The Western Isles</title>
				
				<description>&lt;p&gt;I’ve been a bit quiet and not posted for the last few weeks.&lt;/p&gt;

&lt;p&gt;This is down to a combination of holidays, minor sickness, distractions, and beavering away preparing an MVP of &lt;a href=&quot;https://elegantchaos.com/2025/12/04/the-stack.html&quot;&gt;The Stack&lt;/a&gt;, which I’ve now released.&lt;/p&gt;

&lt;p&gt;I’ve also got a bit of a backlog of other things I mean to post about…&lt;/p&gt;

&lt;!--more--&gt;

&lt;p&gt;… but they will have to wait for another day.&lt;/p&gt;

&lt;p&gt;Potential topics include:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;the solution I settled on for abstracting menus, buttons and toolbar items in SwiftUI&lt;/li&gt;
  &lt;li&gt;extending my action abstraction to support siri, shortcuts and intents&lt;/li&gt;
  &lt;li&gt;more refinements to my Release Tools&lt;/li&gt;
  &lt;li&gt;localized strings in packages&lt;/li&gt;
  &lt;li&gt;plenty more updates to The Stack&lt;/li&gt;
  &lt;li&gt;potential next projects&lt;/li&gt;
&lt;/ul&gt;
</description>
				<pubDate>Fri, 05 Dec 2025 00:00:00 +0000</pubDate>
				<link>https://elegantchaos.com/2025/12/05/all-quiet.html</link>
				<guid isPermaLink="true">https://elegantchaos.com/2025/12/05/all-quiet.html</guid>
			</item>
        
		
        
			<item>
				<title>The Stack</title>
				
				<description>&lt;p&gt;I’ve just quietly released the first version of The Stack.&lt;/p&gt;

&lt;p&gt;It represents a slightly different and fairly opinionated take on solving the To Do list / note taking problem, and I made it mostly to scratch my own itches&lt;sup id=&quot;fnref:1&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;. I’m hoping that some other people think the way I do and will also find it useful.&lt;/p&gt;

&lt;p&gt;You can download it &lt;a href=&quot;https://apps.apple.com/app/the-stack-notes-to-self/id6615075283&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For some more detailed answers as to why I made it, read on…&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;isnt-this-a-solved-problem&quot;&gt;Isn’t This A Solved Problem?&lt;/h2&gt;

&lt;p&gt;Well, yes, sort of.&lt;/p&gt;

&lt;p&gt;To Do lists are paradoxical though. Much like &lt;a href=&quot;https://youtu.be/IjGEw9UDbgk?t=120&quot; target=&quot;_blank&quot;&gt;Rimmer’s revision timetable in Red Dwarf&lt;/a&gt;, they can become a distraction and a source of anxiety.&lt;/p&gt;

&lt;h2 id=&quot;the-stack-isnt-reminders&quot;&gt;The Stack Isn’t Reminders&lt;/h2&gt;

&lt;p&gt;There are some categories of thing-to-remember where an app like Reminders is perfect. It’s helpful to be able to set alerts, especially . It’s helpful to be able to have multiple lists, and to share them with other people.&lt;/p&gt;

&lt;p&gt;I use Reminders to alert me to things that repeat periodically (“file your taxes”, “put the bins out”, etc). I also use it to share a shopping list with my partner.&lt;/p&gt;

&lt;p&gt;The Stack definitely isn’t trying to be that kind of app.&lt;/p&gt;

&lt;h2 id=&quot;the-stack-isnt-a-methodology&quot;&gt;The Stack Isn’t A Methodology&lt;/h2&gt;

&lt;p&gt;There are some mature methodologies in this problem space - one of the best known being Get Things Done.&lt;/p&gt;

&lt;p&gt;There are some great apps out there to support these methodologies, and if you swear by a methodology then you probably want an app that supports you - lets you put tasks into buckets, or give them colors, or group them in a way that makes sense for the methodology.&lt;/p&gt;

&lt;p&gt;The Stack isn’t trying to be that kind of app either. Or, at least, it’s not trying to support another methodology. Arguably, it’s trying to introduce a new one, albeit a very lightweight one.&lt;/p&gt;

&lt;h2 id=&quot;the-stack-isnt-an-issue-tracker&quot;&gt;The Stack Isn’t An Issue Tracker&lt;/h2&gt;

&lt;p&gt;When trying to keep track of tasks, at the heavier end of the spectrum are fully-fledged issue tracking system.&lt;/p&gt;

&lt;p&gt;If you’re in a team, the ability to share tasks with everyone, prioritize them and plan them, set deadlines, add keywords, etc, etc, is essential.&lt;/p&gt;

&lt;p&gt;Even if you’re working on your own, the features of issue trackers may be helpful for you.&lt;/p&gt;

&lt;p&gt;There are plenty of issue tracker apps out there for you. The Stack isn’t one of them!&lt;/p&gt;

&lt;h2 id=&quot;so-whats-left&quot;&gt;So What’s Left?&lt;/h2&gt;

&lt;p&gt;The full name of my app is actually “The Stack - Notes To Self”, and that should give you a clue as to where I think it fits.&lt;/p&gt;

&lt;p&gt;When I’m in the middle of something, and an idea pops into my head, I just want to record it and move on. This is what I call a “note to self”.&lt;/p&gt;

&lt;p&gt;What matters most to me in this situation is just to capture the thought.&lt;/p&gt;

&lt;p&gt;I don’t want to have to think about which list it goes in. I don’t really want to have to think about dates or priorities or any of that stuff.&lt;/p&gt;

&lt;p&gt;I find having to make those decisions is off-putting and can slow me down or even paralyze me into indecision.&lt;/p&gt;

&lt;p&gt;Even as a solo software developer, I find that I don’t really need a proper issue tracker a lot of the time. I’ve used many different trackers, and have come to realize that the simpler they were, the fewer options and fields they had, the more I liked them.&lt;/p&gt;

&lt;p&gt;The Stack is built for these situations.&lt;/p&gt;

&lt;p&gt;It isn’t intended to replace your shopping list app, or your issue tracking database, although it can, if you want it to.&lt;/p&gt;

&lt;p&gt;It can complement a methodology-based app (or perhaps, it can replace the methodology with a simpler one?).&lt;/p&gt;

&lt;h2 id=&quot;triage&quot;&gt;Triage?&lt;/h2&gt;

&lt;p&gt;The Stack is essentially just a sequential list of notes, with the latest at the top.&lt;/p&gt;

&lt;p&gt;You may be worried that this will rapidly get too big and unruly, and you’ll not be able to find anything.&lt;/p&gt;

&lt;p&gt;I can understand this concern, and if you want, you can think of it as just being for initial capture of thoughts.&lt;/p&gt;

&lt;p&gt;You can then triage them periodically and move into other more permanent places, that the the stack itsef stays quite light and empty.&lt;/p&gt;

&lt;p&gt;I don’t use it that way, but you can if you want.&lt;/p&gt;

&lt;h2 id=&quot;a-big-heap-o-stuff&quot;&gt;A Big Heap ‘O Stuff&lt;/h2&gt;

&lt;p&gt;My idea is that it will work even when there are lots of notes in the stack, and it feels a bit like a big heap of unsorted stuff.&lt;/p&gt;

&lt;p&gt;Embrace the Chaos, I say!&lt;/p&gt;

&lt;p&gt;It is perhaps counter-intuitive, but I actually suspect that many notes get added to many to-do lists in many apps, and then never actually actioned.&lt;/p&gt;

&lt;p&gt;My thesis is that it’s more important to just capture quickly and move, on. Later, when you’re looking for something, The Stack can help you find it.&lt;/p&gt;

&lt;p&gt;There are three main mechanisms for this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ordering&lt;/strong&gt; is one. Another of my theories is that a kind of &lt;a href=&quot;https://en.wikipedia.org/wiki/Locality_of_reference&quot; target=&quot;_blank&quot;&gt;locality of reference&lt;/a&gt; applies here. The things near the top of the stack, generally the things you added most recently, are often the things care about most.&lt;/p&gt;

&lt;p&gt;For the few cases where this isn’t the case, I’ve added a mechanism for pulling older items back to the top. Note that this isn’t full re-ordering. You can’t move notes around in the list, you can just pull them to the top. This is a deliberate choice!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tagging&lt;/strong&gt; is another way to organise things. You can add #hashtags into notes, just like you do on your favourite social media site. You can then filter the stack by a tag. I find that this is all I need most of the time.&lt;/p&gt;

&lt;p&gt;When I’m focussing on a task, I filter by the tag for that task, and I can see what I need to do next, in a list that isn’t too large. Periodically I will scan this list, and move items to the top if I think they are a priority.&lt;/p&gt;

&lt;p&gt;For everything else, &lt;strong&gt;free text search&lt;/strong&gt; lets you find notes that have fallen through the cracks.&lt;/p&gt;

&lt;h2 id=&quot;give-it-a-go&quot;&gt;Give It A Go&lt;/h2&gt;

&lt;p&gt;Anyway… that’s the idea behind the stack. I’m finding it useful, and will keep maintaining it for as long as I actually do. Your mileage may vary.&lt;/p&gt;

&lt;p&gt;For now it’s free. I am hoping to use it as a vehicle to test some monetisation ideas based on true micropayments, in which case you might one day have to pay something for it, but if that ever happens, it will be a small amount derived from the real usage you make. Even then, the changes are that there will be an honour-based opt out.&lt;/p&gt;

&lt;p&gt;If you’re interested in trying The Stack, download it from the app store on your &lt;a href=&quot;https://apps.apple.com/app/the-stack-notes-to-self/id6615075283&quot;&gt;phone&lt;/a&gt; or &lt;a href=&quot;https://apps.apple.com/us/app/the-stack-notes-to-self/id6615075283?platform=mac&quot;&gt;mac&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you like it and would be interested in helping me test it, there’s also a public &lt;a href=&quot;https://testflight.apple.com/join/unjNz9fh&quot;&gt;Testflight&lt;/a&gt; link.&lt;/p&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:1&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;Also, of course, The Stack exists as a place for me to experiment with new Apple technologies, and generally keep my skills up to date. One day it might also be a source of income, but for now, it’s free. &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</description>
				<pubDate>Thu, 04 Dec 2025 00:00:00 +0000</pubDate>
				<link>https://elegantchaos.com/2025/12/04/the-stack.html</link>
				<guid isPermaLink="true">https://elegantchaos.com/2025/12/04/the-stack.html</guid>
			</item>
        
		
        
			<item>
				<title>App Storage</title>
				
				<description>&lt;p&gt;Whilst adding some settings to &lt;a href=&quot;https://thestack.elegantchaos.com&quot;&gt;The Stack&lt;/a&gt;&lt;sup id=&quot;fnref:1&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;, I was reminded of something that has bugged me for a while about SwiftUI’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@AppStorage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In a classic act of &lt;a href=&quot;https://en.wiktionary.org/wiki/yak_shaving&quot;&gt;yak shaving&lt;/a&gt;, I decided to fix the annoyance instead of doing the task I actually set out to do…&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;is-that-the-dry-violation-klaxon-i-hear&quot;&gt;Is That The DRY-Violation Klaxon I Hear?&lt;/h2&gt;

&lt;p&gt;SwiftUI’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@AppStorage&lt;/code&gt; macro is a neat way to access a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UserDefaults&lt;/code&gt; value from within a SwiftUI view&lt;sup id=&quot;fnref:2&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:2&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;You use it like this:&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ContentView&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;kd&quot;&gt;@AppStorage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;username&quot;&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;username&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;Anonymous&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;kd&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;kt&quot;&gt;Text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Welcome, &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;!&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

            &lt;span class=&quot;kt&quot;&gt;Button&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Log in&quot;&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;n&quot;&gt;username&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;@samdeane&quot;&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;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The ability to declare a default value is useful for the first time the user runs the app.&lt;/p&gt;

&lt;p&gt;Unfortunately though, for most apps of a non-trivial size, you are likely to have some user-interface code in one place to allow the user to change the setting, and some other UI or application logic elsewhere that reads the value.&lt;/p&gt;

&lt;p&gt;Which leaves you with something more like this:&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;
&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;SettingsView&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;kd&quot;&gt;@AppStorage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;doTheThing&quot;&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;doTheThing&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;kc&quot;&gt;true&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;kd&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;Toggle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;isOn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doTheThing&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;s&quot;&gt;&quot;Show the button to do the thing?&quot;&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;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AppLogicView&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;kd&quot;&gt;@AppStorage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;doTheThing&quot;&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;doTheThing&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;kc&quot;&gt;false&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;kd&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;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doTheThing&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;Button&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Do The Thing!) {
          performTheThing()
        }
      }
    }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h3 id=&quot;shome-mishtake-shurely&quot;&gt;Shome Mishtake Shurely?&lt;/h3&gt;

&lt;p&gt;Did you spot my deliberate mistake?&lt;/p&gt;

&lt;p&gt;I set the default to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;true&lt;/code&gt; in one view, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;false&lt;/code&gt; in the other.&lt;/p&gt;

&lt;p&gt;My assumption is that what will actually happen here will depend on which view the user visits first, in the situation where they don’t already have a value set for the setting.&lt;/p&gt;

&lt;p&gt;Which is… non-optimal.&lt;/p&gt;

&lt;p&gt;It also bothers me that I have to type the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;doTheThing&quot;&lt;/code&gt; key in both places.&lt;/p&gt;

&lt;p&gt;I might change it or get it wrong in one place and not the other.&lt;/p&gt;

&lt;p&gt;I can even potentially use a different type for the same setting in different places.&lt;/p&gt;

&lt;h2 id=&quot;a-better-way&quot;&gt;A Better Way™&lt;/h2&gt;

&lt;p&gt;After thinking about this for a bit, I added the following extension&lt;sup id=&quot;fnref:3&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:3&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;import SwiftUI

public struct AppStorageKey&amp;lt;Value&amp;gt; {
  let key: String
  let defaultValue: Value
  
  public init(_ key: StringLiteralType, defaultValue: Value) {
    self.key = key
    self.defaultValue = defaultValue
  }
  
}

public extension AppStorage {
  init(_ key: AppStorageKey&amp;lt;Value&amp;gt;, store: UserDefaults? = nil) where Value == Bool {
    self.init(wrappedValue: key.defaultValue, key.key, store: store)
  }
  
  init(_ key: AppStorageKey&amp;lt;Value&amp;gt;, store: UserDefaults? = nil) where Value == Int {
    self.init(wrappedValue: key.defaultValue, key.key, store: store)
  }
  
  init(_ key: AppStorageKey&amp;lt;Value&amp;gt;, store: UserDefaults? = nil) where Value == Double {
    self.init(wrappedValue: key.defaultValue, key.key, store: store)
  }
  
  init(_ key: AppStorageKey&amp;lt;Value&amp;gt;, store: UserDefaults? = nil) where Value == String {
    self.init(wrappedValue: key.defaultValue, key.key, store: store)
  }
  
  init(_ key: AppStorageKey&amp;lt;Value&amp;gt;, store: UserDefaults? = nil) where Value == URL {
    self.init(wrappedValue: key.defaultValue, key.key, store: store)
  }
  
  init(_ key: AppStorageKey&amp;lt;Value&amp;gt;, store: UserDefaults? = nil) where Value == Date {
    self.init(wrappedValue: key.defaultValue, key.key, store: store)
  }
  
  init(_ key: AppStorageKey&amp;lt;Value&amp;gt;, store: UserDefaults? = nil) where Value == Data {
    self.init(wrappedValue: key.defaultValue, key.key, store: store)
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I can use this in my application:&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extension&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AppStorageKey&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Bool&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;doTheThing&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AppStorageKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;doTheThing&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;defaultValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&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;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;SettingsView&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;kd&quot;&gt;@AppStorage&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;doTheThing&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;doTheThing&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;kd&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;Toggle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;isOn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doTheThing&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;s&quot;&gt;&quot;Show the button to do the thing?&quot;&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;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AppLogicView&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;kd&quot;&gt;@AppStorage&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;doTheThing&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;doTheThing&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;kd&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;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doTheThing&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;Button&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Do The Thing!) {
          performTheThing()
        }
      }
    }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;These declarations can all live in different swift files (or even different packages).&lt;/p&gt;

&lt;p&gt;I can define the settings key, type and default value once.&lt;/p&gt;

&lt;p&gt;I get type inference everywhere that I use @AppStorage in this way, so I don’t have to declare a type.&lt;/p&gt;

&lt;p&gt;As a bonus, when I type &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@AppStorage(.&lt;/code&gt; Xcode will auto-suggest any static AppStorageKey values that it knows about.&lt;/p&gt;

&lt;p&gt;Which is nice…&lt;/p&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:1&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;I’m still looking for Test Flight testers for this. If you’d be up for giving it a go - even just to help me out by confirming that it doesn’t immediately explode, then please &lt;a href=&quot;https://testflight.apple.com/join/unjNz9fh&quot;&gt;sign up&lt;/a&gt;. &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:2&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;Or modifier, command, observable object, or various other SwiftUI structures. &lt;a href=&quot;#fnref:2&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:3&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;The repetition here with different types is ugly, but it mirrors the way that AppStorage defines its own API. It would be nice if there was a better way, but I’ve not found it yet. &lt;a href=&quot;#fnref:3&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</description>
				<pubDate>Thu, 30 Oct 2025 00:00:00 +0000</pubDate>
				<link>https://elegantchaos.com/2025/10/30/appstorage.html</link>
				<guid isPermaLink="true">https://elegantchaos.com/2025/10/30/appstorage.html</guid>
			</item>
        
		
        
			<item>
				<title>Caveat Emptor</title>
				
				<description>&lt;p&gt;I was looking back on some of my old blog posts from 2021 earlier&lt;sup id=&quot;fnref:1&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;, and I came across something that I thought bears repeating.&lt;/p&gt;

&lt;p&gt;It was from &lt;a href=&quot;https://elegantchaos.com/2021/04/30/matchable.html&quot;&gt;this post&lt;/a&gt;, and the section I though was worth re-visiting was called “Caveat Emptor”.&lt;/p&gt;

&lt;p&gt;It was about how my open source projects are often unfinished, and unpolished, and very much work in progress.&lt;/p&gt;

&lt;p&gt;What I said then still applies today, and I’ll repeat it below.&lt;/p&gt;

&lt;!--more--&gt;

&lt;p&gt;As mentioned above, the context for the following is an old blog post, and so when it says “this post”, or “this project”, it’s talking about &lt;a href=&quot;https://elegantchaos.com/2021/04/30/matchable.html&quot;&gt;the original post&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;caveat-emptor&quot;&gt;Caveat Emptor&lt;/h2&gt;

&lt;p&gt;Part of the barrier to telling people about things I’ve done is sheer time it takes to write even quite a simple post like this one.&lt;/p&gt;

&lt;p&gt;So my first disclaimer is just to say that this post is mostly a re-hash of the README file from the Github Repository. Nothing wrong with that I think, but just to be clear…&lt;/p&gt;

&lt;p&gt;My second disclaimer is that this is work-in-progress code from the real world.&lt;/p&gt;

&lt;p&gt;I’ve encountered a few people who subscribe to a fundamentalist view of open source code: that it’s useless unless it is fully polished, fully tested, 100% supported and actively maintained.&lt;/p&gt;

&lt;p&gt;I understand this point of view; we’ve all encountered code that makes great claims and turns out to be broken or mostly unfinished.&lt;/p&gt;

&lt;p&gt;Respectfully though, those people are wrong.&lt;/p&gt;

&lt;p&gt;Imperfect open-source code can be frustrating. However, it can also be a helpful foundation for someone else to build on, a good example of the pros and cons of particular technique, or a useful supplier of that one crucial line you have been searching the internet for.&lt;/p&gt;

&lt;p&gt;Aiming for perfection is setting the barrier way too high. I am as insecure as the next person when it comes to showing my workings in public. I’ve been a professional programmer for more than three decades, but I still suffer from impostor syndrome.&lt;/p&gt;

&lt;p&gt;It’s tempting to hide away, but I’m trying to fight the urge, and I’d like to contribute in some small way to an environment where we aren’t scared to risk being wrong.&lt;/p&gt;

&lt;p&gt;I offer up all of my open-source code in this spirit. It’s not perfect, because I am busy, and because I am still writing it. I find this code useful, and I hope someone else might. If you do find that it is fundamentally broken, please tell me why. That way I learn something.&lt;/p&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:1&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;Which sounds a little bit narcissistic now that I say it out loud. Ah well - bite me. I fell into a rabbit hole ok, and it just so happens to be one of my own making. &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</description>
				<pubDate>Fri, 17 Oct 2025 00:00:00 +0000</pubDate>
				<link>https://elegantchaos.com/2025/10/17/caveat-emptor.html</link>
				<guid isPermaLink="true">https://elegantchaos.com/2025/10/17/caveat-emptor.html</guid>
			</item>
        
		
        
			<item>
				<title>Release Tools Tagging</title>
				
				<description>&lt;p&gt;After &lt;a href=&quot;https://elegantchaos.com/2025/09/26/release-tools.html&quot;&gt;my last post&lt;/a&gt;, I thought a bit more about Release Tools, and decided that requiring a tag was definitely the right way to go, and over a couple of days last week I implemented it.&lt;/p&gt;

&lt;p&gt;I also decided that it was a big enough change that I may as well call this Release Tools 4.0, and take the opportunity to clean up and remove some legacy code.&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;tagging&quot;&gt;Tagging&lt;/h2&gt;

&lt;p&gt;Managing release tags in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt&lt;/code&gt; is now a separate step.&lt;/p&gt;

&lt;p&gt;Running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt tag&lt;/code&gt; will examine the existing tags, and the HEAD commit.&lt;/p&gt;

&lt;p&gt;If there’s a release tag already at HEAD, it will exit with an error.&lt;/p&gt;

&lt;p&gt;If not, it will figure out the latest version number build number in use, by examining the existing tags.&lt;/p&gt;

&lt;p&gt;It will then make a new tag using the same version, and with an incremented build.&lt;/p&gt;

&lt;p&gt;This tag will be in the form: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vX.Y.Z-NNNN&lt;/code&gt; where &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;X.Y.Z&lt;/code&gt; is the semantic version, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NNNN&lt;/code&gt; is the build number.&lt;/p&gt;

&lt;p&gt;Note that this does not include the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-platform&lt;/code&gt; component that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt&lt;/code&gt; previously used.&lt;/p&gt;

&lt;p&gt;From 4.0 forwards, we assume that there will be just one version tag for any given commit, and we will use that tag for all platforms.&lt;/p&gt;

&lt;p&gt;We do however still support the old format when scanning previous tags – so &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt&lt;/code&gt; will pick up any legacy tags and correctly calculate the new build number.&lt;/p&gt;

&lt;h2 id=&quot;archiving-and-submitting&quot;&gt;Archiving And Submitting&lt;/h2&gt;

&lt;p&gt;With 4.0, if you run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt archive&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt submit&lt;/code&gt;, the first thing that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt&lt;/code&gt; does is examine the HEAD commit, looking for a version tag in the format that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt tag&lt;/code&gt; creates.&lt;/p&gt;

&lt;p&gt;If no tag is found, it will exit with an error. This ensures that any build you submit from a given commit will be tagged correctly, and that if you submit multiple platforms from the same commit, they are guaranteed to have the same build number.&lt;/p&gt;

&lt;h2 id=&quot;continuous-build-injection&quot;&gt;Continuous Build Injection&lt;/h2&gt;

&lt;p&gt;If your project is set up to run the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt update-build&lt;/code&gt; every time you build, it would clearly be unworkable for it to require a version tag to exist at the HEAD commit.&lt;/p&gt;

&lt;p&gt;If there is a tag at HEAD, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt update-build&lt;/code&gt; will use it.&lt;/p&gt;

&lt;p&gt;Otherwise, it scans backwards to find the highest previous build tag, and then adds one to the build number.&lt;/p&gt;

&lt;p&gt;In general I would discourage using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt&lt;/code&gt; for debug builds, since running a script for every build will slow Xcode down a bit, and having the build number available is of limited value.&lt;/p&gt;

&lt;p&gt;However, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt update-build&lt;/code&gt; command may still be useful if you have a custom build pipeline but want to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt&lt;/code&gt; to calculate and/or inject build information.&lt;/p&gt;

&lt;h2 id=&quot;build-variables&quot;&gt;Build Variables&lt;/h2&gt;

&lt;p&gt;Version 4.0 of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt&lt;/code&gt; also changes the names of the variables that are injected, and adds a new variable.&lt;/p&gt;

&lt;p&gt;We now set the following variables in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.h&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.xcconfig&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.plist&lt;/code&gt; files:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;RT_BUILD &lt;build number=&quot;&quot;&gt;&lt;/build&gt;&lt;/li&gt;
  &lt;li&gt;RT_COMMIT &lt;commit hash=&quot;&quot;&gt;&lt;/commit&gt;&lt;/li&gt;
  &lt;li&gt;RT_VERSION &lt;semantic version=&quot;&quot;&gt;&lt;/semantic&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These three variables are also passed to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xcodebuild&lt;/code&gt; on the command line.&lt;/p&gt;

&lt;p&gt;If you were using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt&lt;/code&gt; already, you will need to adjust your project accordingly.&lt;/p&gt;

&lt;p&gt;The rationale for the change was that it was probably better to use our own variables, rather than using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CURRENT_PROJECT_VERSION&lt;/code&gt; which already has a meaning within Xcode.&lt;/p&gt;

&lt;p&gt;That said, you probably will want to set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CURRENT_PROJECT_VERSION&lt;/code&gt; to the value of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RT_BUILD&lt;/code&gt;. You can do this in the build settings for the project, or each target, or by setting up an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.xcconfig&lt;/code&gt; file which includes the line &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CURRENT_PROJECT_VERSION = $(RT_BUILD)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The semantic version that is injected is taken from the build tag that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt tag&lt;/code&gt; creates. By default it’s the same version that the previous tag had, but you can change it explicitly by doing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt tag --explicit-version &amp;lt;x.y.z&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This allows you to make the git version tag the single-source-of-truth for all version information, if you so desire, and to inject the semantic version into all other settings and plists.&lt;/p&gt;

&lt;h2 id=&quot;legacy-cleanup&quot;&gt;Legacy Cleanup&lt;/h2&gt;

&lt;p&gt;In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt&lt;/code&gt; 4.0, two commands have been removed.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt install&lt;/code&gt; command used to exist as a quick way of linking &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt&lt;/code&gt; into your path. In general I think it’s better that you install &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt&lt;/code&gt; with a tool such as &lt;a href=&quot;https://github.com/yonaskolb/Mint&quot;&gt;Mint&lt;/a&gt;. However, some people will want to build from source, or do something else, and each installation method is likely to involve a different way to update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$PATH&lt;/code&gt;. On the whole, this feels like it’s something that it out-of-scope for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt&lt;/code&gt; itself.&lt;/p&gt;

&lt;p&gt;In the early days of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt&lt;/code&gt;, the design encouraged you to run it via some standard shell scripts, and also to include some standard &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.xcconfig&lt;/code&gt; files in every target. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt bootstrap&lt;/code&gt; command existed to help you copy (or update) these scripts and config files into your project. This way of working with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt&lt;/code&gt; is obsolete, and so the command was a bit of an unnecessary legacy.&lt;/p&gt;

&lt;p&gt;For the sake of simplicity, these two commands are no longer supported.&lt;/p&gt;

&lt;p&gt;The fallback mechanism for calculating a build number by counting git commits has also been removed.&lt;/p&gt;

&lt;h2 id=&quot;development-notes&quot;&gt;Development Notes&lt;/h2&gt;

&lt;p&gt;Doing these updates was another opportunity to experiment with AI code generation, and that’s what I did.&lt;/p&gt;

&lt;p&gt;Rather than making the code changes myself, I largely tried to instruct Copilot to do them for me.&lt;/p&gt;

&lt;p&gt;This was an interesting process.&lt;/p&gt;

&lt;p&gt;The codebase is fairly mature, and a lot of what I was asking for involved refactoring existing code to add a little bit of new functionality. The results were mixed, and once again, it felt quite akin to working with an enthusiastic and willing colleague who had a bit less experience.&lt;/p&gt;

&lt;p&gt;The first challenge was to express clearly what exactly it was I wanted. Much like test-driven development, I quite enjoy the fact that this forces you, early on, to work out what you actually do want. Also much like test-driven development, I didn’t always do a good job of working it out to begin with. Luckily, AI coding agents have infinite patience, and probably don’t bitch about you behind your back whilst having lunch with the other juniors&lt;sup id=&quot;fnref:1&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;In general the code that came back did work, and performed as intended. It was however often repetitious. The AI rarely showed insight about the codebase as a whole, and it had to be explicit prompted to clean up duplication and to create appropriate abstractions, methods, or constants in order to generalize. Again, this is quite like working with someone less experienced. They focus on the problem you’ve set them, and they stop when they think they’ve got it working. They don’t see the larger picture, and don’t consider consistency or maintainability of the codebase.&lt;/p&gt;

&lt;p&gt;Part of this I think may be down to the fact that I had not set any general instructions in the project. I have now added a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;copilot-instructions.md&lt;/code&gt; file, and am experimenting with trying to set some standards in it.&lt;/p&gt;

&lt;p&gt;One thing that I did like, and do think is useful, is that the AI generally created accompanying tests as a way of verifying each change I’d asked it to make. This is something that I aspire to, but don’t always manage, and having another “person” encourage me to work that way is excellent.&lt;/p&gt;

&lt;p&gt;It also proved to be the right approach, in that it unearthed some problems; not just in the new code, but in some of the dependencies that I was using. These were subtle concurrency issues which mostly manifested when performing tests in parallel, and they may not have impacted actual normal usage of the tool&lt;sup id=&quot;fnref:2&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:2&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;, but that makes it all the more valuable that I was forced to confront them.&lt;/p&gt;

&lt;p&gt;At the end of this process I did find myself wondering whether I could have achieved what I wanted faster if I’d just done it myself. I think that perhaps I could, and the code would have been tighter, for a narrow definition of “what I wanted”.&lt;/p&gt;

&lt;p&gt;However, having an essentially tireless worker, who pro-actively volunteered to make tests, encouraged me overall to do a better job. Whilst getting Copilot to fix the duplication that it had created, I was also motivated to ask it to take a number of other refactoring steps aimed at improving consistency and making the code that I had previously written more idiomatic. Whilst tracking down the concurrency issues in the dependency I was using to run subprocesses (another of my projects, called &lt;a href=&quot;https://github.com/elegantchaos/Runner&quot;&gt;Runner&lt;/a&gt;), I think I improved my general understanding of the surrounding issues, whilst also fixing some bugs I probably didn’t know I had in other projects using the same package.&lt;/p&gt;

&lt;p&gt;Overall, I continue to be cautiously positive about using Copilot. I think that the problem domain of programming is a good one, especially when the output from the AI tool is small enough that it can be reviewed by a human expert who can provide feedback.&lt;/p&gt;

&lt;h2 id=&quot;asymmetry&quot;&gt;Asymmetry&lt;/h2&gt;

&lt;p&gt;I was talking to friend over the weekend about a situation where they were using AI to review large documents. This is a very different scenario, and the balance is completely reversed.&lt;/p&gt;

&lt;p&gt;In my case, the AI tool is taking a vast amount of external knowledge and applying it to produce a small amount of new content, which is easily reviewed.&lt;/p&gt;

&lt;p&gt;I don’t need to know where the ideas behind the solution came from, I just need to be able to read the new code and evaluate whether it works, and whether it fits in with the existing codebase.&lt;/p&gt;

&lt;p&gt;This is quite asymmetrical, but in a direction that is positive.&lt;/p&gt;

&lt;p&gt;In my friend’s case, the output from the AI was going to be a report, but the accuracy of that report could only be evaluated by essentially doing the same job that the AI had been asked to do.&lt;/p&gt;

&lt;p&gt;From my own experience I’ve seen the AI make more than enough mistakes, and disappear down enough rabbit holes, that I would never trust its answers without being able to at least scan the output to verify it.&lt;/p&gt;

&lt;p&gt;I wouldn’t want to use it to analyze or summarize a large body of information unless I had a pretty reliable way to validate the results.&lt;/p&gt;

&lt;h2 id=&quot;future&quot;&gt;Future&lt;/h2&gt;

&lt;p&gt;I continue to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt&lt;/code&gt; for my own projects, and so I expect that it will evolve further over time.&lt;/p&gt;

&lt;p&gt;One item on my to-do list is to try to add support for installing it via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;homebrew&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That’s not something I really need myself right now though, and so it may take me a while. Pull requests &lt;a href=&quot;https://github.com/elegantchaos/ReleaseTools/pulls&quot;&gt;gratefully received&lt;/a&gt;…&lt;/p&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:1&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;This may be a wildly optimistic assumption. &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:2&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;Most of the time. Maybe… &lt;a href=&quot;#fnref:2&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</description>
				<pubDate>Tue, 14 Oct 2025 00:00:00 +0000</pubDate>
				<link>https://elegantchaos.com/2025/10/14/rt-tagging.html</link>
				<guid isPermaLink="true">https://elegantchaos.com/2025/10/14/rt-tagging.html</guid>
			</item>
        
		
        
			<item>
				<title>Release Tools (Fastlane For Idiots)</title>
				
				<description>&lt;p&gt;My latest detour was to add an extra option to &lt;a href=&quot;https://github.com/elegantchaos/ReleaseTools&quot;&gt;ReleaseTools&lt;/a&gt;, a command-line tool, written in Swift, that I made a few years ago.&lt;/p&gt;

&lt;p&gt;Actually I’ve just checked, and I started it in 2019 🤯.&lt;/p&gt;

&lt;!--more--&gt;

&lt;p&gt;ReleaseTools (or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt&lt;/code&gt; for short) basically exists to automate the process of uploading an app to the app store, or packaging it up for external distribution via Sparkle.&lt;/p&gt;

&lt;p&gt;I guess you could think of it as a bit like &lt;a href=&quot;https://fastlane.tools/&quot;&gt;Fastlane&lt;/a&gt;, but without any of the bells and whistles&lt;sup id=&quot;fnref:1&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;It is yet another thing that I’ve never really got around to talking about! Largely because I made it for myself, and didn’t really want to end up supporting it for other people.&lt;/p&gt;

&lt;p&gt;That said, it’s fairly mature now and I’ve used it in at least one client project as well as for a number of my own. Whilst I can’t guarantee that it will work for you, and I’m sure the documentation needs some improvement, I would certainly be happy to try to help someone else give it a go.&lt;/p&gt;

&lt;h2 id=&quot;scripting-like-an-animal&quot;&gt;Scripting Like An Animal&lt;/h2&gt;

&lt;p&gt;Under the hood, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt&lt;/code&gt; uses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xcodebuild&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;altool&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Prior to it existing, I was doing most of the same things that it does, using hand-rolled shell scripts for each project.&lt;/p&gt;

&lt;p&gt;Or, in the case of &lt;a href=&quot;https://bornsleepy.com/content/sams-past-lives&quot;&gt;my time working on Sketch&lt;/a&gt;, a big hairy load of Python I wrote&lt;sup id=&quot;fnref:2&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:2&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;…&lt;/p&gt;

&lt;p&gt;At some point I got annoyed with the fact that I was writing shell script, came to my senses, and decided that using Swift would be much more &lt;del&gt;efficient&lt;/del&gt; fun.&lt;/p&gt;

&lt;h2 id=&quot;basic-design&quot;&gt;Basic Design&lt;/h2&gt;

&lt;p&gt;If there was one thing I learned whilst hand-rolling all the previous bespoke solutions, it was that debugging build scripts is a massive pain in the arse.&lt;/p&gt;

&lt;p&gt;Partly because debugging scripts is just generally painful, but also specifically because the build process tends to be slow, and it’s really annoying to have to keep waiting for a long task like archiving to run, only to have the subsequent exporting fail because you messed something up.&lt;/p&gt;

&lt;p&gt;So when I made &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt&lt;/code&gt;, I decided to try to factor it out into a series of small commands that were intended to be run in sequence.&lt;/p&gt;

&lt;p&gt;Thus the basic design is that you can individually run commands to do things like:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;archive&lt;/li&gt;
  &lt;li&gt;export from the archive&lt;/li&gt;
  &lt;li&gt;upload the export for distribution&lt;/li&gt;
  &lt;li&gt;upload the export for notarisation instead&lt;/li&gt;
  &lt;li&gt;wait for notarisation to finish&lt;/li&gt;
  &lt;li&gt;compress the notarised app&lt;/li&gt;
  &lt;li&gt;rebuild a sparkle feed to include the compressed app&lt;/li&gt;
  &lt;li&gt;publish the sparkle feed to your website&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This lets you write a simple shell script&lt;sup id=&quot;fnref:3&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:3&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;3&lt;/a&gt;&lt;/sup&gt; to run the commands in sequence during normal operation, but when something goes wrong you can just re-run that command, and/or the subsequent ones.&lt;/p&gt;

&lt;p&gt;Anyway, I won’t go into a lot more detail about the specifics - if you want those you can check out the &lt;a href=&quot;https://github.com/elegantchaos/ReleaseTools&quot;&gt;README&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Suffice to say that it basically works these days, and I use it daily.&lt;/p&gt;

&lt;p&gt;In fact I’ve added a fast path where you just run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt submit&lt;/code&gt; and it performs the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;archive&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;export&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;upload&lt;/code&gt; commands in sequence.&lt;/p&gt;

&lt;h2 id=&quot;build-numbers&quot;&gt;Build Numbers&lt;/h2&gt;

&lt;p&gt;One of the nice things &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt&lt;/code&gt; does is calculate and inject a build number.&lt;/p&gt;

&lt;p&gt;I prefer to do this, rather than relying on the app store portal to automatically assign one, as it lets me tag the exact commit that I built from in git. Once it knows that the upload has succeeded, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt&lt;/code&gt; makes a tag in git including the exact build number, version, and platform.&lt;/p&gt;

&lt;p&gt;Originally, I calculated the number just using the count-the-git-commits trick.&lt;/p&gt;

&lt;p&gt;This works surprisingly well most of the time, but it is a little fragile. In particular, if you switch branches, the count can change, and consequently the build number &lt;strong&gt;can go down&lt;/strong&gt;. This is Not A Good Thing™.&lt;/p&gt;

&lt;p&gt;In practice, I rarely encountered this problem. I commit little-and-often, and for projects I’m working on myself, I would probably only submit from a single branch.&lt;/p&gt;

&lt;p&gt;However, when I working on &lt;a href=&quot;https://clubgame.app/&quot;&gt;Club&lt;/a&gt;, we did have this problem. That project didn’t use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt&lt;/code&gt; at all - it was cross-platform, and written in Dart/Flutter, with an entirely different toolchain. It started out doing the commit-counting, but we ended up switching to a solution that explicitly read the existing git tags and used them to work out the next build number.&lt;/p&gt;

&lt;p&gt;When I subsequently came back to my own projects, I was inspired by the tag-based approach and decided that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt&lt;/code&gt; should use it too.&lt;/p&gt;

&lt;h2 id=&quot;version-tags&quot;&gt;Version Tags&lt;/h2&gt;

&lt;p&gt;So I added an option to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt&lt;/code&gt; which calculated the new build number by:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;scanning for tags in the form &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vX.Y.Z-platform-NNN&lt;/code&gt;, eg &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;v1.0.2-iOS-234&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;treating the highest existing NNN for our platform as the current number&lt;/li&gt;
  &lt;li&gt;adding 1 to it for the new number&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That works really well, and because &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt&lt;/code&gt; also creates and pushes a tag in the same format, it’s pretty reliable.&lt;/p&gt;

&lt;p&gt;The only wrinkle with it is that when you are supporting two or more platforms, you have to submit each one with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt&lt;/code&gt; separately, e.g:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;rt submit &lt;span class=&quot;nt&quot;&gt;--platform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;iOS
rt submit &lt;span class=&quot;nt&quot;&gt;--platform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;macOS
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Sometimes a problem will occur, or something will get broken, such that one upload succeeds and the other fails.&lt;/p&gt;

&lt;p&gt;This becomes a problem subsequently because you’ve now got one platform with a tag that worked, and which the app store knows to have that build number, but another platform that only has an older tag.&lt;/p&gt;

&lt;p&gt;So when you subsequently upload the next build, you end up with different build numbers for each platform, even though they are built from the same commit. Which is confusing and a bit annoying.&lt;/p&gt;

&lt;h2 id=&quot;why-are-you-telling-me-all-of-this-again&quot;&gt;Why Are You Telling Me All Of This Again?&lt;/h2&gt;

&lt;p&gt;Good question.&lt;/p&gt;

&lt;p&gt;Come to think of it, the main point of this whole post - other than just answering, in a rambling way, the “how was your day dear?” that you didn’t ask - was to mention the existence of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Which I’ve done.&lt;/p&gt;

&lt;p&gt;You can stop reading now!&lt;/p&gt;

&lt;p&gt;What follows is more detail than you probably want to know about why I started actually working on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt&lt;/code&gt; recently, what I did to try to fix the tagging problem, and why I’ve realised that I need to go back to my fix!&lt;/p&gt;

&lt;h2 id=&quot;inconsistent-tags&quot;&gt;Inconsistent Tags&lt;/h2&gt;

&lt;p&gt;To fix the inconsistent tags manually, the easiest thing to do has been to use git on the command line to make a tag with the right build number, for the platform that had fallen behind, on the commit that it should have been uploaded from.&lt;/p&gt;

&lt;p&gt;That way, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt&lt;/code&gt; will find the tag next time, add 1 to it, and come up with the right value.&lt;/p&gt;

&lt;p&gt;This is a bit clunky. Also, occasionally, I only want to release one platform, as I may have changed code that just affects it. Next time I submit though, I want to release both platforms using the highest build number for any platform.&lt;/p&gt;

&lt;p&gt;So I decided to try to add some new capabilities to support this objective.&lt;/p&gt;

&lt;p&gt;First, I’ve added an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--explicit-build&lt;/code&gt; option to allow you to specify the build number to use. That is a cleaner way to reset everything when doing a new upload, as you can just force the number for both platforms to get them back in sync.&lt;/p&gt;

&lt;p&gt;I’ve also added an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--existing-tag&lt;/code&gt; option. This looks for the highest tag for any platform, as well as for the platform you’re uploading, and compares the two. If there’s a higher number for another platform, it will use that. If not, it will do what it used to do and increment the previous build number by one.&lt;/p&gt;

&lt;p&gt;This works nicely for the situation where a build or upload failed for one platform in a transient way, you’ve fixed it with some minor commit, and you just want to upload something with the same number as the other platform(s).&lt;/p&gt;

&lt;p&gt;I actually thought that I was done, but whilst writing this blog post, I’ve realised that I’m not.&lt;/p&gt;

&lt;p&gt;If the commit has changed, you really should use a higher build number - but it needs to be higher than any platform has used, and not just higher than the platform you’re uploading.&lt;/p&gt;

&lt;p&gt;I think posibly what &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt&lt;/code&gt; needs to do is to figure out whether the tag from the other platform is on the HEAD commit. If it is, then what you’re doing is “catching up” and it should just use the same build number - it’s the same commit, so using the same number makes sense.&lt;/p&gt;

&lt;p&gt;If there’s no version tag on HEAD though, then we probably want to increment the highest number by one more, and use that. This will create a new baseline with the first platform that you upload, and subsequent platforms will then catch up to it.&lt;/p&gt;

&lt;p&gt;So I guess that’s the next job then…&lt;/p&gt;

&lt;h2 id=&quot;or&quot;&gt;Or…&lt;/h2&gt;

&lt;p&gt;… although, maybe the problem here is that the design is wrong.&lt;/p&gt;

&lt;p&gt;The basic problem is that when making the tag, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt&lt;/code&gt; doesn’t know which platforms you’re uploading, as it does them one at a time.&lt;/p&gt;

&lt;p&gt;I can see a couple of ways to fix this.&lt;/p&gt;

&lt;p&gt;One would be a way to pass a list of platforms in, and have them all upload at once.&lt;/p&gt;

&lt;p&gt;Another… which I am coming round to instead… is making the tag creation an explicit and separate step.&lt;/p&gt;

&lt;p&gt;This feels more in keeping with the small-individual-commands design.&lt;/p&gt;

&lt;p&gt;Maybe version tags should not be platform-specific, and just in the form &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vX.Y.Z-NNN&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt submit&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt archive&lt;/code&gt; and there isn’t a version tag on HEAD, it should just refuse to do anything.&lt;/p&gt;

&lt;p&gt;Then we can have a separate &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt tag&lt;/code&gt; command which makes a new version tag. So you run that first, then run the archive or submit commands for each platform in turn.&lt;/p&gt;

&lt;p&gt;We can still have a small shell script that you can run for the normal situation where you want to upload everything at once:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;rt tag
rt submit &lt;span class=&quot;nt&quot;&gt;--platform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;iOS
rt submit &lt;span class=&quot;nt&quot;&gt;--platform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;macOS
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can still run the individual bits though, and they will do the right thing. If you’re on a tagged commit they will work. If not, they won’t. If you run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rt tag&lt;/code&gt; on a commit that’s already tagged, it can spot that and complain.&lt;/p&gt;

&lt;p&gt;This feels a lot cleaner actually.&lt;/p&gt;

&lt;p&gt;I’m glad we had this talk.&lt;/p&gt;

&lt;p&gt;Thanks for listening!&lt;/p&gt;

&lt;hr /&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:1&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;No offence to Fastlane, but I’ve always found it to be complicated and confusing, and the dependency on Ruby annoys me. &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:2&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;To be fair, these scripts did a whole load of other stuff too. They were a bit hairy though… &lt;a href=&quot;#fnref:2&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:3&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;Yes we’re back to shell scripts. Teeny tiny ones though. Honest. &lt;a href=&quot;#fnref:3&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</description>
				<pubDate>Fri, 26 Sep 2025 00:00:00 +0000</pubDate>
				<link>https://elegantchaos.com/2025/09/26/release-tools.html</link>
				<guid isPermaLink="true">https://elegantchaos.com/2025/09/26/release-tools.html</guid>
			</item>
        
		
        
			<item>
				<title>Dipping A Toe Into The AI Waters</title>
				
				<description>&lt;p&gt;Ok, I admit it, I’ve been tinkering with AI again.&lt;/p&gt;

&lt;p&gt;Not using it, this time, but trying to add it to my app &lt;a href=&quot;https://elegantchaos.com/2025/08/06/this-is-not-gtd.html&quot;&gt;The Stack&lt;/a&gt;.&lt;/p&gt;

&lt;!--more--&gt;

&lt;p&gt;Yet another person shamelessly trying to shoe-horn some AI into their app, so that they can jump on the bandwagon?&lt;/p&gt;

&lt;p&gt;Well yeah, guilty as charged, I guess. Anyhoo…&lt;/p&gt;

&lt;h3 id=&quot;summarising-notes&quot;&gt;Summarising Notes&lt;/h3&gt;

&lt;p&gt;Individual notes in the stack are generally quite short. Things like “add a blog post talking about adding AI to The Stack”. There’s probably not a lot of mileage in summarising them.&lt;/p&gt;

&lt;p&gt;Given a long list of them though, it’s easy to get swamped.&lt;/p&gt;

&lt;p&gt;Could we use some sort of AI to generate a summary of all items, or of all items with a given tag? Maybe it could pull out one or two emerging themes? Perhaps also suggest the item to tackle next?&lt;/p&gt;

&lt;h3 id=&quot;on-device&quot;&gt;On-Device&lt;/h3&gt;

&lt;p&gt;For iOS and macOS 26.0, Apple offer a new &lt;a href=&quot;https://developer.apple.com/documentation/FoundationModels&quot;&gt;on-device Foundation Model&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This seems like it’s tailor made for my use case.&lt;/p&gt;

&lt;p&gt;The query doesn’t leave your device, so there are no privacy issues to contend with.&lt;/p&gt;

&lt;p&gt;So I decided to give it a go.&lt;/p&gt;

&lt;h3 id=&quot;adding-the-code&quot;&gt;Adding The Code&lt;/h3&gt;

&lt;p&gt;On the plus side, adding the code for this was ridiculously easy. About three lines of actual Swift:&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;  &lt;span class=&quot;n&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;LanguageModelSession&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;instructions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;summarizerInstructions&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;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;respond&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;to&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;prompt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;trimmingCharacters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;in&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;whitespacesAndNewlines&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It’s no exageration to say that the instructions and prompt that I feed to the AI is a lot longer than the code I use to invoke it.&lt;/p&gt;

&lt;p&gt;Therein lies one problem, however. The prompt. &lt;strong&gt;Getting a prompt that actually produces useful output is difficult&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I’m initialising the model with these instructions:&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;summarizerInstructions&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
      You summarize a list of to‑do notes for planning.

      Rules:
      - Output: one concise paragraph, no more than 50 words
      - Use plain text, no headings or lists.
      - Weigh recently updated items slightly more, but consider all items.
      - Use tags only as background context; never mention tag names in the output.
      - Prefer synthesis over enumeration; use actionable, neutral language.
      - Summarise for the current user only. Do not refer to teams or other users.
      - Write in the imperative mood where possible. Do not address the user directly.
      - Do not invent notes or details.
      - Write in the same language as the notes.
      &quot;&quot;&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then giving it the following prompt:&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;
  &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;prompt&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
      Summarize the following to‑do items into a single paragraph that captures the main themes and priorities.

      Each item is in the form &amp;lt;date&amp;gt;: &amp;lt;body&amp;gt;, where: 
      - &amp;lt;date&amp;gt; is the date that the note was last modified (more recent = higher weight)
      - &amp;lt;body&amp;gt; is the note body, including any hashtags

      Items:
      &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;summarisedItems&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;joined&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;separator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n\n&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;
      &quot;&quot;&quot;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This took quite a lot of tweaking. Inevitably, I asked Copilot to help 😆.&lt;/p&gt;

&lt;p&gt;I am sure it could be greatly improved.&lt;/p&gt;

&lt;h3 id=&quot;so-far-so-meh&quot;&gt;So Far, So Meh…&lt;/h3&gt;

&lt;p&gt;The current prototype sometimes does a decent job, but the output has a tendency to end up a bit wordy.&lt;/p&gt;

&lt;p&gt;I haven’t had it completely hallucinate, but it does sometimes completely ignores parts of its instructions!&lt;/p&gt;

&lt;p&gt;I understand enough about the process to not be surprised by this, but the level of unpredicability makes it quite difficult to rely on.&lt;/p&gt;

&lt;p&gt;Worse, I’m not entirely sure that the output is particularly helpful!&lt;/p&gt;

&lt;p&gt;More testing required I think, particular with large lists of items.&lt;/p&gt;

&lt;p&gt;There’s a bigger problem with that, however, when using the on-device model.&lt;/p&gt;

&lt;p&gt;Unfortunately the context window is small. It only supports 4096 tokens, where each token is a few characters of text.&lt;/p&gt;

&lt;p&gt;Even with a modest collection of a couple of hundred notes, it’s easy to exceed that window, resulting in an error.&lt;/p&gt;

&lt;p&gt;I suspect that this means that the on-device approach is a non starter, and it will be necessary to send the entire item list to an online model. Which opens up a whole other can of worms…&lt;/p&gt;

&lt;p&gt;Oh well.&lt;/p&gt;

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

&lt;p&gt;Interesting experiment.&lt;/p&gt;

&lt;p&gt;I’m not yet convinced, but will keep playing…&lt;/p&gt;

</description>
				<pubDate>Tue, 23 Sep 2025 00:00:00 +0000</pubDate>
				<link>https://elegantchaos.com/2025/09/23/ai-toe-dipping.html</link>
				<guid isPermaLink="true">https://elegantchaos.com/2025/09/23/ai-toe-dipping.html</guid>
			</item>
        
		
	</channel>
</rss>
