<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	xmlns:media="http://search.yahoo.com/mrss/"
>

<channel>
	<title>Bugs! &#8211; Wade Tregaskis</title>
	<atom:link href="https://wadetregaskis.com/tags/bugs/feed/" rel="self" type="application/rss+xml" />
	<link>https://wadetregaskis.com</link>
	<description></description>
	<lastBuildDate>Mon, 26 Jan 2026 01:48:08 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://wadetregaskis.com/wp-content/uploads/2016/03/Stitch-512x512-1-256x256.png</url>
	<title>Bugs! &#8211; Wade Tregaskis</title>
	<link>https://wadetregaskis.com</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">226351702</site>	<item>
		<title>Bugs Apple Loves &#038; Apps Apple Hates</title>
		<link>https://wadetregaskis.com/bugs-apple-loves-apps-apple-hates/</link>
					<comments>https://wadetregaskis.com/bugs-apple-loves-apps-apple-hates/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Mon, 26 Jan 2026 01:48:06 +0000</pubDate>
				<category><![CDATA[Ramblings]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[Snafu]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8717</guid>

					<description><![CDATA[It&#8217;s been a while since I&#8217;ve seen such a pithy and accurate representation of what it&#8217;s like being an Apple Mac &#38; iPhone user these days (well done Nick Hodulik!). The externalities cost estimates might be a little tongue-in-cheek, but honestly, are they all that wrong? One small irritation at the wrong moment can ricochet&#8230; <a class="read-more-link" href="https://wadetregaskis.com/bugs-apple-loves-apps-apple-hates/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>It&#8217;s been a while since I&#8217;ve seen such <a href="https://www.bugsappleloves.com" data-wpel-link="external" target="_blank" rel="external noopener">a pithy and accurate representation of what it&#8217;s like being an Apple Mac &amp; iPhone user these days</a> (well done <a href="https://github.com/nhod" data-wpel-link="external" target="_blank" rel="external noopener">Nick Hodulik</a>!).</p>



<p>The externalities cost estimates might be a little tongue-in-cheek, but honestly, are they all that wrong?  One small irritation at the wrong moment can ricochet my happy mood off into the doldrums, and Apple&#8217;s products produce a hundred &#8220;small&#8221; irritations every day &#8211; which compound in their irritation when you see them software update after software update, year after year, product after product.  It&#8217;s hard not to take it personally.  Like Apple is <em>deliberately</em> being cruel.</p>



<p><a href="https://ikennd.ac/about/" data-wpel-link="external" target="_blank" rel="external noopener">Daniel Kennett</a> wrote, in <a href="https://ikennd.ac/blog/2026/01/old-man-yells-at-modern-software-design/" data-wpel-link="external" target="_blank" rel="external noopener">his memorial to Aperture</a>, about how the Mac-using experience wasn&#8217;t <em>actually</em> rainbows and perfection even back in whatever you personally feel was the golden age (I&#8217;m with him that circa System 7.1 was glorious, though the early days of Mac OS X were also very special to me).  Which is true &#8211; fire up your favourite old Mac on <a href="https://infinitemac.org" data-wpel-link="external" target="_blank" rel="external noopener">Infinite Mac</a> and see the strength of your rose-tinted nostalgia glasses.</p>



<p><em>But</em>, the big difference is that Apple back then was a relatively tiny company struggling just to survive in a brutal industry dominated by <a href="https://www.ibm.com" data-wpel-link="external" target="_blank" rel="external noopener">soulless</a>, <a href="https://www.microsoft.com" data-wpel-link="external" target="_blank" rel="external noopener">greedy</a> <a href="https://www.intel.com" data-wpel-link="external" target="_blank" rel="external noopener">monsters</a>.</p>



<p>There is a point at which mere indifference or incompetence transitions into negligence, and it&#8217;s <em>long</em> before you become one of the wealthiest companies on the planet with a veritable army of engineers.</p>



<p>Having worked at Apple &#8211; among other big tech companies &#8211; I can say with confidence that there&#8217;s no valid reason why they cannot fix long-standing, <em>infamous</em> bugs.  It&#8217;s.  Not.  That.  Hard.  One half-decent engineer could fix everything listed on Bugs Apple Loves in six months, single-handed.</p>



<p>It&#8217;s not apparent why that doesn&#8217;t happen, but it&#8217;s not that Apple are technically incapable of it, and it cannot be that they&#8217;re unaware, so it must be that they&#8217;re choosing not to.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/bugs-apple-loves-apps-apple-hates/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">8717</post-id>	</item>
		<item>
		<title>Fixing sudden, random iPhone disconnects from Image Capture</title>
		<link>https://wadetregaskis.com/fixing-sudden-random-iphone-disconnects-from-image-capture/</link>
					<comments>https://wadetregaskis.com/fixing-sudden-random-iphone-disconnects-from-image-capture/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Wed, 21 Jan 2026 18:33:09 +0000</pubDate>
				<category><![CDATA[Howto]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[Image Capture]]></category>
		<category><![CDATA[iPhone]]></category>
		<category><![CDATA[Tethering]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8666</guid>

					<description><![CDATA[It appears that each time tethering is enabled or disabled on the iPhone, it disconnects Image Capture. So if you have spotty cellular service &#8211; because perhaps you live in the United States, where that&#8217;s the only kind of cellular service on offer &#8211; you might find that happens so often that you can&#8217;t complete&#8230; <a class="read-more-link" href="https://wadetregaskis.com/fixing-sudden-random-iphone-disconnects-from-image-capture/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>It appears that each time tethering is enabled or disabled on the iPhone, it disconnects Image Capture.  So if you have spotty cellular service &#8211; because perhaps you live in the United States, where that&#8217;s the only kind of cellular service on offer &#8211; you might find that happens so often that you can&#8217;t complete basic media transfers in Image Capture.</p>



<p>Thankfully the workaround is simple &#8211; disable tethering, or enable Airplane mode, while you&#8217;re using Image Capture.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/fixing-sudden-random-iphone-disconnects-from-image-capture/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2026/01/camera-disconnected-while-importing-error-dialog.webp" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">8666</post-id>	</item>
		<item>
		<title>How to make a macOS screen saver</title>
		<link>https://wadetregaskis.com/how-to-make-a-macos-screen-saver/</link>
					<comments>https://wadetregaskis.com/how-to-make-a-macos-screen-saver/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Sat, 06 Dec 2025 22:46:22 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Howto]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[com.apple.screensaver.willstop]]></category>
		<category><![CDATA[SaveHollywood]]></category>
		<category><![CDATA[Screen saver]]></category>
		<category><![CDATA[ScreenSaverView]]></category>
		<category><![CDATA[Snafu]]></category>
		<category><![CDATA[Swift]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8580</guid>

					<description><![CDATA[First, make sure you really want to. macOS&#8217;s screen saver system is absurdly buggy and broken. It&#8217;s frustrating to work with and very difficult to make work right. If you&#8217;re determined, read on. Screen savers are basically just applications. Same bundle format and structure, just with a &#8220;saver&#8221; extension instead of &#8220;app&#8221;. Setting up the&#8230; <a class="read-more-link" href="https://wadetregaskis.com/how-to-make-a-macos-screen-saver/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>First, make sure you <em>really</em> want to.  macOS&#8217;s screen saver system is absurdly buggy and broken.  It&#8217;s frustrating to work with and very difficult to make work right.</p>



<p>If you&#8217;re determined, read on.</p>



<p>Screen savers are basically just applications.  Same bundle format and structure, just with a &#8220;saver&#8221; extension instead of &#8220;app&#8221;.</p>



<h2 class="wp-block-heading">Setting up the Xcode project</h2>



<p>In Xcode, create a new project using the <code>Screen Saver</code> template.</p>



<p>Delete the Objective-C code &amp; header files it creates by default (unless, I suppose, you want to write your screen saver in Objective-C &#8211; woo, retro! 😆).</p>



<p>You need to import the ScreenSaver module (framework), subclass ScreenSaverView, and implement a couple of method overrides.  Here&#8217;s the basic skeleton:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#000000;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>import ScreenSaver

class MyScreenSaver: ScreenSaverView {
    override init?(frame: NSRect, isPreview: Bool) {
        super.init(frame: frame, isPreview: isPreview)
        setup()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setup()
    }

    private func setup() {
        // TODO
    }

    override func startAnimation() {
        super.startAnimation()

        // TODO
    }

    override func animateOneFrame() {  // Optional.
        // TODO
    }

    override func stopAnimation() { //  Only for the live preview in System Settings.
        // TODO

        super.stopAnimation()
    }

    override var hasConfigureSheet: Bool {
        true
    }

    private var configureSheetController: ConfigureSheetController?

    override var configureSheet: NSWindow? {
        configureSheetController = ConfigureSheetController(windowNibName: "ConfigureSheet")
        return configureSheetController?.window
    }
}

class ConfigureSheetController: NSWindowController {
    override var windowNibName: NSNib.Name? {
        return "ConfigureSheet"
    }

    override func windowDidLoad() {
        super.windowDidLoad()

        // TODO
    }

    @IBAction func okButtonClicked(_ sender: NSButton) {
        // TODO

        window!.sheetParent!.endSheet(window!, returnCode: .OK)
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #AF00DB">import</span><span style="color: #000000"> </span><span style="color: #267F99">ScreenSaver</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">class</span><span style="color: #000000"> </span><span style="color: #267F99">MyScreenSaver</span><span style="color: #000000">: ScreenSaverView {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">override</span><span style="color: #000000"> </span><span style="color: #0000FF">init?</span><span style="color: #000000">(</span><span style="color: #795E26">frame</span><span style="color: #000000">: NSRect, </span><span style="color: #795E26">isPreview</span><span style="color: #000000">: </span><span style="color: #267F99">Bool</span><span style="color: #000000">) {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #0000FF">super</span><span style="color: #000000">.</span><span style="color: #0000FF">init</span><span style="color: #000000">(</span><span style="color: #795E26">frame</span><span style="color: #000000">: frame, </span><span style="color: #795E26">isPreview</span><span style="color: #000000">: isPreview)</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #795E26">setup</span><span style="color: #000000">()</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">required</span><span style="color: #000000"> </span><span style="color: #0000FF">init?</span><span style="color: #000000">(</span><span style="color: #795E26">coder</span><span style="color: #000000">: NSCoder) {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #0000FF">super</span><span style="color: #000000">.</span><span style="color: #0000FF">init</span><span style="color: #000000">(</span><span style="color: #795E26">coder</span><span style="color: #000000">: coder)</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #795E26">setup</span><span style="color: #000000">()</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">private</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">setup</span><span style="color: #000000">() {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #008000">// TODO</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">override</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">startAnimation</span><span style="color: #000000">() {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #0000FF">super</span><span style="color: #000000">.</span><span style="color: #795E26">startAnimation</span><span style="color: #000000">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #008000">// TODO</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">override</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">animateOneFrame</span><span style="color: #000000">() {  </span><span style="color: #008000">// Optional.</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #008000">// TODO</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">override</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">stopAnimation</span><span style="color: #000000">() { </span><span style="color: #008000">//  Only for the live preview in System Settings.</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #008000">// TODO</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #0000FF">super</span><span style="color: #000000">.</span><span style="color: #795E26">stopAnimation</span><span style="color: #000000">()</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">override</span><span style="color: #000000"> </span><span style="color: #0000FF">var</span><span style="color: #000000"> hasConfigureSheet: </span><span style="color: #267F99">Bool</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #0000FF">true</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">private</span><span style="color: #000000"> </span><span style="color: #0000FF">var</span><span style="color: #000000"> configureSheetController: ConfigureSheetController?</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">override</span><span style="color: #000000"> </span><span style="color: #0000FF">var</span><span style="color: #000000"> configureSheet: NSWindow? {</span></span>
<span class="line"><span style="color: #000000">        configureSheetController = </span><span style="color: #795E26">ConfigureSheetController</span><span style="color: #000000">(</span><span style="color: #795E26">windowNibName</span><span style="color: #000000">: </span><span style="color: #A31515">&quot;ConfigureSheet&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #AF00DB">return</span><span style="color: #000000"> configureSheetController?.</span><span style="color: #001080">window</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">class</span><span style="color: #000000"> </span><span style="color: #267F99">ConfigureSheetController</span><span style="color: #000000">: NSWindowController {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">override</span><span style="color: #000000"> </span><span style="color: #0000FF">var</span><span style="color: #000000"> windowNibName: NSNib.Name? {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #AF00DB">return</span><span style="color: #000000"> </span><span style="color: #A31515">&quot;ConfigureSheet&quot;</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">override</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">windowDidLoad</span><span style="color: #000000">() {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #0000FF">super</span><span style="color: #000000">.</span><span style="color: #795E26">windowDidLoad</span><span style="color: #000000">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #008000">// TODO</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">@IBAction</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">okButtonClicked</span><span style="color: #000000">(</span><span style="color: #795E26">_</span><span style="color: #000000"> </span><span style="color: #001080">sender</span><span style="color: #000000">: NSButton) {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #008000">// TODO</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        window!.</span><span style="color: #001080">sheetParent</span><span style="color: #000000">!.</span><span style="color: #795E26">endSheet</span><span style="color: #000000">(window!, </span><span style="color: #795E26">returnCode</span><span style="color: #000000">: .</span><span style="color: #001080">OK</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<h2 class="wp-block-heading">Providing a preferences sheet</h2>



<p>If you don&#8217;t have any options to configure, you can of course change <code>hasConfigureSheet</code> to return <code>false</code>.  Otherwise, you&#8217;ll need to create a &#8220;ConfigureSheet&#8221; xib file with a window in it containing your screen saver&#8217;s settings.  You can use UserDefaults to save your settings (if you wish), same as any other app.  And you&#8217;ll need to add an &#8220;Okay&#8221; or &#8220;Save&#8221; or similar button to dismiss the sheet.</p>



<h2 class="wp-block-heading">Getting ready to render</h2>



<p>The key methods to implement are <code>setup</code>, with any initial configuration you wish to do (e.g. allocate image or video views, load assets, set up the view hierarchy, etc).</p>



<p><code>ScreenSaverView</code> is an <code>NSView</code> subclass with a flat black background by default, onto which you can add subviews.  Typically for a screen saver you have a very simple view hierarchy &#8211; often just a single view or CoreAnimation layer that you&#8217;re rendering to &#8211; but you can load a xib and insert elements from it into the view if you like.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>☝️ It&#8217;s wise to <em>not</em> render anything in <code>setup</code>, which includes accidentally &#8211; you might need to set views to hidden, layers to zero opacity, etc.  This is basically because there can be an arbitrarily long gap between <code>setup</code> and <code>startAnimation</code> calls, and it&#8217;s often weird to render something initially, potentially not animate for a noticeable length of time, and <em>then</em> start actually working properly.</p>



<p>Alternatively, you might insert a placeholder image or text, e.g. &#8220;Loading…&#8221;, if you really want.  But in my opinion it&#8217;s more graceful to just let the initial black screen stand for a moment.</p>
</div></div>



<h2 class="wp-block-heading">Rendering</h2>



<p><code>startAnimation</code> is where you should actually start displaying things.  e.g. if you&#8217;re using an <code>AVPlayer</code>, this is where you actually start it playing (after making it visible, e.g. setting its opacity to 1).</p>



<p><code>animateOneFrame</code> is optional, and is only called if you set the <code>animationTimeInterval</code> property (on <code>self</code>) to a finite, non-zero value in <code>setup</code> (in which case it&#8217;ll be called at intervals <em>at least</em> that long &#8211; it might not be called as often as you desire if previous calls overrun or there&#8217;s other bottlenecks in the screen saver framework).  It&#8217;s essentially just a minor convenience vs having to explicitly set up an <code>NSTimer</code>.</p>



<p>Given how buggy Apple&#8217;s screen saver framework is, I suggest <em>not</em> relying on <code>animateOneFrame</code> if you can at all avoid it.  Even if that means setting up your own timer.  That way when they likely break that too in some future macOS release, your screen saver won&#8217;t necessarily break as well.</p>



<h3 class="wp-block-heading">Bonus topic: fading in</h3>



<p>Unless your screen saver inherently appears gently (e.g. starts rendering with a flat black view and only slowly adds to it), it&#8217;s nice to add a fade-in.  You can do that using CoreAnimation on the view&#8217;s layer:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#000000;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>override func startAnimation() {
    // Other code…
    
    if let layer {
        layer.opacity = 0.0  // Should already be zero from `setup`, but just to be sure.

        let fadeAnimation = CABasicAnimation(keyPath: "opacity")
        fadeAnimation.fromValue = 0.0
        fadeAnimation.toValue = 1.0
        fadeAnimation.duration = 5  // Seconds.

        // Essential settings to keep the final state
        fadeAnimation.fillMode = .forwards
        fadeAnimation.isRemovedOnCompletion = false

        layer.add(fadeAnimation, forKey: "fadeAnimation")
    }
    
    // Other code…
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">override</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">startAnimation</span><span style="color: #000000">() {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #008000">// Other code…</span></span>
<span class="line"><span style="color: #000000">    </span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #AF00DB">if</span><span style="color: #000000"> </span><span style="color: #0000FF">let</span><span style="color: #000000"> layer {</span></span>
<span class="line"><span style="color: #000000">        layer.</span><span style="color: #001080">opacity</span><span style="color: #000000"> = </span><span style="color: #098658">0.0</span><span style="color: #000000">  </span><span style="color: #008000">// Should already be zero from `setup`, but just to be sure.</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #0000FF">let</span><span style="color: #000000"> fadeAnimation = </span><span style="color: #795E26">CABasicAnimation</span><span style="color: #000000">(</span><span style="color: #795E26">keyPath</span><span style="color: #000000">: </span><span style="color: #A31515">&quot;opacity&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">        fadeAnimation.</span><span style="color: #001080">fromValue</span><span style="color: #000000"> = </span><span style="color: #098658">0.0</span></span>
<span class="line"><span style="color: #000000">        fadeAnimation.</span><span style="color: #001080">toValue</span><span style="color: #000000"> = </span><span style="color: #098658">1.0</span></span>
<span class="line"><span style="color: #000000">        fadeAnimation.</span><span style="color: #001080">duration</span><span style="color: #000000"> = </span><span style="color: #098658">5</span><span style="color: #000000">  </span><span style="color: #008000">// Seconds.</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #008000">// Essential settings to keep the final state</span></span>
<span class="line"><span style="color: #000000">        fadeAnimation.</span><span style="color: #001080">fillMode</span><span style="color: #000000"> = .</span><span style="color: #001080">forwards</span></span>
<span class="line"><span style="color: #000000">        fadeAnimation.</span><span style="color: #001080">isRemovedOnCompletion</span><span style="color: #000000"> = </span><span style="color: #0000FF">false</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        layer.</span><span style="color: #795E26">add</span><span style="color: #000000">(fadeAnimation, </span><span style="color: #795E26">forKey</span><span style="color: #000000">: </span><span style="color: #A31515">&quot;fadeAnimation&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">    </span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #008000">// Other code…</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>Note that you cannot implement a fade-out when the screen saver exits, because macOS hides your screen saver immediately.  Plus, the user might not want a fade-out as they may be in a rush to do something on their computer.</p>



<h3 class="wp-block-heading">Previewing</h3>



<p>You can determine if you&#8217;re running for real or only the preview via the <code>isPreview</code> property (on <code>self</code>).  Many screen savers don&#8217;t care, but particularly if you save any persistent state, you might want to avoid doing that during preview.  For example, in a screen saver which plays a looping video and resumes where it last left off, you probably don&#8217;t want the preview to quietly advance the video.</p>



<h2 class="wp-block-heading">Stopping</h2>



<p><code>stopAnimation</code> is <em>only</em> used for the live preview thumbnail shown in the Screen Saver System Settings pane.  It is <em>never</em> called in normal operation of the screen saver (contrary to what Apple&#8217;s documentation says &#8211; Apple broke that in macOS Sonoma and later).</p>



<p>And that leads to the first path off the official track.  When the screen saver is dismissed by the user, <em>nothing</em> in Apple&#8217;s framework code does anything.  Your view continues to exist, <code>animateOneFrame</code> continues getting called, etc.  Your screen saver just runs in the background, its output not visible, but wasting CPU cycles and RAM.  Worse, if you have sound, that keeps playing.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>🙏 A big thanks to <a href="https://stackoverflow.com/users/4103152/cwizou" data-wpel-link="external" target="_blank" rel="external noopener">cwizou</a> via <a href="https://stackoverflow.com/questions/66861833/audio-keeps-playing-after-screensaver-ends" data-wpel-link="external" target="_blank" rel="external noopener">StackOverflow</a> for documenting <a href="https://stackoverflow.com/a/67161635" data-wpel-link="external" target="_blank" rel="external noopener">the solution</a>, which I&#8217;ve summarised below.</p>
</div></div>



<p>To get around that, you need to register for the <code>com.apple.screensaver.willstop</code> notification, in <code>setup</code>, like so:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#000000;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>  private func setup() {
    DistributedNotificationCenter.default.addObserver(self,
                                                      selector: #selector(willStop(_:)),
                                                      name: Notification.Name("com.apple.screensaver.willstop"),
                                                      object: nil)
}
    
@objc func willStop(_ notification: Notification) {
    stopAnimation()
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #000000">  </span><span style="color: #0000FF">private</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">setup</span><span style="color: #000000">() {</span></span>
<span class="line"><span style="color: #000000">    DistributedNotificationCenter.</span><span style="color: #001080">default</span><span style="color: #000000">.</span><span style="color: #795E26">addObserver</span><span style="color: #000000">(</span><span style="color: #0000FF">self</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                                      </span><span style="color: #795E26">selector</span><span style="color: #000000">: </span><span style="color: #795E26">#selector</span><span style="color: #000000">(</span><span style="color: #795E26">willStop</span><span style="color: #000000">(_:)),</span></span>
<span class="line"><span style="color: #000000">                                                      </span><span style="color: #795E26">name</span><span style="color: #000000">: Notification.</span><span style="color: #795E26">Name</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;com.apple.screensaver.willstop&quot;</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                                                      </span><span style="color: #795E26">object</span><span style="color: #000000">: </span><span style="color: #0000FF">nil</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">}</span></span>
<span class="line"><span style="color: #000000">    </span></span>
<span class="line"><span style="color: #0000FF">@objc</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">willStop</span><span style="color: #000000">(</span><span style="color: #795E26">_</span><span style="color: #000000"> </span><span style="color: #001080">notification</span><span style="color: #000000">: Notification) {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">stopAnimation</span><span style="color: #000000">()</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>Note that you still need <code>stopAnimation</code> specifically, because in the live preview in System Settings you won&#8217;t receive that <code>com.apple.screensaver.willstop</code> notification (from the system&#8217;s point of view, the screen saver <em>isn&#8217;t</em> running &#8211; it&#8217;s merely previewing).</p>



<h2 class="wp-block-heading">Handling resumption</h2>



<p>Here&#8217;s the second big bug in Apple&#8217;s screen saver framework &#8211; every time the screen saver starts, your <code>ScreenSaverView</code> subclass is created again. But the old one doesn&#8217;t go anywhere. So now you have <em>two</em> copies running simultaneously, which is at the very least wasteful, and can easily lead to gnarly bugs and weird behaviour (e.g. if both are playing sound, or both modify persistent state).</p>



<p>There are essentially two ways to handle this:</p>



<ol class="wp-block-list">
<li>Kill your own process every time you stop animating.</li>



<li>Manually kill or lame-duck older views when a new one is initialised.</li>
</ol>



<p>Note that you <em>cannot</em> simply check at <code>MyScreenSaver</code> initialisation time if an instance already exists and if so fail initialisation (as is prescribed by <a href="https://zsmb.co/building-a-macos-screen-saver-in-kotlin/#macos-sonoma" data-wpel-link="external" target="_blank" rel="external noopener">this</a> otherwise excellent write-up of this problem), because if you don&#8217;t correctly initialise you&#8217;ll sometimes end up with <em>nothing</em> rendering or running (the screen saver framework appears to not gracefully handle initialisation failures).</p>



<p>Killing your own process can work but has some perils:</p>



<ul class="wp-block-list">
<li>If you kill your process in <code>stopAnimation</code> the screen will flash black momentarily before actually exiting screen saver mode, which is visually annoying.</li>



<li>If the screen saver is restarted rapidly after being interrupted, sometimes you&#8217;ll end up with nothing but a black screen (with no screen saver running).  There&#8217;s evidently some race condition in Apple&#8217;s screen saver system between screen saver processes exiting and being [re]launched.</li>
</ul>



<p>So I recommend not taking that approach.  Instead, you can lame-duck the old view instances.  They&#8217;ll stick around, which is a little wasteful of RAM, but as long as they&#8217;re not rendering or otherwise doing anything, they&#8217;re benign.</p>



<p>There are various ways to implement that, but one of the simpler ones is simply a notification between instances:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#000000;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>static let NewInstanceNotification = "com.myapp.MyScreenSaver.NewInstance";

var lameDuck = false

private func setup() {
    // Initial setup…
    
    NotificationCenter.default.post(name: MyScreenSaver.NewInstanceNotification, object: self)

    NotificationCenter.default.addObserver(self,
                                           selector: #selector(neuter(_:)),
                                           name: MyScreenSaver.NewInstanceNotification,
                                           object: nil)

    // Further setup…
}

@objc func neuter(_ notification: Notification) {
    lameDuck = true

    stopAnimation()

    self.removeFromSuperview()

    // TODO: any additional cleanup you can, e.g. release image &amp; video files, throw out transient models and state, etc.

    NotificationCenter.default.removeObserver(self)
    DistributedNotificationCenter.default().removeObserver(self)
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">static</span><span style="color: #000000"> </span><span style="color: #0000FF">let</span><span style="color: #000000"> NewInstanceNotification = </span><span style="color: #A31515">&quot;com.myapp.MyScreenSaver.NewInstance&quot;</span><span style="color: #000000">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">var</span><span style="color: #000000"> lameDuck = </span><span style="color: #0000FF">false</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">private</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">setup</span><span style="color: #000000">() {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #008000">// Initial setup…</span></span>
<span class="line"><span style="color: #000000">    </span></span>
<span class="line"><span style="color: #000000">    NotificationCenter.</span><span style="color: #001080">default</span><span style="color: #000000">.</span><span style="color: #795E26">post</span><span style="color: #000000">(</span><span style="color: #795E26">name</span><span style="color: #000000">: MyScreenSaver.</span><span style="color: #001080">NewInstanceNotification</span><span style="color: #000000">, </span><span style="color: #795E26">object</span><span style="color: #000000">: </span><span style="color: #0000FF">self</span><span style="color: #000000">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    NotificationCenter.</span><span style="color: #001080">default</span><span style="color: #000000">.</span><span style="color: #795E26">addObserver</span><span style="color: #000000">(</span><span style="color: #0000FF">self</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                           </span><span style="color: #795E26">selector</span><span style="color: #000000">: </span><span style="color: #795E26">#selector</span><span style="color: #000000">(</span><span style="color: #795E26">neuter</span><span style="color: #000000">(_:)),</span></span>
<span class="line"><span style="color: #000000">                                           </span><span style="color: #795E26">name</span><span style="color: #000000">: MyScreenSaver.</span><span style="color: #001080">NewInstanceNotification</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                           </span><span style="color: #795E26">object</span><span style="color: #000000">: </span><span style="color: #0000FF">nil</span><span style="color: #000000">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #008000">// Further setup…</span></span>
<span class="line"><span style="color: #000000">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">@objc</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">neuter</span><span style="color: #000000">(</span><span style="color: #795E26">_</span><span style="color: #000000"> </span><span style="color: #001080">notification</span><span style="color: #000000">: Notification) {</span></span>
<span class="line"><span style="color: #000000">    lameDuck = </span><span style="color: #0000FF">true</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">stopAnimation</span><span style="color: #000000">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">self</span><span style="color: #000000">.</span><span style="color: #795E26">removeFromSuperview</span><span style="color: #000000">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #008000">// TODO: any additional cleanup you can, e.g. release image &amp; video files, throw out transient models and state, etc.</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    NotificationCenter.</span><span style="color: #001080">default</span><span style="color: #000000">.</span><span style="color: #795E26">removeObserver</span><span style="color: #000000">(</span><span style="color: #0000FF">self</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">    DistributedNotificationCenter.</span><span style="color: #795E26">default</span><span style="color: #000000">().</span><span style="color: #795E26">removeObserver</span><span style="color: #000000">(</span><span style="color: #0000FF">self</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>You should check <code>lameDuck</code> at the start of methods like <code>startAnimation</code> or <code>animateOneFrame</code> and exit immediately if it&#8217;s set to <code>true</code>.  Unfortunately, Apple&#8217;s screen saver framework will still call those methods on old instances.</p>



<h2 class="wp-block-heading">Exiting</h2>



<p>Unfortunately Apple&#8217;s screen saver system will never terminate your screen saver process.  Worse, even if you do <em>nothing</em> yourself, Apple&#8217;s screen saver framework code will run in an infinite loop, wasting [a small amount of] CPU time.  So it&#8217;s not great to leave your screen saver process running indefinitely.</p>



<p>Thus, I implement an idle timeout in my screen savers, to have them exit if they&#8217;re not active for a while.  This can be done like:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#000000;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>@MainActor var idleTimeoutWorkItem: DispatchWorkItem? = nil

override func startAnimation() {
    // Other code…
    
    DispatchQueue.main.async {
        if let idleTimeoutWorkItem {
            idleTimeoutWorkItem.cancel()
        }

        idleTimeoutWorkItem = nil
    }
    
    // Other code…
}

override func stopAnimation() {
    // Other code…
    
    if !lameDuck {
        DispatchQueue.main.async {
            idleTimeoutWorkItem?.cancel()

            let workItem = DispatchWorkItem(block: {
                NSApplication.shared.terminate(nil)
            })
            
            idleTimeoutWorkItem = workItem
            
            DispatchQueue.main.asyncAfter(wallDeadline: .now() + 65, execute: workItem)
        }
    }
    
    // Other code…
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">@MainActor</span><span style="color: #000000"> </span><span style="color: #0000FF">var</span><span style="color: #000000"> idleTimeoutWorkItem: DispatchWorkItem? = </span><span style="color: #0000FF">nil</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">override</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">startAnimation</span><span style="color: #000000">() {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #008000">// Other code…</span></span>
<span class="line"><span style="color: #000000">    </span></span>
<span class="line"><span style="color: #000000">    DispatchQueue.</span><span style="color: #001080">main</span><span style="color: #000000">.</span><span style="color: #001080">async</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #AF00DB">if</span><span style="color: #000000"> </span><span style="color: #0000FF">let</span><span style="color: #000000"> idleTimeoutWorkItem {</span></span>
<span class="line"><span style="color: #000000">            idleTimeoutWorkItem.</span><span style="color: #795E26">cancel</span><span style="color: #000000">()</span></span>
<span class="line"><span style="color: #000000">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        idleTimeoutWorkItem = </span><span style="color: #0000FF">nil</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">    </span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #008000">// Other code…</span></span>
<span class="line"><span style="color: #000000">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">override</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">stopAnimation</span><span style="color: #000000">() {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #008000">// Other code…</span></span>
<span class="line"><span style="color: #000000">    </span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #AF00DB">if</span><span style="color: #000000"> !lameDuck {</span></span>
<span class="line"><span style="color: #000000">        DispatchQueue.</span><span style="color: #001080">main</span><span style="color: #000000">.</span><span style="color: #001080">async</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">            idleTimeoutWorkItem?.</span><span style="color: #795E26">cancel</span><span style="color: #000000">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #0000FF">let</span><span style="color: #000000"> workItem = </span><span style="color: #795E26">DispatchWorkItem</span><span style="color: #000000">(</span><span style="color: #795E26">block</span><span style="color: #000000">: {</span></span>
<span class="line"><span style="color: #000000">                NSApplication.</span><span style="color: #001080">shared</span><span style="color: #000000">.</span><span style="color: #795E26">terminate</span><span style="color: #000000">(</span><span style="color: #0000FF">nil</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">            })</span></span>
<span class="line"><span style="color: #000000">            </span></span>
<span class="line"><span style="color: #000000">            idleTimeoutWorkItem = workItem</span></span>
<span class="line"><span style="color: #000000">            </span></span>
<span class="line"><span style="color: #000000">            DispatchQueue.</span><span style="color: #001080">main</span><span style="color: #000000">.</span><span style="color: #795E26">asyncAfter</span><span style="color: #000000">(</span><span style="color: #795E26">wallDeadline</span><span style="color: #000000">: .</span><span style="color: #795E26">now</span><span style="color: #000000">() + </span><span style="color: #098658">65</span><span style="color: #000000">, </span><span style="color: #795E26">execute</span><span style="color: #000000">: workItem)</span></span>
<span class="line"><span style="color: #000000">        }</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">    </span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #008000">// Other code…</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>I chose the 65 second timeout somewhat arbitrarily.  I figure there&#8217;s a reasonable chance a user will unlock their screen to do something quick, then engage the screen saver again &#8211; all in the less than a minute &#8211; and the cost of idling in the background for an extra minute is small, compared to the cost of relaunching the whole app and reinitialising your renderer.</p>



<p>I added five extra seconds to reduce the probability of aligning with some one-minute timer (e.g. a spurious wake with the screen saver set to start automatically after one minute of no user activity).</p>



<p>You can adjust it however you like.</p>



<h2 class="wp-block-heading">Testing your screen saver</h2>



<p>Double-clicking the built product (your &#8220;.saver&#8221; app) will prompt the user to install it, replacing an old version if necessary.  So that works, though I find it faster to just manually copy the &#8220;.saver&#8221; app to <code>~/Library/Screen Savers</code>.  Just make sure to kill the existing <code>legacyScreenSaver</code> process, if necessary.</p>



<p>You can test it in System Settings, in the Screen Saver pane.  That&#8217;s the only place you can test the live preview part.</p>



<p>But otherwise, I found it easiest to just set one of the screen hot corners to start the screen saver, and use that immediately after copying the new &#8220;.saver&#8221; file into place.</p>



<p>Just be aware that the first time any new copy of the screen saver runs, macOS runs a verification on the bundle, which can take a while if your screen saver is non-trivial in size (e.g. if you bundle large image or video resources).  You&#8217;ll get a black screen with nothing happening, after invoking the screen saver, while that verification is running.</p>



<h2 class="wp-block-heading">Distributing your screen saver</h2>



<p>You don&#8217;t <em>have</em> to sign your screen saver, necessarily, but users will get some annoying error dialogs trying to run it, and will have to fiddle with things in System Settings &#8211; or, if they&#8217;re on a corporate Mac, they might not be able to run it at all.  So it&#8217;s preferable to just sign it.</p>



<p>Xcode doesn&#8217;t support signing screen savers like it does for plain app targets and the like.  So you have to do it manually via the command line, on the built product (your &#8220;.saver&#8221; app).  Thankfully it&#8217;s just two commands (once you have the appropriate stuff set up in your developer account &#8211; note that you will need a paid Apple Developer account, at $99/year).</p>



<p>Follow the instructions <a href="https://www.gabrieluribe.me/blog/how-to-distribute-a-screensaver-on-macos-2022" data-wpel-link="external" target="_blank" rel="external noopener">here</a>, but note that they&#8217;re missing the final step:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>xcrun stapler staple -v MyScreenSaver.saver</p>
</blockquote>



<p>Note that you run it against the screen saver itself, <em>not</em> the zip file.  The zip file&#8217;s just a hack to get Apple&#8217;s notary tool to accept the submission.  It&#8217;s the screen saver bundle itself that&#8217;s actually notarised and signed.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>⚠️ <code>notarytool</code> uploads your screen saver to Apple.  Be sure it doesn&#8217;t contain anything you&#8217;re not happy being potentially public (Apple will <em>presumably</em> try to keep your uploads private to Apple, and <em>might</em> not intentionally store them forever, but I wouldn&#8217;t bet my life on their confidentiality).</p>
</div></div>



<h2 class="wp-block-heading">Conclusion</h2>



<p>That&#8217;s &#8220;it&#8221; in a superficial sense &#8211; if you&#8217;ve followed all this so far, you have a <em>roughly</em> working screen saver.</p>



<p>But there are a lot more bugs and nuances thereof that may afflict you, depending on what you&#8217;re doing in your screen saver.  So good luck. 😣</p>



<h2 class="wp-block-heading">Addendum: SaveHollywood</h2>



<p>I found that looking at existing open source screen savers was <em>partially</em> helpful, but also sometimes misleading.  e.g. for my most recent screen saver I basically just play a video in a loop (which <em>should</em> be embarrassingly trivial but took two weeks to get working properly, thanks to the aforementioned bugs in Apple&#8217;s frameworks, among many others).  In that case I looked at <a href="https://github.com/packagesdev/savehollywood" data-wpel-link="external" target="_blank" rel="external noopener">SaveHollywood</a>, a similar screen saver, for aid and ideas.</p>



<p>Unfortunately, SaveHollywood is abandoned and doesn&#8217;t work on recent versions of macOS.  The way it does some things is archaic and either not the best way or not functional at all.</p>



<p>Nonetheless, it did help with some of the higher-level aspects, above the screen saver machinery itself, like how to use <code>AVPlayer</code> in a screen saver.</p>



<p>So, <em>do</em> check out similar, existing screen savers (and of course just use them directly if they suit your needs!) but beware of obsolete or otherwise incorrect code.</p>



<h2 class="wp-block-heading">Addendum:  What this says about Apple</h2>



<p>What really troubles me about the screen saver system is what it says about Apple&#8217;s approach to the Mac, software, and their users.  Which is sadly just the same thing we&#8217;ve been seeing for years now.</p>



<p>Screen savers used to work fine.  There was an API established <em>long</em> ago that was lightweight, straight-forward, and effective.  All Apple had to do was <em>not break it</em>.</p>



<p>And <em>how</em> they broke it is troubling.  Perhaps it was prompted by some otherwise unrelated but well-meaning refactor &#8211; pulling screen saver code out into a separate, sandboxed process, perhaps, for improved system security.  Fine.  But if it&#8217;s worth changing it&#8217;s worth changing <em>properly</em>.  It&#8217;s very clear that whomever did the changes either (a) didn&#8217;t care that they broke things badly, or (b) didn&#8217;t care to check.</p>



<p>It&#8217;s that recurring theme of <em>not caring</em> that&#8217;s most disappointing in today&#8217;s Apple.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/how-to-make-a-macos-screen-saver/feed/</wfw:commentRss>
			<slash:comments>3</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2025/12/screen-saver-system-settings-panel-with-my-screen-saver-selected.webp" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">8580</post-id>	</item>
		<item>
		<title>&#8220;Import from iPhone or iPad&#8221; doesn&#8217;t work when any view contains a SwiftUI Toggle</title>
		<link>https://wadetregaskis.com/import-from-iphone-or-ipad-doesnt-work-when-any-view-contains-a-swiftui-toggle/</link>
					<comments>https://wadetregaskis.com/import-from-iphone-or-ipad-doesnt-work-when-any-view-contains-a-swiftui-toggle/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Thu, 22 Aug 2024 23:22:42 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[Continuity Camera]]></category>
		<category><![CDATA[Import from iPhone or iPad]]></category>
		<category><![CDATA[importableFromServices]]></category>
		<category><![CDATA[ImportFromDevicesCommands]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[SwiftUI]]></category>
		<category><![CDATA[Toggle]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8371</guid>

					<description><![CDATA[This is a public reposting of FB14893699, in case it’s helpful to anyone else or especially in case someone else has seen this too and knows how to work around it. If any view in the [active] window contains a Toggle &#8211; even one that’s disabled or hidden &#8211; then Continuity Camera (re. ImportFromDevicesCommands and&#8230; <a class="read-more-link" href="https://wadetregaskis.com/import-from-iphone-or-ipad-doesnt-work-when-any-view-contains-a-swiftui-toggle/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>This is a public reposting of FB14893699, in case it’s helpful to anyone else or especially in case someone else has seen this too and knows how to work around it.</p>
</div></div>



<p>If <em>any</em> view in the [active] window contains a <a href="https://developer.apple.com/documentation/swiftui/toggle" data-wpel-link="external" target="_blank" rel="external noopener"><code>Toggle</code></a> &#8211; even one that’s disabled or hidden &#8211; then Continuity Camera (re. <code><a href="http://ImportFromDevicesCommands" data-wpel-link="external" target="_blank" rel="external noopener">ImportFromDevicesCommands</a></code> and <code><a href="https://developer.apple.com/documentation/scenekit/sceneview/4049460-importablefromservices" data-wpel-link="external" target="_blank" rel="external noopener">importableFromServices</a></code>) doesn’t work; all the submenu items under “Import from iPhone or iPad” are disabled.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img fetchpriority="high" decoding="async" width="412" height="309" src="https://wadetregaskis.com/wp-content/uploads/2024/08/22import-from-iphone-or-ipad22-disabled.webp" alt="Screenshot of the File menu with the &quot;Import from iPhone or iPad&quot; submenu expanded, and all items therein are disabled." class="wp-image-8372" srcset="https://wadetregaskis.com/wp-content/uploads/2024/08/22import-from-iphone-or-ipad22-disabled.webp 412w, https://wadetregaskis.com/wp-content/uploads/2024/08/22import-from-iphone-or-ipad22-disabled-256x192.webp 256w, https://wadetregaskis.com/wp-content/uploads/2024/08/22import-from-iphone-or-ipad22-disabled@2x.webp 824w" sizes="(max-width: 412px) 100vw, 412px" /></figure>
</div>


<p>I don’t know if this is truly specific to <code>Toggle</code>, that’s just the example case I happen to have isolated [first?].</p>



<p>What’s really weird is that once a <code>Toggle</code> has <em>ever</em> been displayed, even if you subsequently remove it from the view hierarchy entirely the “Import from iPhone or iPad” submenu items all remain disabled.</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #AF00DB">import</span><span style="color: #000000"> </span><span style="color: #267F99">SwiftUI</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">@main</span></span>
<span class="line"><span style="color: #0000FF">struct</span><span style="color: #000000"> </span><span style="color: #267F99">Example</span><span style="color: #000000">: App {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">@State</span><span style="color: #000000"> </span><span style="color: #0000FF">var</span><span style="color: #000000"> breakImportFromiDevice = </span><span style="color: #0000FF">true</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">@State</span><span style="color: #000000"> </span><span style="color: #0000FF">var</span><span style="color: #000000"> text = </span><span style="color: #A31515">&quot;&quot;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">var</span><span style="color: #000000"> body: some Scene {</span></span>
<span class="line"><span style="color: #000000">        WindowGroup {</span></span>
<span class="line"><span style="color: #000000">            VStack {</span></span>
<span class="line"><span style="color: #000000">                </span><span style="color: #795E26">TextField</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;Input&quot;</span><span style="color: #000000">, </span><span style="color: #795E26">text</span><span style="color: #000000">: $text) </span><span style="color: #008000">// Doesn&#39;t break anything.</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">                </span><span style="color: #AF00DB">if</span><span style="color: #000000"> breakImportFromiDevice {</span></span>
<span class="line"><span style="color: #000000">                    </span><span style="color: #795E26">Toggle</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;Break import from iDevice&quot;</span><span style="color: #000000">, </span><span style="color: #795E26">isOn</span><span style="color: #000000">: $breakImportFromiDevice)</span></span>
<span class="line"><span style="color: #000000">                }</span></span>
<span class="line"><span style="color: #000000">            }</span></span>
<span class="line"><span style="color: #000000">            .</span><span style="color: #795E26">importableFromServices</span><span style="color: #000000">(</span><span style="color: #795E26">action</span><span style="color: #000000">: { (</span><span style="color: #795E26">images</span><span style="color: #000000">: [NSImage]) -&gt; </span><span style="color: #267F99">Bool</span><span style="color: #000000"> </span><span style="color: #AF00DB">in</span></span>
<span class="line"><span style="color: #000000">                </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;Load image!&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">                </span><span style="color: #AF00DB">return</span><span style="color: #000000"> </span><span style="color: #0000FF">true</span></span>
<span class="line"><span style="color: #000000">            })</span></span>
<span class="line"><span style="color: #000000">        }.</span><span style="color: #001080">commands</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #795E26">ImportFromDevicesCommands</span><span style="color: #000000">()</span></span>
<span class="line"><span style="color: #000000">        }</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">}</span></span>
<span class="line"></span></code></pre></div>



<p>There are some circumstances in which this gets “unbroken” during interactions with other views and so forth, which is 100% reproducible in my real app but I have no idea what the reason is.  The steps involved in my real app are kinda ridiculous (and not in any way remotely a viable workaround) and make absolutely no sense &#8211; it only “unbreaks” when a specific view is in a specific weird state (itself kind of the result of a bug, albeit a benign one).  And that weird state is merely whether it’s displaying one image or another &#8211; which as far as SwiftUI is concerned is not even a difference in view state, since I’m just swapping <code><a href="https://developer.apple.com/documentation/appkit/nsimage" data-wpel-link="external" target="_blank" rel="external noopener">NSImage</a></code>s under the cover.</p>



<p>I figured it must be something to do with view focus, but after much experimentation I believe I can conclusively rule that out.  No matter which view has focus, or how focus is configured, or which views are even focusable at all, the problem persists.  Likewise for window focus and key state.  And, [accessability-]focusable views other than <code>Toggle</code> &#8211; e.g. <code><a href="https://developer.apple.com/documentation/swiftui/textfield" data-wpel-link="external" target="_blank" rel="external noopener">TextField</a></code> &#8211; don’t cause any issues.</p>



<p>Frankly it&#8217;s baffling, and a mite infuriating.  I can&#8217;t even conceive of how SwiftUI can be so incredibly broken regarding such basic functionality, and the apparent interaction of GUI elements that have absolutely no business together.</p>



<hr class="wp-block-separator has-alpha-channel-opacity is-style-dots"/>



<p>Tangentially, a few things I&#8217;ve noticed about this Continuity Camera feature:</p>



<ul class="wp-block-list">
<li>&#8220;Add Sketch&#8221; doesn&#8217;t do anything.  Unlike the other options, which open the camera app on the target iDevice, it has no effect. 🤷‍♂️</li>



<li>Within the Finder, you can right-click empty whitespace within a folder, and the contextual menu has this &#8220;Import from iPhone or iPad&#8221; option at the bottom.  That&#8217;s pretty handy &#8211; until now I&#8217;d been doing it the &#8220;hard&#8221; way by taking a photo on my iPhone and AirDropping it across to my Mac.<br><br>I&#8217;d never noticed that feature prior to debugging this problem (I wanted to confirm that multiple other apps worked just fine; that it wasn&#8217;t an issue with Continuity Camera system-wide).</li>



<li>TextEdit has the same feature but tweaks the wording to &#8220;Insert…&#8221; rather than &#8220;Import…&#8221;, which I thought is both a nice touch and frustratingly not something you can do in your own apps (at least, not in SwiftUI). 😕</li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/import-from-iphone-or-ipad-doesnt-work-when-any-view-contains-a-swiftui-toggle/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2024/08/22import-from-iphone-or-ipad22-disabled.webp" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">8371</post-id>	</item>
		<item>
		<title>NSPasteboard crashes due to unsafe, internal concurrent memory mutation when handling file promises</title>
		<link>https://wadetregaskis.com/nspasteboard-crashes-due-to-unsafe-internal-concurrent-memory-mutation-when-handling-file-promises/</link>
					<comments>https://wadetregaskis.com/nspasteboard-crashes-due-to-unsafe-internal-concurrent-memory-mutation-when-handling-file-promises/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Thu, 22 Aug 2024 05:01:02 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[AppKit]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[Drag & drop]]></category>
		<category><![CDATA[memory corruption]]></category>
		<category><![CDATA[NSItemProvider]]></category>
		<category><![CDATA[NSPasteboard]]></category>
		<category><![CDATA[NSPasteboardItem]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[SwiftUI]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8369</guid>

					<description><![CDATA[This is a public reposting of FB14885505, in case it&#8217;s helpful to anyone else or especially in case someone else has seen this too and knows how to work around it. NSPasteboard mutates itself simultaneously from the main thread and the global concurrent Dispatch pool, w.r.t. to its internal type cache. This is surprisingly trivial&#8230; <a class="read-more-link" href="https://wadetregaskis.com/nspasteboard-crashes-due-to-unsafe-internal-concurrent-memory-mutation-when-handling-file-promises/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>This is a public reposting of FB14885505, in case it&#8217;s helpful to anyone else or especially in case someone else has seen this too and knows how to work around it.</p>
</div></div>



<p><code><a href="https://developer.apple.com/documentation/appkit/nspasteboard" data-wpel-link="external" target="_blank" rel="external noopener">NSPasteboard</a></code> mutates itself simultaneously from the main thread and the global concurrent <a href="https://developer.apple.com/documentation/DISPATCH" data-wpel-link="external" target="_blank" rel="external noopener">Dispatch</a> pool, w.r.t. to its internal type cache. This is surprisingly trivial to reproduce (sample code below) by just dropping, e.g. a file promise (such as by opening a PNG in Preview, revealing the thumbnails sidebar, and then dragging the thumbnail onto the sample project’s window).</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #AF00DB">import</span><span style="color: #000000"> </span><span style="color: #267F99">SwiftUI</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">struct</span><span style="color: #000000"> </span><span style="color: #267F99">ContentView</span><span style="color: #000000">: View {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">var</span><span style="color: #000000"> body: some View {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #795E26">Rectangle</span><span style="color: #000000">().</span><span style="color: #795E26">onDrop</span><span style="color: #000000">(</span><span style="color: #795E26">of</span><span style="color: #000000">: NSImage.</span><span style="color: #001080">imageTypes</span><span style="color: #000000">, </span><span style="color: #795E26">isTargeted</span><span style="color: #000000">: </span><span style="color: #0000FF">nil</span><span style="color: #000000">) { </span><span style="color: #001080">_</span><span style="color: #000000"> </span><span style="color: #AF00DB">in</span></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #0000FF">let</span><span style="color: #000000"> pb = </span><span style="color: #795E26">NSPasteboard</span><span style="color: #000000">(</span><span style="color: #795E26">name</span><span style="color: #000000">: .</span><span style="color: #001080">drag</span><span style="color: #000000">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #001080">_</span><span style="color: #000000"> = pb.</span><span style="color: #001080">pasteboardItems</span><span style="color: #000000"> </span><span style="color: #008000">// Seems to be necessary for the crash.</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #001080">_</span><span style="color: #000000"> = NSImage.</span><span style="color: #001080">imageTypes</span><span style="color: #000000"> </span><span style="color: #008000">// Not strictly necessary for the crash, but seems to make it more likely. 🤷‍♂️</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #AF00DB">return</span><span style="color: #000000"> </span><span style="color: #0000FF">true</span></span>
<span class="line"><span style="color: #000000">        }</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>Judging from the callstack that runs in the concurrent pool, this is specific to file promises (and that seems to match my experience &#8211; it only crashes for some test cases, all of which involve file promises being present in the drag pasteboard at the time of the drop).</p>



<p>Since this bug causes semi-random memory corruption, it manifests in a large number of ways &#8211; not all of which are all that helpful.  But at least one case I’ve seen a few times is helpful, as it clearly shows the offending internal <code>NSPasteboard</code> code running concurrently with itself, e.g.:</p>



<pre class="wp-block-preformatted">Main queue / thread:
#0	0x00007ff80108dab5 in _platform_bzero$VARIANT$Haswell ()
#1	0x000000010df8774d in GuardMalloc_mallocInternal ()
#2	0x00007ff90792542f in stack_logging_lite_malloc ()
#3	0x00007ff800ea8733 in _malloc_zone_malloc_instrumented_or_legacy ()
#4	0x00007ff800f39a72 in _vasprintf ()
#5	0x00007ff800f16922 in asprintf ()
#6	0x00007ff80125655a in -[NSObject(NSObject) __dealloc_zombie] ()
#7	0x00007ff8020e039a in -[NSConcreteMapTable dealloc] ()
#8	0x00007ff804876983 in -[NSPasteboard _updateTypeCacheIfNeeded] ()
#9	0x00007ff8048763df in -[NSPasteboard _typesAtIndex:combinesItems:] ()
#10	0x00007ff804aad148 in NSCoreDragReceiveMessageProc ()
#11	0x00007ff807517b1a in CallReceiveMessageCollectionWithMessage ()
#12	0x00007ff8075124fa in DoMultipartDropMessage ()
#13	0x00007ff8075122ce in DoDropMessage ()
#14	0x00007ff8075159a9 in CoreDragMessageHandler ()
#15	0x00007ff8011d776b in __CFMessagePortPerform ()
#16	0x00007ff80113e5b7 in __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ ()
#17	0x00007ff80113e4ee in __CFRunLoopDoSource1 ()
#18	0x00007ff80113d166 in __CFRunLoopRun ()
#19	0x00007ff80113c112 in CFRunLoopRunSpecific ()
#20	0x00007ff80bb55a09 in RunCurrentEventLoopInMode ()
#21	0x00007ff80bb55646 in ReceiveNextEventCommon ()
#22	0x00007ff80bb55561 in _BlockUntilNextEventMatchingListInModeWithFilter ()
#23	0x00007ff8047acc61 in _DPSNextEvent ()
#24	0x00007ff8050c0dc0 in -[NSApplication(NSEventRouting) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] ()
#25	0x00007ff80479e075 in -[NSApplication run] ()
#26	0x00007ff804771ff3 in NSApplicationMain ()
#27	0x00007ff90dc24557 in ___lldb_unnamed_symbol57096 ()
#28	0x00007ff90e31fe64 in ___lldb_unnamed_symbol104448 ()
#29	0x00007ff90e6e63ff in static SwiftUI.App.main() -&gt; () ()
#30	0x000000010dfa5cce in static NSPasteboardItem_CrashApp.$main() ()
#31	0x000000010dfa5d69 in main at /Users/SadPanda/Documents/NSPasteboardItem Crash/NSPasteboardItem Crash/NSPasteboardItem_CrashApp.swift:11
#32	0x00007ff800cd5366 in start ()

Dispatch concurrent queue (default QoS):
#0	0x00007ff80111b45c in -[__NSSetM addObject:] ()
#1	0x00007ff80487692e in -[NSPasteboard _updateTypeCacheIfNeeded] ()
#2	0x00007ff8048763df in -[NSPasteboard _typesAtIndex:combinesItems:] ()
#3	0x00007ff804aa9597 in -[NSPasteboard _canRequestDataForType:index:usesPboardTypes:combinesItems:] ()
#4	0x00007ff804fdd161 in -[NSPasteboard _dataForType:index:usesPboardTypes:combinesItems:securityScoped:] ()
#5	0x00007ff804aa7c4b in -[NSPasteboardItem dataForType:] ()
#6	0x00007ff8055804af in -[NSFilePromiseReceiver receivePromisedFilesAtDestination:options:operationQueue:reader:] ()
#7	0x00007ff90e787b51 in ___lldb_unnamed_symbol131674 ()
#8	0x00007ff90e9bc340 in ___lldb_unnamed_symbol148147 ()
#9	0x00007ff8020d00ba in __NSBLOCKOPERATION_IS_CALLING_OUT_TO_A_BLOCK__ ()
#10	0x00007ff8020cffb8 in -[NSBlockOperation main] ()
#11	0x00007ff8020cff4b in __NSOPERATION_IS_INVOKING_MAIN__ ()
#12	0x00007ff8020cf1ec in -[NSOperation start] ()
#13	0x00007ff8020cef0d in __NSOPERATIONQUEUE_IS_STARTING_AN_OPERATION__ ()
#14	0x00007ff8020cedde in __NSOQSchedule_f ()
#15	0x000000010e68ce7d in _dispatch_block_async_invoke2 ()
#16	0x000000010e67ca7b in _dispatch_client_callout ()
#17	0x000000010e67fa09 in _dispatch_continuation_pop ()
#18	0x000000010e67eae8 in _dispatch_async_redirect_invoke ()
#19	0x000000010e6906a9 in _dispatch_root_queue_drain ()
#20	0x000000010e6911ba in _dispatch_worker_thread2 ()
#21	0x000000010dfb832f in _pthread_wqthread ()
#22	0x000000010dfbebeb in start_wqthread ()</pre>



<p>There doesn’t appear to be any workaround (short of not supporting drops at all!).</p>



<p>The more complicated the drop handler the more likely it is to promptly crash upon drop &#8211; in my real code with a non-trivial handler, it’s virtually guaranteed to crash on the second drop containing a file promise, while in the vastly reduced sample code (above) it can take dozens of drops before it finally crashes outright.</p>



<p>I have not <em>directly</em> tested whether this <code>NSPasteboard</code> bug occurs in the absence of <a href="https://developer.apple.com/documentation/SwiftUI" data-wpel-link="external" target="_blank" rel="external noopener">SwiftUI</a>, so I don’t strictly know if the root cause is in <a href="https://developer.apple.com/documentation/appkit" data-wpel-link="external" target="_blank" rel="external noopener">AppKit</a> or SwiftUI. However, since most SwiftUI apps don’t support drag-and-drop, but plenty of AppKit ones do and manage to not crash when given the exact same test cases, I do strongly suspect SwiftUI is causing this somehow.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>☝️ You may wonder why I’m directly accessing the drag pasteboard rather than using the <code><a href="https://developer.apple.com/documentation/foundation/nsitemprovider" data-wpel-link="external" target="_blank" rel="external noopener">NSItemProvider</a></code>s provided by SwiftUI. It’s because that API is horribly broken &#8211; in many cases the provided <code>NSItemProvider</code>(s) are duds that contain no actual data. So I have to use the drag pasteboard directly in order to stand any chance of supporting drag &amp; drop.</p>



<p>Also, the <code>NSItemProvider</code>-based API is harder to use and doesn&#8217;t support important aspects of drag-and-drop, like file promises (although, with <code>NSPasteboard</code> apparently corrupting itself when file promises are received, I guess none of Apple&#8217;s APIs do anymore 😔).</p>
</div></div>



<hr class="wp-block-separator has-alpha-channel-opacity is-style-dots"/>



<h3 class="wp-block-heading">Follow-up (September 12th, 2024)</h3>



<p>I actually received a response from Apple, from a real human (or at least a convincing AI).  Ultimately their response didn&#8217;t help as it contained some mistakes, but I&#8217;m hopeful there&#8217;ll be more follow-up and a productive conclusion.  And in the interim, they did assert a few things which are important to know, and are not otherwise documented by Apple:</p>



<ul class="wp-block-list">
<li>SwiftUI&#8217;s <code><a href="https://developer.apple.com/documentation/swiftui/view/ondrop(of:istargeted:perform:)-f15m" data-wpel-link="external" target="_blank" rel="external noopener">onDrop(of:isTargeted:perform:)</a></code> method makes no claims or promises as to what thread / queue it executes the closure on, and in fact according to the anonymous Apple engineer it <em>never</em> executes the closure on the main thread.<br><br>Now, while that may be the intent, the reality of that is wrong &#8211; in my experience it <em>always</em> executes the closure on the main thread (which makes a lot of sense to me as drag-and-drop event handling in AppKit has always been on the main thread in practice).<br><br>Nonetheless, Apple says one cannot rely on the current behaviour and should in fact assume it <em>never</em> executes on the main thread (though in practice that means you have to <em>check</em>, not assume, since if you blindly do something like <code>DispatchQueue.main.sync</code><code style="font-size: 15px;"> { … }</code> in your drop handler your code <em>will</em> deadlock, today).</li>



<li><code><a href="https://developer.apple.com/documentation/appkit/nspasteboard" data-wpel-link="external" target="_blank" rel="external noopener">NSPasteboard</a></code> is not safe to use outside the main thread / queue.<br><br>This isn&#8217;t documented anywhere public &#8211; not in <code>NSPasteboard</code>&#8216;s documentation itself, nor the ancient <a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/ThreadSafetySummary/ThreadSafetySummary.html#//apple_ref/doc/uid/10000057i-CH12-123351-BBCFIIEB" data-wpel-link="external" target="_blank" rel="external noopener">Application Kit Framework Thread Safety</a> documentation.<br><br>I wouldn&#8217;t be surprised if it&#8217;s broadly true, as the AppKit APIs involving it always seemed main-thread centric anyway (all the handlers and delegate methods involving pasteboards are invoked on the main thread, in my experience).  And it&#8217;s generally best to assume everything in AppKit is main-thread-only unless it&#8217;s explicitly documented otherwise.<br><br>However, it&#8217;s important to note that Apple&#8217;s <em>own</em> code doesn&#8217;t follow this rule &#8211; e.g. <code><a href="https://developer.apple.com/documentation/appkit/nsfilepromisereceiver" data-wpel-link="external" target="_blank" rel="external noopener">NSFilePromiseReceiver</a></code>, internally, uses <code>NSPasteboard</code> from the global concurrent queue.</li>
</ul>



<p>Even though Apple&#8217;s initial response to this bug report hasn&#8217;t been all that fruitful, I do want to emphasise the fact that they <em>did</em> respond, which was a pleasant surprise and very much appreciated.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/nspasteboard-crashes-due-to-unsafe-internal-concurrent-memory-mutation-when-handling-file-promises/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">8369</post-id>	</item>
		<item>
		<title>Stellaris AI construction &#038; science ship spam bugs</title>
		<link>https://wadetregaskis.com/stellaris-ai-construction-science-ship-spam-bugs/</link>
					<comments>https://wadetregaskis.com/stellaris-ai-construction-science-ship-spam-bugs/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Thu, 01 Aug 2024 00:33:12 +0000</pubDate>
				<category><![CDATA[Gaming]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[Stellaris]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8329</guid>

					<description><![CDATA[There&#8217;s a couple of bugs in Stellaris, that have been present for year(s), where the AI inexplicably builds absurd numbers of construction and/or science ships, then has them sit idle forever. This AI behaviour would be merely eccentric if it didn&#8217;t cause major performance issues with the game. For the case of too many science&#8230; <a class="read-more-link" href="https://wadetregaskis.com/stellaris-ai-construction-science-ship-spam-bugs/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>There&#8217;s a couple of bugs in <a href="https://www.paradoxinteractive.com/games/stellaris/about" data-wpel-link="external" target="_blank" rel="external noopener">Stellaris</a>, <a href="https://www.reddit.com/r/Stellaris/comments/11s0wva/lag_spike_every_7_days/" data-wpel-link="external" target="_blank" rel="external noopener">that have been present for year(s)</a>, where the AI inexplicably builds absurd numbers of construction and/or science ships, then has them sit idle forever.</p>


<div class="wp-block-image is-style-default">
<figure class="aligncenter size-full"><img decoding="async" width="5120" height="2880" src="https://wadetregaskis.com/wp-content/uploads/2024/07/stellaris-legion-of-science-ships.avif" alt="" class="wp-image-8333" srcset="https://wadetregaskis.com/wp-content/uploads/2024/07/stellaris-legion-of-science-ships.avif 5120w, https://wadetregaskis.com/wp-content/uploads/2024/07/stellaris-legion-of-science-ships-256x144.avif 256w, https://wadetregaskis.com/wp-content/uploads/2024/07/stellaris-legion-of-science-ships-1024x576.avif 1024w, https://wadetregaskis.com/wp-content/uploads/2024/07/stellaris-legion-of-science-ships-768x432.avif 768w, https://wadetregaskis.com/wp-content/uploads/2024/07/stellaris-legion-of-science-ships-2048x1152.avif 2048w, https://wadetregaskis.com/wp-content/uploads/2024/07/stellaris-legion-of-science-ships-256x144@2x.avif 512w, https://wadetregaskis.com/wp-content/uploads/2024/07/stellaris-legion-of-science-ships-2048x1152@2x.avif 4096w" sizes="(max-width: 5120px) 100vw, 5120px" /></figure>
</div>


<p>This AI behaviour would be merely eccentric if it didn&#8217;t cause major performance issues with the game.</p>



<p>For the case of too many science ships, the effect seems subtle &#8211; just a small, evenly-spread slow-down of the game.  Probably not worth worrying about generally, though if you happen to notice an AI empire spamming science ships, it may be worth addressing.</p>



<p>For the case of too many construction ships, the effect is much more pronounced.   Every AI construction ship triggers an expensive script periodically, and all aligned together in time, which can result in noticeable hangs every seven in-game days if there&#8217;s many of them.</p>



<p>And &#8220;too many&#8221; is a surprisingly small number &#8211; as few as ten can cause five-second-long hangs every seven in-game days, in my experience.  I suspect their detrimental effect is superlinear to their number (because the AI having only a couple seems to incur no noticeable overhead).  It&#8217;s likely also contingent on some other factor, that I have not yet identified, as the hangs only manifest themselves in some games.</p>



<p>I&#8217;m very thankful to <a href="https://www.reddit.com/user/Confident_Chance_770/" data-wpel-link="external" target="_blank" rel="external noopener">Confident_Chance_770</a> for pinning the periodic hangs down to this cause.</p>



<h2 class="wp-block-heading">Confirming the too-many-construction-ships diagnosis</h2>



<p>Since this can be caused by a mere few construction ships &#8211; as little as four or five, which is not otherwise remarkable &#8211; one way to confirm this is the issue is to profile the game&#8217;s script execution:</p>



<ol class="wp-block-list">
<li>Pause the game (e.g. hit the spacebar).  Ideally a day or so before the next hang day, and clear of the first day of the in-game month.</li>



<li>Hit the backtick (`) key (top-left on most keyboard layouts, above tab and left of 1) to bring up <a href="https://stellaris.paradoxwikis.com/Console_commands" data-wpel-link="external" target="_blank" rel="external noopener">the game&#8217;s debugger / cheat console</a>.</li>



<li>Type &#8220;script_profiler&#8221; and hit return.</li>



<li>Unpause the game (e.g. close the console by hitting the backtick key again, then hit the spacebar).</li>



<li>Let the game run until one of the hang days is encountered.  Note that the game will run slower than normal while profiling is enabled, especially on the hang days.</li>



<li>Pause the game.</li>



<li>Open the console again.</li>



<li>Type &#8220;script_profiler&#8221; again and hit return.  This concludes the profiling and dumps the results to two files.</li>



<li>Open the summary results from ~/Documents/Paradox Interactive/Stellaris/logs/script_profiling_summary.log.</li>



<li>Check if &#8220;planet_possible.find_planet&#8221; is the topmost item and listed as consuming significant amounts of CPU time (e.g. 5s, for a single hang day).  If it is, you&#8217;re likely experiencing the construction ship bug.<br><br>If it&#8217;s not, it&#8217;s <em>possible</em> it&#8217;s still the same bug, but possibly not.  You could try the usual diagnostic routine &#8211; e.g. websearch for the symptoms and top script names from the profile, contact the author of any suspicious or newly-added plug-ins you&#8217;re using, contact Paradox Interactive, etc.</li>
</ol>



<h2 class="wp-block-heading">Remedying the bugs</h2>



<p>If you determine that it likely is the construction / science ship spam bug, you can rectify it via these steps:</p>



<ol class="wp-block-list">
<li>Save your game.  Just in case something goes wrong with this process.</li>



<li>Pause the game.  <strong>This is very important</strong> &#8211; if you don&#8217;t, the AI is likely to complete screw with your empire while you&#8217;re away (during steps 4 through 6).</li>



<li>Open the console.</li>



<li>Type &#8220;play N&#8221; where N is an integer starting at one.  Not all integers are used &#8211; just keep incrementing until you get a correct result, which is that it switches your control to one of the AI empires.</li>



<li>Close the console.</li>



<li>Check the civilian ships list to see if there are absurd numbers of science ships, or more than a couple of construction ships.  If there are, kill them.  Usually when this bug occurs most of the ships are together in one system, so you can accelerate this process by finding those big group(s) of construction &amp; science ships, drag-selecting them within the system view, then hitting the Delete key (and return to confirm decommissioning).  It&#8217;s pretty safe to just delete <em>all</em> the AI empire&#8217;s construction &amp; science ships, since the AI can cheaply and quickly build new ones if they <em>actually</em> want them (in my experience they often don&#8217;t, at least not promptly, suggesting the ships were unnecessary to begin with).<br><br>If there are not, go back to step 4 and try subsequent integers, to check other AI empires.</li>



<li>Once you&#8217;ve found and destroyed the offending ships, type &#8220;play 0&#8221; into the console to restore control to your own empire.</li>



<li>Unpause the game and continue playing &#8211; hopefully now without the noticeable hangs every seven in-game days.</li>
</ol>



<p>Repeat as necessary when the problem recurs (and perhaps focus your in-game efforts on wiping out the offending AIs, as a more permanent solution).</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/stellaris-ai-construction-science-ship-spam-bugs/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2024/07/stellaris-legion-of-science-ships-2048x1152.avif" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">8329</post-id>	</item>
		<item>
		<title>Lightroom could not import this catalog because of an unknown error</title>
		<link>https://wadetregaskis.com/lightroom-could-not-import-this-catalog-because-of-an-unknown-error/</link>
					<comments>https://wadetregaskis.com/lightroom-could-not-import-this-catalog-because-of-an-unknown-error/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Fri, 07 Jun 2024 22:23:07 +0000</pubDate>
				<category><![CDATA[Howto]]></category>
		<category><![CDATA[Photography]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[Lightroom]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8219</guid>

					<description><![CDATA[I don&#8217;t know why, but it&#8217;s apparently impossible to directly import a Lightroom catalog from one computer into the catalog of another. It always fails at the end of the import with the same infuriatingly useless error message. However, I seem to have found a fairly reliable workaround: In my experience you must perform the&#8230; <a class="read-more-link" href="https://wadetregaskis.com/lightroom-could-not-import-this-catalog-because-of-an-unknown-error/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>I don&#8217;t know why, but it&#8217;s apparently impossible to directly import a Lightroom catalog from one computer into the catalog of another.  It <em>always</em> fails at the end of the import with the same infuriatingly useless error message.</p>



<p>However, I seem to have found a fairly reliable workaround:</p>



<ol class="wp-block-list">
<li>If you&#8217;re directly plugging in a removable SSD, as your means of moving files between computers, then skip to step 2.<br><br>Copy the catalog-to-be-imported, along with all the original files it references, to local storage on the target computer.  You must preserve their relative paths, so it&#8217;s easiest if you pre-arrange your source catalog&#8217;s files (the &#8220;.lrcat&#8221; file and its entourage) to be in the same root folder as your original files (images etc)<sup data-fn="259394ac-eabd-4e24-94e5-641e67e4fc07" class="fn"><a href="#259394ac-eabd-4e24-94e5-641e67e4fc07" id="259394ac-eabd-4e24-94e5-641e67e4fc07-link">1</a></sup>.<br><br>Any attempt to import directly from a network drive will fail, always.<br><br>All the following steps are performed on the target computer.</li>



<li>Open the catalog-to-be-imported in Lightroom on the target computer.<br><br>This will automatically close whatever other catalog you have open, first.</li>



<li>Choose &#8220;Export as catalog…&#8221; from the File menu.</li>



<li>Adjust settings to suit, and export to a new catalog.<br><br>Note that this will duplicate all the files referenced by the catalog, into the new catalog.  So it might take a while even though it&#8217;s all localised to the one computer (and even if it&#8217;s on the same volume &#8211; Lightroom is not smart enough to perform <a href="https://wadetregaskis.com/copy-on-write-on-apfs/" data-wpel-link="internal">APFS COW clones</a>).</li>



<li>Open the target catalog.</li>



<li>Import the catalog you just exported (&#8220;Import from Another Catalog…&#8221; in the File menu).  Make sure to choose to copy the files to a new destination, not just reference them.</li>



<li>Delete the temporary catalog.</li>
</ol>



<p>In my experience you <em>must</em> perform the export-to-an-otherwise-pointless-new-catalog <em>after</em> copying everything to the target computer.  Somehow, something about copying Lightroom&#8217;s files from one computer to another [over a network] &#8220;breaks&#8221; them such that Lightroom will refuse to import them.</p>


<ol class="wp-block-footnotes"><li id="259394ac-eabd-4e24-94e5-641e67e4fc07">You can do this by &#8211; on the source computer &#8211; selecting all the photos in the catalog and using the &#8220;Folders&#8221; subsection of the left panel to adjust their location on disk.  Typically by selecting an existing location, right-clicking, and selecting &#8220;Move Selected Photos to this Folder&#8221;.  If necessary, you can first add the desired location by clicking the &#8216;plus&#8217; icon to the right of the &#8220;Folders&#8221; section header, and choosing &#8220;Add Folder…&#8221;.<br><br>Yes, Lightroom&#8217;s file management UI is a pain in the arse, and badly designed. <a href="#259394ac-eabd-4e24-94e5-641e67e4fc07-link" aria-label="Jump to footnote reference 1">↩︎</a></li></ol>]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/lightroom-could-not-import-this-catalog-because-of-an-unknown-error/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2024/06/Lightroom-could-not-import-this-catalog-because-of-an-unknown-error.webp" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">8219</post-id>	</item>
		<item>
		<title>The only usable ByteCountFormatStyle is decimal</title>
		<link>https://wadetregaskis.com/the-only-usable-bytecountformatstyle-is-decimal/</link>
					<comments>https://wadetregaskis.com/the-only-usable-bytecountformatstyle-is-decimal/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Sat, 18 May 2024 20:43:10 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[binary]]></category>
		<category><![CDATA[binary prefix]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[ByteCountFormatStyle]]></category>
		<category><![CDATA[decimal]]></category>
		<category><![CDATA[FormatStyle]]></category>
		<category><![CDATA[SI units]]></category>
		<category><![CDATA[Swift]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8165</guid>

					<description><![CDATA[Swift makes it relatively easy to format numbers as byte counts, with appropriate suffixes to indicate units and generally sensible auto-selection of scale factors. e.g.: 1 kB (in English &#8211; results may vary depending on locale) This is just a small subset of Swift&#8217;s FormatStyle-based formatting capabilities, with which I have a bit of a&#8230; <a class="read-more-link" href="https://wadetregaskis.com/the-only-usable-bytecountformatstyle-is-decimal/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>Swift makes it relatively easy to format numbers as byte counts, with appropriate suffixes to indicate units and generally sensible auto-selection of scale factors.  e.g.:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #098658">1234</span><span style="color: #000000">.</span><span style="color: #795E26">formatted</span><span style="color: #000000">(.</span><span style="color: #795E26">byteCount</span><span style="color: #000000">(</span><span style="color: #795E26">style</span><span style="color: #000000">: .</span><span style="color: #001080">decimal</span><span style="color: #000000">))</span></span></code></pre></div>



<figure class="wp-block-pullquote"><blockquote><p>1 kB</p><cite>(in English &#8211; results may vary depending on locale)</cite></blockquote></figure>



<p>This is just a small subset of Swift&#8217;s <code><a href="https://developer.apple.com/documentation/foundation/formatstyle" data-wpel-link="external" target="_blank" rel="external noopener">FormatStyle</a></code>-based formatting capabilities, with which <a href="https://wadetregaskis.com/fucking-formatstyle/" data-wpel-link="internal">I have a bit of a love-hate relationship</a> even when they&#8217;re working correctly.</p>



<p>Alas, they don&#8217;t always work correctly; some of these formatters contain egregious bugs.</p>



<p>In particular, <code>ByteCountFormatStyle</code> pretends to support multiple numeric bases &#8211; decimal and binary &#8211; but it doesn&#8217;t, because what it renders for binary is:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #098658">1234</span><span style="color: #000000">.</span><span style="color: #795E26">formatted</span><span style="color: #000000">(.</span><span style="color: #795E26">byteCount</span><span style="color: #000000">(</span><span style="color: #795E26">style</span><span style="color: #000000">: .</span><span style="color: #001080">binary</span><span style="color: #000000">))</span></span></code></pre></div>



<figure class="wp-block-pullquote"><blockquote><p>1 kB</p><cite>(in English &#8211; results may vary depending on locale)</cite></blockquote></figure>



<p>Note how it still uses <em>decimal</em> units, &#8220;kB&#8221;.  Decimal is not binary.  I mean, duh, right?  But apparently Apple don&#8217;t know this.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>☝️ <a href="https://en.wikipedia.org/wiki/Binary_prefix" data-wpel-link="external" target="_blank" rel="external noopener">Binary prefix</a> abbreviations are always two characters, the second always &#8220;i&#8221;.  In this case, &#8220;KiB&#8221;<sup data-fn="360480d9-2d8c-44ac-ae55-be1417b55320" class="fn"><a href="#360480d9-2d8c-44ac-ae55-be1417b55320" id="360480d9-2d8c-44ac-ae55-be1417b55320-link">1</a></sup>.  They&#8217;re easy to remember because they use the same first letter as their decimal cousins, e.g. &#8220;Ti&#8221; &amp; &#8220;T&#8221; for tebi and tera.  Their derivation is really simple &#8211; take the first two letters of the decimal cousin, e.g. &#8220;te&#8221;, and the first two letters from &#8220;binary&#8221;, &#8220;bi&#8221; &#8211; voilà, &#8220;tebi&#8221;.  To abbreviate, just drop the middle letters and use titlecase.</p>
</div></div>



<h2 class="wp-block-heading">So what?</h2>



<p>While the above example is tolerable because, given the rounding applied, the numeric result (&#8220;1&#8221;) is technically corrected in both bases, see what happens when you use larger values:</p>



<figure class="wp-block-table aligncenter"><table><thead><tr><th class="has-text-align-right" data-align="right">Input number</th><th class="has-text-align-center" data-align="center"><code><a href="https://developer.apple.com/documentation/foundation/bytecountformatstyle/style/decimal" data-wpel-link="external" target="_blank" rel="external noopener">decimal</a></code></th><th class="has-text-align-center" data-align="center"><code><a href="https://developer.apple.com/documentation/foundation/bytecountformatstyle/style/binary" data-wpel-link="external" target="_blank" rel="external noopener">binary</a></code></th></tr></thead><tbody><tr><td class="has-text-align-right" data-align="right">1,000</td><td class="has-text-align-center" data-align="center">1 kB</td><td class="has-text-align-center" data-align="center">1,000 bytes</td></tr><tr><td class="has-text-align-right" data-align="right">1,024</td><td class="has-text-align-center" data-align="center">1 kB</td><td class="has-text-align-center" data-align="center">1 kB</td></tr><tr><td class="has-text-align-right" data-align="right">1,000,000</td><td class="has-text-align-center" data-align="center">1 MB</td><td class="has-text-align-center" data-align="center">977 kB</td></tr><tr><td class="has-text-align-right" data-align="right">1,048,576</td><td class="has-text-align-center" data-align="center">1 MB</td><td class="has-text-align-center" data-align="center">1 MB</td></tr><tr><td class="has-text-align-right" data-align="right">1,000,000,000</td><td class="has-text-align-center" data-align="center">1 GB</td><td class="has-text-align-center" data-align="center">953.7 MB</td></tr><tr><td class="has-text-align-right" data-align="right">1,073,741,824</td><td class="has-text-align-center" data-align="center">1.07 GB</td><td class="has-text-align-center" data-align="center">1 GB</td></tr><tr><td class="has-text-align-right" data-align="right">1,000,000,000,000</td><td class="has-text-align-center" data-align="center">1 TB</td><td class="has-text-align-center" data-align="center">931.32 GB</td></tr><tr><td class="has-text-align-right" data-align="right">1,099,511,627,776</td><td class="has-text-align-center" data-align="center">1.1 TB</td><td class="has-text-align-center" data-align="center">1 TB</td></tr><tr><td class="has-text-align-right" data-align="right">1,000,000,000,000,000</td><td class="has-text-align-center" data-align="center">1 PB</td><td class="has-text-align-center" data-align="center">909.49 TB</td></tr><tr><td class="has-text-align-right" data-align="right">1,125,899,906,842,624</td><td class="has-text-align-center" data-align="center">1.13 PB</td><td class="has-text-align-center" data-align="center">1 PB</td></tr></tbody></table></figure>



<p>Once you get up to non-trivial byte counts, you start to get different results even with the heavy rounding that&#8217;s applied by default.  By the time you&#8217;re talking about GBs the error is on the order of 10%.  And the error just gets proportionately larger as the input number increases.  <a href="https://en.wikipedia.org/wiki/Binary_prefix#Comparison_of_binary_and_decimal_prefixes" data-wpel-link="external" target="_blank" rel="external noopener">Wikipedia has a neat little table showing this</a>.</p>



<h2 class="wp-block-heading">Okay, but that&#8217;s just <code>binary</code>, what about <code><a href="https://developer.apple.com/documentation/foundation/bytecountformatstyle/style/file" data-wpel-link="external" target="_blank" rel="external noopener">file</a></code> and <code><a href="https://developer.apple.com/documentation/foundation/bytecountformatstyle/style/memory" data-wpel-link="external" target="_blank" rel="external noopener">memory</a></code>?</h2>



<p>Those are just aliases to <code>decimal</code> and <code>binary</code>, respectively.  So <code>memory</code> is right out.</p>



<p>You might think <code>file</code> is still okay, because it essentially means <code>decimal</code>.  But that&#8217;s only <em>currently</em>.  The entire point of its existence is to allow its behaviour to change over time, following whatever convention Apple believes is most appropriate for files.  In the past that was in fact <code>binary</code>, as noted previously (pre-Snow Leopard).  It might be <code>binary</code> again in future.  So it&#8217;s dangerous to use since you can neither know what units it&#8217;s going to use nor whether Apple will have fixed their formatters by the time it switches off of <code>decimal</code>.</p>



<h2 class="wp-block-heading">But context will save us!</h2>



<p>Probably not.  Go look at file sizes in the macOS Finder.  Can you tell whether they&#8217;re actual decimal (as they appear) or binary?</p>



<p>They&#8217;re <em>decimal</em>, but they <em>used</em> to be binary, <a href="https://www.macworld.com/article/199831/snow_leopard_math.html" data-wpel-link="external" target="_blank" rel="external noopener">up until Mac OS X Snow Leopard (10.6)</a>.  And the Finder used the <em>same</em> (decimal) unit abbreviations throughout.  So at least it&#8217;s using the correct units now &#8211; almost by accident &#8211; but it created a confused transition and history.</p>



<p>Worse and more presently pertinent, not everything Finder-like uses decimal, nor correct units when they don&#8217;t.  e.g. <a href="https://superuser.com/questions/1519532/why-do-mac-and-synology-report-different-file-sizes" data-wpel-link="external" target="_blank" rel="external noopener">Synology&#8217;s products</a>, not to mention countless websites.  That creates a lot of confusion which bleeds across into even well-behaved software.</p>



<p>Point being, you both can&#8217;t rely on context to help, because context includes things you can&#8217;t control like other software, and by getting the units wrong you&#8217;re making it worse for everyone, not just yourself.</p>



<h2 class="wp-block-heading">Why is this still happening?</h2>



<p>Going back twenty years, this kind of error &#8211; confusing decimal with binary w.r.t. unit prefixes &#8211; was both the norm and <em>somewhat</em> excusable &#8211; binary prefixes were <a href="https://en.wikipedia.org/wiki/IEC_60027" data-wpel-link="external" target="_blank" rel="external noopener">only formally standardised upon in 1999</a>, so it&#8217;s only to be expected that it will take some time for everyone to adopt them.</p>



<p>But it&#8217;s been a quarter of a century already.  There is absolutely no excuse anymore for getting this wrong.</p>


<ol class="wp-block-footnotes"><li id="360480d9-2d8c-44ac-ae55-be1417b55320">If you&#8217;re paying attention you&#8217;ll have noticed &#8220;k&#8221; vs &#8220;K&#8221;, i.e. the difference in case.  This is not arbitrary &#8211; it&#8217;s because units have to be distinct to be functional, and &#8220;K&#8221; is the abbreviation for Kelvin (and apparently wins because it&#8217;s one of <a href="https://en.wikipedia.org/wiki/SI_base_unit" data-wpel-link="external" target="_blank" rel="external noopener">the seven <em>base</em> physical units</a>, from which pretty much all other physical units are derived).<br><br>&#8220;Ki&#8221; is fine because it&#8217;s a distinct prefix (and &#8220;i&#8221; is not a valid abbreviation by itself so there&#8217;s no potential confusion about whether it means &#8220;Kelvin • i&#8221; or &#8220;kibi&#8221;). <a href="#360480d9-2d8c-44ac-ae55-be1417b55320-link" aria-label="Jump to footnote reference 1">↩︎</a></li></ol>]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/the-only-usable-bytecountformatstyle-is-decimal/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">8165</post-id>	</item>
		<item>
		<title>Matching prefixes in Swift strings</title>
		<link>https://wadetregaskis.com/matching-prefixes-in-swift-strings/</link>
					<comments>https://wadetregaskis.com/matching-prefixes-in-swift-strings/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Thu, 02 May 2024 01:46:02 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Howto]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[NSString]]></category>
		<category><![CDATA[Snafu]]></category>
		<category><![CDATA[String]]></category>
		<category><![CDATA[Swift]]></category>
		<category><![CDATA[Undocumented]]></category>
		<category><![CDATA[Unicode]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=7953</guid>

					<description><![CDATA[How do you determine if one string starts with another, in Swift? Surely that&#8217;s exactly what the hasPrefix(_:) method is for: No can haz etiquette? Wot? The problem is that hasPrefix is not meant for general use with human text; it&#8217;s barely better than a byte-wise comparison. It only guarantees that it won&#8217;t be fooled&#8230; <a class="read-more-link" href="https://wadetregaskis.com/matching-prefixes-in-swift-strings/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>How do you determine if one string starts with another, in Swift?</p>



<p>Surely that&#8217;s exactly what the <code><a href="https://developer.apple.com/documentation/swift/substring/hasprefix(_:)" data-wpel-link="external" target="_blank" rel="external noopener">hasPrefix(_:)</a></code> method is for:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> greeting = </span><span style="color: #A31515">&quot;Hello&quot;</span></span>
<span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> sentence = </span><span style="color: #A31515">&quot;hello, this is dog.&quot;</span></span>
<span class="line"></span>
<span class="line"></span>
<span class="line"><span style="color: #AF00DB">if</span><span style="color: #000000"> sentence.</span><span style="color: #795E26">hasPrefix</span><span style="color: #000000">(greeting) {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;Hi!  Nice to meet you!&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">} </span><span style="color: #AF00DB">else</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;No can haz etiquette?&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<figure class="wp-block-pullquote"><blockquote><p><code>No can haz etiquette?</code></p></blockquote></figure>



<p>Wot?</p>



<p>The problem is that <code>hasPrefix</code> is not meant for general use with <em>human</em> text; it&#8217;s barely better than a byte-wise comparison. It <em>only</em> guarantees that it won&#8217;t be fooled by mere differences in Unicode encoding, which is a good start, but not remotely sufficient for general use.</p>



<p>Let&#8217;s step back a bit, and first consider the slightly simpler case of just comparing two whole strings. We can worry about the prefix-matching aspect later.</p>



<p><code><a href="https://developer.apple.com/documentation/foundation/nsstring" data-wpel-link="external" target="_blank" rel="external noopener">NSString</a></code> (from <a href="https://developer.apple.com/documentation/foundation" data-wpel-link="external" target="_blank" rel="external noopener">Foundation</a>) provides Swift <code><a href="https://developer.apple.com/documentation/swift/string" data-wpel-link="external" target="_blank" rel="external noopener">String</a></code>s with a variety of more powerful comparison methods, such as <code><a href="https://developer.apple.com/documentation/foundation/nsstring/1414769-caseinsensitivecompare" data-wpel-link="external" target="_blank" rel="external noopener">caseInsensitiveCompare(_:)</a></code> (which is really just an alias for <code><a href="https://developer.apple.com/documentation/foundation/nsstring/1414561-compare" data-wpel-link="external" target="_blank" rel="external noopener">compare(_:options:range:locale:)</a></code> with the <code><a href="https://developer.apple.com/documentation/foundation/nsstring/compareoptions/1412382-caseinsensitive" data-wpel-link="external" target="_blank" rel="external noopener">caseInsensitive</a></code> option).</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> a = </span><span style="color: #A31515">&quot;hello&quot;</span></span>
<span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> b = </span><span style="color: #A31515">&quot;Hello&quot;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #AF00DB">if</span><span style="color: #000000"> a.</span><span style="color: #795E26">caseInsensitiveCompare</span><span style="color: #000000">(b) {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;Hello indeed.&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">} </span><span style="color: #AF00DB">else</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;Hmph… rude.&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<figure class="wp-block-pullquote"><blockquote><p><code>Hello indeed.</code></p></blockquote></figure>



<p>So that works. For <em>case sensitivity</em>. But what about other situations?</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> plain = </span><span style="color: #A31515">&quot;cafe&quot;</span></span>
<span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> fancy = </span><span style="color: #A31515">&quot;café&quot;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #AF00DB">if</span><span style="color: #000000"> plain.</span><span style="color: #795E26">caseInsensitiveCompare</span><span style="color: #000000">(fancy) {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;Right, either way it&#39;s a shop that sells coffee.&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">} </span><span style="color: #AF00DB">else</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;So… no coffee, then?&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<figure class="wp-block-pullquote"><blockquote><p><code>So… no coffee, then?</code></p></blockquote></figure>



<p>Well, shit.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>It may vary in other languages, but in English &#8220;café&#8221; is just an alternative spelling of &#8220;cafe&#8221;, and you almost always want to consider them equal. In fact, in English it&#8217;s basically never really required that you observe accents on letters &#8211; some are <em>technically</em> required, such as blasé, but English speakers are very blase about such things. Unlike e.g. Spanish, with n vs ñ, accented letters are not considered <em>distinct</em> letters in English.</p>



<p>But, letter accents may creep into English text anyway (just like spoken accents). Some people prefer them, for any of numerous reasons, like:</p>



<ul class="wp-block-list">
<li>In proper nouns out of respect for the so-named.</li>



<li>To honour words&#8217; roots in other languages.</li>



<li>For technical correctness of pronunciation.</li>



<li>Just aesthetically.</li>
</ul>



<p>So you do need to <em>support</em> them, which means accepting and preserving them, but (usually) otherwise ignoring them.</p>
</div></div>



<p>Ah, but wait, <a href="https://developer.apple.com/documentation/foundation/nsstring/1414769-caseinsensitivecompare" data-wpel-link="external" target="_blank" rel="external noopener">the documentation for <code>caseInsensitiveCompare(_:)</code></a> has a footnote which surely addresses exactly this problem, albeit obliquely:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><strong>Important</strong></p>



<p>When working with text that’s presented to the user, use the&nbsp;<a href="https://developer.apple.com/documentation/foundation/nsstring/1417333-localizedcaseinsensitivecompare" data-wpel-link="external" target="_blank" rel="external noopener"><code>localizedCaseInsensitiveCompare(_:)</code></a>&nbsp;method instead.</p>
</blockquote>



<p>No worries &#8211; we&#8217;ll just use that instead:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> plain = </span><span style="color: #A31515">&quot;cafe&quot;</span></span>
<span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> fancy = </span><span style="color: #A31515">&quot;café&quot;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #AF00DB">if</span><span style="color: #000000"> plain.</span><span style="color: #795E26">localizedCaseInsensitiveCompare</span><span style="color: #000000">(fancy) {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;Finally!&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">} </span><span style="color: #AF00DB">else</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;Oh come on!&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<figure class="wp-block-pullquote"><blockquote><p><code>Oh come on!</code></p></blockquote></figure>



<p>It turns out this mistake is made by <em>most</em> of the <code>String</code> / <code>NSString</code> methods of similar ilk. And the discrepancies are inscrutable &#8211; e.g. <code><a href="https://developer.apple.com/documentation/swift/stringprotocol/localizedstandardcompare(_:)" data-wpel-link="external" target="_blank" rel="external noopener">localizedStandardCompare(_:)</a></code> doesn&#8217;t handle accents correctly but <code><a href="https://developer.apple.com/documentation/foundation/nsstring/1413574-localizedstandardrange" data-wpel-link="external" target="_blank" rel="external noopener">localizedStandardRange(of:)</a></code> does.</p>



<p>Long story short, you need to base most (if not all) your string comparison on <a href="https://developer.apple.com/documentation/foundation/nsstring/1414561-compare" data-wpel-link="external" target="_blank" rel="external noopener"><code>compare(_:options:range:locale:)</code> </a>or its sibling <code><a href="https://developer.apple.com/documentation/foundation/nsstring/1417348-range" data-wpel-link="external" target="_blank" rel="external noopener">range(of:options:range:locale:)</a></code>, because the other string methods don&#8217;t work properly.</p>



<p>So, with <code>compare(…)</code> you can do e.g.:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> plain = </span><span style="color: #A31515">&quot;cafe&quot;</span></span>
<span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> fancy = </span><span style="color: #A31515">&quot;Café&quot;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #AF00DB">if</span><span style="color: #000000"> .</span><span style="color: #001080">orderedSame</span><span style="color: #000000"> == plain.</span><span style="color: #795E26">compare</span><span style="color: #000000">(fancy,</span></span>
<span class="line"><span style="color: #000000">                                 </span><span style="color: #795E26">options</span><span style="color: #000000">: [.</span><span style="color: #001080">caseInsensitive</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                           .</span><span style="color: #001080">diacriticInsensitive</span><span style="color: #000000">],</span></span>
<span class="line"><span style="color: #000000">                                 </span><span style="color: #795E26">locale</span><span style="color: #000000">: .</span><span style="color: #001080">current</span><span style="color: #000000">) {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;Finally!&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">} </span><span style="color: #AF00DB">else</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;😤&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<figure class="wp-block-pullquote"><blockquote><p><code>Finally</code>.</p></blockquote></figure>



<p>But there&#8217;s two other options you should almost always use, which are easy to overlook:</p>



<ul class="wp-block-list">
<li><code><a href="https://developer.apple.com/documentation/foundation/nsstring/compareoptions/1409350-widthinsensitive" data-wpel-link="external" target="_blank" rel="external noopener">widthInsensitive</a></code>.<br><br>In all the Latin-alphabet languages of which I&#8217;m aware, there is no notion of &#8220;width&#8221; and therefore no issues with width [in]sensitivity. It seems it most-often comes up in Japanese, where for historical reasons there were multiple versions <em>of the same character</em> that merely different in their visual dimensions. e.g. &#8220;カ&#8221; and &#8220;ｶ&#8221;. They are semantically the <em>exact</em> same character, even moreso than &#8220;a&#8221; is to &#8220;A&#8221;.<br><br>Even if the locale uses a Latin alphabet, there may still be mixed character sets and languages in the text your app processes &#8211; e.g. someone writing mostly in English but including Japanese names.</li>



<li><code><a href="https://developer.apple.com/documentation/foundation/nsstring/compareoptions/1415530-numeric" data-wpel-link="external" target="_blank" rel="external noopener">numeric</a></code>.<br><br>There are <a href="https://en.wikipedia.org/wiki/Arabic_numerals#Comparison_with_other_digits" data-wpel-link="external" target="_blank" rel="external noopener">more numeric systems than just the modern Arabic numerals</a> as used in English. e.g. &#8220;٤٢&#8221; is 42 in Eastern Arabic. What matters is usually their <em>meaning</em> (i.e. numeric value), not their representation, just like the other factors we&#8217;ve already covered.</li>
</ul>



<p>So, incorporating all that, the magic incantation required to <em>correctly</em> compare two human pieces of English text, in Swift, is:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> plain = </span><span style="color: #A31515">&quot;cafe カ 42&quot;</span></span>
<span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> fancy = </span><span style="color: #A31515">&quot;Café ｶ ٤٢&quot;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #AF00DB">if</span><span style="color: #000000"> .</span><span style="color: #001080">orderedSame</span><span style="color: #000000"> == plain.</span><span style="color: #795E26">compare</span><span style="color: #000000">(fancy,</span></span>
<span class="line"><span style="color: #000000">                                 </span><span style="color: #795E26">options</span><span style="color: #000000">: [.</span><span style="color: #001080">caseInsensitive</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                           .</span><span style="color: #001080">diacriticInsensitive</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                           .</span><span style="color: #001080">numeric</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                           .</span><span style="color: #001080">widthInsensitive</span><span style="color: #000000">],</span></span>
<span class="line"><span style="color: #000000">                                 </span><span style="color: #795E26">locale</span><span style="color: #000000">: .</span><span style="color: #001080">current</span><span style="color: #000000">) {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;Actually equivalent.&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">} </span><span style="color: #AF00DB">else</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;Not equivalent&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<figure class="wp-block-pullquote"><blockquote><p><code>Actually equivalent.</code></p></blockquote></figure>



<p>Note that the explicit <code>locale</code> argument may be important for some use-cases, for two reasons:</p>



<ul class="wp-block-list">
<li><em>Generally</em> it seems to turn on <em>some</em> &#8211; but not all &#8211; locale-appropriate options, in addition to any you specify explicitly.<br><br>While that may be redundant when you&#8217;re explicitly turning them on anyway, it&#8217;s possible it will have <em>additional</em> effects that aren&#8217;t expressible with the <code>options</code> parameter. You&#8217;ll probably want those too, as if they exist they&#8217;ll be things like special handling of unusual cases and exceptions to the rules.</li>



<li><em>Sometimes</em> it turns hidden options <em>off</em>, such as whether to consider superscripts &amp; subscripts equivalent. This might be a reason to <em>not</em> use it sometimes, if you don&#8217;t like the end result.</li>
</ul>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>It&#8217;s less clear, even just considering English, what the correct default behaviour is regarding superscripts and subscripts, or &#8220;baseline sensitivity&#8221;. It&#8217;s quite conceivable that a user might intend to match a superscript or subscript even though they entered a plain digit, because most people don&#8217;t know how to actually type superscripts and subscripts (it&#8217;s not easy on most computers, at least not without 3rd party utilities like <a href="https://matthewpalmer.net/rocket/" data-wpel-link="external" target="_blank" rel="external noopener">Rocket</a>, and practically impossible on mobile devices).<br><br>And plenty of programs &#8211; particularly those not written in Swift, that might not handle Unicode correctly even at the most basic levels &#8211; erroneously devolve superscripts and subscripts to plain digits, which ideally wouldn&#8217;t prevent subsequent tools from still working with them (e.g. still finding &#8220;but1&#8221; when looking for &#8220;but¹&#8221;).<br><br>Yet in some contexts the differences very much do matter &#8211; e.g. in mathematical notation, x² is <em>very</em> different to x₂.</p>
</div></div>



<p>For reference, here&#8217;s a breakdown of the behaviour of some key <code>String</code> / <code>NSString</code> methods (as tested in the en_AU locale), to help you decide what specific incantation you need in a given situation:</p>



<figure class="wp-block-table"><table><thead><tr><th class="has-text-align-left" data-align="left">Method</th><th class="has-text-align-center" data-align="center">Case insensitive<br>(&#8220;Hello&#8221; vs &#8220;hello&#8221;)</th><th class="has-text-align-center" data-align="center">Diacritic insensitive<br>(&#8220;cafe&#8221; vs &#8220;café&#8221;)</th><th class="has-text-align-center" data-align="center">Width insensitive<br>(&#8220;カ&#8221; vs &#8220;ｶ&#8221;)</th><th class="has-text-align-center" data-align="center">Numerals insensitive<br>(&#8220;42&#8221; vs &#8220;٤٢&#8221;)</th><th class="has-text-align-center" data-align="center">Baseline insensitive<br>(&#8220;but1&#8221; vs &#8220;but¹&#8221;)</th></tr></thead><tbody><tr><td class="has-text-align-left" data-align="left"><code>==</code></td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td></tr><tr><td class="has-text-align-left" data-align="left"><code>hasPrefix</code></td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td></tr><tr><td class="has-text-align-left" data-align="left"><code>commonPrefix(…, options: .caseInsensitive)</code></td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td></tr><tr><td class="has-text-align-left" data-align="left"><code>commonPrefix(…, options: .diacriticInsensitive)</code></td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td></tr><tr><td class="has-text-align-left" data-align="left"><code>commonPrefix(…, options: .widthInsensitive)</code></td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td></tr><tr><td class="has-text-align-left" data-align="left"><code>commonPrefix(…, options: .numeric)</code></td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌<sup data-fn="0a002391-051b-43f5-90c2-52da80012369" class="fn"><a href="#0a002391-051b-43f5-90c2-52da80012369" id="0a002391-051b-43f5-90c2-52da80012369-link">1</a></sup></td><td class="has-text-align-center" data-align="center">✅</td></tr><tr><td class="has-text-align-left" data-align="left"><code>localizedCompare</code></td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">❌</td></tr><tr><td class="has-text-align-left" data-align="left"><code>localizedCaseInsensitiveCompare</code></td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">❌</td></tr><tr><td class="has-text-align-left" data-align="left"><code>localizedStandardCompare</code></td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td></tr><tr><td class="has-text-align-left" data-align="left"><code>localizedStandardRange(of:)</code></td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td></tr><tr><td class="has-text-align-left" data-align="left"><code>compare(…, options: .caseInsensitive)</code></td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td></tr><tr><td class="has-text-align-left" data-align="left"><code>compare(…, options: .caseInsensitive, locale: .current)</code></td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">✅</td></tr><tr><td class="has-text-align-left" data-align="left"><code>compare(…, options: .diacriticInsensitive)</code></td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td></tr><tr><td class="has-text-align-left" data-align="left"><code>compare(…, options: .diacriticInsensitive, locale: .current)</code></td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">✅</td></tr><tr><td class="has-text-align-left" data-align="left"><code>compare(…, options: .widthInsensitive)</code></td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td></tr><tr><td class="has-text-align-left" data-align="left"><code>compare(…, options: .widthInsensitive, locale: .current)</code></td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">✅</td></tr><tr><td class="has-text-align-left" data-align="left"><code>compare(…, options: .numeric)</code></td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">✅</td></tr><tr><td class="has-text-align-left" data-align="left"><code>compare(…, options: .numeric, locale: .current)</code></td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">❌<sup data-fn="6ac6f3c3-cfbc-4d7e-a4ec-b02e3304d489" class="fn"><a href="#6ac6f3c3-cfbc-4d7e-a4ec-b02e3304d489" id="6ac6f3c3-cfbc-4d7e-a4ec-b02e3304d489-link">2</a></sup></td></tr><tr><td class="has-text-align-left" data-align="left"><code>compare(…, options: [.caseInsensitive, .diacriticInsensitive, .numeric, .widthInsensitive])</code></td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">✅</td></tr><tr><td class="has-text-align-left" data-align="left"><code>compare(…, options: [.caseInsensitive, .diacriticInsensitive, .numeric, .widthInsensitive], locale: .current)</code></td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">❌</td></tr></tbody></table></figure>



<p>Now, the real challenge is making code that works across <em>all</em> locales. In a nutshell, that&#8217;s practically impossible with Swift&#8217;s standard libraries today &#8211; they just don&#8217;t support it. To do it right, you&#8217;d have to determine what the appropriate comparison options are for every possible locale, manually, and bundle that database with your app.</p>



<p>But, given it&#8217;s usually better anyway to err on the side of matching rather than not matching, you can get pretty far by just assuming insensitivity to the above five factors.</p>



<p>Even in cases where this does cause mistakes &#8211; e.g. conflating &#8220;Maßen&#8221; (<em>in moderation</em>) with &#8220;Massen&#8221; (<em>en masse</em>) in German &#8211; it&#8217;s potentially unavoidable without deeper, context-specific knowledge anyway (since &#8220;ß&#8221; <em>is</em> normally equivalent to &#8220;ss&#8221; in German, just not regarding those specific two words &#8211; you can read more about this unpleasant situation on e.g. <a href="https://en.wikipedia.org/wiki/German_alphabet#Sharp_s" data-wpel-link="external" target="_blank" rel="external noopener">Wikipedia</a>).</p>



<h2 class="wp-block-heading">So, back to prefixes…</h2>



<p>Fortunately, <code>compare(_:options:range:locale:)</code> and <code>range(of:options:range:locale:)</code> have a couple of additional options which make them easier to apply to situations other than just comparing whole strings.</p>



<h3 class="wp-block-heading">Checking for a specific prefix</h3>



<p>There is an <code><a href="https://developer.apple.com/documentation/foundation/nsstring/compareoptions/1410321-anchored" data-wpel-link="external" target="_blank" rel="external noopener">anchored</a></code> option which is perfect for this &#8211; it restricts the match to the <em>start</em> of the receiving string. e.g.:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> reaction = </span><span style="color: #A31515">&quot;😁👍&quot;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #AF00DB">if</span><span style="color: #000000"> </span><span style="color: #0000FF">nil</span><span style="color: #000000"> != reaction.</span><span style="color: #001080">range</span><span style="color: #000000">(</span><span style="color: #795E26">of</span><span style="color: #000000">: </span><span style="color: #A31515">&quot;😁&quot;</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                         </span><span style="color: #795E26">options</span><span style="color: #000000">: [.</span><span style="color: #001080">anchored</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                   .</span><span style="color: #001080">caseInsensitive</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                   .</span><span style="color: #001080">diacriticInsensitive</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                   .</span><span style="color: #001080">numeric</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                   .</span><span style="color: #001080">widthInsensitive</span><span style="color: #000000">],</span></span>
<span class="line"><span style="color: #000000">                         </span><span style="color: #795E26">locale</span><span style="color: #000000">: .</span><span style="color: #001080">current</span><span style="color: #000000">)) {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;Happy!&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">} </span><span style="color: #AF00DB">else</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;Sad¡&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<figure class="wp-block-pullquote"><blockquote><p><code>Happy!</code></p></blockquote></figure>



<p>Note that you <em>must</em> use the <code>range(of:…)</code> variant, not <code>compare(…)</code>, because the latter essentially requires that the two strings <em>fully</em> match, not merely that one is a prefix of the other (more on that <a href="#beware-the-range-parameter">later</a>, in case you&#8217;re not convinced).</p>



<h3 class="wp-block-heading">Finding common prefixes</h3>



<p>Fortunately, there&#8217;s a convenience method for exactly this, <code><a href="https://developer.apple.com/documentation/foundation/nsstring/1408169-commonprefix" data-wpel-link="external" target="_blank" rel="external noopener">commonPrefix(with:options:)</a></code>:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> happy = </span><span style="color: #A31515">&quot;😁👍&quot;</span></span>
<span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> party = </span><span style="color: #A31515">&quot;😁🎉&quot;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;Similarities:&quot;</span><span style="color: #000000">, happy.</span><span style="color: #795E26">commonPrefix</span><span style="color: #000000">(</span><span style="color: #795E26">with</span><span style="color: #000000">: party,</span></span>
<span class="line"><span style="color: #000000">                                          </span><span style="color: #795E26">options</span><span style="color: #000000">: [.</span><span style="color: #001080">caseInsensitive</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                                    .</span><span style="color: #001080">diacriticInsensitive</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                                    .</span><span style="color: #001080">numeric</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                                    .</span><span style="color: #001080">widthInsensitive</span><span style="color: #000000">]))</span></span></code></pre></div>



<figure class="wp-block-pullquote"><blockquote><p><code>Similarities: 😁</code></p></blockquote></figure>



<p>Do <em>not</em> use this if you merely want to see if they share a <em>specific</em> prefix, because:</p>



<ul class="wp-block-list">
<li>It&#8217;s more efficient to just check that directly on each string separately (rather than allocating and returning an intermediary string).</li>



<li>You still have to use <code>compare(…)</code> with the full set of options to check the result.</li>
</ul>



<p>Note also that it does not have a locale parameter, so you cannot opt in to any system-default options defined for the current locale; you must explicitly specify <em>every</em> option you need.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>⚠️ Beware: it doesn&#8217;t honour the <code>numeric</code> option.</p>
</div></div>



<h2 class="wp-block-heading">Working with suffixes</h2>



<p>You can of course reverse both strings and then compare what are now their prefixes, but this is expensive and awkward, since the result of <code>String</code>&#8216;s <code><a href="https://developer.apple.com/documentation/swift/emptycollection/reversed()" data-wpel-link="external" target="_blank" rel="external noopener">reversed()</a></code> method is a <code>ReversedCollection&lt;String&gt;</code>, not a <code>String</code> or even a <code>Substring</code>, and it does not have the necessary comparison methods, so you have to convert it to a real <code>String</code> first.</p>



<p>Far easier and more efficient is to make use the <code><a href="https://developer.apple.com/documentation/foundation/nsstring/compareoptions/1415204-backwards" data-wpel-link="external" target="_blank" rel="external noopener">backwards</a></code> option to <code>range(of:…)</code>. e.g.:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> word = </span><span style="color: #A31515">&quot;doing&quot;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #AF00DB">if</span><span style="color: #000000"> </span><span style="color: #0000FF">nil</span><span style="color: #000000"> != word.</span><span style="color: #001080">range</span><span style="color: #000000">(</span><span style="color: #795E26">of</span><span style="color: #000000">: </span><span style="color: #A31515">&quot;ing&quot;</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                     </span><span style="color: #795E26">options</span><span style="color: #000000">: [.</span><span style="color: #001080">anchored</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                               .</span><span style="color: #001080">backwards</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                               .</span><span style="color: #001080">caseInsensitive</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                               .</span><span style="color: #001080">diacriticInsensitive</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                               .</span><span style="color: #001080">numeric</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                               .</span><span style="color: #001080">widthInsensitive</span><span style="color: #000000">],</span></span>
<span class="line"><span style="color: #000000">                     </span><span style="color: #795E26">locale</span><span style="color: #000000">: .</span><span style="color: #001080">current</span><span style="color: #000000">)) {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;It&#39;s an &#39;ing&#39; word.&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">} </span><span style="color: #AF00DB">else</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;Nyet.&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<figure class="wp-block-pullquote"><blockquote><p><code>It's an 'ing' word.</code></p></blockquote></figure>



<p>Note how &#8211; conveniently &#8211; it does not require reversal of the argument string (&#8220;ing&#8221; in the above example).</p>



<h2 class="wp-block-heading" id="beware-the-range-parameter">Beware the <code>range</code> parameter</h2>



<p>The <code>compare(…)</code> and <code>range(of:…)</code> methods also have a <code>range</code> parameter. This seems like a great idea &#8211; you can specify which specific subset of a string you care about, without having to actually break it out into a whole new <code>String</code> instance.</p>



<p>However, the <code>range</code> parameter is both a little unintuitive in its behaviour and fundamentally hard to use correctly.</p>



<p>On the first aspect, it&#8217;s critical to realise that it specifies the range within <em>only</em> the receiver (&#8220;happy&#8221; in the example below). It has no effect on the argument string (&#8220;party&#8221; in the example below). So you might innocently write the following:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> happy = </span><span style="color: #A31515">&quot;😁👍&quot;</span></span>
<span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> party = </span><span style="color: #A31515">&quot;😁🎉&quot;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #AF00DB">if</span><span style="color: #000000"> .</span><span style="color: #001080">orderedSame</span><span style="color: #000000"> == happy.</span><span style="color: #795E26">compare</span><span style="color: #000000">(party,</span></span>
<span class="line"><span style="color: #000000">                                 </span><span style="color: #795E26">options</span><span style="color: #000000">: [.</span><span style="color: #001080">caseInsensitive</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                           .</span><span style="color: #001080">diacriticInsensitive</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                           .</span><span style="color: #001080">numeric</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                           .</span><span style="color: #001080">widthInsensitive</span><span style="color: #000000">],</span></span>
<span class="line"><span style="color: #000000">                                 </span><span style="color: #795E26">range</span><span style="color: #000000">: happy.</span><span style="color: #001080">startIndex</span><span style="color: #000000"> ..&lt; happy.</span><span style="color: #795E26">index</span><span style="color: #000000">(</span><span style="color: #795E26">after</span><span style="color: #000000">: happy.</span><span style="color: #001080">startIndex</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                                 </span><span style="color: #795E26">locale</span><span style="color: #000000">: .</span><span style="color: #001080">current</span><span style="color: #000000">)) {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;Grins all round.&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">} </span><span style="color: #AF00DB">else</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;…not happy?&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<figure class="wp-block-pullquote"><blockquote><p><code>…not happy?</code></p></blockquote></figure>



<p>If you want to compare subsets of <em>both</em> strings, you need to explicitly slice the second string, e.g.:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> happy = </span><span style="color: #A31515">&quot;😁👍&quot;</span></span>
<span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> party = </span><span style="color: #A31515">&quot;😁🎉&quot;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #AF00DB">if</span><span style="color: #000000"> .</span><span style="color: #001080">orderedSame</span><span style="color: #000000"> == happy.</span><span style="color: #795E26">compare</span><span style="color: #000000">(party.</span><span style="color: #795E26">prefix</span><span style="color: #000000">(</span><span style="color: #098658">1</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                                 </span><span style="color: #795E26">options</span><span style="color: #000000">: [.</span><span style="color: #001080">caseInsensitive</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                           .</span><span style="color: #001080">diacriticInsensitive</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                           .</span><span style="color: #001080">numeric</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                           .</span><span style="color: #001080">widthInsensitive</span><span style="color: #000000">],</span></span>
<span class="line"><span style="color: #000000">                                 </span><span style="color: #795E26">range</span><span style="color: #000000">: happy.</span><span style="color: #001080">startIndex</span><span style="color: #000000"> ..&lt; happy.</span><span style="color: #795E26">index</span><span style="color: #000000">(</span><span style="color: #795E26">after</span><span style="color: #000000">: happy.</span><span style="color: #001080">startIndex</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                                 </span><span style="color: #795E26">locale</span><span style="color: #000000">: .</span><span style="color: #001080">current</span><span style="color: #000000">)) {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;Grins all round.&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">} </span><span style="color: #AF00DB">else</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;…not happy?&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<figure class="wp-block-pullquote"><blockquote><p><code>Grins all round.</code></p></blockquote></figure>



<p>Or, more simply:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> happy = </span><span style="color: #A31515">&quot;😁👍&quot;</span></span>
<span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> party = </span><span style="color: #A31515">&quot;😁🎉&quot;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #AF00DB">if</span><span style="color: #000000"> .</span><span style="color: #001080">orderedSame</span><span style="color: #000000"> == happy.</span><span style="color: #795E26">prefix</span><span style="color: #000000">(</span><span style="color: #098658">1</span><span style="color: #000000">).</span><span style="color: #795E26">compare</span><span style="color: #000000">(party.</span><span style="color: #795E26">prefix</span><span style="color: #000000">(</span><span style="color: #098658">1</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                                           </span><span style="color: #795E26">options</span><span style="color: #000000">: [.</span><span style="color: #001080">caseInsensitive</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                                     .</span><span style="color: #001080">diacriticInsensitive</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                                     .</span><span style="color: #001080">numeric</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                                     .</span><span style="color: #001080">widthInsensitive</span><span style="color: #000000">],</span></span>
<span class="line"><span style="color: #000000">                                           </span><span style="color: #795E26">locale</span><span style="color: #000000">: .</span><span style="color: #001080">current</span><span style="color: #000000">)) {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;Grins all round.&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">} </span><span style="color: #AF00DB">else</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;…not happy?&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<figure class="wp-block-pullquote"><blockquote><p><code>Grins all round.</code></p></blockquote></figure>



<p>But, you should rarely if ever actually do the above, because of the second aspect: slicing strings is actually really hard. Not technically, obviously, but if you want to do it <em>correctly</em>. The crux of the challenge is that two strings can have <em>different lengths</em> but still be equivalent (e.g. &#8220;ß&#8221; and &#8220;ss&#8221; in German), so slicing them independently is error-prone, unless you somehow account for the specific differences in their encoding. If you naively assume things like a specific length for a target string (e.g. the single character of &#8220;ß&#8221;) and apply that length to the input string, you might get incorrect results. e.g.:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> input = </span><span style="color: #A31515">&quot;Füssen&quot;</span></span>
<span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> target = </span><span style="color: #A31515">&quot;Füß&quot;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #AF00DB">if</span><span style="color: #000000"> .</span><span style="color: #001080">orderedSame</span><span style="color: #000000"> == input.</span><span style="color: #795E26">prefix</span><span style="color: #000000">(target.</span><span style="color: #001080">count</span><span style="color: #000000">).</span><span style="color: #795E26">compare</span><span style="color: #000000">(target,</span></span>
<span class="line"><span style="color: #000000">                                                      </span><span style="color: #795E26">options</span><span style="color: #000000">: [.</span><span style="color: #001080">caseInsensitive</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                                                .</span><span style="color: #001080">diacriticInsensitive</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                                                .</span><span style="color: #001080">numeric</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                                                .</span><span style="color: #001080">widthInsensitive</span><span style="color: #000000">],</span></span>
<span class="line"><span style="color: #000000">                                                      </span><span style="color: #795E26">locale</span><span style="color: #000000">: .</span><span style="color: #001080">current</span><span style="color: #000000">)) {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;Something about feet.&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">} </span><span style="color: #AF00DB">else</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;Nothing afoot.&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<figure class="wp-block-pullquote"><blockquote><p><code>Nothing afoot.</code></p></blockquote></figure>



<p>(in case you don&#8217;t speak German, that&#8217;s the <em>wrong</em> result logically &#8211; Füß <em>is</em> a prefix of Füssen)</p>


<ol class="wp-block-footnotes"><li id="0a002391-051b-43f5-90c2-52da80012369">Yes, really.  I have no idea why <code>commonPrefix(with:options:)</code> doesn&#8217;t work correctly with the <code>numeric</code> option, given it presumably uses <code>compare(_:options:range:locale:)</code> or <code>range(of:options:range:locale:)</code> under the hood.  Possibly some bad interaction with locale-specific settings, given it doesn&#8217;t let you specify the locale and it doesn&#8217;t document what it hard-codes it to. <a href="#0a002391-051b-43f5-90c2-52da80012369-link" aria-label="Jump to footnote reference 1">↩︎</a></li><li id="6ac6f3c3-cfbc-4d7e-a4ec-b02e3304d489">Yes, really.  I don&#8217;t know why using the current locale (en_AU in this case) turns <em>off</em> baseline insensitivity when the <code>numeric</code> option is used, whereas it turns it <em>on</em> otherwise.  Seems like a bug in Apple&#8217;s framework. <a href="#6ac6f3c3-cfbc-4d7e-a4ec-b02e3304d489-link" aria-label="Jump to footnote reference 2">↩︎</a></li></ol>]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/matching-prefixes-in-swift-strings/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">7953</post-id>	</item>
		<item>
		<title>getBitmapDataPlanes can break NSImages &#038; NSBitmapImageReps</title>
		<link>https://wadetregaskis.com/getbitmapdataplanes-can-break-nsimages-nsbitmapimagereps/</link>
					<comments>https://wadetregaskis.com/getbitmapdataplanes-can-break-nsimages-nsbitmapimagereps/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Thu, 21 Mar 2024 04:44:26 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[10-bit]]></category>
		<category><![CDATA[bitmapData]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[getBitmapDataPlanes]]></category>
		<category><![CDATA[NSBitmapImageRep]]></category>
		<category><![CDATA[NSImage]]></category>
		<category><![CDATA[Sad]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=7889</guid>

					<description><![CDATA[Today was one of those days where you plan to real make a dent in your todo list, and end up spending the entire day debugging why the hell some images are suddenly rendering as completely opaque black. Long story short, on at least some bitmap images, as soon as you call getBitmapDataPlanes it somehow&#8230; <a class="read-more-link" href="https://wadetregaskis.com/getbitmapdataplanes-can-break-nsimages-nsbitmapimagereps/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>Today was one of those days where you plan to real make a dent in your todo list, and end up spending the entire day debugging why the hell some images are suddenly rendering as completely opaque black.</p>



<p>Long story short, on at least some bitmap images, as soon as you call <code><a href="https://developer.apple.com/documentation/appkit/nsbitmapimagerep/1395490-getbitmapdataplanes" data-wpel-link="external" target="_blank" rel="external noopener">getBitmapDataPlanes</a></code> it somehow permanently breaks that <code><a href="https://developer.apple.com/documentation/appkit/nsimage" data-wpel-link="external" target="_blank" rel="external noopener">NSImage</a></code> and <code><a href="https://developer.apple.com/documentation/appkit/nsbitmapimagerep" data-wpel-link="external" target="_blank" rel="external noopener">NSBitmapImageRep</a></code>.  The telltale sign of this &#8211; aside from the image rendering incorrectly &#8211; is the stderr message from AppKit:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><code>Failed to extract pixel data from NSBitmapImageRep. Error: -21778</code></p>
</blockquote>



<p>This definitely occurs with 10-bit AVIFs, but it&#8217;s not limited to that bit depth nor that file format, because I found <a href="https://developer.apple.com/forums/thread/700010" data-wpel-link="external" target="_blank" rel="external noopener">the <em>one</em> other mention of this error message online</a>, where the source image was 1-bit.</p>



<p>I&#8217;ve never seen this happen with 8-bit, 12-bit, or 16-bit images, AVIF or otherwise.</p>



<p>This makes me suspect it&#8217;s tied to the internal pixel format &#8211; 10-bit AVIFs always load as 10 / 40 (bits per sample / bits per pixel), irrespective of whether they have an alpha channel.  8-bit AVIFs are always 8 / 32, 12-bit are always 12 / 48.  Maybe <code>NSBitmapImageRep</code> has a bug when the bits per pixel isn&#8217;t a multiple of 16?</p>



<p>If you call <code><a href="https://developer.apple.com/documentation/appkit/nsbitmapimagerep/1395421-bitmapdata" data-wpel-link="external" target="_blank" rel="external noopener">bitmapData</a></code> instead you get the same error message but it does <em>not</em> stop the <code>NSImage</code> / <code>NSBitmapImageRep</code> from rendering correctly.  But the pointer returned from <code>bitmapData</code> points to opaque black.  It&#8217;s particularly weird that it returns a pointer to a valid memory allocation, of at least the expected size, yet the contents are nothing but zeroes.  Seems like it pre-allocates some output buffer, as zeroed memory, and then fails to actually write to that buffer.  Yet it returns it anyway, instead of returning nil. 😕</p>



<p>FB13693411.</p>



<h2 class="wp-block-heading">Partial workaround</h2>



<p>If you call <code><a href="https://developer.apple.com/documentation/appkit/nsimage/1519890-recache" data-wpel-link="external" target="_blank" rel="external noopener">recache</a></code> on the <code>NSImage</code> afterwards, the image is capable of rendering correctly again.  That doesn&#8217;t help if your objective is to access the bitmap bytes, but at least if you don&#8217;t &#8211; e.g. you&#8217;re encountering this only because some library code is triggering the bug &#8211; you might be able to work around it through <code>recache</code>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/getbitmapdataplanes-can-break-nsimages-nsbitmapimagereps/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2024/03/Rainbow-Stitch-10-bit-sRGB.avif" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">7889</post-id>	</item>
		<item>
		<title>A hint that the Mac Messages app is not native</title>
		<link>https://wadetregaskis.com/a-hint-that-the-mac-messages-app-is-not-native/</link>
					<comments>https://wadetregaskis.com/a-hint-that-the-mac-messages-app-is-not-native/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Wed, 20 Mar 2024 00:33:43 +0000</pubDate>
				<category><![CDATA[Ramblings]]></category>
		<category><![CDATA[Apple Messages]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[Catalyst]]></category>
		<category><![CDATA[iMessage]]></category>
		<category><![CDATA[Snafu]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=7869</guid>

					<description><![CDATA[I must admit, the Messages team have done a somewhat good job of making Messages pretend to be a native Mac app, at least in my experience &#8211; most of the time it works as it should. But then it&#8217;s a very simple app which I use in a pretty limited fashion. Even so, there&#8230; <a class="read-more-link" href="https://wadetregaskis.com/a-hint-that-the-mac-messages-app-is-not-native/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>I must admit, the Messages team have done a <em>somewhat</em> good job of making Messages pretend to be a native Mac app, at least in my experience &#8211; <em>most</em> of the time it works as it should.  But then it&#8217;s a very simple app which I use in a pretty limited fashion.</p>



<p>Even so, there are things which just scream &#8220;I&#8217;m actually an iOS app&#8221;, like when you right-click a message or attachment while the Messages window is on a secondary display, and the contextual menu appears in the top-left corner of the main display.  And disappears when you click on any item in it, <em>without</em> activating the item.  Because it&#8217;s reacting to the mouse location <em>as if</em> it were in the correct place on the secondary display.  But if you click on the secondary display, it just dismisses the pop-up menu without activating the selected item &#8211; I don&#8217;t know how they managed to get that discontinuity between mouse move events and mouse down events, but there you go, the miracle that is Catalyst.</p>



<p>Sigh.</p>



<p>Also, keyboard navigation doesn&#8217;t work correctly in the contextual menu &#8211; it&#8217;s fine for the plain text items, but it&#8217;s impossible to select any of the reactions.</p>



<p>Fortunately, for reactions specifically, there&#8217;s a workaround!  ⌘T brings up a special contextual menu <em>just</em> for reactions, and you can then just hit a number key, from 1 through 6, to apply the corresponding reaction.  It only works on the most recently received message in the selected conversation, but then that&#8217;s usually the one you want.</p>



<p>Of course, the GUI for that special contextual menu is ripped <em>directly</em> out of iOS, further emphasising Message&#8217;s unnatural nature.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://signal.org" data-wpel-link="external" target="_blank" rel="external noopener"><img decoding="async" width="373" height="284" src="https://wadetregaskis.com/wp-content/uploads/2024/03/macOS-Messages-non-native-but-useful-reactions-pop-up-menu.webp" alt="Screenshot from a Messages conversation thread showing the reactions pop-up above a message." class="wp-image-7870" srcset="https://wadetregaskis.com/wp-content/uploads/2024/03/macOS-Messages-non-native-but-useful-reactions-pop-up-menu.webp 373w, https://wadetregaskis.com/wp-content/uploads/2024/03/macOS-Messages-non-native-but-useful-reactions-pop-up-menu-256x195.webp 256w, https://wadetregaskis.com/wp-content/uploads/2024/03/macOS-Messages-non-native-but-useful-reactions-pop-up-menu@2x.webp 746w" sizes="(max-width: 373px) 100vw, 373px" /></a><figcaption class="wp-element-caption">Genuinely chosen as the example merely because it was the first impersonal message I came across in my recent conversations &#8211; but, a sign, perhaps? 😄</figcaption></figure>
</div>]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/a-hint-that-the-mac-messages-app-is-not-native/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2024/03/macOS-Messages-non-native-but-useful-reactions-pop-up-menu.webp" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">7869</post-id>	</item>
		<item>
		<title>no platform load command found in &#8216;libxyz.a&#8217;, assuming: macOS</title>
		<link>https://wadetregaskis.com/no-platform-load-command-found-in-libxyz-a-assuming-macos/</link>
					<comments>https://wadetregaskis.com/no-platform-load-command-found-in-libxyz-a-assuming-macos/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Thu, 14 Mar 2024 23:36:16 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[LC_BUILD_VERSION]]></category>
		<category><![CDATA[ld]]></category>
		<category><![CDATA[ld_prime]]></category>
		<category><![CDATA[ld-classic]]></category>
		<category><![CDATA[ld64]]></category>
		<category><![CDATA[linker load commands]]></category>
		<category><![CDATA[NASM]]></category>
		<category><![CDATA[Undocumented]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=7856</guid>

					<description><![CDATA[This is a linker warning I see frequently since Xcode 15.0. It appears it&#8217;s a result of Apple&#8217;s new linker, &#8220;ld_prime&#8221;, which replaced &#8220;ld64&#8221; that was in use [by Apple] since around 2005 (per Quinn the Eskimo). ☝️ &#8220;ld_prime&#8221; might be an internal code name, or perhaps is just Quinn&#8217;s personal nomenclature. The actual binary&#8230; <a class="read-more-link" href="https://wadetregaskis.com/no-platform-load-command-found-in-libxyz-a-assuming-macos/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>This is a linker warning I see frequently <a href="https://developer.apple.com/documentation/xcode-release-notes/xcode-15-release-notes#New-Features" data-wpel-link="external" target="_blank" rel="external noopener">since Xcode 15.0</a>.  It appears it&#8217;s a result of Apple&#8217;s new linker, &#8220;ld_prime&#8221;, which replaced &#8220;ld64&#8221; that was in use [by Apple] since around 2005 (<a href="https://forums.developer.apple.com/forums/thread/715385" data-wpel-link="external" target="_blank" rel="external noopener">per Quinn the Eskimo</a>).</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>☝️ &#8220;ld_prime&#8221; might be an internal code name, or perhaps is just Quinn&#8217;s personal nomenclature.  The actual binary and project name is simply <code>ld</code> (not to be confused with the <em>original</em> <code>ld</code>, the <em>old</em> old linker, that Apple used until <code>ld64</code> replaced it in 2005).</p>



<p>The old linker &#8211; &#8220;ld64&#8221; &#8211; was retroactively renamed <code>ld-classic</code>.  They emit <em>almost</em> identical version strings, making them easy to confuse at a glance, but for subtly different program and project names:</p>



<pre class="wp-block-preformatted"><strong>👾 /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld -v
</strong>@(#)PROGRAM:<strong>ld</strong> PROJECT:<strong>ld</strong>-1053.12
BUILD 15:44:24 Feb  3 2024
configured to support archs: armv6 armv7 armv7s arm64 arm64e arm64_32 i386 x86_64 x86_64h armv6m armv7k armv7m armv7em
will use ld-classic for: armv6 armv7 armv7s arm64_32 i386 armv6m armv7k armv7m armv7em
LTO support using: LLVM version 15.0.0 (static support for 29, runtime is 29)
TAPI support using: Apple TAPI version 15.0.0 (tapi-1500.3.2.2)

<strong>👾 /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld-classic -v</strong>
@(#)PROGRAM:<strong>ld-classic</strong>  PROJECT:<strong>ld64</strong>-951.9
BUILD 15:44:42 Feb  3 2024
configured to support archs: armv6 armv7 armv7s arm64 arm64e arm64_32 i386 x86_64 x86_64h armv6m armv7k armv7m armv7em
LTO support using: LLVM version 15.0.0 (static support for 29, runtime is 29)
TAPI support using: Apple TAPI version 15.0.0 (tapi-1500.3.2.2)</pre>



<p>Tangentially, I believe the similarity of their version output is entirely intentional, as a lot of programs (particularly within build systems like CMake) make assumptions about the output, for better or worse.</p>
</div></div>



<p>Apple&#8217;s new linker appears to be much more pedantic than the old one &#8211; it warns about a lot of things that the old one didn&#8217;t care about.  One of these is missing platform load commands:</p>



<pre class="wp-block-preformatted">/Users/SadPanda/Documents/vmaf/libvmaf/ld:1:1: no platform load command found in 'src/libvmaf.a[62](cpuid.obj)', assuming: macOS</pre>



<p>This doesn&#8217;t technically break anything &#8211; assuming it guessed the platform correctly, which I suspect it just takes as being the host&#8217;s platform &#8211; but it&#8217;s super annoying because it&#8217;s emitted for every afflicted object file the linker sees (that&#8217;s individual files, even if they&#8217;re buried in archive files &#8211; e.g. libfoo.a).  You can have hundreds or even thousands of these warnings for a single library.  Worse, they&#8217;re emitted when you link against the library, not just when you build it.  And with nested static libraries they can propagate up a build chain endlessly.</p>



<p>If we look at the offending file with <code>otool</code>, we see that indeed it has no platform load command:</p>



<pre class="wp-block-preformatted"><strong>👾 otool -vl cpuid.obj</strong>
Load command 0
      cmd LC_SEGMENT_64
  cmdsize 152
  segname 
   vmaddr 0x0000000000000000
   vmsize 0x000000000000002d
  fileoff 208
 filesize 45
  maxprot rwx
 initprot rwx
   nsects 1
    flags (none)
Section
  sectname __text
   segname __TEXT
      addr 0x0000000000000000
      size 0x000000000000002d
    offset 208
     align 2^4 (16)
    reloff 0
    nreloc 0
      type S_REGULAR
attributes PURE_INSTRUCTIONS SOME_INSTRUCTIONS
 reserved1 0
 reserved2 0
Load command 1
     cmd LC_SYMTAB
 cmdsize 24
  symoff 256
   nsyms 4
  stroff 320
 strsize 56</pre>



<p>For reference, here&#8217;s an example of the load command it&#8217;s looking for:</p>



<pre class="wp-block-preformatted">Load command 1
      cmd LC_BUILD_VERSION
  cmdsize 24
 platform MACOS
    minos 14.2
      sdk 14.4
   ntools 0</pre>



<h2 class="wp-block-heading">Why is it missing?</h2>



<p>There&#8217;s likely many reasons for this, but they all come down to the same general reason:  something is wrong with one [or more] of the tools in the toolchain that created the object file.  All object files <em>should</em> contain this load command, on Apple platforms.  Though the linker currently treats it as optional, the warning is likely a hint from Apple that it may become required in future.  More importantly, without it the linker cannot know which SDK the file is compatible with, and therefore cannot validate that it&#8217;s linking the right things.</p>



<p>In the example case above, the problem tool is <code><a href="https://www.nasm.us" data-wpel-link="external" target="_blank" rel="external noopener">nasm</a></code>.  It&#8217;s an open-source, non-Apple x86-family <a href="https://en.wikipedia.org/wiki/Assembly_language#Assembler" data-wpel-link="external" target="_blank" rel="external noopener">assembler</a>.  It&#8217;s been around for nearly thirty years &#8211; its first platform was DOS.  Though it supports Apple&#8217;s platforms, it is predominately focused on Linux &amp; Windows.  So it&#8217;s not entirely surprising that it&#8217;s causing issues.</p>



<p>It also hasn&#8217;t released even a minor patch update since 2022 (with version 2.16.01), so nominally it predates and doesn&#8217;t support Xcode 15 (announced &amp; released mid-2023).</p>



<p>However, the absence of the <code>LC_BUILD_VERSION</code> command was in fact a problem even before the new Apple linker in Xcode 15, because of cross-compiling (e.g. targeting the iOS simulator on a Mac).  There was even a patch to address the problem, <a href="https://lists.nasm.us/archives/nasm-devel/2021-July/000035.html" data-wpel-link="external" target="_blank" rel="external noopener">submitted by Byoungchan Lee way back in 2021</a>, which was never accepted (it seems to have been completely ignored &#8211; not even a response on the mailing list).</p>



<h2 class="wp-block-heading">Workarounds?</h2>



<p>No good ones, that I can find.  Stoically ignoring the warning for now is probably the best option.</p>



<p>It is still possible (in Xcode 15, at least) to switch to using the old linker instead, using the <code>-Wl,-ld_classic</code> flags, although that&#8217;s not a long-term solution and may have downsides (e.g. some folks report significantly smaller object files with the new linker).</p>



<p>It&#8217;s <em>probably</em> possible to manually insert the missing load command &#8211; if your build process is amenable to that &#8211; although it&#8217;s not apparent to me what tool or command invocation you would use to do that.  Possibly <code>ld</code> itself, though its documentation provides no insight into this.</p>



<p>For some projects, you might be able to switch to a different assembler (e.g. Apple&#8217;s native <code>as</code> from Clang, or <a href="https://yasm.tortall.net" data-wpel-link="external" target="_blank" rel="external noopener">Yasm</a>, etc).  Apple&#8217;s toolchains don&#8217;t have this problem (so far as I&#8217;ve seen) but are of course not available on other platforms (though, being based on Clang, you can probably get essentially the same thing direct from <a href="https://clang.llvm.org" data-wpel-link="external" target="_blank" rel="external noopener">Clang</a>).  I haven&#8217;t tested whether Yasm has the issue.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/no-platform-load-command-found-in-libxyz-a-assuming-macos/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">7856</post-id>	</item>
		<item>
		<title>Another Swift release, another day wasted</title>
		<link>https://wadetregaskis.com/another-swift-release-another-day-wasted/</link>
					<comments>https://wadetregaskis.com/another-swift-release-another-day-wasted/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Wed, 06 Mar 2024 22:32:35 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Ramblings]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[Swift]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=7838</guid>

					<description><![CDATA[I really wish Swift releases would stop having major regressions in the ability to parse non-trivial expressions. The installation of a new version of Xcode &#8211; with a corresponding new Swift version &#38; toolchain &#8211; should be a joyous occasion, not one I increasingly dread.]]></description>
										<content:encoded><![CDATA[
<p>I really wish Swift releases would stop having major regressions in the ability to parse non-trivial expressions.  The installation of a new version of Xcode &#8211; with a corresponding new Swift version &amp; toolchain &#8211; should be a <em>joyous</em> occasion, not one I increasingly dread.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/another-swift-release-another-day-wasted/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2024/03/Sad-Keanu.avif" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">7838</post-id>	</item>
		<item>
		<title>Private Relay is not currently supported in this region</title>
		<link>https://wadetregaskis.com/private-relay-is-not-currently-supported-in-this-region/</link>
					<comments>https://wadetregaskis.com/private-relay-is-not-currently-supported-in-this-region/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Tue, 23 Jan 2024 23:40:18 +0000</pubDate>
				<category><![CDATA[Ramblings]]></category>
		<category><![CDATA[Apple Private Relay]]></category>
		<category><![CDATA[Bugs!]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=7528</guid>

					<description><![CDATA[I&#8217;ve been getting this notification every morning, on both my Macs, for several days now. System Settings says Private Relay is active like normal, even so. Yet Mail refuses to load any images in emails. Yet Safari works fine. So it&#8217;s not at all apparent if Private Relay actually is working or not. Sometime during&#8230; <a class="read-more-link" href="https://wadetregaskis.com/private-relay-is-not-currently-supported-in-this-region/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>I&#8217;ve been getting this notification every morning, on both my Macs, for several days now.  System Settings says Private Relay is active like normal, even so.  Yet Mail refuses to load any images in emails.  Yet Safari works fine.  So it&#8217;s not at all apparent if Private Relay actually is working or not.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="582" height="476" src="https://wadetregaskis.com/wp-content/uploads/2024/01/Private-Relay-in-Settings.webp" alt="Screenshot of the macOS 14 Sonoma iCloud Private Relay settings dialog, showing it as ostensibly enabled even when it is not." class="wp-image-7556" srcset="https://wadetregaskis.com/wp-content/uploads/2024/01/Private-Relay-in-Settings.webp 582w, https://wadetregaskis.com/wp-content/uploads/2024/01/Private-Relay-in-Settings-256x209.webp 256w, https://wadetregaskis.com/wp-content/uploads/2024/01/Private-Relay-in-Settings-512x419.webp 512w, https://wadetregaskis.com/wp-content/uploads/2024/01/Private-Relay-in-Settings@2x.webp 1164w" sizes="auto, (max-width: 582px) 100vw, 582px" /><figcaption class="wp-element-caption">Is Private Relay enabled?  Who knows?!  The Settings app thinks it is, even when the rest of the system thinks it is not.</figcaption></figure>
</div>


<p>Sometime during each day I get a follow-up notification telling me that Private Relay is now active.  Even though by all appearances it already was.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="370" height="96" src="https://wadetregaskis.com/wp-content/uploads/2024/01/Private-Relay-is-Active.webp" alt="Screenshot of a macOS notification titled &quot;Private Relay is Active&quot;, with the text &quot;Your IP address is hidden and your Safari browsing activity is protected&quot;." class="wp-image-7554" srcset="https://wadetregaskis.com/wp-content/uploads/2024/01/Private-Relay-is-Active.webp 370w, https://wadetregaskis.com/wp-content/uploads/2024/01/Private-Relay-is-Active-256x66.webp 256w, https://wadetregaskis.com/wp-content/uploads/2024/01/Private-Relay-is-Active@2x.webp 740w" sizes="auto, (max-width: 370px) 100vw, 370px" /></figure>
</div>


<p>This is frustrating on multiple levels &#8211; obviously there&#8217;s the question of why this notification is appearing to begin with, and lying<sup data-fn="7e1d4d3f-9dfc-4bdc-84cb-8d4b6f26630e" class="fn"><a href="#7e1d4d3f-9dfc-4bdc-84cb-8d4b6f26630e" id="7e1d4d3f-9dfc-4bdc-84cb-8d4b6f26630e-link">1</a></sup> about the support status of Private Relay, but it&#8217;s <em>also</em> concerning that it&#8217;s sending mixed messages about whether Private Relay is working or not.</p>


<ol class="wp-block-footnotes"><li id="7e1d4d3f-9dfc-4bdc-84cb-8d4b6f26630e">I mean, I <em>assume</em> it&#8217;s lying about the status &#8211; I suppose it&#8217;s possible that Apple, headquartered in the San Francisco bay area, might have revoked support for Private Relay in their own region.  Seems like an odd decision, though. <a href="#7e1d4d3f-9dfc-4bdc-84cb-8d4b6f26630e-link" aria-label="Jump to footnote reference 1">↩︎</a></li></ol>]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/private-relay-is-not-currently-supported-in-this-region/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2024/01/Private-Relay-Not-Supported.webp" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">7528</post-id>	</item>
		<item>
		<title>Secret Sonoma design lead: Barbie</title>
		<link>https://wadetregaskis.com/secret-sonoma-design-lead-barbie/</link>
					<comments>https://wadetregaskis.com/secret-sonoma-design-lead-barbie/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Tue, 12 Dec 2023 21:13:50 +0000</pubDate>
				<category><![CDATA[Howto]]></category>
		<category><![CDATA[Ramblings]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[CGContextHighlight2xScaledImages]]></category>
		<category><![CDATA[macOS Sonoma]]></category>
		<category><![CDATA[pink]]></category>
		<category><![CDATA[Retina]]></category>
		<category><![CDATA[Snafu]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=6579</guid>

					<description><![CDATA[macOS Sonoma is in so many ways a dumpster fire. It&#8217;s the worst Apple OS update I can remember (although admittedly there&#8217;s been some real stinkers over the years, so maybe I&#8217;m overlooking some repressed memories). There was of course the bug whereby encrypted external drives no longer mounted automatically. That was pretty special, because&#8230; <a class="read-more-link" href="https://wadetregaskis.com/secret-sonoma-design-lead-barbie/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>macOS Sonoma is in <em>so</em> many ways a dumpster fire.  It&#8217;s the worst Apple OS update I can remember (although admittedly there&#8217;s been some real stinkers over the years, so maybe I&#8217;m overlooking some repressed memories).</p>



<p>There was of course the bug whereby encrypted external drives no longer mounted automatically.  That was pretty special, because it reveals that Apple doesn&#8217;t test encrypted external drives anywhere in their QA process &#8211; possibly not even ad-hoc internally, since it&#8217;s hard to image that an Apple employee stumbling on this bug by accident would not bother to report it.  Or maybe they did but even Apple&#8217;s own employees&#8217; bug reports are ignored.  Consistent, at least.</p>



<p>Since virtually <em>every</em> external drive should be encrypted, this suggests Apple either believes nobody actually uses external drives, or that they don&#8217;t care.  At least that bug was mostly fixed in 14.1, although it took Apple a full month to get around to it.</p>



<h2 class="wp-block-heading">Radical pink redesign</h2>



<p>But perhaps the most <em>glaring</em> bug, in a very literal sense, was the rampant pinkification of the GUI.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="960" height="324" src="https://wadetregaskis.com/wp-content/uploads/2023/12/Safari-website-thumbnails.webp" alt="Screenshot of Safari's website thumbnails view showing inconsistent pink tinting of some thumbnails" class="wp-image-6580" style="width:480px" srcset="https://wadetregaskis.com/wp-content/uploads/2023/12/Safari-website-thumbnails.webp 960w, https://wadetregaskis.com/wp-content/uploads/2023/12/Safari-website-thumbnails-256x86.webp 256w, https://wadetregaskis.com/wp-content/uploads/2023/12/Safari-website-thumbnails-512x173.webp 512w" sizes="auto, (max-width: 960px) 100vw, 960px" /><figcaption class="wp-element-caption">Safari website thumbnails</figcaption></figure>
</div>

<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="406" height="164" src="https://wadetregaskis.com/wp-content/uploads/2023/12/Random-UI-elements-in-web-pages-and-native-apps.png" alt="Small screenshot of a section of the Amazon website toolbar showing pink tinting of the gear &quot;settings&quot; icon" class="wp-image-6581" style="width:203px" srcset="https://wadetregaskis.com/wp-content/uploads/2023/12/Random-UI-elements-in-web-pages-and-native-apps.png 406w, https://wadetregaskis.com/wp-content/uploads/2023/12/Random-UI-elements-in-web-pages-and-native-apps-256x103.png 256w" sizes="auto, (max-width: 406px) 100vw, 406px" /><figcaption class="wp-element-caption">Random GUI controls in web pages</figcaption></figure>
</div>

<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="1700" height="1104" src="https://wadetregaskis.com/wp-content/uploads/2023/12/DPReview-article-hero-images.webp" alt="Screenshot of a section of the DPReview homepage showing pink tinting of an article hero image" class="wp-image-6582" style="width:850px" srcset="https://wadetregaskis.com/wp-content/uploads/2023/12/DPReview-article-hero-images.webp 1700w, https://wadetregaskis.com/wp-content/uploads/2023/12/DPReview-article-hero-images-256x166.webp 256w, https://wadetregaskis.com/wp-content/uploads/2023/12/DPReview-article-hero-images-512x332@2x.webp 1024w, https://wadetregaskis.com/wp-content/uploads/2023/12/DPReview-article-hero-images-512x332.webp 512w" sizes="auto, (max-width: 1700px) 100vw, 1700px" /><figcaption class="wp-element-caption">…not to mention many actual images on web pages.</figcaption></figure>
</div>

<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="282" height="242" src="https://wadetregaskis.com/wp-content/uploads/2023/12/Quartz-Debug-1.webp" alt="" class="wp-image-6607" style="object-fit:cover" srcset="https://wadetregaskis.com/wp-content/uploads/2023/12/Quartz-Debug-1.webp 282w, https://wadetregaskis.com/wp-content/uploads/2023/12/Quartz-Debug-1-256x220.webp 256w, https://wadetregaskis.com/wp-content/uploads/2023/12/Quartz-Debug-1@2x.webp 564w" sizes="auto, (max-width: 282px) 100vw, 282px" /><figcaption class="wp-element-caption">Even Apple&#8217;s own apps &#8211; and most poetically of all Quartz Debug &#8211; are afflicted.</figcaption></figure>
</div>


<p>As you can see, Sonoma brought with it a penchant for tinting things a gross light pink colour, including the rectangular extents of images that are otherwise invisible because they&#8217;re transparent.</p>



<h2 class="wp-block-heading">WTF?!</h2>


<div class="wp-block-image">
<figure class="alignright size-full is-resized"><img loading="lazy" decoding="async" width="528" height="1016" src="https://wadetregaskis.com/wp-content/uploads/2023/12/Quartz-Debug-settings-window.webp" alt="Screenshot of Quartz Debug's settings window" class="wp-image-6587" style="width:264px" srcset="https://wadetregaskis.com/wp-content/uploads/2023/12/Quartz-Debug-settings-window.webp 528w, https://wadetregaskis.com/wp-content/uploads/2023/12/Quartz-Debug-settings-window-133x256.webp 133w, https://wadetregaskis.com/wp-content/uploads/2023/12/Quartz-Debug-settings-window-266x512.webp 266w" sizes="auto, (max-width: 528px) 100vw, 528px" /></figure>
</div>


<p>I spent a <em>lot</em> of time debugging this.  Over days, weeks, and months.  Trying out new avenues as they came to me, even repeating some web &amp; forum searches over and over again in frustrated desperation.</p>



<p>I only just today figured out what&#8217;s going on.  84 days later.</p>



<p>One of my first instincts &#8211; right on day 0 &#8211; was that it was some Quartz Debug thing, because it looks a lot like some of the tinting options Quartz Debug has for things like flashing screen updates or showing opaque regions.  But the Quartz Debug app insisted everything was disabled.  I enabled everything anyway, and then disabled it all again, in case that would jog something free.  No bueno.</p>



<p>I then spent the better part of three months trying every superstition, every rumour or snippet of hearsay online, that might in any way related to this problem or conceivably fix it.  Nothing helped.</p>



<h2 class="wp-block-heading">Epiphany</h2>



<p>It was last night, when I was fiddling with some images in a gallery here on my website, that I discovered that adjusting the CSS dimensions of an image by just <em>a single pixel</em> could pinkify them.  Most importantly, restoring the dimensions to the <em>actual image dimensions</em> would fix them.  I&#8217;d already observed something similar to that, in various apps and websites, but it <em>felt</em> different to be directly triggering the problem myself.</p>



<p>That kept me up half the night, tossing and turning in bed, highly suspicious that this was a pivotal clue, but unsure what it pointed to.</p>



<p>Eventually, it led me right back to my very first instinct &#8211; Quartz Debug &#8211; and the tentative conclusion that this <em>was</em> in fact a Quartz Debug problem even if Quartz Debug claimed otherwise.  I become suspicious that the app was either not showing me the full range of settings, or just plain wrong about them.</p>



<p>And that was the insight I needed &#8211; right on the very first page of search results for &#8220;quartz debug defaults&#8221; was a link to <a href="https://developer.apple.com/library/archive/documentation/GraphicsAnimation/Conceptual/HighResolutionOSX/Testing/Testing.html" data-wpel-link="external" target="_blank" rel="external noopener">Testing and Troubleshooting High-Resolution Content</a>.  Buried behind a disclosure triangle near the bottom is the magic string:</p>



<p class="has-text-align-center has-large-font-size" style="font-style:normal;font-weight:500"><code>CGContextHighlight2xScaledImages</code></p>



<p>That was set to YES, on my system.</p>



<p>Deleting it fixes the issue once affected apps are restarted (something Apple&#8217;s own documentation fails to mention &#8211; <em>kind of an important step, Apple!</em>).</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><code>defaults delete -g CGContextHighlight2xScaledImages</code></p>
<cite>The magic incantation to exorcise Barbie</cite></blockquote>



<p>In theory you can also disable this from the Quartz Debug app itself, it&#8217;s just hidden &#8211; in the Tools menu, <em>not</em> the actual settings window, is an item labelled &#8220;Color 1x Artwork&#8221;.  Note how the setting applies even when Quartz Debugging is disabled.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="688" height="596" src="https://wadetregaskis.com/wp-content/uploads/2023/12/Screenshot-of-the-Quartz-Debug-Tools-menu.webp" alt="" class="wp-image-6588" style="width:344px" srcset="https://wadetregaskis.com/wp-content/uploads/2023/12/Screenshot-of-the-Quartz-Debug-Tools-menu.webp 688w, https://wadetregaskis.com/wp-content/uploads/2023/12/Screenshot-of-the-Quartz-Debug-Tools-menu-256x222.webp 256w, https://wadetregaskis.com/wp-content/uploads/2023/12/Screenshot-of-the-Quartz-Debug-Tools-menu-512x444.webp 512w" sizes="auto, (max-width: 688px) 100vw, 688px" /></figure>
</div>


<h2 class="wp-block-heading">So, wait, how did this come about?</h2>



<p>I have no idea why this only cropped up when Sonoma was installed.  Either Sonoma turned this on (unlikely…?) or it was already on yet wasn’t having any effect in prior OS releases (from 2017 onwards, at least, since my iMac Pro never exhibited this issue before Sonoma).</p>



<p>I know for sure that this setting did work at least going back over a decade, as I remember using this &#8211; on a different computer &#8211; for doing then-new-fangled Retina development… but I’m pretty sure I’ve never set this on my iMac Pro since it way post-dates the introduction of Retina.  Conceivably it was pulled across by Apple&#8217;s Migration Assistant.  Though, again, why did no earlier version of the OS, going back over a decade, exhibit this behaviour if it was supposedly enabled the whole time?</p>



<p>I know it&#8217;s not just me that&#8217;s been afflicted by this to date, as I found a sporadic few reports of this going back years, e.g. <a href="https://apple.stackexchange.com/questions/155602/highly-abnormal-graphic-glitch-on-yosemite-retina-all-white-backgrounds-appea" data-wpel-link="external" target="_blank" rel="external noopener">Highly abnormal graphic glitch on yosemite / retina: all white backgrounds appear pink</a>.  It pisses me off immensely that this page has <em>exactly</em> the keywords I was searching for right from the start &#8211; right in its title! &#8211; yet neither Bing nor Google can find it.</p>



<h2 class="wp-block-heading">Apple&#8217;s response?  &#8220;Fuck you&#8221; in mime.</h2>



<p>I actually filed a pretty detailed bug report with Apple about this, the day I discovered it (September 19th, 2023).  FB13190068.</p>



<p>Of course, they never responded in the slightest.</p>



<p>I provided a <em>bunch</em> of example images, even a screen recording to show how the pink comes and goes in certain circumstances.</p>



<p>I studiously tested every new macOS update and updated the bug report to note that they each had not fixed it.</p>



<p>The morale of this is, as always, that filing bug reports with Apple is an infuriating waste of time.  Mea culpa.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/secret-sonoma-design-lead-barbie/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2023/12/Quartz-Debug-1.webp" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">6579</post-id>	</item>
		<item>
		<title>SwiftData pitfalls</title>
		<link>https://wadetregaskis.com/swiftdata-pitfalls/</link>
					<comments>https://wadetregaskis.com/swiftdata-pitfalls/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Wed, 15 Nov 2023 21:04:36 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[Core Data]]></category>
		<category><![CDATA[MySQL Workbench]]></category>
		<category><![CDATA[Snafu]]></category>
		<category><![CDATA[SQL]]></category>
		<category><![CDATA[SQLite]]></category>
		<category><![CDATA[Swift]]></category>
		<category><![CDATA[SwiftData]]></category>
		<category><![CDATA[Undocumented]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=5378</guid>

					<description><![CDATA[I&#8217;ve been exploring SwiftData lately, and I&#8217;ve been unpleasantly surprised by how many sharp edges it has. I&#8217;m going to try to describe some of them here, so that hopefully others can avoid them (or perhaps be dissuaded from using SwiftData to begin with). I&#8217;m using Xcode 15.0.1 (Swift 5.9) on macOS 14.1 (Sonoma). Background&#8230; <a class="read-more-link" href="https://wadetregaskis.com/swiftdata-pitfalls/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>I&#8217;ve been exploring SwiftData lately, and I&#8217;ve been unpleasantly surprised by how many sharp edges it has.  I&#8217;m going to try to describe some of them here, so that hopefully others can avoid them (or perhaps be dissuaded from using SwiftData to begin with).</p>



<p>I&#8217;m using Xcode 15.0.1 (Swift 5.9) on macOS 14.1 (Sonoma).</p>



<h2 class="wp-block-heading">Background</h2>



<p>I&#8217;ve dabbled in Core Data over the years &#8211; mostly pre-Swift &#8211; and have a kind of begrudging, distant respect.  I understand what it&#8217;s trying to do and I can appreciate some of the ways in which it makes that easier, but I&#8217;m also all too familiar with its limitations and caveats.  So mostly I&#8217;ve ignored it, preferring to use other approaches &#8211; whether that be JSON or plists for simple cases, or just directly using a SQL database when that feature-set genuinely is warranted.</p>



<p>When SwiftData was introduced, I was intrigued.  On the surface it seemed like it greatly reduced the boilerplate and some of the complexity of using Core Data.  It at least seemed like an appealing option for light use, with relatively simple models and especially with SwiftUI.  It seems pretty obviously targeted at the typical, pretty trivial patterns that most iOS apps have &#8211; i.e. not much more than lists of objects.</p>



<p>But it&#8217;s only recently that I actually tried using SwiftData for more than just some trivial toy cases.  I had a textbook case of a hierarchical type tree, with fairly simple 1-to-1 or many-to-1 relationships, the need for cascade deletes in some cases but not others, and a dataset a little larger than what I could get away with just stuffing into User Defaults.</p>



<p>Alas, what I&#8217;ve found is that SwiftData has so many glaring bugs and limitations, that even for this textbook example case, it just doesn&#8217;t make any sense to use it.</p>



<h2 class="wp-block-heading">Example use case</h2>



<p>My model is fairly simple &#8211; a root object called <code>House</code> which can contain multiple <code>Floor</code>s each of which has multiple <code>Room</code>s.  Each of those has a few other attributes &#8211; simple strings and numbers &#8211; and the <code>Room</code>s can reference a few other model types, like <code>Weapon</code>s and <code>Monster</code>s, as 1-to-1 relationships.</p>



<p>The order of floors and rooms matters &#8211; they correspond to top to bottom (attic to basement) and left to right as will be rendered in the GUI.</p>



<h2 class="wp-block-heading">Design flaws</h2>



<h3 class="wp-block-heading" id="autosave-does-not-work">Auto-save doesn&#8217;t work</h3>



<p>By default, model containers are <em>supposed</em> to auto-save.  That&#8217;s what the documentation says, both in <a href="https://developer.apple.com/documentation/swiftui/view/modelcontainer(for:inmemory:isautosaveenabled:isundoenabled:onsetup:)-8oc48" data-wpel-link="external" target="_blank" rel="external noopener">the API reference</a> and the tutorials:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>In both instances, the returned context periodically checks whether it contains unsaved changes, and if so, implicitly saves those changes on your behalf. For contexts you create manually, set the&nbsp;<a class="" href="https://developer.apple.com/documentation/swiftdata/modelcontext/autosaveenabled"><code>autosaveEnabled</code></a>&nbsp;property to&nbsp;<code>true</code>&nbsp;to get the same behavior [sic].</p>
<cite>Apple&#8217;s <a href="https://developer.apple.com/documentation/swiftdata/preserving-your-apps-model-data-across-launches#Save-models-for-later-use" data-type="link" data-id="https://developer.apple.com/documentation/swiftdata/preserving-your-apps-model-data-across-launches#Save-models-for-later-use" data-wpel-link="external" target="_blank" rel="external noopener">Save models for later use</a> (subsection of <a href="https://developer.apple.com/documentation/swiftdata/preserving-your-apps-model-data-across-launches" data-wpel-link="external" target="_blank" rel="external noopener">Preserving your app’s model data across launches</a>)</cite></blockquote>



<p>I suppose it hinges on the word &#8220;periodically&#8221;.  In my testing, that period must be tens of seconds, or worse.  It&#8217;s trivial to see that it doesn&#8217;t work &#8211; just make some changes and close the window, or quit your app.  Virtually all the time &#8211; even if you wait several seconds &#8211; those changes are silently lost!</p>



<p>It&#8217;s clear that SwiftData has no hooks into deinitialisation, view / window closure, app exit, etc.  You would think it <em>has</em> to, in order to save things reliably even in the happy-path cases (as opposed to e.g. app crashes).  But it simply does not, and thus it does not.</p>



<h4 class="wp-block-heading">Workarounds</h4>



<p>If you want full resilience to unexpected app termination (e.g. crashes) then <em>any</em> time you make a change to <em>any</em> SwiftData-managed object, you have to immediately call <code>context.save()</code> manually.  Which is especially annoying since that method throws, so you have to figure out some sane way to handle that failure possibility in every situation in which you make any changes to your models.</p>



<p>For non-trivial applications, this leads to your code being <em>riddled</em> with save-management code.  Think about every SwiftUI field which reflects an element of your model and enables mutation of it.  Every button that adds or removes or reorders anything.  Every editable list view.  Etc.</p>



<p>It&#8217;s easy to miss a spot where you make a modification, leading to silent data loss.</p>



<p>If you&#8217;re willing to lose data when your app crashes (which you really <em>shouldn&#8217;t</em> be willing to do so easily do), you can limit your saves to certain key paths &#8211; e.g. window closure, app exit, etc.  But you have to find and hook into all those things manually.</p>



<p>This is nominally no different to some SwiftData alternatives (e.g. <code>NSCoding</code>), although below par compared to User Defaults and most other SQL database frameworks.</p>



<h3 class="wp-block-heading">Arrays of <code>@Model</code> objects are randomly shuffled</h3>



<p><code>Array</code> in Swift is very explicitly an <em>ordered</em> collection, in the sense that the elements within have a specific albeit arbitrary order, based on how they&#8217;re inserted.  This is the same as arrays / vectors / lists in practically every programming language.</p>



<p>SwiftData violates this by randomly reordering elements at various times, such as when [re]loading model objects from persist storage.</p>



<p>The reason is pretty simple &#8211; it fails to record the order of elements in its underlying SQLite database, instead using a randomly-assigned, arbitrary integer as a uniquing key, and nothing else.</p>



<p>This is a pretty startling design flaw.  I&#8217;m not sure how SwiftData got out the door with this.</p>



<h4 class="wp-block-heading">Workarounds</h4>



<p>I haven&#8217;t found any good workarounds.  The advice you&#8217;ll generally see online (e.g. <a href="https://stackoverflow.com/questions/76889986/swiftdata-ios-17-array-in-random-order" data-wpel-link="external" target="_blank" rel="external noopener">on StackOverflow</a>) is to manually load the objects in the array, specifying a sort order as part of the query.  But this basically has you manually reimplementing the relational aspects of SwiftData:</p>



<ul class="wp-block-list">
<li>You have to add a member variable to the target class in question <em>just</em> to store its index in the array.  You need one of these variables for <em>every</em> many-to relationship it can be a part of.</li>



<li>You have to then somehow populate those variables <em>and</em> keep them in sync with any changes to the arrays.</li>



<li>You can&#8217;t actually use the array member variables &#8211; since the objects are in random order &#8211; and instead have to manually query the database separately.  Or, you can sort the array member variables at some point, but it&#8217;s difficult to do that efficiently (you can&#8217;t really be sure when SwiftData is going to mangle the sort order, so you basically have to do the sorting on the fly every single time you access the array).</li>
</ul>



<p>Rather than going through all that hassle, it&#8217;s easier to just not use SwiftData &#8211; e.g. you can simply use a normal object graph that&#8217;s persisted to disk as JSON, or in property list form, or using <code>NSCoding</code>, etc.</p>



<h3 class="wp-block-heading"><code>let</code> cannot be used for bidirectional relationships</h3>



<p>All member variables (of <code>@Model</code> classes) that refer bidirectionally to other model classes <em>must</em> be variables (<code>var</code>), not constants (<code>let</code>).  Irrespective of their actual intended semantics.</p>



<p>This is seemingly just an inherent part of SwiftData (and Core Data underneath) &#8211; the presumption that <em>all</em> relationships are optional <em>and</em> can change arbitrarily.  It worked okay in Objective-C because that was closer to the reality of Objective-C objects, but it flies in the face of Swift&#8217;s much stronger controls and expectations on mutability and optionality.</p>



<p>It would be a relatively minor annoyance &#8211; losing compile-time protection of immutability is frustrating but not technically a show-stopper &#8211; except that there are <em>no</em> warnings or indications otherwise, of any kind, that you&#8217;re &#8220;holding it wrong&#8221;.  If you declare the member with <code>let</code>, everything will compile without so much as a warning, but you&#8217;ll run into obtuse runtime errors and crashes, e.g.:</p>



<pre class="wp-block-preformatted">💣 Could not cast value of type 'Swift.KeyPath&lt;Game.House, Swift.Array&lt;Game.Floor&gt;&gt;' (0x12e00cc98) to 'Swift.ReferenceWritableKeyPath&lt;Game.House, Swift.Array&lt;Floor.Region&gt;&gt;' (0x13e0b4198).</pre>



<p>What that error message is <em>trying</em> but failing to convey is that you cannot (outside of initialisation) write to a <code>let</code>, of course, so its attempt to modify the property failed.  So it crashed your app.</p>



<h4 class="wp-block-heading">Workarounds</h4>



<p>None, really.  You just have to remember, <em>every time</em>, to avoid <code>let</code>.  Possibly you could obtain &amp; configure some linter to check this for you.</p>



<h3 class="wp-block-heading" id="relationships-are-always-optional">Relationships are secretly implicitly unwrapped by default</h3>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">@Model</span></span>
<span class="line"><span style="color: #0000FF">final</span><span style="color: #000000"> </span><span style="color: #0000FF">class</span><span style="color: #000000"> </span><span style="color: #267F99">Room</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">let</span><span style="color: #000000"> monster: Monster</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    …</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>That&#8217;s fine, right &#8211; every room has a <code>Monster</code>.  Simple.</p>



<p>Except it&#8217;s not, if <code>Monster</code> is a <code>@Model</code>.  In that case, despite the fact you&#8217;ve declared it as non-optional, it actually is optional.  <code>@Model</code> essentially makes it implicitly unwrapped without telling you (and without the important <code>!</code> character after the type to warn you).</p>



<p>The technical reason it&#8217;s implicitly and unavoidably optional is because <code>Room</code> and <code>Monster</code> are stored in separate SQLite tables, with a nullable foreign key relationship between them.  SwiftData does not support non-nullable foreign keys.  So it&#8217;s totally valid, as far as the actual database layer and persistent storage are concerned, for the relevant <code>Monster</code> to disappear entirely.  But that means your model instance is basically corrupt when you load it, which can have a variety of ill-defined effects such as crashing your app.</p>



<p>You might think this is largely hypothetical, or academic &#8211; SwiftData&#8217;s not going to erroneously delete your <code>Monster</code> out from under the <code>Room</code> (you assume), and you&#8217;re not going to do it yourself, so it&#8217;ll never happen, right?  Well, maybe.  Consider what happens as your app evolves, undergoes refactors, etc.  It&#8217;s quite possible you unwittingly create a situation in which what <em>was</em> previously valid data is no longer valid to your app.</p>



<p>Or, you might run into a SwiftData design flaw or bug which causes the data to be corrupted on save, such as is detailed in the next section.  Even if the root cause is that your data was corrupted at save time, it&#8217;s frustrating to have that manifest only through obtuse crashes at <em>some ill-defined time</em> in the future after your app is relaunched.</p>



<h4 class="wp-block-heading">Workarounds</h4>



<p>None, really.</p>



<p>At the very least, you can explicitly make <em>all</em> relationships optional.  That way you can code defensively around them, by putting in appropriate unwrapping code and guards.  If nothing else you can at the very least crash with a clear failure message (there is no log message at all when SwiftData crashes your app via implicit unwrapping).</p>



<h3 class="wp-block-heading" id="you-cannot-directly-initialise-bidirectional-relationship-properties">You cannot directly initialise bidirectional relationship properties</h3>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">@Model</span></span>
<span class="line"><span style="color: #0000FF">final</span><span style="color: #000000"> </span><span style="color: #0000FF">class</span><span style="color: #000000"> </span><span style="color: #267F99">House</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">@Relationship</span><span style="color: #000000">(deleteRule: .</span><span style="color: #001080">cascade</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                  inverse: \Floor.</span><span style="color: #001080">house</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">let</span><span style="color: #000000"> floors: [Floor]</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">init</span><span style="color: #000000">(</span><span style="color: #795E26">floors</span><span style="color: #000000">: [Floor]) {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #0000FF">self</span><span style="color: #000000">.</span><span style="color: #001080">floors</span><span style="color: #000000"> = floors</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>If you do something like the above &#8211; making use of Xcode to generate your initialiser for you, like you normally would for classes &#8211; you&#8217;ll get the wonderful behaviour that everything works fine until you quit your app and reload it.  Then you&#8217;ll discover that <code>floors</code> is empty.</p>



<p>There are no error messages, neither at compile time nor runtime.  No warnings.  <em>Nothing</em>.</p>



<p>Looking at the underlying SQLite database, its apparent that <em>something</em> is seriously broken because while it <em>does</em> save the <code>Floor</code> instances, their foreign key back to <code>House</code> is <code>NULL</code> (even if logically that should be impossible &#8211; refer back to the prior point on how <a href="#relationships-are-always-optional" data-type="internal" data-id="#relationships-are-always-optional">SwiftData forces <em>all</em> relationships to be optional</a>).</p>



<p>What&#8217;s happening is that the initialisation of <code>floors</code> in <code>init</code> is in some sense bypassing SwiftData&#8217;s custom hooks; it&#8217;s &#8220;directly&#8221; setting the value in memory without SwiftData updating its corresponding Core Data model underneath (this is another place SwiftData&#8217;s abstraction is leaky &#8211; your <code>@Model</code> classes aren&#8217;t the <em>real</em> model classes, they&#8217;re merely a layer atop the <em>actual</em> Core Data models).</p>



<h4 class="wp-block-heading">Workarounds</h4>



<p>Thankfully this <em>does</em> have a simple workaround (iff you know it and can remember to always use it):  never <em>assign</em> the relationship in <code>init</code>.  You can <em>append</em> to it, and you can assign it <em>outside</em> of <code>init</code>.  So e.g.:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">@Model</span></span>
<span class="line"><span style="color: #0000FF">final</span><span style="color: #000000"> </span><span style="color: #0000FF">class</span><span style="color: #000000"> </span><span style="color: #267F99">House</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">@Relationship</span><span style="color: #000000">(deleteRule: .</span><span style="color: #001080">cascade</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                  inverse: \Floor.</span><span style="color: #001080">house</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">let</span><span style="color: #000000"> floors: [Floor] = []</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">init</span><span style="color: #000000">(</span><span style="color: #795E26">floors</span><span style="color: #000000">: [Floor]) {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #0000FF">self</span><span style="color: #000000">.</span><span style="color: #001080">floors</span><span style="color: #000000">.</span><span style="color: #795E26">append</span><span style="color: #000000">(</span><span style="color: #795E26">contentsOf</span><span style="color: #000000">: floors)</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<h3 class="wp-block-heading">Relationship constraints are only enforced at <em>save</em> time</h3>



<p><code>@Relationship</code> lets you specify the arity of a relationship &#8211; the minimum and maximum number of related objects that are permitted.  However, this is not actually enforced at any time <em>except</em> save time (when you <em>explicitly</em> call <code>context.save()</code>).</p>



<p>Arguably this is better than nothing &#8211; vanilla <code>Array</code> member variables provide no way to constrain the array&#8217;s contents &#8211; but it can lead to mistakes if you unwittingly rely on more rigorous enforcement.</p>



<h4 class="wp-block-heading">Workarounds</h4>



<p>None, really, that I&#8217;ve been able to come up with.</p>



<p>It sort of helps to think of these &#8216;constraints&#8217; as actually just &#8216;guidelines&#8217;; to not <em>actually</em> rely on them being enforced.  That may mean you have to do manual validation, that&#8217;s in principle redundant, in order to ensure the rules are enforced.</p>



<p>That said, since you need to manually save aggressively anyway (since <a href="#autosave-does-not-work">auto-save doesn&#8217;t work</a>), you might be able to conjoin the two workarounds.</p>



<h3 class="wp-block-heading">Singletons are unsupported</h3>



<p>In some use-cases your root object is a singleton &#8211; you don&#8217;t ever actually want to have multiple instances of it, it&#8217;s merely the top-level of your object graph, the object which organises all other objects.</p>



<p>Unfortunately SwiftData doesn&#8217;t support this.  Every <code>@Model</code> class can have multiple instances.</p>



<p>This can make things pretty awkward when dealing with your singleton, as you cannot do intuitive and simple things like:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">struct</span><span style="color: #000000"> </span><span style="color: #267F99">Map</span><span style="color: #000000">: View {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">@Environment</span><span style="color: #000000">(\.</span><span style="color: #001080">modelContext</span><span style="color: #000000">) </span><span style="color: #0000FF">private</span><span style="color: #000000"> </span><span style="color: #0000FF">var</span><span style="color: #000000"> context</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">@Query</span><span style="color: #000000"> </span><span style="color: #0000FF">var</span><span style="color: #000000"> house: House</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">var</span><span style="color: #000000"> body: some View {</span></span>
<span class="line"><span style="color: #000000">        VStack {</span></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #795E26">ForEach</span><span style="color: #000000">(house.</span><span style="color: #001080">floors</span><span style="color: #000000">) {</span></span>
<span class="line"><span style="color: #000000">                …</span></span>
<span class="line"><span style="color: #000000">            }</span></span>
<span class="line"><span style="color: #000000">        }</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<h4 class="wp-block-heading">Workarounds</h4>



<p>Basically you have three choices:</p>



<ul class="wp-block-list">
<li>Remove all your singletons, redesigning your model and app to accomodate.  This <em>might</em> make some contrived sense if you rationalise it as a way to have multiple game sessions concurrently, or to facilitate concurrent unit testing, or somesuch.  But you&#8217;ll know in your heart that it&#8217;s not real.</li>



<li>Use force-unwrapping (or equivalent), making your app crash or otherwise fail if your singleton expectations are violated by SwiftData.</li>



<li>Silently ignore all but an arbitrary instance of the model class in question.</li>
</ul>



<p>These are all pretty flawed.  And no matter which approach you choose, they introduce some significant boilerplate into your code.  e.g.:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">struct</span><span style="color: #000000"> </span><span style="color: #267F99">Map</span><span style="color: #000000">: View {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">@Environment</span><span style="color: #000000">(\.</span><span style="color: #001080">modelContext</span><span style="color: #000000">) </span><span style="color: #0000FF">private</span><span style="color: #000000"> </span><span style="color: #0000FF">var</span><span style="color: #000000"> context</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">@Query</span><span style="color: #000000"> </span><span style="color: #0000FF">var</span><span style="color: #000000"> houses: [House]</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">var</span><span style="color: #000000"> body: some View {</span></span>
<span class="line"><span style="color: #000000">        VStack {</span></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #795E26">ForEach</span><span style="color: #000000">(house[</span><span style="color: #098658">0</span><span style="color: #000000">].</span><span style="color: #001080">floors</span><span style="color: #000000">) { </span><span style="color: #008000">// Silently ignore other houses.</span></span>
<span class="line"><span style="color: #000000">                …</span></span>
<span class="line"><span style="color: #000000">            }</span></span>
<span class="line"><span style="color: #000000">        }</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>In the above example, which house is chosen is arbitrary and may change between view updates.  You <em>can</em> do extra work to pick a house deterministically &#8211; e.g. sort by some unique attribute and pick the first &#8211; and you probably <em>should</em> if you&#8217;re going to use this hack, but you&#8217;ll still be operating in a messed-up state.</p>



<h3 class="wp-block-heading">You cannot create model objects outside of SwiftData contexts</h3>



<p>This limitation is explicable and reasonable on the face of it, but does limit your app architecture in ways that can be annoying.  e.g. you cannot initialise your views with test models like you normally would:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #000000">#Preview {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">MapView</span><span style="color: #000000">(</span><span style="color: #795E26">house</span><span style="color: #000000">: House.</span><span style="color: #795E26">default</span><span style="color: #000000">())</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>That will compile without any complaints, but when you try to actually view the preview in Xcode you&#8217;ll get the dreaded &#8220;Cannot preview in this file&#8221; error, with absolutely no indication why.</p>



<p><em>If</em> you happen to try a similar thing in your app initialisation, e.g.:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">@main</span></span>
<span class="line"><span style="color: #0000FF">struct</span><span style="color: #000000"> </span><span style="color: #267F99">Game</span><span style="color: #000000">: App {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">var</span><span style="color: #000000"> body: some Scene {</span></span>
<span class="line"><span style="color: #000000">        WindowGroup {</span></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #795E26">Overview</span><span style="color: #000000">(</span><span style="color: #795E26">house</span><span style="color: #000000">: House.</span><span style="color: #795E26">default</span><span style="color: #000000">())</span></span>
<span class="line"><span style="color: #000000">                .</span><span style="color: #795E26">modelContainer</span><span style="color: #000000">(</span><span style="color: #795E26">for</span><span style="color: #000000">: [House.self])</span></span>
<span class="line"><span style="color: #000000">        }</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>…then you can at least run your app and will see an error message before it crashes, like:</p>



<pre class="wp-block-preformatted">💣 SwiftData/ModelContainer.swift:144: Fatal error: failed to find a currently active container for Room
Failed to find any currently loaded container for Room) [sic]</pre>



<p>So far as Apple error messages go, this is practically perfect &#8211; it at least does mention <em>almost</em> the pertinent object (the [SwiftData] container is <em>similar</em> to the SwiftData context) and provide some explanation of the genuine problem.</p>



<h4 class="wp-block-heading">Workarounds</h4>



<p>I haven&#8217;t explored this in great detail, so there might be other options, but the best I could promptly come up with is to move initialisation inside your SwiftUI views.  Whether that be through explicit user interaction (e.g. a &#8220;New Game&#8221; GUI in my use case, when there&#8217;s no existing <code>House</code> in the database), or in a suitable <code>onAppear</code> handler, etc.</p>



<p>That&#8217;s reasonable for actual app execution, but doesn&#8217;t immediately help you for SwiftUI previews.  However, if you attach a suitable model container to your preview instance:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #000000">#Preview {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">MapView</span><span style="color: #000000">()</span></span>
<span class="line"><span style="color: #000000">        .</span><span style="color: #795E26">modelContainer</span><span style="color: #000000">(</span><span style="color: #795E26">for</span><span style="color: #000000">: [House.self])</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>…then your preview will actually use the real app data.  This can actually be handy sometimes, as you can manipulate the state &#8211; either in the preview or your actual app execution &#8211; which can be handy for debugging view problems (e.g. you&#8217;re running your app, playing around, and you notice a rendering error &#8211; you can quit your app and its saved state will be used in your Xcode previews, automatically showing you the view in its broken state).</p>



<p>Alternatively, you can go through a more laborious process of [more-]manually creating the SwiftData context, in order to allow you to use a <em>distinct</em> database and to create test objects, e.g.:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #000000">#Preview {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">let</span><span style="color: #000000"> container = </span><span style="color: #AF00DB">try</span><span style="color: #000000">! </span><span style="color: #795E26">ModelContainer</span><span style="color: #000000">(</span><span style="color: #795E26">for</span><span style="color: #000000">: House.self,</span></span>
<span class="line"><span style="color: #000000">                                         </span><span style="color: #795E26">configurations</span><span style="color: #000000">: </span><span style="color: #795E26">ModelConfiguration</span><span style="color: #000000">(</span><span style="color: #795E26">isStoredInMemoryOnly</span><span style="color: #000000">: </span><span style="color: #0000FF">true</span><span style="color: #000000">))</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">MapView</span><span style="color: #000000">(</span><span style="color: #795E26">house</span><span style="color: #000000">: House.</span><span style="color: #795E26">forPreviewing</span><span style="color: #000000">())</span></span>
<span class="line"><span style="color: #000000">        .</span><span style="color: #795E26">modelContainer</span><span style="color: #000000">(container)</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>I <em>believe</em> using an in-memory-only store will prevent it touching your actual app&#8217;s saved state, but I haven&#8217;t poked at it much.  I also believe you can use <a href="https://developer.apple.com/documentation/swiftdata/modelconfiguration/init(_:schema:url:allowssave:cloudkitdatabase:)" data-wpel-link="external" target="_blank" rel="external noopener">the more complicated configuration initialiser</a> to explicitly specify a URL, if you want a <em>persistent</em> test dataset that&#8217;s nonetheless separate from your app&#8217;s real dataset, but I haven&#8217;t played with it myself.</p>



<p>There are various tutorials online that go into more detail, e.g. <a href="https://www.hackingwithswift.com/about" data-wpel-link="external" target="_blank" rel="external noopener">Paul Hudson</a>&#8216;s <a href="https://www.hackingwithswift.com/quick-start/swiftdata/how-to-use-swiftdata-in-swiftui-previews" data-type="link" data-id="https://www.hackingwithswift.com/quick-start/swiftdata/how-to-use-swiftdata-in-swiftui-previews" data-wpel-link="external" target="_blank" rel="external noopener">How to use SwiftData in SwiftUI previews</a>.</p>



<h3 class="wp-block-heading">Apple&#8217;s documentation is wrong</h3>



<p>This one barely rates a mention since Apple&#8217;s documentation is pretty infamously unreliable (what little of it exists at all).  Still, it&#8217;s important to point out a couple of particularly glaring errors that will hit most SwiftData newbies:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">@Model</span></span>
<span class="line"><span style="color: #0000FF">class</span><span style="color: #000000"> </span><span style="color: #267F99">Trip</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">var</span><span style="color: #000000"> name: </span><span style="color: #267F99">String</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">var</span><span style="color: #000000"> destination: </span><span style="color: #267F99">String</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">var</span><span style="color: #000000"> startDate: Date</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">var</span><span style="color: #000000"> endDate: Date</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">var</span><span style="color: #000000"> accommodation: Accommodation?</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>
<cite>Apple&#8217;s <a href="https://developer.apple.com/documentation/swiftdata/preserving-your-apps-model-data-across-launches" data-type="link" data-id="https://developer.apple.com/documentation/swiftdata/preserving-your-apps-model-data-across-launches" data-wpel-link="external" target="_blank" rel="external noopener">Preserving your app’s model data across launches</a></cite></blockquote>



<p>That is Apple&#8217;s <em>very first</em> code example in introducing SwiftData, and it&#8217;s wrong.  It has no initialiser, therefore you cannot actually use this <code>Trip</code> class.</p>



<p>Now, you might say that&#8217;s pedantic, but it&#8217;s actually quite pertinent &#8211; Apple <em>never</em> show a complete example of a <code>@Model</code> class, so they never show you how you&#8217;re supposed to initialise one, and thus they never even try to prevent people falling into pits such as <a href="#you-cannot-directly-initialise-bidirectional-relationship-properties">the aforementioned issues with arrays</a>.</p>



<p>Another poignant example:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">@Relationship</span><span style="color: #000000">(.</span><span style="color: #001080">cascade</span><span style="color: #000000">) </span><span style="color: #0000FF">var</span><span style="color: #000000"> accommodation: Accommodation?</span></span></code></pre></div>
<cite>Apple&#8217;s <a href="https://developer.apple.com/documentation/swiftdata/preserving-your-apps-model-data-across-launches" data-type="link" data-id="https://developer.apple.com/documentation/swiftdata/preserving-your-apps-model-data-across-launches" data-wpel-link="external" target="_blank" rel="external noopener">Preserving your app’s model data across launches</a></cite></blockquote>



<p>That&#8217;s not valid; it doesn&#8217;t even compile.  The syntax <em>actually</em> is:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">@Relationship</span><span style="color: #000000">(deleteRule: .</span><span style="color: #001080">cascade</span><span style="color: #000000">) </span><span style="color: #0000FF">var</span><span style="color: #000000"> accommodation: Accommodation?</span></span></code></pre></div>



<h4 class="wp-block-heading">Workarounds</h4>



<p>There are quite a few tutorials available online which provide an alternative onboarding guide, to Apple&#8217;s official documentation.  I did find a broad perusal of them useful in gleaning bits and pieces that Apple themselves don&#8217;t cover (or are wrong about).  Unfortunately many are essentially just copy-pastes or paraphrases of Apple&#8217;s documentation, and even those that aren&#8217;t tend to make the same mistakes.</p>



<p>But, since you&#8217;ve now read this post, you&#8217;re better equipped than most to try diving into SwiftData.</p>



<h2 class="wp-block-heading">Non-pitfalls</h2>



<p>Just as a bit of a bonus section, I want to call out one aspect of SwiftData which <em>actually</em> works as you might expect.</p>



<h3 class="wp-block-heading">You only need to register the root(s) of your model graph</h3>



<p>When setting up your model container, you don&#8217;t have to manually enumerate <em>every</em> model object you use, merely some root(s).  SwiftData automatically walks your class(es) member variables to find relationships to other <code>@Model</code> classes, and implicitly registers those too.  So e.g.:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">@main</span></span>
<span class="line"><span style="color: #0000FF">struct</span><span style="color: #000000"> </span><span style="color: #267F99">Game</span><span style="color: #000000">: App {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">var</span><span style="color: #000000"> body: some Scene {</span></span>
<span class="line"><span style="color: #000000">        WindowGroup {</span></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #795E26">Map</span><span style="color: #000000">()</span></span>
<span class="line"><span style="color: #000000">                .</span><span style="color: #795E26">modelContainer</span><span style="color: #000000">(</span><span style="color: #795E26">for</span><span style="color: #000000">: [House.self])</span></span>
<span class="line"><span style="color: #000000">        }</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>You don&#8217;t need to list <code>Floor.self</code>, <code>Room.self</code>, etc.  You <em>can</em> &#8211; it doesn&#8217;t hurt anything, and might make your app a little more robust against future refactors &#8211; but it&#8217;s nice that you don&#8217;t have to.</p>



<h2 class="wp-block-heading">Bonus: TablePlus recommendation</h2>



<p>It&#8217;s unfortunately critical, when working with SwiftData (or Core Data), to be able to view &amp; edit the underlying SQLite database.  You can do that out-of-the-box on your Mac using just the built-in <code>sqlite3</code> CLI, but that&#8217;s pretty awkward.</p>



<p>There are <em>many</em> GUI apps for working with SQLite databases.  Quite a few are free.  Most are functional but clumsy.</p>



<p>Years ago &#8211; when I was mostly working with MySQL &#8211; I came across <a href="https://tableplus.com" data-type="link" data-id="https://tableplus.com" data-wpel-link="external" target="_blank" rel="external noopener">TablePlus</a>.  <a href="https://tableplus.com/pricing" data-wpel-link="external" target="_blank" rel="external noopener">It&#8217;s a bit pricey</a> at $90 up front &#8211; plus $50 per subsequent year if you want to keep getting software updates &#8211; but it&#8217;s by far the best database client I&#8217;ve found, on any platform.  It&#8217;s easily the most native-feeling on a Mac, even though it&#8217;s cross-platform (macOS, iOS / iPadOS, Linux, and Windows!), and one of the fastest.  It doesn&#8217;t <em>quite</em> have all the features &#8211; I do sometimes fire up <a href="https://www.mysql.com/products/workbench/" data-type="link" data-id="https://www.mysql.com/products/workbench/" data-wpel-link="external" target="_blank" rel="external noopener">MySQL Workbench</a> to access its query performance visualisation &#8211; but it covers 99% of what I need.</p>



<p>My only notable complaint with it is that it has historically been a tad buggy.  Not dramatically, but consistently.  That said, I only just got back into it as a result of this SwiftData work &#8211; and it&#8217;s worked flawlessly so far &#8211; so it may well have upped its game since I last properly used it a couple of years ago. 🤞</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/swiftdata-pitfalls/feed/</wfw:commentRss>
			<slash:comments>4</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5378</post-id>	</item>
		<item>
		<title>getifaddrs never specifies broadcast addresses</title>
		<link>https://wadetregaskis.com/getifaddrs-never-specifies-broadcast-addresses/</link>
					<comments>https://wadetregaskis.com/getifaddrs-never-specifies-broadcast-addresses/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Thu, 27 Apr 2023 19:25:36 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[getifaddrs]]></category>
		<category><![CDATA[macOS]]></category>
		<category><![CDATA[Undocumented]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=5250</guid>

					<description><![CDATA[Apple &#8220;Feedback&#8221; #12149764. According to man 3 getifaddrs: The ifa_dstaddr field references the destination address on a P2P interface, if one exists, otherwise it contains the broadcast address. In my testing the ifa_dstaddr field is never non-null. I&#8217;m not sure I have any suitably configured P2P interfaces, but I definitely have interfaces with broadcast capabilities&#8230; <a class="read-more-link" href="https://wadetregaskis.com/getifaddrs-never-specifies-broadcast-addresses/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>Apple &#8220;Feedback&#8221; #12149764.</p>



<p>According to <code>man 3 getifaddrs</code>:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>The ifa_dstaddr field references the destination address on a P2P interface, if one exists, otherwise it contains the broadcast address.</p>
</blockquote>



<p>In my testing the <code>ifa_dstaddr</code> field is never non-null.  I&#8217;m not sure I have any suitably configured P2P interfaces, but I definitely have interfaces with broadcast capabilities and <code>getifaddrs</code> reports them as having no broadcast address (even though it correctly notes they support broadcasting, in <code>ifa_flags</code>).</p>



<p>This is on macOS Ventura 13.3.1.  I have no idea if this applies to other versions of macOS, or other OS&#8217;s.</p>



<p>It appears there&#8217;s a workaround, though &#8211; the broadcast address is deterministic based on the IP address &amp; netmask, <a rel="noreferrer noopener external" href="https://en.wikipedia.org/wiki/Broadcast_address#IP_networking" data-type="URL" data-id="https://en.wikipedia.org/wiki/Broadcast_address#IP_networking" target="_blank" data-wpel-link="external">according to Wikipedia</a>, so you don&#8217;t technically need <code>getifaddrs</code> to spell it out for you.  That&#8217;s for AF_INET (IPv4) networks.  IPv6 doesn&#8217;t really have the same notion of a broadcast address anyway, and for other network types I have no idea if the concept applies or if there&#8217;s workarounds for this <code>getifaddr</code> bug.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/getifaddrs-never-specifies-broadcast-addresses/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5250</post-id>	</item>
		<item>
		<title>getifaddrs returns truncated sockaddr_in&#8217;s for AF_INET ifa_netmasks</title>
		<link>https://wadetregaskis.com/getifaddrs-returns-truncated-sockaddr_ins-for-af_inet-ifa_netmasks/</link>
					<comments>https://wadetregaskis.com/getifaddrs-returns-truncated-sockaddr_ins-for-af_inet-ifa_netmasks/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Thu, 27 Apr 2023 18:57:02 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[getifaddrs]]></category>
		<category><![CDATA[macOS]]></category>
		<category><![CDATA[Undocumented]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=5247</guid>

					<description><![CDATA[Apple &#8220;Feedback&#8221; #12149675. Some netmasks returned by getifaddrs have family of AF_INET yet a length less than sizeof(sockaddr_in), e.g. 5, 6, 7, or 8. On macOS Ventura 13.3.1, at least. It looks like it&#8217;s actually allocating only eight bytes for the ifa_netmask (not the 16 that is the size of sockaddr_in per MacOSX13.3.sdk/usr/include/netinet/in.h), as it&#8230; <a class="read-more-link" href="https://wadetregaskis.com/getifaddrs-returns-truncated-sockaddr_ins-for-af_inet-ifa_netmasks/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>Apple &#8220;Feedback&#8221; #12149675.</p>



<p>Some netmasks returned by <code>getifaddrs</code> have family of <code>AF_INET</code> yet a length less than <code>sizeof(sockaddr_in)</code>, e.g. 5, 6, 7, or 8.  On macOS Ventura 13.3.1, at least.</p>



<p>It looks like it&#8217;s actually allocating only eight bytes for the <code>ifa_netmask</code> (not the 16 that is the size of <code>sockaddr_in</code> per <code>MacOSX13.3.sdk/usr/include/netinet/in.h</code>), as it clearly has a different <code>sockaddr_in</code> immediately after it, e.g.:</p>



<pre class="wp-block-preformatted">sockaddr
  - sa_len : 5
  - sa_family : 2
  ▿ sa_data : 14 elements
    - .0 : 0
    - .1 : 0
    - .2 : 255
    - .3 : 0
    - .4 : 0
    - .5 : 0
    - .6 : 16 // This is obviously the start of the next sockaddr_in; a correct entry with length of 16.
    - .7 : 2
    - .8 : 0
    - .9 : 0
    - .10 : 127
    - .11 : 0
    - .12 : 0
    - .13 : 1</pre>



<p>It&#8217;s thus clear that it&#8217;s both wrong about the actual length in memory and it is indeed truncating the <code>sockaddr_in</code> structure, although it looks like it&#8217;s actually including the full address (the lo0 netmask in the above example is indeed 255.0.0.0).</p>



<p>Here&#8217;s another example, where the netmask is 255.255.255.0:</p>



<pre class="wp-block-preformatted">▿ sockaddr
  - sa_len : 7
  - sa_family : 2
  ▿ sa_data : 14 elements
    - .0 : 0
    - .1 : 0
    - .2 : 255
    - .3 : 255
    - .4 : 255
    - .5 : 0
    - .6 : 16 // Again, clearly the start of the next sockaddr_in.
    - .7 : 2
    - .8 : 0
    - .9 : 0
    - .10 : 192
    - .11 : 168
    - .12 : 0
    - .13 : 24</pre>



<p>So you can see it&#8217;s:</p>



<ul class="wp-block-list">
<li>Only allocating up to the address of <code>sockaddr_in</code>, not counting the padding (so <code>sizeof(sockaddr_in)</code>, <code>MemoryLayout&lt;sockaddr_in>.size</code>, etc disagree with what <code>getifaddrs</code> is doing), and</li>



<li>Weirdly truncating the stated length (<code>sin_len</code>) to the end of the non-zero bytes in the netmask.</li>
</ul>



<p>This is undocumented (well, all these data structures are completely undocumented 😔) and is all very messy.  <code>getifaddrs</code> should just provide the full <code>sockaddr_in</code> without any of this weirdness, so that users which blindly cast to <code>sockaddr_in</code> (without checking <code>sin_len</code>) don&#8217;t risk segmentation faults or reading corrupt data.</p>



<p>I have no idea why it does this <em>only</em> for <code>ifa_netmask</code>, not <code>ifa_addr</code> or <code>ifa_dstaddr</code>.  That discrepancy just makes it even more troublesome.</p>



<p>As far as I can tell there are no similar issues with <code>AF_INET6</code> netmasks, although without knowing under what criteria it&#8217;s truncating, I can&#8217;t be sure I have all relevant examples covered on the happenstance networks of my laptop.</p>



<p>For these systems, there&#8217;s seemingly not much you can do… it seems you have to hard-code the assumption that <code>ifa_netmask</code> contains exactly &amp; only the first eight bytes of the <code>sockaddr_in</code>.  You have to completely ignore <code>sin_len</code> (at least if it&#8217;s less than eight).</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/getifaddrs-returns-truncated-sockaddr_ins-for-af_inet-ifa_netmasks/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5247</post-id>	</item>
		<item>
		<title>Apple Watch Ultra is a poor dive computer</title>
		<link>https://wadetregaskis.com/apple-watch-ultra-is-a-poor-dive-computer/</link>
					<comments>https://wadetregaskis.com/apple-watch-ultra-is-a-poor-dive-computer/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Tue, 06 Dec 2022 04:59:28 +0000</pubDate>
				<category><![CDATA[Reviews]]></category>
		<category><![CDATA[Apple Watch Ultra]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[Oceanic+]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[scuba diving]]></category>
		<category><![CDATA[Shearwater Peregrine]]></category>
		<category><![CDATA[Tested]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=5191</guid>

					<description><![CDATA[Note: this was written in 2021 (and updated in 2022) based on version 1 of the Oceanic+ app. In September 2023 version 2 of that app was released, and it appears to have fixed quite a few limitations (e.g. you can finally export your log book, as a standard UDDF file). Once I&#8217;ve gathered some&#8230; <a class="read-more-link" href="https://wadetregaskis.com/apple-watch-ultra-is-a-poor-dive-computer/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p><strong>Note</strong>:  this was written in 2021 (and updated in 2022) based on version 1 of the Oceanic+ app.  In September 2023 version 2 of that app was released, and it appears to have fixed quite a few limitations (e.g. you can finally export your log book, as a standard UDDF file).  Once I&#8217;ve gathered some real-world dive experience with the updated watch, I may write a new review.</p>
</div></div>



<p>A major reason I purchased an <a href="https://web.archive.org/web/20220907183643/https://www.apple.com/apple-watch-ultra/" data-type="URL" data-id="https://www.apple.com/apple-watch-ultra" data-wpel-link="external" target="_blank" rel="external noopener">Apple Watch Ultra</a> was for its loudly advertised ability to function as a dive computer, much like <a href="https://www.garmin.com/en-US/c/sports-fitness/dive-computers-smartwatches/" data-type="URL" data-id="https://www.garmin.com/en-US/c/sports-fitness/dive-computers-smartwatches/" data-wpel-link="external" target="_blank" rel="external noopener">some Garmins</a>.</p>



<p>It&#8217;s been a rough and disappointing road.</p>



<p>Right out of the gate, it didn&#8217;t work.  It requires a 3rd-party application, <a href="https://apps.apple.com/us/app/scuba-diving-watch-oceanic/id1610517133" type="URL" id="https://apps.apple.com/us/app/oceanic-dive-computer-app/id1610517133" data-wpel-link="external" target="_blank" rel="external noopener">Oceanic+</a>, which didn&#8217;t exist at Apple Watch Ultra release time.  It was over two months before <a href="https://www.apple.com/newsroom/2022/11/reach-new-depths-with-the-oceanic-plus-app-and-apple-watch-ultra/" type="URL" id="https://www.apple.com/newsroom/2022/11/reach-new-depths-with-the-oceanic-plus-app-and-apple-watch-ultra" data-wpel-link="external" target="_blank" rel="external noopener">Oceanic+ was finally released, on November 28th</a>.</p>



<p>As it happens, my dive trip plans were delayed a bit anyway, resulting in my first dive [since purchasing the Apple Watch Ultra] being on that exact day.  Hallelujah, I thought.  I was able to hastily install the app in the morning, before leaving for a remote, internet-less island for a week.</p>



<p>But then there was the surprise that it basically requires an expensive subscription ($80/year, or even more if you commit to less than a whole year).  Without it you don&#8217;t get tissue load and NDL tracking &#8211; critical functions of a dive computer.  I was not aware before purchase, from any of the Apple Watch Ultra advertising or product pages on Apple&#8217;s website, that this subscription would be required.  It feels surprisingly shifty and dishonest from a company like Apple.</p>



<p>Arguably the above frustrations (and costs) could be overlooked if it actually worked well.  Unfortunately, it does not.</p>



<p>I discovered serious flaws with Oceanic+ right from the first dive.  Flaws that any qualified diver would immediately recognise, which begs the question of why Apple &amp; Oceanic+ somehow haven&#8217;t.</p>



<p>There are two major design flaws in its most basic function, the recording of dives:</p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="1000" height="1126" src="https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-EULA.jpg" alt="" class="wp-image-5197" style="width:250px;height:282px" srcset="https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-EULA.jpg 1000w, https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-EULA-909x1024.jpg 909w, https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-EULA-227x256.jpg 227w, https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-EULA-455x512.jpg 455w, https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-EULA-227x256@2x.jpg 454w, https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-EULA-455x512@2x.jpg 910w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /></figure>
</div>


<ol class="wp-block-list">
<li>It doesn&#8217;t start recording automatically.<br><br>You have to hit the &#8220;Action&#8221; button to acknowledge a lawyer-smelling disclaimer that you&#8217;re &#8220;fit to dive&#8221;.  If you forget, it doesn&#8217;t record.<br><br>I&#8217;m not aware of any other dive computer that does this.  e.g. my <a rel="noopener external" href="https://shearwater.com/products/peregrine" type="URL" id="https://www.shearwater.com/products/peregrine" target="_blank" data-wpel-link="external">Shearwater Peregrine</a> autodetects submersion below one metre <em>even if it&#8217;s not on</em>.  It turns itself on and starts recording automatically.</li>



<li>If you ascend above one metre, even just for a split second, it immediately ends recording.<br><br>This makes shore entries, in particular, likely to go unrecorded (unless you&#8217;re able to swim out to deep water and descend rapidly, which isn&#8217;t always an option or the best dive plan).  Descents in significant swell, currents, or surge could also fall victim to this design flaw.<br><br>So on my first dive I got <em>five</em> &#8220;dives&#8221; recorded, representing the four times where the swell floated me up to <em>just</em> above one metre (I never actually broke the surface).  Even just reaching up momentarily with your watch hand, such as to grab a line or brush away a fin, could trigger it to fail.<br><br>Furthermore, there&#8217;s no way to merge these together in Oceanic+ &#8211; you can either keep them, messing up your dive counts and stats, or delete them, throwing away [parts of] actual dive records.<br><br>And on later dives I didn&#8217;t always notice it had failed and stopped recording, so it basically didn&#8217;t record the dive at all.  This will be less of an issue if you&#8217;re using it as your <em>only</em> computer, since you&#8217;ll be looking at it periodically throughout your dive (I was using a separate dive computer as my primary, since I wisely didn&#8217;t trust the Apple Watch Ultra untested).<br><br>Again, I&#8217;m not aware of any other dive computer that has this flaw.  e.g. my Peregrine allows a sixty second grace period before ending the dive (configurable for up to ten minutes).</li>
</ol>



<p>These two combine to make it <strong>unsafe for diving</strong>.  It <em>might</em> be better than nothing, or acceptable as a backup computer (as long as you&#8217;re religious about ensuring it&#8217;s recording all the time), but it&#8217;s arguably worse than no dive computer at all in that it provides a false sense of security &#8211; you might plan many dives in one day, relying on the Apple Watch Ultra to precisely track your tissue loading, but have it fail midway and leave you with a dangerous decision to make.</p>



<p>What makes this all the more frustrating is that there&#8217;s a lot of things to like about it otherwise:</p>



<ul class="wp-block-list">
<li>The Apple Watch Ultra screen is <em>so</em> much better than what you find on most dive watches &#8211; clear and readable, with relatively low glare, even in harsh daylight.  Not to mention that it&#8217;s a touch screen, so [when dry, on the surface] it&#8217;s faster to change your settings, review your dive log, etc.</li>



<li>The Apple Watch Ultra is a lot smaller than most dive computers.  Even those that are nominally intended to dual-purpose as watches.</li>



<li>The information display during diving is well-designed and user-friendly (certainly not as powerful as what you can get on other dive computers, but quite sufficient for basic recreational diving).</li>
</ul>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="300" height="537" src="https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-app-1.webp" alt="" class="wp-image-5198" style="width:300px;height:537px" srcset="https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-app-1.webp 300w, https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-app-1-143x256.webp 143w, https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-app-1-286x512.webp 286w" sizes="auto, (max-width: 300px) 100vw, 300px" /></figure>
</div>


<ul class="wp-block-list">
<li>The iPhone integration is smoother and easier than with other dive computers &#8211; dives appear on your phone automatically (albeit sometimes after a short delay).</li>



<li>It records the geographic location of the dive, which many dive computers do not.</li>



<li>Its configurable alerts &#8211; e.g. for depth, dive duration, etc &#8211; are nice and clear when they trigger underwater, with a clear and prominent visual display and strong vibration.  I find that its alert vibration is much more likely to actually get my attention than that of my Peregrine.</li>
</ul>



<p>It feels like it&#8217;s actually close to being a pretty good dive computer &amp; companion app, if not for a handful of bizarrely obvious, serious flaws.</p>



<p>It feels, in fact, like it was very rushed &#8211; from the obviously daft design flaws noted above, to even just simple things like bad grammar, poor alignment, and broken layout in the GUI (it smells like they&#8217;re using SwiftUI and haven&#8217;t figure out how to work around all its layout problems).  It seems they put more time into <a rel="noopener external" href="https://www.oceanicworldwide.com/oceanic-plus" data-type="URL" data-id="https://www.oceanicworldwide.com/oceanic-plus" target="_blank" data-wpel-link="external">their slick website</a> than their actual product.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="1046" height="968" src="https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-home-screen.webp" alt="" class="wp-image-5199" style="width:523px;height:484px" srcset="https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-home-screen.webp 1046w, https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-home-screen-512x474@2x.webp 1024w, https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-home-screen-256x237.webp 256w, https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-home-screen-512x474.webp 512w" sizes="auto, (max-width: 1046px) 100vw, 1046px" /><figcaption class="wp-element-caption">&#8220;Dives number&#8221;… wot?  And why are the sizes &amp; baselines different for every single number on this screen?</figcaption></figure>
</div>

<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="1084" height="1076" src="https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-dive-profile.jpg" alt="" class="wp-image-5201" style="width:542px;height:538px" srcset="https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-dive-profile.jpg 1084w, https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-dive-profile-512x508@2x.jpg 1024w, https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-dive-profile-256x254.jpg 256w, https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-dive-profile-512x508.jpg 512w" sizes="auto, (max-width: 1084px) 100vw, 1084px" /><figcaption class="wp-element-caption">This can&#8217;t be intentional.  And that colour-coding scheme is quite hostile to colourblind people.</figcaption></figure>
</div>

<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="1095" height="389" src="https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-22Your-Plan22-display.jpg" alt="" class="wp-image-5200" style="width:548px;height:195px" srcset="https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-22Your-Plan22-display.jpg 1095w, https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-22Your-Plan22-display-512x182@2x.jpg 1024w, https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-22Your-Plan22-display-256x91.jpg 256w, https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-22Your-Plan22-display-512x182.jpg 512w" sizes="auto, (max-width: 1095px) 100vw, 1095px" /><figcaption class="wp-element-caption">I guess it could be a stylistic choice to have the &#8220;Edit&#8221; button escape its bounds… but I suspect not.  I&#8217;m also not convinced my life is improved by the omission of &#8220;nths&#8221;.</figcaption></figure>
</div>

<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="1047" height="1304" src="https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-settings-screen.webp" alt="" class="wp-image-5202" style="width:524px;height:652px" srcset="https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-settings-screen.webp 1047w, https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-settings-screen-411x512@2x.webp 822w, https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-settings-screen-206x256.webp 206w, https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-settings-screen-411x512.webp 411w, https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-settings-screen-206x256@2x.webp 412w" sizes="auto, (max-width: 1047px) 100vw, 1047px" /><figcaption class="wp-element-caption">Why is &#8220;Gas&#8221; so tiny and lonely in all that white space?  Why isn&#8217;t &#8220;Scuba&#8221; vertically centred?  What is &#8220;PPO2 Dive&#8221; and do they mean &#8220;PPO<sub>2</sub> Limit&#8221;?</figcaption></figure>
</div>

<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="1179" height="2556" src="https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-sharing-screen.webp" alt="" class="wp-image-5204" style="width:473px;height:1024px" srcset="https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-sharing-screen.webp 1179w, https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-sharing-screen-236x512@2x.webp 472w, https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-sharing-screen-945x2048.webp 945w, https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-sharing-screen-118x256.webp 118w, https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-sharing-screen-236x512.webp 236w, https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-sharing-screen-472x1024@2x.webp 944w" sizes="auto, (max-width: 1179px) 100vw, 1179px" /><figcaption class="wp-element-caption">Good chart.  Really captures the essence of my dive.</figcaption></figure>
</div>


<p>And of course there&#8217;s the other hint that maybe real-world testing was skipped &#8211; the fact that the iOS Oceanic+ app crashes on launch if you don&#8217;t have a good internet connection.  On a boat, far from land?  Or a remote island?  Or in Airplane mode?  Or just in an area with poor internet connectivity?  No app for you.  Crash on launch, every time.  Forget about entering your dive details into the log while you actually remember them.</p>



<p>This remains the case even after six app updates over a month.  Apparently Oceanic+ either don&#8217;t care that their app usually crashes on launch, or are incapable of fixing it.</p>



<p><em>Maybe</em> there&#8217;s hope that in time they&#8217;ll be able to straighten all this out.  But until then, I cannot in good conscience recommend the Apple Watch Ultra for diving.  (it remains a fantastic watch for health-tracking and hiking, though)</p>



<p>For completeness, a list of other miscellaneous flaws and limitations:</p>



<ul class="wp-block-list">
<li>Any time you ascend past six metres, it throws up an alert about a safety stop.  Which keeps buzzing at you forever, until you hit the action button.  It&#8217;s hard to overstate how annoying this is when doing shallow dives.  It is super distracting and may put you in harms way (e.g. if you&#8217;re constantly having to fiddle with the Apple Watch Ultra instead of paying attention to the reef around you).<br><br>I simply can&#8217;t fathom why they feel the need to alert for this at all.  An alert would be warranted for <em>skipping</em> your safety stop, yet it <em>doesn&#8217;t</em> do that.</li>



<li>It vibrates the Apple Watch Ultra frequently without any indication why (nothing changes on the display). Observationally, I suspect it&#8217;s something to do with ascending &#8220;too fast&#8221;, but if so then it&#8217;s way too sensitive to small depth changes &#8211; it vibrates at me when simply ascending less than a metre (even when tens of metres deep, where a metre makes very little difference in pressure).<br><br>Overall, the Apple Watch Ultra is too chatty.  It&#8217;s a classic boy-who-cried wolf problem waiting to happen.</li>



<li>It doesn&#8217;t show CNS, current PPO<sub>2</sub>, [surface] GF, etc. Especially when using enriched air (Nitrox), nearing no-decompression limits, or deep diving, these are important for safety. They are purely software features so it&#8217;s especially odd that they&#8217;re not included.<br><br><a href="https://web.archive.org/web/20240115011033/https://www.shearwater.com/monthly-blog-posts/surface-gf-teric-musings/" target="_blank" rel="noreferrer noopener external" data-wpel-link="external">Surface GF</a> is arguably the most important of these &#8211; it basically tells you how dangerous it is to surface immediately. When everything goes smoothly &#8211; and assuming you&#8217;re planning safe, conservative dives &#8211; you don&#8217;t need to worry much about it. But when things go awry it can be critical in helping you make the right decision under pressure.</li>



<li>There&#8217;s no Oceanic+ Mac app, or even a web version, which strongly discourages actually using Oceanic+ as your full dive log.  Entering all the details of your dive &#8211; gear, notes, etc &#8211; is very tedious on an iPhone.<br><br>Some other dive computer manufacturers do have Mac apps (e.g. Shearwater, albeit one that&#8217;s clearly made in some janky cross-platform framework and looks like something you&#8217;d find in X11 in the 90s), and there are a few viable 3rd party options (e.g. <a href="https://www.mac-dive.com" data-type="URL" data-id="https://www.mac-dive.com" data-wpel-link="external" target="_blank" rel="external noopener">MacDive</a>, <a href="https://subsurface.github.io" data-type="URL" data-id="https://subsurface.github.io" data-wpel-link="external" target="_blank" rel="external noopener">Subsurface</a>).</li>



<li>The map it shows, of your entry &amp; exit points, is useless most of the time, because it just shows as flat blue (for water) with no identifying geography.  There&#8217;s no way to switch it to anything useful, like a satellite view that would actually reveal the reefs, sand bars, atolls, etc.<br><br>This is exacerbated by Apple Maps&#8217; limitations.  Google Maps, for example, often <em>does</em> show atolls &amp; islands at least, and other surface features.  Apple Maps simply lacks actual maps for most of the world&#8217;s oceans.</li>
</ul>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="1030" height="580" src="https://wadetregaskis.com/wp-content/uploads/2022/12/Useless-map.jpeg" alt="" class="wp-image-5221" style="width:515px;height:290px" srcset="https://wadetregaskis.com/wp-content/uploads/2022/12/Useless-map.jpeg 1030w, https://wadetregaskis.com/wp-content/uploads/2022/12/Useless-map-512x288@2x.jpeg 1024w, https://wadetregaskis.com/wp-content/uploads/2022/12/Useless-map-256x144.jpeg 256w, https://wadetregaskis.com/wp-content/uploads/2022/12/Useless-map-512x288.jpeg 512w" sizes="auto, (max-width: 1030px) 100vw, 1030px" /></figure>
</div>


<ul class="wp-block-list">
<li>The iOS Oceanic+ app lets you record what gear you were using, but bizarrely requires you to pick from a pre-defined list <em>and</em> that list is missing gear from major brands (e.g. Aqua Lung, Cressi).  Another reason why it won&#8217;t be your real dive log.</li>



<li>There&#8217;s no way to import or export dive data.  This is both a lock-in concern &#8211; your dive data will be deleted if you end your subscription &#8211; and also a roadblock to using the Oceanic+ app as your real dive log, unless you have never and will never dive without an Apple Watch Ultra.</li>



<li>You can&#8217;t change the activity type &#8211; dive vs snorkelling &#8211; underwater, nor after the fact.  So if you forget to change it before going for a snorkel, you&#8217;ll forever have a bogus &#8220;dive&#8221; in your log (or you can delete the record entirely, but then you lose record of any free diving you do).</li>



<li>There&#8217;s five entries in the main menu in the Oceanic+ watch app, which are arranged as a scrolling carousel… which is just weird and annoying since they&#8217;d fit all on one screen as simple buttons, which would make navigation much faster and easier.</li>



<li>A lot of actions on the Oceanic+ watch app require more steps than seem necessary. e.g. changing numeric values requires not just selecting a different value but also tapping back to the previous screen (which also feels unnatural, like I&#8217;m backing out of the change without applying it). There are menus trees five or more levels deep, whereas it seems like they could be flattened into just two or three levels.</li>
</ul>



<figure class="wp-block-video aligncenter apple-watch-video"><video height="844" style="aspect-ratio: 746 / 844;" width="746" autoplay loop muted src="https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-menu-deep-diving.mp4" playsinline></video></figure>



<ul class="wp-block-list">
<li>Relatedly, you can&#8217;t edit any of your settings on your iPhone, only the watch.  You can <em>view</em> the settings on the iPhone, which just makes it even more baffling why you can&#8217;t edit them there.  Editing them on the watch is great in a pinch &#8211; you might not have your phone with you &#8211; but it&#8217;s a pain compared to on an iPhone.</li>



<li>The Oceanic+ iPhone app &#8220;Home&#8221; screen &#8211; what&#8217;s displayed when you launch the app &#8211; just shows a handful of stats of dubious merit. Minimum temperature over the last four weeks? Who cares. Cumulative total max depth? That clearly has no purpose. It seems like they knew they needed to show the most basic numbers &#8211; total dive count and duration &#8211; and felt compelled to stuff in a bunch more numbers for some reason. It&#8217;s also unclear why they think these are more important than your actual dive logs, or the dive planner.</li>
</ul>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="1179" height="2556" src="https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-stats-screen.webp" alt="" class="wp-image-5203" style="width:473px;height:1024px" srcset="https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-stats-screen.webp 1179w, https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-stats-screen-236x512@2x.webp 472w, https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-stats-screen-945x2048.webp 945w, https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-stats-screen-118x256.webp 118w, https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-stats-screen-236x512.webp 236w, https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-stats-screen-472x1024@2x.webp 944w" sizes="auto, (max-width: 1179px) 100vw, 1179px" /><figcaption class="wp-element-caption">…and why is &#8220;m&#8221; sulking under the 2?</figcaption></figure>
</div>


<ul class="wp-block-list">
<li>There&#8217;s supposedly seven watch complications available once you have Oceanic+ installed, but on my watch only two are available (&#8220;Max altitude&#8221; and &#8220;Oceanic Launcher&#8221;).  This might be because I use a digital time display (the &#8220;Modular&#8221; face), something Apple seems to hate.</li>



<li>The battery life is surprisingly short &#8211; about five hours of dive time.  Given that the Apple Watch Ultra can record workarounds that are 16 hours long (at least &#8211; longer if you use energy-saving features), it&#8217;s a bit of a mystery to me why it chews through the battery so fast while diving.  It&#8217;s not even monitoring your heart rate or other health metrics &#8211; just water pressure &#8211; and the screen brightness tends to be low since you&#8217;re underwater in low light.</li>
</ul>



<p>Here&#8217;s a few things which are more just wishlist items (or: things you can get from <em>some</em> other dive computers, though you usually pay a lot more for those than you do an Apple Watch Ultra):</p>



<ul class="wp-block-list">
<li>Gas usage recording.  Upmarket dive computers support wireless communication with a dongle that attaches to your first stage regulator.  This means you can forgo a whole separate hose and dangly, annoying air gauge, and have a unified view of your dive status.  It also means you get more accurate tracking of tissue loading, and more advanced functionality like gas consumption rates (great for extra safety &#8211; know if you&#8217;re going to run out of air too early &#8211; and for optimising your gas consumption over time).<br><br>Radio protocols like Bluetooth don&#8217;t work underwater &#8211; lower frequencies are required.  So the Apple Watch Ultra would require an additional built-in antenna.  It&#8217;s arguably reasonable to omit this in a watch that&#8217;s not intended solely for diving, given the cost or other trade-offs it might require.<br><br>That said, some air-integrated dive computers use sound instead of electromagnetism, and I suspect it&#8217;s not hard to support the necessary, inaudible frequencies in the Apple Watch Ultra&#8217;s microphone(s).  Maybe this support is already present in hardware, and a transmitter dongle will be released later?</li>



<li>Multi-gas support.  Most dive computers support this, even those that are much cheaper than an Apple Watch Ultra.  This is arguably a more &#8220;serious&#8221; or &#8220;technical&#8221; feature, that most recreational divers won&#8217;t ever need, but it&#8217;s also easy to do &#8211; it&#8217;s purely a software feature.</li>



<li>The ability to enter key dive details on the Apple Watch Ultra (as opposed to an iPhone), such as gas start &amp; end pressures.  It&#8217;s all too easy to forget these in-between the dive itself and when you get back to land and your iPhone.</li>



<li>The dive planner functionality is pretty rudimentary.  e.g. you can&#8217;t do anything like actually enter a dive depth profile (whether as a squiggle with your finger, to give the rough idea, or importing it from a previous dive at the same site).  The GUI is also a bit obtuse, especially in the Oceanic+ iOS app, as rather than showing a simple table or chart of depth vs no-deco times it makes you pick a single depth and gives you a single no-deco time.  For planning you often want to consider multiple depth options and pick the right trade-off against dive (or at least bottom) duration.</li>



<li>It might actually be nice to have some &#8220;social&#8221; functionality.  Not for bragging and other vanity purposes &#8211; I&#8217;m not talking about inane integrations with Facebook or whatever &#8211; but for sharing amongst dive buddies and the like.  I suspect there&#8217;s some neat, innovative possibilities here (e.g. automatically detect physically- &amp; temporally-nearby dive friends, and be able to automagically see their photos &amp; notes on what they saw on what&#8217;s presumably the same dive with you &#8211; maybe even add your own notes on theirs like &#8220;don&#8217;t forget about that Whale Shark eating that Orca!&#8221;).</li>
</ul>



<p>And lastly just some miscellanea:</p>



<ul class="wp-block-list">
<li>Apple / Oceanic say that it won&#8217;t record below forty metres, but it does.  Thankfully &#8211; the last thing you want if you do a deep dive, intentionally or <em>especially</em> unintentionally, is for your dive computer to not record your dive profile correctly.</li>
</ul>



<p>I don&#8217;t regret getting my Apple Watch Ultra &#8211; it&#8217;s proven a worthwhile upgrade even just for its other features like battery life and a relatively large screen &#8211; but I am sad that I can&#8217;t actually rely on it for diving.  And that I had to spend a lot of extra money to get a dive computer &#8211; the Peregrine &#8211; that I <em>can</em> rely on.</p>



<p>For reference, I&#8217;ve completed about fifty dives with the Apple Watch Ultra.</p>



<h2 class="wp-block-heading">Addendum (May 2023)</h2>



<p>I&#8217;ve now had the Apple Watch Ultra accompany me on about 150 dives.  Sadly, despite it being a long six months later, little has changed.  The Oceanic+ app is still awkward and very rudimentary, with the same data and platform lock-in problems.  The Apple Watch component is almost unchanged &#8211; same limitations and GUI frustrations.</p>



<p>Two things which did improve at some point:</p>



<ul class="wp-block-list">
<li>Recording now happens when submerged even if you haven&#8217;t clicked through the lawyer screen.  This is a significant safety improvement.<br><br>However, if you go the entire dive and return to the surface without clicking through the lawyer screen, the recording is discarded.  So there&#8217;s still danger here.  You just get (a lot) more time to realise the watch is being obstinate (and you really should be looking at your Apple Watch Ultra at least <em>once</em> during the whole dive anyway, even if it&#8217;s just your backup, to ensure it&#8217;s working and to check its data against your primary).</li>



<li>The Oceanic+ app seems to have fixed many of the glaring GUI bugs &#8211; e.g. the charts that rendered in the wrong places on the screen (or not at all), inconsistent font sizes and baselines, etc.</li>
</ul>



<p>I haven&#8217;t systematically re-reviewed the Oceanic+ app, so perhaps there&#8217;s been additional fixes or improvements too.  I&#8217;ve barely used it since my initial review, since I can only rely on my primary dive computer (<a href="https://shearwater.com/products/peregrine" data-wpel-link="external" target="_blank" rel="external noopener">Shearwater Peregrine</a>) anyway.  For what it&#8217;s worth, I use <a href="https://mac-dive.com" data-type="URL" data-id="https://mac-dive.com" data-wpel-link="external" target="_blank" rel="external noopener">MacDive</a> on my iPhone &amp; Mac and am reasonably happy with it.</p>



<p>I wish I&#8217;d just bought the <a href="https://shearwater.com/products/teric" data-wpel-link="external" target="_blank" rel="external noopener">Shearwater Teric</a>, though &#8211; the Peregrine was me hedging my bets and hoping that the Apple Watch Ultra would prove sufficient alone.  The Peregrine is good but the Teric is much nicer (and for clear reason by far the most popular dive computer on the six boats I&#8217;ve dived from).</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/apple-watch-ultra-is-a-poor-dive-computer/feed/</wfw:commentRss>
			<slash:comments>3</slash:comments>
		
		<enclosure url="https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-Ultra-menu-deep-diving.mp4" length="425059" type="video/mp4" />

			<media:content url="https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-app.webp" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">5191</post-id>	</item>
		<item>
		<title>Never import by copy into Final Cut Pro</title>
		<link>https://wadetregaskis.com/never-import-by-copy-into-final-cut-pro/</link>
					<comments>https://wadetregaskis.com/never-import-by-copy-into-final-cut-pro/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Sat, 19 Nov 2022 00:04:30 +0000</pubDate>
				<category><![CDATA[Photography]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[Final Cut Pro]]></category>
		<category><![CDATA[GoPro]]></category>
		<category><![CDATA[GPMD]]></category>
		<category><![CDATA[MOV]]></category>
		<category><![CDATA[MP4]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=5187</guid>

					<description><![CDATA[By default, Final Cut Pro prefers to &#8220;copy&#8221; all files on import. Indeed you&#8217;d think this is the only sensible option most of the time, since most of the time you&#8217;re importing from a memory card and you do need to make a local copy somewhere on your computer. However, Final Cut Pro has a&#8230; <a class="read-more-link" href="https://wadetregaskis.com/never-import-by-copy-into-final-cut-pro/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>By default, Final Cut Pro prefers to &#8220;copy&#8221; all files on import.  Indeed you&#8217;d think this is the <em>only</em> sensible option most of the time, since most of the time you&#8217;re importing from a memory card and you <em>do</em> need to make a local copy somewhere on your computer.</p>



<p>However, Final Cut Pro has a design flaw which causes data loss.  You see, Final Cut Pro never merely <em>copies</em> the files.  It extracts their audiovisual contents and puts it into a <em>new</em> file.  You might have noticed this already from the fact that Final Cut Pro&#8217;s copied version of the files is always a MOV container, whereas your inputs are more likely an MP4 container.</p>



<p>This would be annoying enough in itself &#8211; it means you can&#8217;t do simple bitwise comparisons of the files to e.g. ensure the imported copy is <em>actually</em> valid and not corrupt, before you erase the original from your memory card.  But it gets worse.</p>



<p><strong>Final Cut Pro doesn&#8217;t copy all the contents</strong>.  It only copies <em>some</em> types of tracks &#8211; i.e. audio, video, and timestamp tracks.  It does <em>not</em> copy tracks such as <a rel="noreferrer noopener external" href="https://github.com/gopro/gpmf-parser" data-type="URL" data-id="https://github.com/gopro/gpmf-parser" target="_blank" data-wpel-link="external">GoPro&#8217;s metadata track</a> (GPMD).  Final Cut Pro just silently discards those.</p>



<p>Those tracks can contain important information.  GPMD tracks, for example, contain a whole host of telemetry from the GoPro including its location during recording, rotation &amp; movement data, and much more.  Even if you think you don&#8217;t care about things like geotagging, consider this:  that rotation &amp; movement data can be used to enhance image stabilisation.  By losing the data at import, you&#8217;re losing the ability to ever utilise that enhanced image stabilisation.</p>



<p>So never let Final Cut Pro &#8220;copy&#8221; your files &#8211; always copy them yourself first, as needed, and then import them into Final Cut Pro by reference only (the &#8220;Leave files in place&#8221; option).  Thankfully Final Cut Pro doesn&#8217;t mangle the files when you use them that way (underlining the question: why does it force a lossy conversion to MOV to begin with, since it clearly works just fine with MP4 originals).</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/never-import-by-copy-into-final-cut-pro/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5187</post-id>	</item>
		<item>
		<title>Nikon Z9 doesn&#8217;t work with USB power on a flat battery</title>
		<link>https://wadetregaskis.com/nikon-z9-doesnt-work-with-usb-power-on-a-flat-battery/</link>
					<comments>https://wadetregaskis.com/nikon-z9-doesnt-work-with-usb-power-on-a-flat-battery/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Mon, 21 Mar 2022 22:14:40 +0000</pubDate>
				<category><![CDATA[Photography]]></category>
		<category><![CDATA[battery]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[Nikon]]></category>
		<category><![CDATA[USB]]></category>
		<category><![CDATA[Z9]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=5104</guid>

					<description><![CDATA[The Nikon Z9 is supposed to be able to work using USB power. And it appears to &#8211; if you have a battery installed that&#8217;s not flat. Otherwise, it refuses to function &#8211; showing a &#8220;Shutter release disabled. Recharge battery&#8221; message in the viewfinder for a second or two before turning itself off again. I&#8230; <a class="read-more-link" href="https://wadetregaskis.com/nikon-z9-doesnt-work-with-usb-power-on-a-flat-battery/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>The Nikon Z9 is <em>supposed</em> to be able to work using USB power.  And it appears to &#8211; <em>if</em> you have a battery installed that&#8217;s not flat.</p>



<p>Otherwise, it refuses to function &#8211; showing a &#8220;Shutter release disabled. Recharge battery&#8221; message in the viewfinder for a second or two before turning itself off again.</p>



<p>I know of no technical reason why this has to be.  And it&#8217;s contrary to Nikon&#8217;s documentation.</p>



<p>(it also refuses to show this message on the LCD, just like for the 24-70/4 error message about extending the lens, making it challenging to figure out why the camera&#8217;s not functioning correctly)</p>



<p>I don&#8217;t know how much charge the battery needs before it will allow the camera to operate on USB power, but it&#8217;s more than one minute of charging and ≤20% charge (my two current data points).</p>



<p>This is pretty annoying.  If I leave the camera on accidentally, as I did yesterday, then even on a fully charged battery it&#8217;ll be flat within eight hours or so.  So it&#8217;s not actually uncommon to pick it up and realise it&#8217;s dead flat.  And in that situation it&#8217;s a complete paperweight for some unspecified amount of time, until you&#8217;ve managed to trickle enough juice into the battery.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/nikon-z9-doesnt-work-with-usb-power-on-a-flat-battery/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5104</post-id>	</item>
		<item>
		<title>Z9 GPS disables itself</title>
		<link>https://wadetregaskis.com/z9-gps-disables-itself/</link>
					<comments>https://wadetregaskis.com/z9-gps-disables-itself/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Sun, 13 Mar 2022 05:35:32 +0000</pubDate>
				<category><![CDATA[Photography]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[GPS]]></category>
		<category><![CDATA[Nikon]]></category>
		<category><![CDATA[Z9]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=5071</guid>

					<description><![CDATA[I&#8217;ve been out using my Z9 every weekend, but a bit behind on actually going through all the photos. Just now I was reviewing some and was surprised to realise there were no GPS coordinates. In any from that day. Where I was outdoors for hours in a flat grass field. I then discovered that&#8230; <a class="read-more-link" href="https://wadetregaskis.com/z9-gps-disables-itself/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>I&#8217;ve been out using my Z9 every weekend, but a bit behind on actually going through all the photos.</p>



<p>Just now I was reviewing some and was surprised to realise there were no GPS coordinates.  In any from that day.  Where I was outdoors for hours in a flat grass field.</p>



<p>I then discovered that <em>none</em> of my photos from the Z9 have included their geolocation for a couple of weeks.</p>



<p>Lo and behold, in the settings on the Z9 I now find that recording location data is turned off.  <em>I</em> did not turn it off.</p>



<p>I don&#8217;t have much idea how it could have turned itself off.  <s>About the only thing I can think of is that it might relate to configuring or switching between setting banks.  That&#8217;s the only thing I can think of that might coincide timing-wise.</s></p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-flow wp-block-group-is-layout-flow">
<p>Update:  it&#8217;s now happened <em>twice</em>, and the second time definitely had nothing to do with changing <em>any</em> camera settings.  My new hypothesis is that if the battery gets to empty it disables GPS recording, for some reason.</p>
</div></div>



<p>In any case, there&#8217;s no valid reason for it to have disabled itself.  Rather frustrating.</p>



<p>As far as I can tell no other settings have been reset or otherwise modified from what I set them to.</p>



<p>It&#8217;s a long shot, but if anyone else sees this behaviour in their Z9 (or knows why it happens), please do leave a comment.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/z9-gps-disables-itself/feed/</wfw:commentRss>
			<slash:comments>4</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5071</post-id>	</item>
		<item>
		<title>iCloud ‘Optimize Mac Storage’ breaks the Mojave installer</title>
		<link>https://wadetregaskis.com/icloud-optimize-mac-storage-breaks-the-mojave-installer/</link>
					<comments>https://wadetregaskis.com/icloud-optimize-mac-storage-breaks-the-mojave-installer/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Fri, 01 Feb 2019 19:42:00 +0000</pubDate>
				<category><![CDATA[Howto]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[High Sierra]]></category>
		<category><![CDATA[iCloud]]></category>
		<category><![CDATA[macOS]]></category>
		<category><![CDATA[Mojave]]></category>
		<category><![CDATA[NTP]]></category>
		<category><![CDATA[Optimize Mac Storage]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[Snafu]]></category>
		<category><![CDATA[Useless error message]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/icloud-optimize-mac-storage-breaks-the-mojave-installer/</guid>

					<description><![CDATA[Yet another example of a really bizarre macOS bug that&#8217;s pretty inexcusable as a test escape, given it occurs with the default installation settings on a completely clean OS install. In short, the Mojave update installer does not work (on High Sierra at least) if you have &#8216;Optimize Mac Storage&#8217; enabled for iCloud Drive (System&#8230; <a class="read-more-link" href="https://wadetregaskis.com/icloud-optimize-mac-storage-breaks-the-mojave-installer/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[<p>Yet another example of a really bizarre macOS bug that&#8217;s pretty inexcusable as a test escape, given it occurs with the default installation settings on a completely clean OS install.</p>
<p>In short, the Mojave update installer does not work (on High Sierra at least) if you have &#8216;Optimize Mac Storage&#8217; enabled for iCloud Drive (System Preferences &gt; iCloud pane &gt; iCloud Drive Options… button &gt; Documents tab &gt; Optimize Mac Storage checkbox).</p>
<p>Specifically, the installer reports:</p>
<blockquote><p>Installation requires downloading important content. That content can&#8217;t be downloaded at this time. Try again later.</p>
</blockquote>
<p>…and indeed fails to download the actual Mojave update files (the installer app as &#8216;installed&#8217; via the App Store is merely a 22 MB bootstrapping app, that downloads the actual image only after you run it &amp; start the installation).</p>
<p>Even more obnoxiously, if you use the <a href="https://dosdude1.com/mojave/" data-wpel-link="external" target="_blank" rel="external noopener">dosdude1 Mojave Patcher Tool</a> to force-download the entire installer, as soon as it completes the 6.5 GB download and produces the &#8216;Install macOS Mojave&#8217; app in /Applications, the system deletes the downloaded installation files out from under that app, rendering it just as broken as the official App Store version. Infuriating.</p>
<p>Aside: to be clear, turning off &#8216;Optimize Mac Storage&#8217; enabled me to produce &#8211; and <em>keep</em> &#8211; a working installer as downloaded by dosdude1&#8217;s tool. I did not verify that it also fixes the regular installer as downloaded via the App Store.</p>
<p>I also ran into the &#8220;The recovery server could not be contacted&#8221; error message even before all the above, but thankfully that was fixable via the means normally prescribed online &#8211; running &#8220;sudo ntpdate -u time.apple.com&#8221;.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/icloud-optimize-mac-storage-breaks-the-mojave-installer/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4337</post-id>	</item>
		<item>
		<title>Blink XT review</title>
		<link>https://wadetregaskis.com/blink-xt-review/</link>
					<comments>https://wadetregaskis.com/blink-xt-review/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Sun, 28 Oct 2018 00:50:14 +0000</pubDate>
				<category><![CDATA[Reviews]]></category>
		<category><![CDATA[Amazon]]></category>
		<category><![CDATA[Blink XT]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[Tested]]></category>
		<category><![CDATA[video]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=4295</guid>

					<description><![CDATA[Normally I&#8217;d just post a review like this on the merchant&#8217;s website &#8211; in this case Amazon. &#160;Yet perplexingly when I tried to do so, I was given the error message: Sorry, we are unable to accept reviews for this product. This product has limitations on submitting reviews. There can be a number of reasons&#8230; <a class="read-more-link" href="https://wadetregaskis.com/blink-xt-review/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>Normally I&#8217;d just post a review like this on the merchant&#8217;s website &#8211; in this case Amazon. &nbsp;Yet perplexingly when I tried to do so, I was given the error message:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Sorry, we are unable to accept reviews for this product. This product has limitations on submitting reviews. There can be a number of reasons for this, including unusual reviewing activity.</p>
</blockquote>



<p>Hmmm… curious. &nbsp;I tried revising my star rating from 2 to 5 to see if it were so blatantly influenced by that, but it did not make a difference.</p>



<p>Anyway, FWIW here&#8217;s my review:</p>



<p>First up, the Blink XT cameras do not work with normal batteries &#8211; you have to buy quite expensive Lithium batteries. &nbsp;Use of any other types of AAs will result in the camera not triggering reliably, failing to record full videos (or at all), etc. &nbsp;So factor in about $20 extra per camera for a pair of such batteries. &nbsp;Also, the two year quoted battery life appears to be a joke &#8211; I had to replace the first set of batteries after only a month or so.</p>



<p>Second, the video quality is not great. &nbsp;They&#8217;re ostensibly 1080p but it looks both upscaled (probably from 720p) and it appears the video is recorded on the sync dongle, not the camera itself, so it&#8217;s subject to any radio interference issues that might exist, which will result in noticeably degraded video quality &#8211; or recording corrupting or cutting out entirely. &nbsp;Overall the video quality, even in the best case, is like that of a <em>very</em> cheap smartphone (as of 2018), or say a 2010 iPhone.</p>



<p>Third, the only way to remotely control the cameras, and view recorded videos, is via mobile apps. &nbsp;No desktop apps, no website, nothing. &nbsp;So it&#8217;s very tedious to view the recordings, manage them, etc.</p>



<p>Fourth, the mobile app for iOS is not great. &nbsp;It&#8217;s very slow &#8211; Cloud-saved videos are never loaded in advance, only on demand, and can take up to a minute to start playing. &nbsp;It&#8217;s also a bit buggy. &nbsp;e.g. a lot of the time it&#8217;ll fail to do whatever you asked, responding instead with a long delay ended with an error message along the lines of &#8220;the camera is busy&#8221;.</p>



<p>Fifth, wireless range is limited &#8211; I have one camera only about ten metres from both my wireless router &amp; the sync module, through one exterior wall, and the video quality is noticeably degraded sometimes. &nbsp;I tried placing one camera with line of sight about 30 metres away, and it worked (barely) for an hour or two and then never again, until I moved it much closer.</p>



<p>Sixth, motion triggering is inconsistent and lacks important configuration options (like zoning to denote areas to ignore or conversely to focus on). &nbsp;e.g. for one video looking out the front of my place, it unavoidably has the street in view, which means that even on minimal sensitivity, we get a video &amp; notification every single time a car goes by on the street. &nbsp;Yet it still won&#8217;t reliably trigger when a human walks up to the front door, until they&#8217;re right in front of the camera. &nbsp;Yet it&#8217;s nonetheless sometimes triggered by squirrels up to 10 metres away.</p>



<p>So, solidly not recommended. &nbsp;Not the worst thing ever &#8211; the system does function in a very minimal sense, and I&#8217;ve managed to get some utility out of it, but it&#8217;s definitely disappointing &#8211; and many of these errors could surely be easily fixed by better software, firmware, or hardware design (e.g. support for normal batteries).</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/blink-xt-review/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2018/10/Blink-XT.webp" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">4295</post-id>	</item>
		<item>
		<title>Sigma lens compatibility with the Nikon Z7</title>
		<link>https://wadetregaskis.com/sigma-lens-compatibility-with-the-nikon-z7/</link>
					<comments>https://wadetregaskis.com/sigma-lens-compatibility-with-the-nikon-z7/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Sat, 27 Oct 2018 02:27:31 +0000</pubDate>
				<category><![CDATA[Photography]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[Nikon]]></category>
		<category><![CDATA[Sigma]]></category>
		<category><![CDATA[Z7]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=4292</guid>

					<description><![CDATA[Sigma just released an updated Z7 compatibility study, which thankfully is mostly just &#8220;everything works as it should&#8221;, though there four exceptions, and one which is pertinent to me: 50mm F1.4 DG HSM &#124; Art:  When starting to shoot with the subject completely out of focus, the response to the AF operation is intermittent. It&#8230; <a class="read-more-link" href="https://wadetregaskis.com/sigma-lens-compatibility-with-the-nikon-z7/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[<p><a href="https://www.the-digital-picture.com/News/News-Post.aspx?News=29757" data-wpel-link="external" target="_blank" rel="external noopener">Sigma just released an updated Z7 compatibility study</a>, which thankfully is mostly just &#8220;everything works as it should&#8221;, though there four exceptions, and one which is pertinent to me:</p>
<blockquote><p>50mm F1.4 DG HSM | Art:  When starting to shoot with the subject completely out of focus, the response to the AF operation is intermittent. It is necessary to release several times or to turn the focus ring once to release. It is planned to be resolved by a firmware update.</p></blockquote>
<p>I have seen what might be that issue, but I&#8217;ve also seen the same symptoms with the kit 24-70/4, and Nikon&#8217;s own F-mount lenses, so, I&#8217;m curious what the distinction is between the Nikon Z7&#8217;s general autofocus issue of this nature, and this specific issue that&#8217;s supposedly only applicable to the Sigma 50/1.4 Art.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/sigma-lens-compatibility-with-the-nikon-z7/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4292</post-id>	</item>
	</channel>
</rss>
