<?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>Snafu &#8211; Wade Tregaskis</title>
	<atom:link href="https://wadetregaskis.com/tags/snafu/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>Snafu &#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>Recording audio to an iPhone via a Tascam Portacapture X8</title>
		<link>https://wadetregaskis.com/recording-audio-to-an-iphone-via-a-tascam-portacapture-x8/</link>
					<comments>https://wadetregaskis.com/recording-audio-to-an-iphone-via-a-tascam-portacapture-x8/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Thu, 22 Jan 2026 06:04:04 +0000</pubDate>
				<category><![CDATA[Howto]]></category>
		<category><![CDATA[audio recording]]></category>
		<category><![CDATA[iPhone]]></category>
		<category><![CDATA[Snafu]]></category>
		<category><![CDATA[Tascam Portacapture X8]]></category>
		<category><![CDATA[Undocumented]]></category>
		<category><![CDATA[USB audio]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8672</guid>

					<description><![CDATA[It is possible to use the Tascam Portacapture X8 as an ADC, input converter, and mixing board for an iPhone, but there&#8217;s a few things you&#8217;ll need to know. Thankfully, any USB-C or USB-C-to-Lightning cable will do You just connect one end of the USB-C cable to the Portacapture X8 (to its built-in USB port)&#8230; <a class="read-more-link" href="https://wadetregaskis.com/recording-audio-to-an-iphone-via-a-tascam-portacapture-x8/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>It <em>is</em> possible to use the Tascam Portacapture X8 as an ADC, input converter, and mixing board for an iPhone, but there&#8217;s a few things you&#8217;ll need to know.</p>



<h3 class="wp-block-heading">Thankfully, any USB-C or USB-C-to-Lightning cable will do</h3>



<p>You just connect one end of the USB-C cable to the Portacapture X8 (to its built-in USB port) and the other to your iPhone (USB-C or Lightning work, as suits your particular model of iPhone).</p>



<p>The Portacapture X8 can both record audio via USB and output it.  It will automatically output it to any connected device that accepts it.  As far as I can tell it uses the master track mix (there doesn&#8217;t seem to be any way to send multiple tracks simultaneously &#8211; I assume the USB audio protocol only allows a plain stereo transmission).</p>



<h3 class="wp-block-heading">You must use 48kHz sampling</h3>



<p>If you don&#8217;t, you&#8217;ll quickly get an error dialog on the Portacapture X8 saying &#8220;USB FS Mismatch&#8221;.  That&#8217;s its daft way of trying to say that the receiving USB device won&#8217;t accept the sampling frequency it&#8217;s outputting.  Why it can&#8217;t have a coherent error message, we&#8217;ll probably never know.</p>



<p>You can set the sampling rate in the &#8220;General Settings&#8221; app, under &#8220;Rec Settings&#8221; (along with the file format and bit depth / rate, though it doesn&#8217;t seem to matter what those are set to w.r.t. iPhone compatibility &#8211; they affect only recording to the microSD card).</p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img fetchpriority="high" decoding="async" width="6034" height="8044" src="https://wadetregaskis.com/wp-content/uploads/2026/01/tascam-portacapture-x8-error-dialog-usb-fs-mismatch.webp" alt="" class="wp-image-8674" style="width:500px" srcset="https://wadetregaskis.com/wp-content/uploads/2026/01/tascam-portacapture-x8-error-dialog-usb-fs-mismatch.webp 6034w, https://wadetregaskis.com/wp-content/uploads/2026/01/tascam-portacapture-x8-error-dialog-usb-fs-mismatch-192x256.webp 192w, https://wadetregaskis.com/wp-content/uploads/2026/01/tascam-portacapture-x8-error-dialog-usb-fs-mismatch-768x1024.webp 768w, https://wadetregaskis.com/wp-content/uploads/2026/01/tascam-portacapture-x8-error-dialog-usb-fs-mismatch-1536x2048.webp 1536w, https://wadetregaskis.com/wp-content/uploads/2026/01/tascam-portacapture-x8-error-dialog-usb-fs-mismatch-192x256@2x.webp 384w, https://wadetregaskis.com/wp-content/uploads/2026/01/tascam-portacapture-x8-error-dialog-usb-fs-mismatch-1536x2048@2x.webp 3072w" sizes="(max-width: 6034px) 100vw, 6034px" /></figure>
</div>


<h3 class="wp-block-heading">You don&#8217;t have to use the iPhone to provide power</h3>



<p>By default the Portacapture X8 will try to use the iPhone to provide power, rather than its batteries. But it doesn&#8217;t trust USB power sources &#8211; it will ask, via a dialog, &#8220;Is the AC adapter 1.5A or more?&#8221;. On USB-C iPhones you <em>can</em> power the Portacapture X8 over USB &#8211; by selecting &#8220;Yes&#8221; &#8211; although it will drain your iPhone&#8217;s battery.  But whichever option you choose, the Portacapture X8 will then refuse to provide phantom power to your mics. If you&#8217;re not using phantom power then no worries, but if you are you must change the Portacapture&#8217;s settings &#8211; in &#8220;General Settings&#8221; app, under &#8220;Power/Display&#8221;, you must set &#8220;Power Source Select&#8221; to &#8220;Battery&#8221; instead of &#8220;Auto&#8221;. That will essentially turn off the Portacapture&#8217;s desire for power from USB, leaving USB as an audio channel only.  With it powering itself from its batteries, the phantom outputs will work like normal.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img decoding="async" width="6034" height="8044" src="https://wadetregaskis.com/wp-content/uploads/2026/01/tascam-portacapture-x8-error-dialog-is-the-ac-adapter-1-5a-or-more.webp" alt="" class="wp-image-8673" style="width:500px" srcset="https://wadetregaskis.com/wp-content/uploads/2026/01/tascam-portacapture-x8-error-dialog-is-the-ac-adapter-1-5a-or-more.webp 6034w, https://wadetregaskis.com/wp-content/uploads/2026/01/tascam-portacapture-x8-error-dialog-is-the-ac-adapter-1-5a-or-more-192x256.webp 192w, https://wadetregaskis.com/wp-content/uploads/2026/01/tascam-portacapture-x8-error-dialog-is-the-ac-adapter-1-5a-or-more-768x1024.webp 768w, https://wadetregaskis.com/wp-content/uploads/2026/01/tascam-portacapture-x8-error-dialog-is-the-ac-adapter-1-5a-or-more-1536x2048.webp 1536w, https://wadetregaskis.com/wp-content/uploads/2026/01/tascam-portacapture-x8-error-dialog-is-the-ac-adapter-1-5a-or-more-192x256@2x.webp 384w, https://wadetregaskis.com/wp-content/uploads/2026/01/tascam-portacapture-x8-error-dialog-is-the-ac-adapter-1-5a-or-more-1536x2048@2x.webp 3072w" sizes="(max-width: 6034px) 100vw, 6034px" /></figure>
</div>


<h3 class="wp-block-heading">You don&#8217;t have to record on the Portacapture X8</h3>



<p>It passes audio through to the iPhone automatically, even if you&#8217;re not actively recording.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>Note: I vaguely recall having some issues with this <em>not</em> happening if you change some settings… it&#8217;s possible that things like &#8220;Pre Rec&#8221;, &#8220;Auto Rec&#8221;, or &#8220;Rec Pause&#8221; affect this.  I have all those off and it works as I&#8217;ve described.</p>
</div></div>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/recording-audio-to-an-iphone-via-a-tascam-portacapture-x8/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2026/01/tascam-portacapture-x8-error-dialog-usb-fs-mismatch-1536x2048.webp" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">8672</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>Calling Swift Concurrency async code synchronously in Swift</title>
		<link>https://wadetregaskis.com/calling-swift-concurrency-async-code-synchronously-in-swift/</link>
					<comments>https://wadetregaskis.com/calling-swift-concurrency-async-code-synchronously-in-swift/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Wed, 21 Aug 2024 00:09:00 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[Snafu]]></category>
		<category><![CDATA[spherical chicken in a vacuum]]></category>
		<category><![CDATA[Swift]]></category>
		<category><![CDATA[Swift Concurrency]]></category>
		<category><![CDATA[Task]]></category>
		<category><![CDATA[withoutActuallyEscaping]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8351</guid>

					<description><![CDATA[Sometimes you just need to shove a round peg into a square hole. Sometimes that genuinely is the best option (or perhaps more accurately: the least bad option). I find my hand is often forced by APIs I don&#8217;t control (most often Apple&#8217;s APIs). e.g. data source or delegate callbacks that are synchronous and require&#8230; <a class="read-more-link" href="https://wadetregaskis.com/calling-swift-concurrency-async-code-synchronously-in-swift/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>Sometimes you just need to shove a round peg into a square hole.  Sometimes that genuinely is the best option (or perhaps more accurately: the least bad option).</p>



<p>I find my hand is often forced by APIs I don&#8217;t control (most often Apple&#8217;s APIs).  e.g. data source or delegate callbacks that are synchronous<sup data-fn="fca8ab2f-af18-4e50-8bde-9aa3e28a1927" class="fn"><a href="#fca8ab2f-af18-4e50-8bde-9aa3e28a1927" id="fca8ab2f-af18-4e50-8bde-9aa3e28a1927-link">1</a></sup> and require you to return a value, but in order to obtain that value you have to run async code (perhaps because yet again that&#8217;s all you&#8217;re given by 3rd parties, or because that code makes sense to be async and is used happily as such in other places and you don&#8217;t want to have to duplicate it in perpetuity just to have a sync version).</p>



<p>If that asynchronosity is achieved through e.g. <a href="https://developer.apple.com/documentation/DISPATCH" data-wpel-link="external" target="_blank" rel="external noopener">GCD</a> or <a href="https://developer.apple.com/documentation/foundation/nsrunloop" data-wpel-link="external" target="_blank" rel="external noopener">NSRunLoop</a> or <a href="https://developer.apple.com/documentation/foundation/process" data-wpel-link="external" target="_blank" rel="external noopener">NSProcess</a> or <a href="https://developer.apple.com/documentation/foundation/nstask" data-wpel-link="external" target="_blank" rel="external noopener">NSTask</a> or <a href="https://developer.apple.com/documentation/foundation/nsthread" data-wpel-link="external" target="_blank" rel="external noopener">NSThread</a> or <a href="https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/pthread.3.html" data-wpel-link="external" target="_blank" rel="external noopener">pthreads</a>, it&#8217;s easy.  There are numerous ways to synchronously wait on their tasks.  In contrast, <a href="https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/" data-wpel-link="external" target="_blank" rel="external noopener">Swift Concurrency</a> <em>really</em> doesn&#8217;t want you to do this.  The language and standard library take an adamant idealogical position on this &#8211; one which is unfortunately impractical; a <a href="https://en.wikipedia.org/wiki/Spherical_cow" data-wpel-link="external" target="_blank" rel="external noopener">spherical chicken in a vacuum</a><sup data-fn="3f7186da-728e-41f5-b12b-6b7ca7456625" class="fn"><a href="#3f7186da-728e-41f5-b12b-6b7ca7456625" id="3f7186da-728e-41f5-b12b-6b7ca7456625-link">2</a></sup>.</p>



<p>Nonetheless, despite Swift&#8217;s best efforts to prevent me, I believe I&#8217;ve come up with a way to do this.  It appears to work reliably, given fairly extensive testing.  Nonetheless, I do not make any promises.  Use at your own risk.</p>



<p>If you know of a better way, please do let me know (e.g. in the comments below).</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" data-code="import Dispatch

extension Task {
    /// Executes the given async closure synchronously, waiting for it to finish before returning.
    ///
    /// **Warning**: Do not call this from a thread used by Swift Concurrency (e.g. an actor, including global actors like MainActor) if the closure - or anything it calls transitively via `await` - might be bound to that same isolation context.  Doing so may result in deadlock.
    static func sync(_ code: sending () async throws(Failure) -&gt; Success) throws(Failure) -&gt; Success { // 1
        let semaphore = DispatchSemaphore(value: 0)

        nonisolated(unsafe) var result: Result&lt;Success, Failure&gt;? = nil // 2

        withoutActuallyEscaping(code) { // 3
            nonisolated(unsafe) let sendableCode = $0 // 4

            let coreTask = Task&lt;Void, Never&gt;.detached(priority: .userInitiated) { @Sendable () async -&gt; Void in // 5
                do {
                    result = .success(try await sendableCode())
                } catch {
                    result = .failure(error as! Failure)
                }
            }

            Task&lt;Void, Never&gt;.detached(priority: .userInitiated) { // 6
                await coreTask.value
                semaphore.signal()
            }

            semaphore.wait()
        }

        return try result!.get() // 7
    }
}" style="color:#000000;display:none" aria-label="Copy" class="code-block-pro-copy-button"><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">Dispatch</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">extension</span><span style="color: #000000"> </span><span style="color: #267F99">Task</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #008000">/// Executes the given async closure synchronously, waiting for it to finish before returning.</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #008000">///</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #008000">/// **Warning**: Do not call this from a thread used by Swift Concurrency (e.g. an actor, including global actors like MainActor) if the closure - or anything it calls transitively via `await` - might be bound to that same isolation context.  Doing so may result in deadlock.</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">static</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">sync</span><span style="color: #000000">(</span><span style="color: #795E26">_</span><span style="color: #000000"> </span><span style="color: #001080">code</span><span style="color: #000000">: sending () </span><span style="color: #AF00DB">async</span><span style="color: #000000"> </span><span style="color: #AF00DB">throws</span><span style="color: #000000">(Failure) -&gt; Success) </span><span style="color: #AF00DB">throws</span><span style="color: #000000">(Failure) -&gt; Success { </span><span style="color: #008000">// 1</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #0000FF">let</span><span style="color: #000000"> semaphore = </span><span style="color: #795E26">DispatchSemaphore</span><span style="color: #000000">(</span><span style="color: #795E26">value</span><span style="color: #000000">: </span><span style="color: #098658">0</span><span style="color: #000000">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #795E26">nonisolated</span><span style="color: #000000">(unsafe) </span><span style="color: #0000FF">var</span><span style="color: #000000"> result: Result&lt;Success, Failure&gt;? = </span><span style="color: #0000FF">nil</span><span style="color: #000000"> </span><span style="color: #008000">// 2</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #795E26">withoutActuallyEscaping</span><span style="color: #000000">(code) { </span><span style="color: #008000">// 3</span></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #795E26">nonisolated</span><span style="color: #000000">(unsafe) </span><span style="color: #0000FF">let</span><span style="color: #000000"> sendableCode = </span><span style="color: #0000FF">$0</span><span style="color: #000000"> </span><span style="color: #008000">// 4</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #0000FF">let</span><span style="color: #000000"> coreTask = Task&lt;</span><span style="color: #267F99">Void</span><span style="color: #000000">, Never&gt;.</span><span style="color: #795E26">detached</span><span style="color: #000000">(</span><span style="color: #795E26">priority</span><span style="color: #000000">: .</span><span style="color: #001080">userInitiated</span><span style="color: #000000">) { </span><span style="color: #0000FF">@Sendable</span><span style="color: #000000"> () async -&gt; </span><span style="color: #267F99">Void</span><span style="color: #000000"> </span><span style="color: #AF00DB">in</span><span style="color: #000000"> </span><span style="color: #008000">// 5</span></span>
<span class="line"><span style="color: #000000">                </span><span style="color: #AF00DB">do</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">                    result = .</span><span style="color: #795E26">success</span><span style="color: #000000">(</span><span style="color: #AF00DB">try</span><span style="color: #000000"> </span><span style="color: #AF00DB">await</span><span style="color: #000000"> </span><span style="color: #795E26">sendableCode</span><span style="color: #000000">())</span></span>
<span class="line"><span style="color: #000000">                } </span><span style="color: #AF00DB">catch</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">                    result = .</span><span style="color: #795E26">failure</span><span style="color: #000000">(error as! Failure)</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: #000000">            Task&lt;</span><span style="color: #267F99">Void</span><span style="color: #000000">, Never&gt;.</span><span style="color: #795E26">detached</span><span style="color: #000000">(</span><span style="color: #795E26">priority</span><span style="color: #000000">: .</span><span style="color: #001080">userInitiated</span><span style="color: #000000">) { </span><span style="color: #008000">// 6</span></span>
<span class="line"><span style="color: #000000">                </span><span style="color: #AF00DB">await</span><span style="color: #000000"> coreTask.</span><span style="color: #001080">value</span></span>
<span class="line"><span style="color: #000000">                semaphore.</span><span style="color: #795E26">signal</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">            semaphore.</span><span style="color: #795E26">wait</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: #AF00DB">return</span><span style="color: #000000"> </span><span style="color: #AF00DB">try</span><span style="color: #000000"> result!.</span><span style="color: #795E26">get</span><span style="color: #000000">() </span><span style="color: #008000">// 7</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>Elaborating on some of the odder or less than self-explanatory aspects of this:</p>



<ol class="wp-block-list">
<li>The closure parameter <em>must</em> be <code><a href="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0430-transferring-parameters-and-results.md" data-wpel-link="external" target="_blank" rel="external noopener">sending</a></code> otherwise this deadlocks if e.g. you call it from the main thread (even if the closure, and all its transitive async calls, are not isolated to the main thread).  I don&#8217;t understand why this happens &#8211; it&#8217;s <em>possibly</em> explicable and working as intended, but <a href="https://github.com/swiftlang/swift/issues/75866" data-wpel-link="external" target="_blank" rel="external noopener">I wonder if it&#8217;s simply a bug</a>.  Nobody has been able to explain why it happens.<br><br>Note: in the initial version of this post I accidentally omitted this essential keyword.  I apologise for the error, and hope it didn&#8217;t cause grief for anyone.</li>



<li>Since there&#8217;s no sync way to retrieve the result of a <code><a href="https://developer.apple.com/documentation/swift/task" data-wpel-link="external" target="_blank" rel="external noopener">Task</a></code>, the result has to be passed out through a side-channel. The <code><a href="https://www.swift.org/blog/swift-5.10-released/#unsafe-opt-outs" data-wpel-link="external" target="_blank" rel="external noopener">nonisolated(unsafe)</a></code> is to silence the Swift 6 compiler&#8217;s erroneous error diagnostics about concurrent mutation of shared state.</li>



<li><code>Task</code> constructors only accept <a href="https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures/#Escaping-Closures" data-wpel-link="external" target="_blank" rel="external noopener">escaping closures</a>, even though as they&#8217;re used here the closure never <em>actually</em> escapes. Fortunately the <code><a href="https://developer.apple.com/documentation/swift/withoutactuallyescaping(_:do:)" data-wpel-link="external" target="_blank" rel="external noopener">withoutActuallyEscaping</a></code> escape hatch is available.</li>



<li><code>code</code> isn&#8217;t <code><a href="https://developer.apple.com/documentation/swift/sendable#Sendable-Functions-and-Closures" data-wpel-link="external" target="_blank" rel="external noopener">@Sendable</a></code> &#8211; since it doesn&#8217;t <em>actually</em> have to be sent in the sense of executing concurrently &#8211; so trying to use it in the <code>Task</code> closure below, which is <code>@Sendable</code>, results in an erroneous compiler error (&#8220;<code>Capture of 'code' with non-sendable type '() async throws(Failure) -&gt; Success' in a @Sendable closure</code>&#8220;). Assigning to a variable lets us apply <code>nonisolated(unsafe)</code> to disable the incorrect compiler diagnostic.</li>



<li>Several key aspects happen on this line:
<ul class="wp-block-list">
<li>It&#8217;s important to use a detached task, in case we&#8217;re already running in an isolated context (e.g. the MainActor) as we&#8217;re going to block the current thread waiting on the task to finish, via the semaphore.</li>



<li>The task logically needs to be run at the current task&#8217;s priority (or higher) in order to ensure it does actually run (re. priority inversion problems), although I&#8217;m not sure that technically matters here since we&#8217;re blocking in a non-await way anyway. One could use <code>Task.<a href="https://developer.apple.com/documentation/swift/task/currentpriority" data-wpel-link="external" target="_blank" rel="external noopener">currentPriority</a></code> here, but I&#8217;ve chosen to hard-code the highest priority (<code><a href="https://developer.apple.com/documentation/swift/taskpriority/userinitiated" data-wpel-link="external" target="_blank" rel="external noopener">userInitiated</a></code>) because it&#8217;s not great to block (in a non-await manner) on async code; although async code isn&#8217;t <em>necessarily</em> slow, I feel it&#8217;s wise to eliminate task prioritisation as a variable.</li>



<li>This closure must be explicitly marked as <code>@Sendable</code> as by default the compiler mistakenly infers it to be <em>not</em> <code>@Sendable</code>, even though all closure arguments to <code>Task</code> initialisers have to be <code>@Sendable</code>.  The compiler diagnostics in this case are frustratingly obtuse and misleading (although the sad saving grace is that this is a relatively common problem, so once you hit it enough times you start to develop a spidey sense for it).</li>
</ul>
</li>



<li>This otherwise pointless second <code>Task</code> is critical to prevent <code>withoutActuallyEscaping</code> from crashing.<br><br><code>withoutActuallyEscaping</code> basically relies on reference-counting &#8211; it records the retain count of its primary argument going in (<code>code</code> in this case) and compares that to the retain count going out &#8211; if they disagree, it crashes. There&#8217;s no way to disable or directly work around this<sup data-fn="ed8f4dd0-bc9b-4237-bf6f-fe6dad9cc344" class="fn"><a href="#ed8f4dd0-bc9b-4237-bf6f-fe6dad9cc344" id="ed8f4dd0-bc9b-4237-bf6f-fe6dad9cc344-link">3</a></sup>.<br><br>That&#8217;s a problem here because if we just signal the semaphore in the first task, right before exiting the task, we have a race &#8211; maybe the task <em>will</em> actually exit before the signal is acted on (<code>semaphore.<a href="https://developer.apple.com/documentation/dispatch/dispatchsemaphore/2016071-wait" data-wpel-link="external" target="_blank" rel="external noopener">wait</a>()</code> returns and allows execution to exit the <code>withoutActuallyEscaping</code> block), but maybe it won&#8217;t. Since the task is retaining the closure, it <em>must</em> exit before we wake up from the semaphore and exit the <code>withoutActuallyEscaping</code> block, otherwise crash.<br><br>The only way I found to <em>ensure</em> the problematic task has fully exited &#8211; after hours of experimenting, covering numerous methods &#8211; is to wait for it in a <em>second</em> task. Surprisingly, the second task &#8211; despite having a strong reference to the first task &#8211; seemingly doesn&#8217;t prevent the first task from being cleaned up. This makes me suspicious, but despite extensive testing I&#8217;m unable to get <code>withoutActuallyEscaping</code> to crash when using this workaround.</li>



<li>There&#8217;s no practical way to avoid this forced unwrap, even though it&#8217;s impossible for it to fail unless something goes <em>very</em> wrong with Swift&#8217;s built-ins (like <code>withoutActuallyEscaping</code> and <code>Task</code>).<br><br>If you don&#8217;t wish to use typed throws, you could unwrap it more gently and throw an error of your own type if it&#8217;s nil, but it&#8217;s extra work and a loss of type safety for something that realistically cannot happen.</li>
</ol>


<ol class="wp-block-footnotes"><li id="fca8ab2f-af18-4e50-8bde-9aa3e28a1927">Specifically meaning &#8220;not run through Swift Concurrency, as async functions / closures&#8221;.  Lots of APIs will execute the callback on the main thread, which is the most difficult case, but even those that execute on a user-specified GCD queue aren&#8217;t helpful here &#8211; at least, <a href="https://github.com/swiftlang/swift/issues/74626" data-wpel-link="external" target="_blank" rel="external noopener">not until <code>assumeIsolated</code> actually works</a>. <a href="#fca8ab2f-af18-4e50-8bde-9aa3e28a1927-link" aria-label="Jump to footnote reference 1">↩︎</a></li><li id="3f7186da-728e-41f5-b12b-6b7ca7456625">Incidentally, Wikipedia seems to think the canonical version of the joke is about spherical cows, but I&#8217;ve only ever heard it about chickens.  Indeed <a href="https://www.science.org/doi/10.1126/science.182.4119.1296.c" data-wpel-link="external" target="_blank" rel="external noopener">the very first known instance of the joke</a> used chickens, and all the pop-culture uses of it that I could find use chickens (most notably <a href="https://www.youtube.com/watch?v=Id0Ppz4OBKE" data-wpel-link="external" target="_blank" rel="external noopener">the ninth episode of The Big Bang Theory</a>). <a href="#3f7186da-728e-41f5-b12b-6b7ca7456625-link" aria-label="Jump to footnote reference 2">↩︎</a></li><li id="ed8f4dd0-bc9b-4237-bf6f-fe6dad9cc344">There&#8217;s no <code>unsafeWithoutActuallyEscaping</code> that forgoes the runtime checking, nor any environment variables or similar that influence it; the check is <em>always</em> included and cannot be disabled at runtime.  Even when it&#8217;s unnecessary or &#8211; as in this case &#8211; outright erroneous.<br><br>Nor is there a way to replicate <code>withoutActuallyEscaping</code>&#8216;s core functionality of merely adding <code>@escaping</code> to the closure&#8217;s signature (e.g. via <code>unsafeBitCast</code> or similar) because it&#8217;s a special interaction with the compiler&#8217;s escape checker which is evaluated purely at compile-time (whether a closure is escaping or not is not actually encoded into the output binary nor memory representation of closures &#8211; the only time escapingness ever leaks into the binary is when you use <code>withoutActuallyEscaping</code> and the compiler inserts the special runtime assertion). <a href="#ed8f4dd0-bc9b-4237-bf6f-fe6dad9cc344-link" aria-label="Jump to footnote reference 3">↩︎</a></li></ol>]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/calling-swift-concurrency-async-code-synchronously-in-swift/feed/</wfw:commentRss>
			<slash:comments>3</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">8351</post-id>	</item>
		<item>
		<title>Bipolar customer support</title>
		<link>https://wadetregaskis.com/bipolar-customer-support/</link>
					<comments>https://wadetregaskis.com/bipolar-customer-support/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Thu, 18 Jul 2024 22:37:30 +0000</pubDate>
				<category><![CDATA[Photography]]></category>
		<category><![CDATA[Ramblings]]></category>
		<category><![CDATA[Flickr]]></category>
		<category><![CDATA[Lightroom]]></category>
		<category><![CDATA[Photo Upload for Lightroom]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[Snafu]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8297</guid>

					<description><![CDATA[Consider these two examples involving Flickr, that occurred within just one day of each other. Nice gal Amanda I submitted the following feedback, not really expecting much of it. In fact, I fully expected some useless, boilerplate response that completely ignored the point and directed me to the upload requirements page (despite it being the&#8230; <a class="read-more-link" href="https://wadetregaskis.com/bipolar-customer-support/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>Consider these two examples involving Flickr, that occurred within just one day of each other.</p>



<h2 class="wp-block-heading">Nice gal Amanda</h2>



<p>I submitted the following feedback, not really expecting much of it.  In fact, I fully expected some useless, boilerplate response that completely ignored the point and directed me to the upload requirements page (despite it being the very first thing I myself pointed to) or was nothing but mail-merge platitudes.</p>



<p>And in retrospect I could have phrased this more respectfully, too.  So I wasn&#8217;t even going in as a great customer.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>Per&nbsp;<a href="https://www.flickrhelp.com/hc/en-us/articles/4404079649300-Flickr-upload-requirements" data-wpel-link="external" target="_blank" rel="external noopener">https://www.flickrhelp.com/hc/en-us/articles/4404079649300-Flickr-upload-requirements</a>, Flickr still doesn&#8217;t support modern, more efficient formats like HEIF, WebP, and AVIF (for images).</p>



<p>It&#8217;s also unclear what video codecs are actually supported, since they&#8217;re not specified &#8211; merely container formats. &nbsp;e.g. does AV1 work? &nbsp;Or even just HEVC?</p>



<p>Being relegated to JPEG makes my files way larger and therefore uploads take a lot longer, and also precludes use of high-quality images featuring e.g. greater than 8-bit depths and HDR support.</p>



<p>(I realise TIFF is nominally supported and does at least offer 16-bit support, but the article notes that it&#8217;s degraded to JPEG anyway on the server, plus even compressed TIFFs are still huge and particularly slow to upload)</p>



<p>On your end, you&#8217;d save a lot of storage space if you supported and prioritised modern, more efficient formats &#8211; not to mention bandwidth costs (every browser of note supports WebP &amp; AVIF at the very least, so you could even transcode from older formats).</p>
</div></div>



<p>What I got surprised me greatly, in the best way:</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>Hi Wade,<br><br>Thank you for reaching out to us! My name is Amanda and I&#8217;m happy to help you.<br><br>I understand you would like to see more photo formats accepted on Flickr (namely: HEIF/HEIC, WebP, AVIF). I&#8217;ve passed a feature suggestion along to our product development team for review regarding this. Hopefully, being able to offer support for these newer file formats is something we can address in a future site update.<br><br>While I do not have a timeframe for if/when this would be put into effect, they are aware that this is something that members of Flickr would like to see as an option.<br><br>You also mentioned some specific video codecs. As these aren&#8217;t listed in that Help Center article you linked, I&#8217;m double checking with our team to see if these are supported or not. As soon as I hear back from them, I&#8217;ll follow up and let you know here.<br><br>In the meantime, please let me know if you have any other questions or suggestions. I&#8217;ll keep an eye out for your response!</p>



<p>Warmly,</p>



<p>Amanda<br>Flickr Support</p>
</div></div>



<p>Holy shit.  A humane response from a real human.  Better yet, a couple of hours later that was followed by (unprompted by me):</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>Hi Wade,<br><br>Thank you for your patience! I checked with our team and it appears AV1 and HEVC are not currently supported. I have added these along to the other formats you had already asked about in our feedback to the Product Development team.&nbsp;<br><br>If there is anything else I can help with, please do feel free to reach out!</p>



<p>Warmly,</p>



<p>Amanda<br>Flickr Support</p>
</div></div>



<p>Even though these responses made no conclusive promises (regarding support for these file formats), I was thrilled.  Beyond the pleasant tone and clear demonstration that Amanda actually read and comprehended my feedback, just knowing that my feedback <em>actually</em> got to the intended recipients (the dev team) is heartening; it made me feel <em>good</em> about choosing to use Flickr for all these years.</p>



<p>But then…</p>



<h2 class="wp-block-heading">Bad guy Marc</h2>



<p>Coincidentally, while Amanda was getting back to me I ran into a bug in Flickr&#8217;s file upload API.  It was apparent from the logs that Flickr was the cause of the issue, but nonetheless I took the time to first confirm that with the <a href="https://www.newpproducts.com/lightroom-plug-ins/photo-upload/" data-wpel-link="external" target="_blank" rel="external noopener">Photo Upload</a> plug-in&#8217;s author.  Then, I submitted the bug report to Flickr:</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>When trying to upload some (but not all) files, the upload ostensibly fails &#8211; it hangs for a while after the transfer is complete before finally concluding with a 504 error from CloudFront. &nbsp;e.g.:</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary>Log</summary>
<pre class="wp-block-preformatted">HTTP/1.1 504 Gateway Time-out
Content-Type: text/html
Content-Length: 1033
Connection: keep-alive
Server: CloudFront
Date: Thu, 18 Jul 2024 01:42:43 GMT
X-Cache: Error from cloudfront
Via: 1.1 CENSORED.cloudfront.net (CloudFront)
X-Amz-Cf-Pop: CENSORED
X-Amz-Cf-Id: CENSORED

&lt;!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
&lt;HTML>&lt;HEAD>&lt;META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">
&lt;TITLE>ERROR: The request could not be satisfied&lt;/TITLE>
&lt;/HEAD>&lt;BODY>
&lt;H1>504 ERROR&lt;/H1>
&lt;H2>The request could not be satisfied.&lt;/H2>
&lt;HR noshade size="1px">
CloudFront attempted to establish a connection with the origin, but either the attempt failed or the origin closed the connection.
We can't connect to the server for this app or website at this time. There might be too much traffic or a configuration error. Try again later, or contact the app or website owner.
&lt;BR clear="all">
If you provide content to customers through CloudFront, you can find steps to troubleshoot and help prevent this error by reviewing the CloudFront documentation.
&lt;BR clear="all">
&lt;HR noshade size="1px">
&lt;PRE>
Generated by cloudfront (CloudFront)
Request ID: CENSORED
&lt;/PRE>
&lt;ADDRESS>
&lt;/ADDRESS>
&lt;/BODY>&lt;/HTML></pre>
</details>



<p></p>



<p>This happens every time.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary>Log from curl</summary>
<pre class="wp-block-preformatted">* Host up.flickr.com:443 was resolved.<br>* IPv6: (none)<br>* IPv4: 13.227.78.140<br>*   Trying 13.227.78.140:443...<br>* Connected to up.flickr.com (13.227.78.140) port 443<br>* ALPN: curl offers http/1.1<br>* (304) (OUT), TLS handshake, Client hello (1):<br>} [315 bytes data]<br>* (304) (IN), TLS handshake, Server hello (2):<br>{ [122 bytes data]<br>* (304) (IN), TLS handshake, Unknown (8):<br>{ [25 bytes data]<br>* (304) (IN), TLS handshake, Certificate (11):<br>{ [4967 bytes data]<br>* (304) (IN), TLS handshake, CERT verify (15):<br>{ [264 bytes data]<br>* (304) (IN), TLS handshake, Finished (20):<br>{ [36 bytes data]<br>* (304) (OUT), TLS handshake, Finished (20):<br>} [36 bytes data]<br>* SSL connection using TLSv1.3 / AEAD-AES128-GCM-SHA256 / [blank] / UNDEF<br>* ALPN: server accepted http/1.1<br>* Server certificate:<br>*  subject: CN=flickr.com<br>*  start date: Feb  5 00:00:00 2024 GMT<br>*  expire date: Mar  4 23:59:59 2025 GMT<br>*  issuer: C=US; O=Amazon; CN=Amazon RSA 2048 M02<br>*  SSL certificate verify ok.<br>* using HTTP/1.x<br>> POST /services/upload/ HTTP/1.1<br>> Host: up.flickr.com<br>> User-Agent: curl/8.6.0<br>> Accept: */*<br>> Authorization: OAuth oauth_signature="CENSORED",oauth_token="CENSORED",oauth_version="1.0",oauth_nonce="CENSORED",oauth_timestamp="CENSORED",oauth_signature_method="HMAC-SHA1",oauth_consumer_key="CENSORED"<br>> Content-Length: 123113221<br>> Content-Type: multipart/form-data; boundary=------------------------cbLPLbBocYE2oLsFDbSmbX<br>> <br>} [233 bytes data]<br><br>  0  117M    0     0    0  128k      0   453k  0:04:25 --:--:--  0:04:25  452k<br>  5  117M    0     0    5 6848k      0  5670k  0:00:21  0:00:01  0:00:20 5669k<br>  6  117M    0     0    6 7360k      0  3068k  0:00:39  0:00:02  0:00:37 3068k<br>  6  117M    0     0    6 7360k      0  2165k  0:00:55  0:00:03  0:00:52 2165k<br>  6  117M    0     0    6 7488k      0  1780k  0:01:07  0:00:04  0:01:03 1780k<br>  7  117M    0     0    7 9280k      0  1773k  0:01:07  0:00:05  0:01:02 1848k<br>  9  117M    0     0    9 11.5M      0  1906k  0:01:03  0:00:06  0:00:57  997k<br> 12  117M    0     0   12 14.3M      0  2025k  0:00:59  0:00:07  0:00:52 1508k<br> 14  117M    0     0   14 17.4M      0  2175k  0:00:55  0:00:08  0:00:47 2182k<br> 17  117M    0     0   17 20.4M      0  2270k  0:00:52  0:00:09  0:00:43 2682k<br> 19  117M    0     0   19 22.8M      0  2295k  0:00:52  0:00:10  0:00:42 2843k<br> 21  117M    0     0   21 24.9M      0  2274k  0:00:52  0:00:11  0:00:41 2730k<br> 23  117M    0     0   23 27.1M      0  2279k  0:00:52  0:00:12  0:00:40 2648k<br> 25  117M    0     0   25 30.0M      0  2324k  0:00:51  0:00:13  0:00:38 2567k<br> 27  117M    0     0   27 32.6M      0  2342k  0:00:51  0:00:14  0:00:37 2472k<br> 29  117M    0     0   29 34.9M      0  2350k  0:00:51  0:00:15  0:00:36 2462k<br> 32  117M    0     0   32 38.0M      0  2399k  0:00:50  0:00:16  0:00:34 2679k<br> 34  117M    0     0   34 40.5M      0  2403k  0:00:50  0:00:17  0:00:33 2705k<br> 36  117M    0     0   36 42.6M      0  2400k  0:00:50  0:00:18  0:00:32 2601k<br> 38  117M    0     0   38 45.6M      0  2432k  0:00:49  0:00:19  0:00:30 2691k<br> 41  117M    0     0   41 48.5M      0  2456k  0:00:48  0:00:20  0:00:28 2780k<br> 43  117M    0     0   43 51.5M      0  2487k  0:00:48  0:00:21  0:00:27 2772k<br> 46  117M    0     0   46 54.2M      0  2499k  0:00:48  0:00:22  0:00:26 2830k<br> 48  117M    0     0   48 56.5M      0  2489k  0:00:48  0:00:23  0:00:25 2810k<br> 50  117M    0     0   50 59.2M      0  2504k  0:00:48  0:00:24  0:00:24 2782k<br> 52  117M    0     0   52 62.1M      0  2526k  0:00:47  0:00:25  0:00:22 2811k<br> 55  117M    0     0   55 65.1M      0  2547k  0:00:47  0:00:26  0:00:21 2803k<br> 58  117M    0     0   58 68.8M      0  2589k  0:00:46  0:00:27  0:00:19 2990k<br> 61  117M    0     0   61 72.1M      0  2617k  0:00:45  0:00:28  0:00:17 3221k<br> 62  117M    0     0   62 73.8M      0  2556k  0:00:47  0:00:29  0:00:18 2791k<br> 65  117M    0     0   65 77.3M      0  2620k  0:00:45  0:00:30  0:00:15 3091k<br> 67  117M    0     0   67 79.5M      0  2600k  0:00:46  0:00:31  0:00:15 2873k<br> 71  117M    0     0   71 83.3M      0  2651k  0:00:45  0:00:32  0:00:13 2987k<br> 72  117M    0     0   72 85.5M      0  2638k  0:00:45  0:00:33  0:00:12 2753k<br> 73  117M    0     0   73 85.7M      0  2557k  0:00:47  0:00:34  0:00:13 2565k<br> 73  117M    0     0   73 85.8M      0  2494k  0:00:48  0:00:35  0:00:13 1728k<br> 76  117M    0     0   76 89.3M      0  2506k  0:00:47  0:00:36  0:00:11 1934k<br> 76  117M    0     0   76 89.3M      0  2439k  0:00:49  0:00:37  0:00:12 1150k<br> 76  117M    0     0   76 89.3M      0  2375k  0:00:50  0:00:38  0:00:12  726k<br> 76  117M    0     0   76 89.3M      0  2315k  0:00:51  0:00:39  0:00:12  705k<br> 76  117M    0     0   76 89.3M      0  2258k  0:00:53  0:00:40  0:00:13  680k<br> 76  117M    0     0   76 89.3M      0  2203k  0:00:54  0:00:41  0:00:13     0<br> 76  117M    0     0   76 89.3M      0  2151k  0:00:55  0:00:42  0:00:13     0<br> 76  117M    0     0   76 89.3M      0  2102k  0:00:57  0:00:43  0:00:14     0<br> 76  117M    0     0   76 89.3M      0  2054k  0:00:58  0:00:44  0:00:14     0<br> 76  117M    0     0   76 89.3M      0  2009k  0:00:59  0:00:45  0:00:14     0<br> 76  117M    0     0   76 89.3M      0  1966k  0:01:01  0:00:46  0:00:15     0<br> 76  117M    0     0   76 89.3M      0  1924k  0:01:02  0:00:47  0:00:15     0<br> 76  117M    0     0   76 89.3M      0  1885k  0:01:03  0:00:48  0:00:15     0<br> 76  117M    0     0   76 89.3M      0  1846k  0:01:05  0:00:49  0:00:16     0<br> 76  117M    0     0   76 89.3M      0  1810k  0:01:06  0:00:50  0:00:16     0<br> 76  117M    0     0   76 89.3M      0  1775k  0:01:07  0:00:51  0:00:16     0<br> 76  117M    0     0   76 89.3M      0  1741k  0:01:09  0:00:52  0:00:17     0<br> 76  117M    0     0   76 90.3M      0  1739k  0:01:09  0:00:53  0:00:16  231k<br> 77  117M    0     0   77 91.3M      0  1724k  0:01:09  0:00:54  0:00:15  436k<br> 78  117M    0     0   78 92.3M      0  1711k  0:01:10  0:00:55  0:00:15  652k<br> 79  117M    0     0   79 93.3M      0  1700k  0:01:10  0:00:56  0:00:14  886k<br> 80  117M    0     0   80 94.7M      0  1695k  0:01:10  0:00:57  0:00:13 1186k<br> 81  117M    0     0   81 96.0M      0  1689k  0:01:11  0:00:58  0:00:13 1166k<br> 83  117M    0     0   83 97.7M      0  1690k  0:01:11  0:00:59  0:00:12 1319k<br> 84  117M    0     0   84 99.5M      0  1692k  0:01:11  0:01:00  0:00:11 1478k<br> 87  117M    0     0   87  102M      0  1714k  0:01:10  0:01:01  0:00:09 1870k<br> 89  117M    0     0   89  104M      0  1719k  0:01:09  0:01:02  0:00:07 1995k<br> 90  117M    0     0   90  106M      0  1725k  0:01:09  0:01:03  0:00:06 2137k<br> 91  117M    0     0   91  107M      0  1718k  0:01:09  0:01:04  0:00:05 2042k<br> 92  117M    0     0   92  108M      0  1705k  0:01:10  0:01:05  0:00:05 1861k<br> 93  117M    0     0   93  109M      0  1700k  0:01:10  0:01:06  0:00:04 1521k<br> 94  117M    0     0   94  110M      0  1683k  0:01:11  0:01:07  0:00:04 1237k<br> 94  117M    0     0   94  110M      0  1664k  0:01:12  0:01:08  0:00:04  896k<br> 95  117M    0     0   95  111M      0  1651k  0:01:12  0:01:09  0:00:03  803k<br> 95  117M    0     0   95  112M      0  1621k  0:01:14  0:01:10  0:00:04  633k<br> 95  117M    0     0   95  112M      0  1615k  0:01:14  0:01:11  0:00:03  497k<br> 95  117M    0     0   95  112M      0  1595k  0:01:15  0:01:12  0:00:03  419k<br> 96  117M    0     0   96  113M      0  1585k  0:01:15  0:01:13  0:00:02  510k<br> 97  117M    0     0   97  114M      0  1574k  0:01:16  0:01:14  0:00:02  498k<br> 97  117M    0     0   97  114M      0  1541k  0:01:18  0:01:16  0:00:02  462k<br> 97  117M    0     0   97  114M      0  1536k  0:01:18  0:01:16  0:00:02  419k<br> 98  117M    0     0   98  115M      0  1530k  0:01:18  0:01:17  0:00:01  581k<br> 98  117M    0     0   98  116M      0  1520k  0:01:19  0:01:18  0:00:01  564k<br> 99  117M    0     0   99  117M      0  1513k  0:01:19  0:01:19 --:--:--  614k<br>* We are completely uploaded and fine<br>100  117M    0     0  100  117M      0  1495k  0:01:20  0:01:20 --:--:--  695k<br>100  117M    0     0  100  117M      0  1476k  0:01:21  0:01:21 --:--:--  589k<br>100  117M    0     0  100  117M      0  1458k  0:01:22  0:01:22 --:--:--  388k<br>100  117M    0     0  100  117M      0  1441k  0:01:23  0:01:23 --:--:--  252k<br>100  117M    0     0  100  117M      0  1424k  0:01:24  0:01:24 --:--:-- 57489<br>100  117M    0     0  100  117M      0  1407k  0:01:25  0:01:25 --:--:--     0<br>100  117M    0     0  100  117M      0  1391k  0:01:26  0:01:26 --:--:--     0<br>100  117M    0     0  100  117M      0  1375k  0:01:27  0:01:27 --:--:--     0<br>100  117M    0     0  100  117M      0  1359k  0:01:28  0:01:28 --:--:--     0<br>100  117M    0     0  100  117M      0  1344k  0:01:29  0:01:29 --:--:--     0<br>100  117M    0     0  100  117M      0  1329k  0:01:30  0:01:30 --:--:--     0<br>100  117M    0     0  100  117M      0  1314k  0:01:31  0:01:31 --:--:--     0<br>100  117M    0     0  100  117M      0  1300k  0:01:32  0:01:32 --:--:--     0<br>100  117M    0     0  100  117M      0  1286k  0:01:33  0:01:33 --:--:--     0<br>100  117M    0     0  100  117M      0  1273k  0:01:34  0:01:34 --:--:--     0<br>100  117M    0     0  100  117M      0  1259k  0:01:35  0:01:35 --:--:--     0<br>100  117M    0     0  100  117M      0  1246k  0:01:36  0:01:36 --:--:--     0<br>100  117M    0     0  100  117M      0  1233k  0:01:37  0:01:37 --:--:--     0<br>100  117M    0     0  100  117M      0  1221k  0:01:38  0:01:38 --:--:--     0<br>100  117M    0     0  100  117M      0  1209k  0:01:39  0:01:39 --:--:--     0<br>100  117M    0     0  100  117M      0  1196k  0:01:40  0:01:40 --:--:--     0<br>100  117M    0     0  100  117M      0  1185k  0:01:41  0:01:41 --:--:--     0<br>100  117M    0     0  100  117M      0  1173k  0:01:42  0:01:42 --:--:--     0<br>100  117M    0     0  100  117M      0  1162k  0:01:43  0:01:43 --:--:--     0<br>100  117M    0     0  100  117M      0  1151k  0:01:44  0:01:44 --:--:--     0<br>100  117M    0     0  100  117M      0  1140k  0:01:45  0:01:45 --:--:--     0<br>100  117M    0     0  100  117M      0  1129k  0:01:46  0:01:46 --:--:--     0<br>100  117M    0     0  100  117M      0  1118k  0:01:47  0:01:47 --:--:--     0<br>100  117M    0     0  100  117M      0  1108k  0:01:48  0:01:48 --:--:--     0<br>100  117M    0     0  100  117M      0  1098k  0:01:49  0:01:49 --:--:--     0<br>100  117M    0     0  100  117M      0  1088k  0:01:50  0:01:50 --:--:--     0<br>100  117M    0     0  100  117M      0  1078k  0:01:51  0:01:51 --:--:--     0<br>100  117M    0     0  100  117M      0  1069k  0:01:52  0:01:52 --:--:--     0<br>100  117M    0     0  100  117M      0  1059k  0:01:53  0:01:53 --:--:--     0<br>100  117M    0     0  100  117M      0  1050k  0:01:54  0:01:54 --:--:--     0<br>100  117M    0     0  100  117M      0  1041k  0:01:55  0:01:55 --:--:--     0<br>100  117M    0     0  100  117M      0  1032k  0:01:56  0:01:56 --:--:--     0<br>100  117M    0     0  100  117M      0  1023k  0:01:57  0:01:57 --:--:--     0<br>100  117M    0     0  100  117M      0  1014k  0:01:58  0:01:58 --:--:--     0<br>100  117M    0     0  100  117M      0  1006k  0:01:59  0:01:59 --:--:--     0<br>100  117M    0     0  100  117M      0   997k  0:02:00  0:02:00 --:--:--     0<br>100  117M    0     0  100  117M      0   989k  0:02:01  0:02:01 --:--:--     0<br>100  117M    0     0  100  117M      0   981k  0:02:02  0:02:02 --:--:--     0<br>100  117M    0     0  100  117M      0   973k  0:02:03  0:02:03 --:--:--     0<br>100  117M    0     0  100  117M      0   965k  0:02:04  0:02:04 --:--:--     0<br>100  117M    0     0  100  117M      0   958k  0:02:05  0:02:05 --:--:--     0<br>100  117M    0     0  100  117M      0   950k  0:02:06  0:02:06 --:--:--     0<br>100  117M    0     0  100  117M      0   943k  0:02:07  0:02:07 --:--:--     0<br>100  117M    0     0  100  117M      0   935k  0:02:08  0:02:08 --:--:--     0<br>100  117M    0     0  100  117M      0   928k  0:02:09  0:02:09 --:--:--     0<br>100  117M    0     0  100  117M      0   921k  0:02:10  0:02:10 --:--:--     0<br>100  117M    0     0  100  117M      0   914k  0:02:11  0:02:11 --:--:--     0<br>100  117M    0     0  100  117M      0   907k  0:02:12  0:02:12 --:--:--     0<br>100  117M    0     0  100  117M      0   900k  0:02:13  0:02:13 --:--:--     0<br>100  117M    0     0  100  117M      0   893k  0:02:14  0:02:14 --:--:--     0<br>100  117M    0     0  100  117M      0   887k  0:02:15  0:02:15 --:--:--     0<br>100  117M    0     0  100  117M      0   880k  0:02:16  0:02:16 --:--:--     0<br>100  117M    0     0  100  117M      0   874k  0:02:17  0:02:17 --:--:--     0<br>100  117M    0     0  100  117M      0   868k  0:02:18  0:02:18 --:--:--     0<br>100  117M    0     0  100  117M      0   861k  0:02:19  0:02:19 --:--:--     0<br>100  117M    0     0  100  117M      0   855k  0:02:20  0:02:20 --:--:--     0<br>&lt; HTTP/1.1 504 Gateway Time-out<br>&lt; Content-Type: text/html<br>&lt; Content-Length: 1033<br>&lt; Connection: keep-alive<br>&lt; Server: CloudFront<br>&lt; Date: Thu, 18 Jul 2024 01:42:43 GMT<br>&lt; X-Cache: Error from cloudfront<br>&lt; Via: 1.1 CENSORED.cloudfront.net (CloudFront)<br>&lt; X-Amz-Cf-Pop: CENSORED<br>&lt; X-Amz-Cf-Id: CENSORED<br>&lt; <br>{ [1033 bytes data]<br><br>100  117M  100  1033  100  117M      7   855k  0:02:27  0:02:20  0:00:07   253<br>* Connection #0 to host up.flickr.com left intact<br></pre>
</details>



<p></p>



<p>Worse, the image&nbsp;<em>is</em>&nbsp;actually uploaded, and appears in the photostream etc. &nbsp;So uploaders that retry on failure (e.g.&nbsp;<a href="https://www.newpproducts.com/?page_id=3306" target="_blank" rel="noreferrer noopener external" data-wpel-link="external">Photo Upload</a>&nbsp;for Lightroom) produce endless duplicate uploads.</p>



<p>According to the author of Photo Upload, Rob, this is a commonly reported problem with Flickr specifically (Photo Upload supports seventeen other destinations). &nbsp;He said it seems to come and go randomly, presumably due to miscellaneous changes on the server side (whether CloudFront or Flickr).</p>



<p>This is preventing me from uploading&nbsp;<em>any</em>&nbsp;photos to Flickr, since it&#8217;s stuck on the one photo and the rest of my enqueued uploads are behind that.</p>
</div></div>



<p>And the response was:</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>Hi Wade,<br><br>Thanks for reaching out to Flickr Support.<br><br>Unfortunately the product you are experiencing issues with was developed by a third-party &amp; therefore we&#8217;re unable to offer specialized support in this area.&nbsp;<br><br>We realize the frustration this causes , but we&#8217;re are limited to providing support for the Flickr website and official mobile applications.<br><br>In this case, we recommend reaching out with this third-party developer or help team for better assistance.&nbsp;</p>



<p>Appreciatively,</p>



<p>Marc J.</p>
</div></div>



<p>What the fuck happened?  You were doing so well, Flickr.  Here I am going out of my way to diagnose and report &#8211; with all pertinent details available to me &#8211; a bug in your API servers, and I get a robotic, bullshit response that&#8217;s purely about refusing to accept responsibility.  This is the kind of response which makes me think your support staff are paid based solely on how fast they close tickets.</p>



<p>And what the hell is Marc &#8216;appreciating&#8217; here?  Amanda&#8217;s &#8220;Warmly&#8221; valediction actually feels genuine, in context (and is human even out of context).  &#8220;Appreciatively&#8221; feels like corporate innuendo.</p>



<h2 class="wp-block-heading">We humans are weird</h2>



<p>Admittedly I started writing this out of catharsis, but it really got me thinking.</p>



<p>Why should I care that Flickr delivered miserable customer support in this second case?  Shouldn&#8217;t I just quietly move on, like I would if it were from most other companies?  Shouldn&#8217;t I be thrilled it&#8217;s at least not a bug in Lightroom itself, since Adobe&#8217;s customer support is a hundred times worse in every case; among the most aggressively evasive and pre-emptively hostile I&#8217;ve ever encountered?</p>



<p>I think we&#8217;re <em>all</em> pretty conditioned to expect terrible experiences with so-called customer support from large software companies, like Adobe, or Google.  We expect it and tolerate it, against all justice and our own interests.</p>



<p>Instead, it&#8217;s often <em>inconsistency</em> in a single company&#8217;s behaviour that&#8217;s the most infuriating and raises our ire.  This makes no logical nor rational sense &#8211; and is very bad from a game theory perspective, as it encourages companies to be <em>consistently</em> dumb and evil.</p>



<p>It seems akin to how most folks pay little attention to e.g. Facebook doing yet another horrible thing &#8211; the headlines might as well all be &#8220;Facebook acts like Facebook yet again&#8221; &#8211; but if e.g. Apple does something a bit clueless, the world gets up in arms.</p>



<p>I guess it boils down to hope, ironically.  If we see a company &#8211; or a person &#8211; demonstrate that they <em>can</em> do better, then we raise the bar for them.  And are then ripe to be disappointed if they merely behave like most of their peers, subsequently.</p>



<p>Which is stupid, really.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/bipolar-customer-support/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">8297</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>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>SwiftUI drag &#038; drop does not support file promises</title>
		<link>https://wadetregaskis.com/swiftui-drag-drop-does-not-support-file-promises/</link>
					<comments>https://wadetregaskis.com/swiftui-drag-drop-does-not-support-file-promises/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Sun, 04 Feb 2024 19:04:31 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Howto]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[Drag & drop]]></category>
		<category><![CDATA[NSFilePromiseProvider]]></category>
		<category><![CDATA[NSItemProvider]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[Snafu]]></category>
		<category><![CDATA[Swift]]></category>
		<category><![CDATA[SwiftUI]]></category>
		<category><![CDATA[Transferable]]></category>
		<category><![CDATA[Undocumented]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=7646</guid>

					<description><![CDATA[SwiftUI doesn’t offer anything equivalent to NSFilePromiseProvider, i.e. to write data to the drop destination. You have to ditch SwiftUI and use AppKit&#8217;s drag &#38; drop APIs instead. FB13583826. Is that it? I know that&#8217;s not a very helpful in some sense, but I wasted days trying to figure out how to implement this very&#8230; <a class="read-more-link" href="https://wadetregaskis.com/swiftui-drag-drop-does-not-support-file-promises/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>SwiftUI doesn’t offer anything equivalent to <code><a href="https://developer.apple.com/documentation/appkit/nsfilepromiseprovider" data-wpel-link="external" target="_blank" rel="external noopener">NSFilePromiseProvider</a></code>, i.e. to write data to the drop destination.  You have to ditch SwiftUI and use AppKit&#8217;s drag &amp; drop APIs instead.</p>



<p>FB13583826.</p>



<h2 class="wp-block-heading">Is that it?</h2>



<p>I know that&#8217;s not a very helpful in some sense, but I wasted <em>days</em> trying to figure out how to implement this very basic drag and drop functionality, in SwiftUI.  From what I <a href="https://stackoverflow.com/questions/76327255/swiftui-receiving-nsfilepromisereceiver-via-nsitemprovider" data-wpel-link="external" target="_blank" rel="external noopener">found</a> <a href="https://stackoverflow.com/questions/69774792/use-nsitemprovider-in-combination-with-nsfilepromiseprovider" data-wpel-link="external" target="_blank" rel="external noopener">online</a>, I&#8217;m not the only person who&#8217;s struggled with this.  So hopefully this post saves others from long and frustrating searches.</p>



<p>A big part of the difficulty in answering this simple question &#8211; does SwiftUI support this or not &#8211; is that a lot of documentation (first &amp; third party) around SwiftUI&#8217;s APIs uses the word &#8220;promise&#8221; but not in the same way.  They merely mean that the pasteboard data is populated on first access, not that it actually follows the file promise protocol.</p>



<h2 class="wp-block-heading">Best alternative (other than ditching SwiftUI)</h2>



<p>The closest you can get is to write data into some arbitrary location that you have to choose blindly (not having any idea where the actual destination is), and then rely on the receiving app to move or copy the file from there.  It doesn&#8217;t matter, in this regard, whether you use <a href="https://developer.apple.com/documentation/swiftui/drag-and-drop#moving-transferable-items" data-wpel-link="external" target="_blank" rel="external noopener">the <code>Transferable</code>-based APIs</a> or <a href="https://developer.apple.com/documentation/swiftui/drag-and-drop#moving-items-using-item-providers" data-wpel-link="external" target="_blank" rel="external noopener">the <code>NSItemProvider</code>-based APIs</a>.</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">view.</span><span style="color: #001080">onDrag</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"> provider = </span><span style="color: #795E26">NSItemProvider</span><span style="color: #000000">()</span></span>
<span class="line"><span style="color: #000000">    </span></span>
<span class="line"><span style="color: #000000">    provider.</span><span style="color: #795E26">registerDataRepresentation</span><span style="color: #000000">(</span><span style="color: #795E26">for</span><span style="color: #000000">: UTType.</span><span style="color: #001080">fileURL</span><span style="color: #000000">) {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #AF00DB">do</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"> tmpDir = </span><span style="color: #AF00DB">try</span><span style="color: #000000"> FileManager.</span><span style="color: #001080">default</span><span style="color: #000000">.</span><span style="color: #001080">url</span><span style="color: #000000">(</span><span style="color: #795E26">for</span><span style="color: #000000">: .</span><span style="color: #001080">itemReplacementDirectory</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                                     </span><span style="color: #795E26">in</span><span style="color: #000000">: .</span><span style="color: #001080">userDomainMask</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                                     </span><span style="color: #795E26">appropriateFor</span><span style="color: #000000">: URL.</span><span style="color: #001080">temporaryDirectory</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                                     </span><span style="color: #795E26">create</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: #008000">// You&#39;ll need to provide the `suggestedFileName` based on the particulars of your use-case.</span></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #0000FF">let</span><span style="color: #000000"> file = tmpDir.</span><span style="color: #795E26">appending</span><span style="color: #000000">(</span><span style="color: #795E26">component</span><span style="color: #000000">: suggestedFileName)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #AF00DB">try</span><span style="color: #000000"> bytes.</span><span style="color: #795E26">write</span><span style="color: #000000">(</span><span style="color: #795E26">to</span><span style="color: #000000">: file, </span><span style="color: #795E26">options</span><span style="color: #000000">: .</span><span style="color: #001080">withoutOverwriting</span><span style="color: #000000">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #0000FF">$0</span><span style="color: #000000">(file.</span><span style="color: #001080">dataRepresentation</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 style="color: #AF00DB">catch</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #0000FF">$0</span><span style="color: #000000">(</span><span style="color: #0000FF">nil</span><span style="color: #000000">, error)</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: #AF00DB">return</span><span style="color: #000000"> </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: #AF00DB">return</span><span style="color: #000000"> provider</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>This makes it impossible to avoid unnecessary file copies, and requires you to keep the file around permanently, since you have no idea when the receiver is done with it.</p>



<p>e.g. if the Finder is the drop destination, you have no idea what volume the promised file was dropped on, so you don’t know which volume to create it on.  If you guess wrong, the Finder is forced to copy the file.  Not only does this confuse the user (by showing a copy badge on the mouse pointer) but it wastes time (in performing the copy) and the original file is left in place, requiring manual clean-up (which is impossible to do correctly because you have no idea when the receiving application is done with the temporary file &#8211; and the receiver might hold a reference to it permanently).</p>



<h2 class="wp-block-heading">Understanding the SwiftUI drag &amp; drop APIs</h2>



<p>Tangentially, I found most documentation on SwiftUI&#8217;s drag &amp; drop APIs &#8211; <em>especially</em> Apple&#8217;s &#8211; to be very poorly written, which is particularly frustrating in this case because the APIs are not intuitive in the slightest.</p>



<p>And most code / usage examples focus exclusively on trivial, disinteresting cases (e.g. dragging images around within a single window).</p>



<p>Only after I&#8217;d spend days reverse-engineering the APIs to figure out how they <em>actually</em> work and how they&#8217;re supposed to be used, did I stumble upon <a href="https://www.humancode.us/about.html" data-wpel-link="external" target="_blank" rel="external noopener">Dave Rahardja</a>&#8216;s <a href="https://www.humancode.us/2023/07/08/all-about-nsitemprovider.html" data-wpel-link="external" target="_blank" rel="external noopener">All about Item Providers</a>.  It&#8217;s <em>by far</em> the best documentation I&#8217;ve found on drag &amp; drop in SwiftUI.  Note though that Dave also uses the word &#8220;promise&#8221; a lot even though he&#8217;s never actually talking about actual file promises.</p>



<p>Also, I found that the newer and ostensibly better <code><a href="https://developer.apple.com/documentation/coretransferable/transferable" data-wpel-link="external" target="_blank" rel="external noopener">Transferable</a></code>-based API was harder to understand and harder to use than the <code><a href="https://developer.apple.com/documentation/foundation/nsitemprovider/" data-wpel-link="external" target="_blank" rel="external noopener">NSItemProvider</a></code>-based APIs.  I recommend going straight to, and sticking with, the latter.  Your code will be simpler and more reliable.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/swiftui-drag-drop-does-not-support-file-promises/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">7646</post-id>	</item>
		<item>
		<title>Mac app sandboxing interferes with drag &#038; drop</title>
		<link>https://wadetregaskis.com/mac-app-sandboxing-interferes-with-drag-drop/</link>
					<comments>https://wadetregaskis.com/mac-app-sandboxing-interferes-with-drag-drop/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Sat, 06 Jan 2024 02:06:10 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[App sandboxing]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[Drag & drop]]></category>
		<category><![CDATA[Insecure]]></category>
		<category><![CDATA[NSEvent]]></category>
		<category><![CDATA[NSPasteboard]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[Snafu]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=7375</guid>

					<description><![CDATA[Failed to get a sandbox extension Right from there, you know you&#8217;re going to have a bad day. 😔 Then you try to actually use the file dropped on your app, and you get: Upload preparation for claim 1C0F9013-4DEB-4E5D-8896-F522AA979BA6 completed with error: Error Domain=NSCocoaErrorDomain Code=513 "“Example.jpg” couldn’t be copied because you don’t have permission to&#8230; <a class="read-more-link" href="https://wadetregaskis.com/mac-app-sandboxing-interferes-with-drag-drop/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<pre class="wp-block-preformatted">Failed to get a sandbox extension</pre>



<p>Right from there, you know you&#8217;re going to have a bad day. 😔</p>



<p>Then you try to actually use the file dropped on your app, and you get:</p>



<pre class="wp-block-preformatted">Upload preparation for claim 1C0F9013-4DEB-4E5D-8896-F522AA979BA6 completed with error: Error Domain=NSCocoaErrorDomain Code=513 "“Example.jpg” couldn’t be copied because you don’t have permission to access “CoordinatedZipFilejxc2lC”." UserInfo={NSSourceFilePathErrorKey=/Users/SadPanda/Pictures/Example.jpg, NSUserStringVariant=(
    Copy
), NSDestinationFilePath=/Users/SadPanda/Library/Containers/com.me.MyApp/Data/tmp/CoordinatedZipFilejxc2lC/Example.jpg, NSFilePath=/Users/SadPanda/Pictures/Example.jpg, NSUnderlyingError=0x600000ad0cf0 {Error Domain=NSPOSIXErrorDomain Code=1 "Operation not permitted"}}</pre>



<p>There&#8217;s a variety of ways to run afoul of this.  Here&#8217;s a particular one, for illustration.</p>



<h2 class="wp-block-heading">Noticing when drag &amp; drop operations <em>start</em></h2>



<p>Say you want to have a drop zone in your app that draws attention to itself whenever the user starts dragging a relevant file.  Merely intelligent, courteous UI.</p>



<p>There&#8217;s no built-in facility for this &#8211; both AppKit and SwiftUI drag &amp; drop APIs only start functioning once the dragged item enters the drop zone itself (the view, or at best window, that you&#8217;ve made a drop receiver).</p>



<p>There <em>is</em> a convention on how to work around this &#8211; you can <em>deduce</em> that a drag is occurring by watching mouse events and the drag pasteboard.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<h3 class="wp-block-heading">Drag &amp; drop implementation detail</h3>



<p>macOS uses <em>pasteboards</em> to share data between applications (represented in AppKit by <a href="https://developer.apple.com/documentation/appkit/nspasteboard" data-wpel-link="external" target="_blank" rel="external noopener">NSPasteboard</a>), not just for copy &amp; paste as you&#8217;re probably already familiar, but for drag &amp; drop as well, among several other things.  You can also create your own custom pasteboards, namespaced to be completely independent of the &#8216;built-in&#8217; ones, for your own purposes.</p>



<h3 class="wp-block-heading">Pasteboard implementation detail</h3>



<p>The standard system pasteboards are <em>always</em> accessible to your application, even when they&#8217;re nominally not relevant (such as when no drag is actually occurring).  In fact the contents of the last drag are left indefinitely on the pasteboard (by default, unless an app explicitly clears it).  Which is an annoyance as it complicates the following…</p>
</div></div>



<h3 class="wp-block-heading">Deducing what&#8217;s going on by spying on global mouse events</h3>



<p>When you see a drag event you don&#8217;t immediately know anything more than that the mouse moved with the left mouse button held down.  That can mean anything &#8211; the user might be pulling out a selection rectangle, or drawing in a graphics application, or just randomly dragging the mouse around.</p>



<p>Since the contents of the drag pasteboard are left there indefinitely after a drag concludes, you can&#8217;t just use the existence of something on the drag pasteboard as an indication that a drag is in progress.</p>



<p>However, pasteboards have a &#8220;change count&#8221;.  What this is actually counting is ill-defined and seemingly not about the actual <em>contents</em> of the pasteboard, but in a nutshell it can be used to mean essentially that &#8211; the count might change even if the contents don&#8217;t change, but it seems it will <em>always</em> change if the contents do change.  So, some false positives, but no false negatives, which is the important thing.</p>



<p>So, when you see a mouse drag event, you can look at the drag pasteboard&#8217;s change count and see if it changed since the last mouse drag event.  If it did, that&#8217;s a pretty strong suggestion &#8211; although admittedly not a guarantee &#8211; that the user is performing a drag &amp; drop operation.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>There is a race condition here.  Mouse events are handled by the WindowServer which runs at <em>very</em> high priority and completely asynchronous to app activity like loading up the pasteboard with dragged items &#8211; and it does actually take some time for an app like the Finder to populate the pasteboard when the drag is initiated.  Only tens of milliseconds, typically, but that&#8217;s an eon in computer terms.</p>



<p>So you might observe the key events &#8211; the pasteboard changing and the first mouse down of a drag &#8211; in any order.</p>



<p>Fortunately, it&#8217;s rare to lose the race in a way that matters, because you&#8217;ll get a mouse dragged event virtually every time the mouse moves, even if just by a single pixel.  So even if you get the first mouse dragged event before the source app has actually populated the pasteboard &#8211; which is in fact common, in my experience &#8211; you&#8217;ll invariably get a bunch more practically immediately as the user continues the drag.  Sooner or later the pasteboard will be updated, so you&#8217;ll eventually notice.  Technically you&#8217;re late in recognising that a drag &amp; drop operation has started, but in general the delay is imperceptible.</p>
</div></div>



<p>The essential code 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"> dragPasteboard = </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 style="color: #0000FF">var</span><span style="color: #000000"> lastDragPasteboardChangeCount: </span><span style="color: #267F99">Int</span><span style="color: #000000"> = dragPasteboard.</span><span style="color: #001080">changeCount</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> mouseDragWatcher = NSEvent.</span><span style="color: #795E26">addGlobalMonitorForEvents</span><span style="color: #000000">(</span><span style="color: #795E26">matching</span><span style="color: #000000">: [.</span><span style="color: #001080">leftMouseDragged</span><span style="color: #000000">],</span></span>
<span class="line"><span style="color: #000000">                                                         </span><span style="color: #795E26">handler</span><span style="color: #000000">: { event </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"> currentChangeCount = dragPasteboard.</span><span style="color: #001080">changeCount</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #AF00DB">if</span><span style="color: #000000"> lastDragPasteboardChangeCount != currentChangeCount {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #008000">// The user very likely just started dragging something.</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    lastDragPasteboardChangeCount = currentChangeCount</span></span>
<span class="line"><span style="color: #000000">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> mouseUpWatcher = NSEvent.</span><span style="color: #795E26">addGlobalMonitorForEvents</span><span style="color: #000000">(</span><span style="color: #795E26">matching</span><span style="color: #000000">: [.</span><span style="color: #001080">leftMouseUp</span><span style="color: #000000">],</span></span>
<span class="line"><span style="color: #000000">                                                       </span><span style="color: #795E26">handler</span><span style="color: #000000">: { event </span><span style="color: #AF00DB">in</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #008000">// The drag ended.</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>Note that I&#8217;ve omitted various ancillary details, such as avoiding strong retains of <code>self</code> (where applicable), error handling (<code><a href="https://developer.apple.com/documentation/appkit/nsevent/1535472-addglobalmonitorforevents" data-wpel-link="external" target="_blank" rel="external noopener">addGlobalMonitorForEvents</a></code> can return nil), removing the monitors when you&#8217;re done with them, etc.</p>



<p>Also, <code><a href="https://developer.apple.com/documentation/appkit/nsevent" data-wpel-link="external" target="_blank" rel="external noopener">NSEvent</a></code>&#8216;s monitoring implicitly relies on <code><a href="https://developer.apple.com/documentation/foundation/runloop" data-wpel-link="external" target="_blank" rel="external noopener">RunLoop</a></code>, so actually using the above &#8211; and <em>actually</em> getting your handler called &#8211; is predicated on having a suitable runloop going (in my experience it has to be the <em>main</em> runloop, but maybe it&#8217;s possible to use a different runloop in the right <a href="https://developer.apple.com/documentation/foundation/runloop/mode" data-wpel-link="external" target="_blank" rel="external noopener">mode</a> &#8211; presumably <code><a href="https://developer.apple.com/documentation/foundation/runloop/mode/1428765-eventtracking" data-wpel-link="external" target="_blank" rel="external noopener">eventTracking</a></code>).  In a typical GUI application that&#8217;s all set up for you automagically, but in other cases you have to do it yourself &#8211; refer to <a href="https://stackoverflow.com/questions/25496336/addglobalmonitorforeventsmatchingmask-not-working" data-wpel-link="external" target="_blank" rel="external noopener">this</a> for more details and suggestions.</p>
</div></div>



<p>And the above code works fine as presented.  The problem arises if &amp; when you start actually looking at the pasteboard during the drag…</p>



<h2 class="wp-block-heading">You get only one look at the dragged files</h2>



<p>Files, when dragged, are represented mainly as their paths (technically, <code><a href="https://developer.apple.com/documentation/foundation/nsurl" data-wpel-link="external" target="_blank" rel="external noopener">NSURL</a></code>s).  That&#8217;s the &#8220;public.file-url&#8221; UTI (and the <a href="https://developer.apple.com/documentation/appkit/nspasteboard/pasteboardtype/2919747-fileurl" data-wpel-link="external" target="_blank" rel="external noopener">Apple URL pasteboard type</a> for backwards compatibility).  Though you&#8217;ll also see a bunch of other types reported if you ask the pasteboard what it&#8217;s carrying, e.g.:</p>



<pre class="wp-block-preformatted">Apple URL pasteboard type
CorePasteboardFlavorType 0x6675726C
NSFilenamesPboardType
com.apple.finder.node
dyn.ah62d4rv4gu8y6y4grf0gn5xbrzw1gydcr7u1e3cytf2gn
dyn.ah62d4rv4gu8yc6durvwwaznwmuuha2pxsvw0e55bsmwca7d3sbwu
public.file-url</pre>



<p>Merely inspecting the UTIs in the pasteboard is fine &#8211; that doesn&#8217;t interfere with anything.  So if all you care about is if <em>any</em> kind of file (or folder) is being dragged, you&#8217;re set.  But if you want to only react to <em>some</em> types of files or folders, you need to know more.</p>



<p>If you ask for the URL &#8211; even without actually <em>using</em> it &#8211; you trigger some behind the scenes activity involving app sandboxing.  This prevents the file being made accessible to your app if &amp; when it actually is dropped into your app.</p>



<p><em>When things are working correctly</em>, when a file is dragged and dropped onto a receptive view in your app a link to that file is created inside your own app&#8217;s container.  It&#8217;s that <em>link</em> that you actually have access to &#8211; the original file cannot be accessed directly.  That link seems to persist for a while &#8211; perhaps until your app is quit &#8211; so once you have it you&#8217;re set.</p>



<p>I don&#8217;t know why merely peeking at the file path (URL) prevents this link being created, but it does.</p>



<p>Sigh.</p>



<p>FB13520048.</p>



<h3 class="wp-block-heading">Failed workarounds</h3>



<p>Sadly the <em>only</em> meaningful existing documentation of this problem that Bing or Google can find is <a href="https://stackoverflow.com/questions/67295419/got-an-error-when-dragging-files-using-nsevent-macos" data-wpel-link="external" target="_blank" rel="external noopener">this one StackOverflow question</a>.  Which is unresolved &#8211; the claimed hacks &amp; workarounds don&#8217;t actually work, at least not anymore.</p>



<p><a href="https://stackoverflow.com/questions/67295419/got-an-error-when-dragging-files-using-nsevent-macos/67295974#67295974" data-wpel-link="external" target="_blank" rel="external noopener">Supposedly</a> on some older versions of macOS you could <a href="https://developer.apple.com/documentation/foundation/nsurl/1417795-bookmarkdata" data-wpel-link="external" target="_blank" rel="external noopener">create a bookmark of the file</a> (presumably a <a href="https://developer.apple.com/documentation/foundation/nsurl/bookmarkcreationoptions/1413824-withsecurityscope" data-wpel-link="external" target="_blank" rel="external noopener">security-scoped</a> one) and that would implicitly secure your access to it.  But that seems pretty clearly to no longer work &#8211; assuming it ever did &#8211; because you can&#8217;t create a bookmark for files you can&#8217;t read, and until the drop occurs you don&#8217;t have read access to the file.</p>



<p>Similarly, you can <em>supposedly</em> use <code><a href="https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso" data-wpel-link="external" target="_blank" rel="external noopener">startAccessingSecurityScopedResource</a></code> to indefinitely lock in your access to the file, but this doesn&#8217;t apply if you don&#8217;t have access to it to begin with.  So, again, not useful here.</p>



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



<p>While I knew that using <code>canReadObject(forClasses: [NSImage.self])</code> ran afoul of this, I happened to notice there&#8217;s a logically equivalent method on <code>NSImage</code> itself &#8211; <code><a href="https://developer.apple.com/documentation/appkit/nsimage/1520039-caninit" data-type="link" data-id="https://developer.apple.com/documentation/appkit/nsimage/1520039-caninit" data-wpel-link="external" target="_blank" rel="external noopener">NSImage.canInit(with:)</a></code> &#8211; which I tried, and lo and behold it works!  No sandboxing issues.</p>



<p>I disassembled it (thanks <a href="https://www.hopperapp.com" data-wpel-link="external" target="_blank" rel="external noopener">Hopper</a>!) and saw that it&#8217;s basically just doing some UTI pre-checks &#8211; already known to be safe &#8211; to see if there&#8217;s a file URL in the pasteboard, and then it fetches that file URL using <code>readObjects(forClasses: [NSURL.self])</code>, the same method that if you or I call results in this bug surfacing.  The difference is it uses a secret <a href="https://developer.apple.com/documentation/appkit/nspasteboard/readingoptionkey" data-wpel-link="external" target="_blank" rel="external noopener">reading option key</a> &#8211; &#8220;NSPasteboardURLReadingSecurityScopedFileURLsKey&#8221;!  Apparently that&#8217;s the magic sauce which allows you to read the URL <em>without</em> triggering the bug.  You still can&#8217;t actually <em>access</em> the file, but at least you can e.g. examine its file extension (which is essentially what <code>NSImage.canInit(with:)</code> does).</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">if</span><span style="color: #000000"> </span><span style="color: #0000FF">let</span><span style="color: #000000"> URLs = dragPasteboard.</span><span style="color: #795E26">readObjects</span><span style="color: #000000">(</span><span style="color: #795E26">forClasses</span><span style="color: #000000">: [NSURL.self],</span></span>
<span class="line"><span style="color: #000000">                                         </span><span style="color: #795E26">options</span><span style="color: #000000">: [NSPasteboard.</span><span style="color: #795E26">ReadingOptionKey</span><span style="color: #000000">(</span><span style="color: #795E26">rawValue</span><span style="color: #000000">: </span><span style="color: #A31515">&quot;NSPasteboardURLReadingSecurityScopedFileURLsKey&quot;</span><span style="color: #000000">) : kCFBooleanTrue]) {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #AF00DB">for</span><span style="color: #000000"> u </span><span style="color: #AF00DB">in</span><span style="color: #000000"> URLs {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #008000">// Go nuts!</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>Incidentally, I noticed it also calls <code>startAccessingSecurityScopedResource</code> but in my testing it always gets a failure response from it too.  So that apparently is irrelevant.</p>



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



<h2 class="wp-block-heading">Addendum: Michael Tsai&#8217;s Blog</h2>



<p>I was reading through articles in <a href="https://netnewswire.com" data-wpel-link="external" target="_blank" rel="external noopener">NetNewsWire</a>, including a few of the latest from <a href="https://mjtsai.com" data-wpel-link="external" target="_blank" rel="external noopener">Michael Tsai</a>, and for one in particular I thought, &#8220;this sounds <em>really</em> familiar for some reason&#8221;.  Hah, because <a href="https://mjtsai.com/blog/2024/01/10/mac-app-sandboxing-interferes-with-drag-drop/" data-wpel-link="external" target="_blank" rel="external noopener">he was quoting part of this post</a>.</p>



<p>I&#8217;m very flattered to be linked to by Michael &#8211; I have no idea how he found my obscure website to begin with &#8211; as his blog is one of my favourite Apple developer news / discussion feeds.  He does such a great job of collecting valuable links and collating them together on timely and/or valuable topics.  His own commentary &#8211; if he adds any &#8211; is concise and valuable.  If you&#8217;re not following <a href="https://mjtsai.com/blog/" data-wpel-link="external" target="_blank" rel="external noopener">his blog</a>, you should rectify that. 🙂</p>



<p>I also now wish I&#8217;d written this post better. 😝</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/mac-app-sandboxing-interferes-with-drag-drop/feed/</wfw:commentRss>
			<slash:comments>3</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">7375</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>iPhone emergency satellite communication is geolocked</title>
		<link>https://wadetregaskis.com/iphone-emergency-satellite-communication-is-geolocked/</link>
					<comments>https://wadetregaskis.com/iphone-emergency-satellite-communication-is-geolocked/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Fri, 06 Jan 2023 22:14:48 +0000</pubDate>
				<category><![CDATA[News]]></category>
		<category><![CDATA[Travels]]></category>
		<category><![CDATA[iPhone]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[Snafu]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=5226</guid>

					<description><![CDATA[Hardly news at this point, I guess, but just in case you&#8217;ve missed it: you cannot use the iPhone 14 Pro&#8217;s satellite communication functionality outside a very limited number of geographical regions (mainly the U.S. mainland). e.g. I thought I&#8217;d give it a whirl as I was flying out of Houston, out over the Gulf&#8230; <a class="read-more-link" href="https://wadetregaskis.com/iphone-emergency-satellite-communication-is-geolocked/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>Hardly news at this point, I guess, but just in case you&#8217;ve missed it:  you cannot use the iPhone 14 Pro&#8217;s satellite communication functionality outside a very limited number of geographical regions (mainly the U.S. mainland).</p>



<p>e.g. I thought I&#8217;d give it a whirl as I was flying out of Houston, out over the Gulf of Mexico.  Alas all I got was a blunt denial:</p>


<div class="wp-block-image">
<figure class="aligncenter size-large is-resized"><img loading="lazy" decoding="async" src="https://wadetregaskis.com/wp-content/uploads/2023/01/IMG_8117-945x2048.webp" alt="" class="wp-image-5227" width="473" height="1024" srcset="https://wadetregaskis.com/wp-content/uploads/2023/01/IMG_8117-945x2048.webp 945w, https://wadetregaskis.com/wp-content/uploads/2023/01/IMG_8117-118x256.webp 118w, https://wadetregaskis.com/wp-content/uploads/2023/01/IMG_8117-236x512.webp 236w, https://wadetregaskis.com/wp-content/uploads/2023/01/IMG_8117.webp 1179w, https://wadetregaskis.com/wp-content/uploads/2023/01/IMG_8117-236x512@2x.webp 472w" sizes="auto, (max-width: 473px) 100vw, 473px" /></figure>
</div>


<p>So don&#8217;t actually travel to remote places and expect this feature to work. 😔</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/iphone-emergency-satellite-communication-is-geolocked/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2023/01/IMG_8117-945x2048.webp" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">5226</post-id>	</item>
		<item>
		<title>Swift on Raspberry Pi (in 2019)</title>
		<link>https://wadetregaskis.com/swift-on-raspberry-pi/</link>
					<comments>https://wadetregaskis.com/swift-on-raspberry-pi/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Tue, 15 Oct 2019 20:31:24 +0000</pubDate>
				<category><![CDATA[Ramblings]]></category>
		<category><![CDATA[ARM]]></category>
		<category><![CDATA[Raspberry Pi 4]]></category>
		<category><![CDATA[Raspbian]]></category>
		<category><![CDATA[SATA]]></category>
		<category><![CDATA[Snafu]]></category>
		<category><![CDATA[Swift]]></category>
		<category><![CDATA[USB]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=4440</guid>

					<description><![CDATA[After the horrible experience just acquiring, installing, &#38; configuring a basic Raspberry Pi, I was anticipating much effort &#8211; likely ending in failure &#8211; to get Swift working. I was pleasantly surprised. There are multiple ways to do it, apparently. One would think that there&#8217;d be the correct &#38; working packages already available through apt,&#8230; <a class="read-more-link" href="https://wadetregaskis.com/swift-on-raspberry-pi/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>After <a href="https://wadetregaskis.com/the-dumpster-fire-that-is-the-raspberry-pi/" data-wpel-link="internal">the horrible experience just acquiring, installing, &amp; configuring a basic Raspberry Pi</a>, I was anticipating much effort &#8211; likely ending in failure &#8211; to get Swift working.</p>



<p>I was pleasantly surprised.</p>



<p>There are multiple ways to do it, apparently.  One would think that there&#8217;d be the correct &amp; working packages already available through <em>apt</em>, but just as with Docker, one would be wrong.  In the case of Swift, there is <a href="https://swift-arm.com" data-wpel-link="external" target="_blank" rel="external noopener">the Swift-ARM site</a> &amp; group that does provide a package repo with pre-built binaries (and various <a href="https://lickability.com/blog/swift-on-raspberry-pi-e44c79fc32f3" data-wpel-link="external" target="_blank" rel="external noopener">tutorials on using them</a>), but oddly they don&#8217;t provide the current version of Swift.</p>



<p>An alternative is <a href="https://github.com/uraimo/buildSwiftOnARM" data-wpel-link="external" target="_blank" rel="external noopener">the buildSwiftOnARM Github repo</a>.  They merely provide tarballs, which is slightly suboptimal, but very straightforward and they have tarballs for essentially every version &#8211; major and minor &#8211; of Swift to date.  The git history also indicates that they&#8217;re very prompt about building tarballs as new versions are released.</p>



<p>Better yet, they provide a couple of shell scripts to build Swift from scratch from source.  Only a couple of dependencies (e.g. clang) need be pre-installed, which can be done quickly &amp; painlessly via <em>apt</em>.</p>



<p>Installing from source is presumably the least reliable approach, but since I had already resigned myself to a miserable experience, I figured I might as well go all in.</p>



<p>However, it works.  Perfectly.  Sure, it takes some time to pull down the huge source base for Swift and all its core dependencies, and some time to build it (though not that long &#8211; hours, not days, contrary to what I read online).  But the end result was a working toolchain.</p>



<p>It remains to be seen exactly how good or bad Swift development on Linux is, given the absence of the numerous Apple system libraries which are what <em>actually</em> distinguish macOS development above other platforms, but at least getting Swift itself installed is painless.</p>



<h4 class="wp-block-heading">Sidenote:  SATA to USB</h4>



<p>The <em>only</em> real speedbump in the whole process, for me, had nothing actually to do with installing Swift itself, but rather the external storage situation on the Raspberry Pi.</p>



<p>The Raspberry Pi doesn&#8217;t offer SATA directly, unfortunately (let-alone any form of pluggable PCIe).  MicroSD is a low-performance, low-reliability, and high-cost option.  So to attach any significant storage you&#8217;re basically going either through Ethernet (e.g. NAS) or USB.</p>



<p>USB to SATA adaptors are a shitshow.  I&#8217;ve tried at least half a dozen different vendors&#8217; offerings over the years, and <em>every single one</em> has been super buggy.  The one I newly acquired for my Raspberry Pi use proved to be no exception.</p>



<p>Long story short on that, the symptoms were I/Os taking incredibly long times to complete (many seconds each, serialised), and generally unusable performance.  <em>/var/log/messages</em> contained countless pages of:</p>



<pre class="wp-block-preformatted">Oct 13 11:13:45 applepi kernel: [  234.087294] sd 0:0:0:0: [sda] tag#2 uas_eh_abort_handler 0 uas-tag 1 inflight: CMD IN
Oct 13 11:13:45 applepi kernel: [  234.087306] sd 0:0:0:0: [sda] tag#2 CDB: opcode=0x28 28 00 77 3b ce 00 00 00 d0 00
Oct 13 11:13:45 applepi kernel: [  234.126541] scsi host0: uas_eh_device_reset_handler start
Oct 13 11:13:45 applepi kernel: [  234.277450] usb 2-2: reset SuperSpeed Gen 1 USB device number 2 using xhci_hcd
Oct 13 11:13:45 applepi kernel: [  234.312541] scsi host0: uas_eh_device_reset_handler success
Oct 13 11:14:15 applepi kernel: [  264.805760] sd 0:0:0:0: [sda] tag#7 uas_eh_abort_handler 0 uas-tag 2 inflight: CMD IN
Oct 13 11:14:15 applepi kernel: [  264.805778] sd 0:0:0:0: [sda] tag#7 CDB: opcode=0x28 28 00 77 3b d1 b8 00 00 48 00</pre>



<p>Turns out this is an incredibly common problem with USB to SATA adaptor chipsets, that&#8217;s documented as such all over the web.  Finding how to solve it was less trivial, because a lot of the advice given is either outright wrong or at least doesn&#8217;t work on Raspbian.  The solution I found, via <a href="https://forums.raspberrypi.com/viewtopic.php?f=28&amp;t=245931" data-wpel-link="external" target="_blank" rel="external noopener">this random thread</a>, was to simply add:</p>



<pre class="wp-block-preformatted">usb-storage.quirks=152d:1561:u</pre>



<p>…to the end of the existing line in <em>/boot/cmdline.txt</em> (where 152d:1561 is the vendor &amp; device IDs for the particular chipset used in my case).  All other variations on this, involving adding similar magic incantations to files in <em>/etc/modprobe.d</em> etc, simply do not do anything on Raspbian.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/swift-on-raspberry-pi/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2019/10/Swift-on-Raspberry-Pi.webp" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">4440</post-id>	</item>
		<item>
		<title>The dumpster fire that is the Raspberry Pi</title>
		<link>https://wadetregaskis.com/the-dumpster-fire-that-is-the-raspberry-pi/</link>
					<comments>https://wadetregaskis.com/the-dumpster-fire-that-is-the-raspberry-pi/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Sun, 13 Oct 2019 23:19:09 +0000</pubDate>
				<category><![CDATA[Ramblings]]></category>
		<category><![CDATA[Amazon]]></category>
		<category><![CDATA[aufs-kdms]]></category>
		<category><![CDATA[balenaEtcher]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[Docker]]></category>
		<category><![CDATA[HDMI]]></category>
		<category><![CDATA[Homebridge]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Mac Mini]]></category>
		<category><![CDATA[overlayfs]]></category>
		<category><![CDATA[Raspberry Pi 4]]></category>
		<category><![CDATA[Raspbian]]></category>
		<category><![CDATA[raspi-config]]></category>
		<category><![CDATA[Snafu]]></category>
		<category><![CDATA[VNC]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=4431</guid>

					<description><![CDATA[For a couple of little home projects I need an always-on computer. In an ideal world, perhaps, this would be something like a Mac Mini. Powerful [enough], easy to install &#38; maintain, runs anything &#38; everything (including anything Linux through Docker or at worst a straight VM). Unfortunately, Mac Minis are surprisingly expensive &#8211; even&#8230; <a class="read-more-link" href="https://wadetregaskis.com/the-dumpster-fire-that-is-the-raspberry-pi/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="2560" height="1507" src="https://wadetregaskis.com/wp-content/uploads/2019/10/Raspberry-Pi-4-Model-B-Side.webp" alt="" class="wp-image-4438" srcset="https://wadetregaskis.com/wp-content/uploads/2019/10/Raspberry-Pi-4-Model-B-Side.webp 2560w, https://wadetregaskis.com/wp-content/uploads/2019/10/Raspberry-Pi-4-Model-B-Side-512x301@2x.webp 1024w, https://wadetregaskis.com/wp-content/uploads/2019/10/Raspberry-Pi-4-Model-B-Side-2048x1205.webp 2048w, https://wadetregaskis.com/wp-content/uploads/2019/10/Raspberry-Pi-4-Model-B-Side-256x151.webp 256w, https://wadetregaskis.com/wp-content/uploads/2019/10/Raspberry-Pi-4-Model-B-Side-512x301.webp 512w" sizes="auto, (max-width: 2560px) 100vw, 2560px" /><figcaption class="wp-element-caption"><em>The Raspberry Pi 4 (image courtesy of Michael Henzler </em><a href="https://commons.wikimedia.org/wiki/File:Raspberry_Pi_4_Model_B_-_Side.jpg" data-wpel-link="external" target="_blank" rel="external noopener">via Wikimedia Commons</a><em>)</em></figcaption></figure>
</div>


<p>For a couple of little home projects I need an always-on computer.  In an ideal world, perhaps, this would be something like a Mac Mini.  Powerful [enough], easy to install &amp; maintain, runs anything &amp; everything (including anything Linux through Docker or at worst a straight VM).  Unfortunately, Mac Minis are surprisingly expensive &#8211; even <em><a href="https://www.amazon.com/Apple-Mini-MC270LL-Desktop-Renewed/dp/B077JH51B6/ref=sr_1_2?keywords=2010+mac+mini&amp;qid=1571007498&amp;sr=8-2" data-wpel-link="external" target="_blank" rel="external noopener">nine year old</a></em><a href="https://www.amazon.com/Apple-Mini-MC270LL-Desktop-Renewed/dp/B077JH51B6/ref=sr_1_2?keywords=2010+mac+mini&amp;qid=1571007498&amp;sr=8-2" data-wpel-link="external" target="_blank" rel="external noopener"> models are a couple of hundred dollars at a minimum</a>.</p>



<p>So, I decided to instead explore this Raspberry Pi thing.</p>



<p>I very quickly started wishing I hadn&#8217;t.</p>



<p>The whole process thus far has just been a series of absurd errors &amp; frustration.</p>



<h3 class="wp-block-heading">Acquiring a Raspberry Pi</h3>



<p>Step zero, of merely buying a Raspberry Pi, is stupidly difficult.  Virtually all <a href="https://web.archive.org/web/20191120041414/https://www.raspberrypi.org/products/raspberry-pi-4-model-b/?variant=raspberry-pi-4-model-b-4gb" data-wpel-link="external" target="_blank" rel="external noopener">the retailers officially listed on raspberrypi.org</a> did not actually have the Raspberry Pi 4 in stock.  Later I discovered that some of these same retailers, that list no stock on their own websites, are <a href="https://www.amazon.com/gp/product/B07TXKY4Z9/ref=as_li_ss_tl?ie=UTF8&amp;psc=1&amp;linkCode=ll1&amp;tag=wasbl08-20&amp;linkId=976676888ba6d03800f8f4eaabb47166&amp;language=en_US" data-wpel-link="external" target="_blank" rel="external noopener">actively selling the Pi on Amazon</a>.  So I bought one through there, which is fine, but why doesn&#8217;t raspberrypi.org just <em>say</em> to use Amazon, if that&#8217;s really the only way to get them?</p>



<p>Next up was all the peripherals &#8211; the Pi by default doesn&#8217;t even come with a power supply, so it&#8217;s useless out of the box.  A cursory internet search reveals a huge amount of FUD about power supplies for the Pi.  I have no idea if it&#8217;s accurate or not, but given some relevant, <a href="https://www.scorpia.co.uk/2019/06/28/pi4-not-working-with-some-chargers-or-why-you-need-two-cc-resistors/" data-wpel-link="external" target="_blank" rel="external noopener">egregious design flaws in the Raspberry Pi 4</a>, it seems plausible.</p>



<p>Plus you need at a minimum some stand-offs, if not a full case, to prevent the Pi damaging the surface it&#8217;s placed on, or damaging itself through shorts.</p>



<p>And addressing those bootstrapping problems ended up sending me down a rabbit hole trying to find a cooling solution too, since it turns out <a href="https://www.tomshardware.com/reviews/pimoroni-fan-shim-heatsink-raspberry-pi-4,6219.html" data-wpel-link="external" target="_blank" rel="external noopener">the Raspberry Pi 4 is infamous for overheating</a> and suffering severe performance &#8211; and presumably reliability &#8211; problems as a result.</p>



<p>In the end, I spent several hours just figuring out how &amp; what to buy, and what is <a href="https://www.raspberrypi.com/products/raspberry-pi-4-model-b/" data-wpel-link="external" target="_blank" rel="external noopener">nominally &#8220;the $35 computer&#8221;</a> cost over $100.  Still without a case, even.</p>



<p>Sidenote:  the <a href="https://www.amazon.com/gp/product/B07TTTCN8H/ref=as_li_ss_tl?ie=UTF8&amp;linkCode=ll1&amp;tag=wasbl08-20&amp;linkId=a441b91c12e9531c45cf8c8722c32c38&amp;language=en_US" data-wpel-link="external" target="_blank" rel="external noopener">Pimoroni Fan Shim for Raspberry Pi</a>, while a little fiddly to assemble, does seem to work very well, and is quite quiet.</p>



<h3 class="wp-block-heading">Booting a Raspberry Pi</h3>



<p>This is the one part of the process thus far that&#8217;s actually worked mostly as it should.  I downloaded <a href="https://web.archive.org/web/20191011200635/https://www.raspberrypi.org/downloads/raspbian/" data-wpel-link="external" target="_blank" rel="external noopener">the full Raspbian Buster image</a>, following <a href="https://web.archive.org/web/20191011200620/https://www.raspberrypi.org/documentation/installation/installing-images/README.md" data-wpel-link="external" target="_blank" rel="external noopener">the installation guide</a>, and using <a href="https://etcher.balena.io/" data-wpel-link="external" target="_blank" rel="external noopener">balenaEtcher</a> to plop the image onto an SD card.  It all worked, even with the Etcher app being a tad dodgy (e.g. it lets you select non-removable volumes, which you cannot possibly intend to flash Raspbian onto, which is unnecessarily dangerous).  The Raspberry Pi 4 booted first time.</p>



<p>I tried to discern whether booting it headless from its birth would work.  <a href="https://projects.raspberrypi.org/en/projects/raspberry-pi-setting-up/2" data-wpel-link="external" target="_blank" rel="external noopener">Officially it does not</a>, but I found that baffling and dug further, reading countless online guides (<a href="https://howtoraspberrypi.com/how-to-raspberry-pi-headless-setup/" data-wpel-link="external" target="_blank" rel="external noopener">e.g.</a>), which seemed to suggest it is possible.</p>



<p>I learnt that there exists <a href="https://www.raspberrypi.com/documentation/computers/configuration.html" data-wpel-link="external" target="_blank" rel="external noopener">the raspi-config tool</a> for headless setup, but it was unclear if it would really work, fully.  Though I did the GUI set up process to be conservative, I&#8217;ve since used raspi-config quite a bit.  Turns out, it not only does work just fine, but it&#8217;s actually <em>necessary</em> because the GUI install doesn&#8217;t do some important things (like resize the root file system to fill the SD card).</p>



<p>One thing which nearly blew the whole enterprise was when it came to join a wifi network.  I have multiple wifi networks, all with [different] emoji for names.  The GUI set up tool can&#8217;t handle emoji, rendering them as octal escape sequences.  I don&#8217;t happen to have memorised the four-character byte codes of each emoji, so it was a tedious game of trial-and-error in which I tried every permutation of unreadable SSID &amp; password.</p>



<p>Worse, it took multiple attempts, too, before it finally worked &#8211; I have no idea why it failed to join the network the first time or two, despite using the right password.  To this day it still arbitrarily fails to join one of the networks, yet joins the other just fine &#8211; both are in the same frequency bands from the exact same router.</p>



<h4 class="wp-block-heading">Aside:  Raspberry Pi 4 as a desktop computer</h4>



<p>Since my intended use is as a headless, touchless server, I played only briefly with it in the GUI, using a makeshift setup involving my TV (the only HDMI viewing device I&#8217;ve ever owned &#8211; lucky I had that at least!).  It&#8217;s fine, but very sluggish &#8211; it was immediately apparent that nobody with any other options would ever try to actually use a Raspberry Pi 4 as a desktop machine.  Just [cold] launching the web browser, before you even navigate to a website, takes up to a minute.  And everything is uncomfortably small, with no apparent system configuration options available to adjust render scaling.  Clearly Raspbian is not really intended to be operated at UHD resolutions.</p>



<h3 class="wp-block-heading">Enabling Remote Access</h3>



<p>Though I ultimately intended to use only SSH to interact with the Pi, I did want to have VNC available as an option in case I ran into anything which required using the GUI (again, based on the heavy bias in all the official documentation, and the uncertainty created by that as to whether GUI interaction is <em>required</em> or merely an option).</p>



<p>Turns out, VNC doesn&#8217;t work out of the box on a Raspberry Pi, unless you buy <a href="https://www.realvnc.com/en/connect/" data-wpel-link="external" target="_blank" rel="external noopener">commercial, proprietary VNC software</a>.  A baffling collusion on the part of the Raspberry Pi / Raspbian people.  You have to <a href="https://raspberrypi.stackexchange.com/questions/59605/access-to-raspberry-pi-vnc-session-from-mac-os-x" data-wpel-link="external" target="_blank" rel="external noopener">do additional work</a> to make it actually work &#8211; work that&#8217;s completely undocumented in any official Raspberry Pi / Raspbian documentation.  (at best you&#8217;ll find the interwebs littered with accounts &amp; instructions on installing a non-proprietary VNC server in replacement, which presumably also works to solve this problem)</p>



<h3 class="wp-block-heading">Installing Homebridge</h3>



<p>It wasn&#8217;t actually my purpose in buying the Raspberry Pi, but I decided that &#8211; before I go down the meat grinder that is presumably getting Swift to work on the Pi, since the Pi sadly lacks support for Swift out of the box &#8211; I figured I&#8217;d just real quickly install <a href="https://github.com/homebridge/homebridge" data-wpel-link="external" target="_blank" rel="external noopener">Homebridge</a>, since I do have a couple of devices I&#8217;ve long wished would work with HomeKit.</p>



<p>Ugh.</p>



<p>What a fucking dumpster fire.</p>



<p>You can install Homebridge raw, but since it&#8217;s written in Node.js, I didn&#8217;t want it going into my real, bare system &#8211; infecting it with npn and JavaScript and all that horror.</p>



<p>This would be a perfect opportunity for Docker, and as one might expect there are many guides on how to install Homebridge via Docker.</p>



<h3 class="wp-block-heading">Installing <s>Homebridge</s> Docker</h3>



<p>Sadly &#8211; and frankly bizarrely &#8211; running Homebridge through Docker <a href="https://github.com/homebridge/homebridge/blob/latest/README.md" data-wpel-link="external" target="_blank" rel="external noopener">isn&#8217;t officially supported</a>.</p>



<p><a href="https://web.archive.org/web/20200804135351/https://stevenbreuls.com/2019/01/homebridge-on-raspberry-pi-using-docker/" data-wpel-link="external" target="_blank" rel="external noopener">This 3rd party guide</a> appeared to be the best, based on <a href="https://github.com/homebridge/docker-homebridge" data-wpel-link="external" target="_blank" rel="external noopener">this third party project to support Homebridge in Docker</a>.  Step zero, of course, is to install Docker itself.  Surely that&#8217;s trivial.  It&#8217;s <em>Docker</em>.  What <em>doesn&#8217;t</em> run Docker these days?  Hell, macOS runs Docker and <em>it doesn&#8217;t even support containers</em>.  I was baffled that Raspbian didn&#8217;t include Docker pre-installed.</p>



<p>Many, <em>many</em> hours later, it still wasn&#8217;t working.  One would think that <em>Docker</em>, of all things, would be a seamless thing to <em>sudo apt install</em>, but far from it.  For example, the <a href="https://download.docker.com/linux/raspbian/" data-wpel-link="external" target="_blank" rel="external noopener">official Docker apt repo for Raspbian</a> tries to install some &#8216;aufs-kdms&#8217; as a dependency, even though &#8211; turns out &#8211; it&#8217;s <em>not</em> a real dependency and doesn&#8217;t even compile on Raspbian.  WTF?!</p>



<p>So that wasted hours, in figuring that out &#8211; predominately consumed in trawling the interwebs for a solution.  After many hours and reading through dozens if not hundreds of StackOverflow, blog, and similar sources quoting similar issues and offering bogus remedies, I <em>finally</em> found <a href="https://github.com/raspberrypi/linux/issues/3021" data-wpel-link="external" target="_blank" rel="external noopener">a thread that&#8217;s actually helpful</a>.</p>



<p>The worst was yet to come.</p>



<p>At some point in this process something <em>also</em> screwed with my Pi&#8217;s boot settings to force the root directory to be mounted &#8211; at boot &#8211; as <a href="https://wiki.archlinux.org/title/Overlay_filesystem" data-wpel-link="external" target="_blank" rel="external noopener">an overlay</a> with writes going to tmpfs (i.e. nowhere).  That wasted yet more hours as I painstakingly root-caused why my Raspberry Pi suddenly had alzheimers (and lost a lot of progress otherwise on installing Homebridge, too).</p>



<p>The web utterly failed in this case, as I couldn&#8217;t even find how to disable overlayfs.  All I got, mockingly, was endless articles explaining how to <em>enable</em> it and voluntarily ruin your day.</p>



<p>Even just figuring out that it <em>was</em> overlayfs that was screwing me took quite some time, since the first failure symptom was a baffling error message when trying to start dockerd:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>failed to start daemon: rename /var/lib/docker/runtimes /var/lib/docker/runtimes-old: invalid cross-device link</p>
<cite>With love and fuck you, dockerd</cite></blockquote>



<p>Ultimately I found a fix, in part thanks to <a href="https://forums.raspberrypi.com/viewtopic.php?p=1044893" data-wpel-link="external" target="_blank" rel="external noopener">this forum post</a> which had enough transparency on enabling this bullshit situation that I could deduce how to <em>disable</em> it &#8211; long story short you need to mount the SD card on another, working computer and remove &#8216;<em>boot=overlay</em>&#8216; from /boot/cmdline.txt and &#8216;<em>initramfs initrd.img-4.19.75-v7l+-overlay</em>&#8216; from /boot/config.txt.</p>



<p>How that ever got enabled I have no idea.  Absolutely no commands I ran had <em>anything</em> to do with that at all.  Evidently something buried inside Docker installation and/or execution performs this system lobotomy.  Even then, I&#8217;ve since reviewed every single command I ran, and nothing seems even remotely like it could nor should have caused that.</p>



<p>Despite ultimately defeating all this failure, I was greeted by merely another fatal failure, just as inscrutable as the last:</p>



<pre class="wp-block-preformatted"><strong>$ docker-compose up -d</strong>
ERROR: Couldn't connect to Docker daemon at http+docker://localhost - is it running?

If it's at a non-standard location, specify the URL with the DOCKER_HOST environment variable.
<strong>$ ps auxww | grep docker
</strong>root &nbsp; &nbsp; &nbsp; 427&nbsp; 0.6&nbsp; 1.4 966720 58856 ?&nbsp; &nbsp; &nbsp; &nbsp; Ssl&nbsp; 15:42 &nbsp; 0:01 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
pi&nbsp; &nbsp; &nbsp; &nbsp; 1942&nbsp; 0.0&nbsp; 0.0 &nbsp; 7348 &nbsp; 472 pts/0&nbsp; &nbsp; S+ &nbsp; 15:46 &nbsp; 0:00 grep --color=auto docker</pre>



<p>Turns out this was because my user (&#8216;pi&#8217;, the default) wasn&#8217;t a member of the &#8216;docker&#8217; group.  I&#8217;d added it previously, but it must have been under the tyrannical overlayfs regime, and all memory of that event purged.  Adding it again (then logging out &amp; back in) fixed it (<em>sudo usermod -aG docker pi</em> btw).</p>



<h3 class="wp-block-heading">This is why Linux can&#8217;t have nice things</h3>



<p>In summary, Linux in general, and certainly Raspbian specifically, continues to be the same giant clusterfuck it&#8217;s always been.  I&#8217;m no Linux novice &#8211; I&#8217;ve been writing software for Linux for over a decade as my day job.  I&#8217;ve just had the luxury of teams of dozens if not hundreds of other engineers to insulate me from the bare wiring that is installing, configuring, &amp; maintaining a Linux installation.</p>



<p>At this point I&#8217;m <em>two days in</em> and have only <em>just</em> gotten Docker working.  For all the time I&#8217;ve wasted I&#8217;ve completely blown the price savings between a Raspberry Pi and even a <a href="https://www.apple.com/shop/buy-mac/mac-mini" data-wpel-link="external" target="_blank" rel="external noopener">brand new, $800 Mac Mini</a>.</p>



<p>And I still haven&#8217;t even <em>started</em> installing Swift, let-alone actually running my Swift app on the Raspberry Pi, which &#8211; contrary to where all my time has gone on this project &#8211; is the <em>actual</em> purpose of this whole sad enterprise.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/the-dumpster-fire-that-is-the-raspberry-pi/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4431</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>Full Disk Access is required to access Time Machine backups in Mojave</title>
		<link>https://wadetregaskis.com/full-disk-access-is-required-to-access-time-machine-backups-in-mojave/</link>
					<comments>https://wadetregaskis.com/full-disk-access-is-required-to-access-time-machine-backups-in-mojave/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Thu, 27 Dec 2018 17:51:51 +0000</pubDate>
				<category><![CDATA[Howto]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[Finder]]></category>
		<category><![CDATA[Full Disk Access]]></category>
		<category><![CDATA[Mojave]]></category>
		<category><![CDATA[Security]]></category>
		<category><![CDATA[Snafu]]></category>
		<category><![CDATA[System Integrity Protection]]></category>
		<category><![CDATA[Terminal]]></category>
		<category><![CDATA[Time Machine]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=4314</guid>

					<description><![CDATA[I&#8217;ve been struggling since Mojave came out to deal with it&#8217;s over-bearing expansion of SIP (&#8220;System Integrity Protection&#8221;), which is basically a super-root notion that blocks access &#8211; even to root &#8211; to lots of basic parts of the system, including obvious &#38; mostly sensible ones like /System and /Library, but also less usefully things&#8230; <a class="read-more-link" href="https://wadetregaskis.com/full-disk-access-is-required-to-access-time-machine-backups-in-mojave/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>I&#8217;ve been struggling since Mojave came out to deal with it&#8217;s over-bearing expansion of SIP (&#8220;System Integrity Protection&#8221;), which is basically a super-root notion that blocks access &#8211; even to root &#8211; to lots of basic parts of the system, including obvious &amp; mostly sensible ones like /System and /Library, but also less usefully things like any &amp; all Time Machine backups.</p>



<p>Blocking access to Time Machine makes it very difficult to actually use Time Machine, since it&#8217;s then difficult to retrieve files from a backup (you <em>have</em> to then use the stupid &#8216;warp&#8217; Time Machine interface, which is slow, ugly, and buggy).</p>



<p>Luckily, it turns out there is a fairly simple solution that <em>isn&#8217;t</em> disabling SIP entirely (which requires multiple reboots in order to do, so is typically quite disruptive &amp; slow).  It appears that any application granted Full Disk Access (System Preferences → Security &amp; Privacy → Full Disk Access) can read Time Machine backups.</p>



<p>In case you&#8217;re unfamiliar, the symptoms of this problem include:</p>



<ul class="wp-block-list"><li>Being unable to navigate into Time Machine backups in the Open / Save / etc dialogs.</li><li>Being unable to see &#8211; through <font face="menlo">ls</font> or similar tools &#8211; the contents of Time Machine backups via Terminal.</li><li>Apps reporting errors like &#8220;<font face="menlo">The file “Foo” couldn’t be opened because you don’t have permission to view it</font>&#8221; or bluntly &#8220;<font face="menlo">Operation not permitted</font>&#8221; when trying to read something in a Time Machine backup.</li></ul>



<p>There&#8217;s a strange &amp; ironically very bad security quirk though &#8211; curiously, any tools run via Terminal inherit Terminal&#8217;s access (or lack thereof) to Full Disk Access.  They <em>don&#8217;t</em> use whatever setting might be specified for them in the Security &amp; Privacy preferences.  This is pretty baffling, as it means to give Full Disk Access to <em>anything</em> you run via Terminal, you have to give it to <em>everything</em> you run via Terminal.  Anything you specifically give Full Disk Access won&#8217;t actually receive it if it happens to be launched via the Terminal (which confused me for a while, since it&#8217;s so unintuitive).</p>



<p>I&#8217;m guessing whatever mechanism enforces all this so-called security is based in LaunchServices or somesuch &#8211; while the Finder and most things in general will launch apps via LaunchServices, as detached &amp; independent process sessions, Terminal doesn&#8217;t &#8211; everything it runs, from the shells down, run under it in the process hierarchy, and seemingly share its security &amp; privacy settings.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/full-disk-access-is-required-to-access-time-machine-backups-in-mojave/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4314</post-id>	</item>
		<item>
		<title>ffmpeg can produce pseudo-corrupt audio when &#8216;copy&#8217;ing to an MP4 container</title>
		<link>https://wadetregaskis.com/ffmpeg-can-produce-pseudo-corrupt-audio-when-copying-to-an-mp4-container/</link>
					<comments>https://wadetregaskis.com/ffmpeg-can-produce-pseudo-corrupt-audio-when-copying-to-an-mp4-container/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Wed, 01 Aug 2018 15:56:08 +0000</pubDate>
				<category><![CDATA[Education]]></category>
		<category><![CDATA[AAC]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[codec]]></category>
		<category><![CDATA[ffmpeg]]></category>
		<category><![CDATA[Finder]]></category>
		<category><![CDATA[H.264]]></category>
		<category><![CDATA[Lightroom]]></category>
		<category><![CDATA[MOV]]></category>
		<category><![CDATA[MP4]]></category>
		<category><![CDATA[Quicktime]]></category>
		<category><![CDATA[Snafu]]></category>
		<category><![CDATA[trail camera]]></category>
		<category><![CDATA[VLC]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=4181</guid>

					<description><![CDATA[I&#8217;ve been using ffmpeg to trim clips from a trail camera, as most of the time there&#8217;s only a few seconds of anything interesting in frame out of the 30+ seconds of video it records each time, but I don&#8217;t want to re-encode them and lose video quality as a result (or balloon file sizes&#8230; <a class="read-more-link" href="https://wadetregaskis.com/ffmpeg-can-produce-pseudo-corrupt-audio-when-copying-to-an-mp4-container/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[<p>I&#8217;ve been using ffmpeg to trim clips from a trail camera, as most of the time there&#8217;s only a few seconds of anything interesting in frame out of the 30+ seconds of video it records each time, but I don&#8217;t want to re-encode them and lose video quality as a result (or balloon file sizes tremendously with a lossless video coding).  Keeping the whole 30 seconds is not just unnecessary and makes viewing the videos much more tedious, but wasteful of storage space as the encoding quality from the trail camera is very inefficient (file sizes are many times larger than they should be for the quality &#8211; clearly the H.264 encoder used in the trail camera is very cheap and very bad at its job).</p>
<p>I was originally doing something like:</p>
<pre>ffmpeg -ss 00:07 -t 00:03 -i "IMG_0164.MP4" -async 1 -c copy "IMG_0164_TRIMMED.MP4"</pre>
<p>The resulting trimmed MP4s play just fine in Quicktime, the Finder &#8211; anywhere that uses Apple&#8217;s decoding libraries (though I didn&#8217;t test iOS).</p>
<p>However, in VLC, or Lightroom, the audio is completely corrupt &#8211; just incoherent noise.  In Lightroom the video doesn&#8217;t even play correctly, because of Lightroom&#8217;s stupid habit of re-encoding the video &amp; audio into internal caches &#8211; apparently their video decoder is somehow thrown off by the audio channel issues, too.</p>
<p>After much trial and error and many dead-ends (thank you completely bogus &amp; wrong Stack Overflow threads… sigh) I eventually realised that the problem is apparently simply that Lightroom, VLC, etc get offended when you include pcm_s16le audio in an MP4.  ffmpeg itself says that&#8217;s not a valid audio codec for the MP4 container, <em>iff</em> you explicitly tell it to use that as the codec.  If you&#8217;re just copying from an existing audio / video file, however, it makes no mention at all of the concern.  Sigh.</p>
<p>So the apparent solution is simply to switch to the MOV container format instead.</p>
<pre>ffmpeg -ss 00:07 -t 00:03 -i "IMG_0164.MP4" -async 1 -c copy "IMG_0164_TRIMMED.MOV"</pre>
<p>The encoded bits remain identical, but the MOV container apparently accepts PCM audio where MP4 does not.  VLC, Lightroom, etc are now happy (and Quicktime et al remain happy).</p>
<p>(another possibility is that the &#8216;incompatibility&#8217; is related to MP4 levels or some other such junk… I didn&#8217;t try deciphering or exploring that)</p>
<p>It&#8217;s frustrating that VLC &amp; Lightroom can&#8217;t handle this when clearly it&#8217;s technically possible (witness Quicktime), and worse they don&#8217;t even properly recognise that they&#8217;re not handling it properly &#8211; they just play completely corrupt audio that&#8217;s literally painful on the ears.</p>
<p>It&#8217;s also very curious that the trail camera uses PCM audio if that&#8217;s not valid in an MP4 container.  It&#8217;s downright bizarre that VLC &amp; Lightroom can play the <em>unmodified</em> MP4s straight from the trail camera, even though they use the same purportedly invalid audio codec… somehow something ffmpeg is doing during its transmutation is making them angry.  I was unable to determine what that might be, though, through trial-and-error with ffmpeg command line options &amp; rudimentary examination of the input &amp; output files.</p>
<p>P.S.  An alternative is to bitwise-copy only the video stream (i.e. change -c copy to -c:v copy), and let VLC transcode the audio into its default AAC for the MP4 container.  That probably wouldn&#8217;t be a problem for me in my case &#8211; the audio from trail cameras is pretty crappy to begin with &#8211; but at the same time the audio tracks in these files are insignificant in size, so re-encoding them (and lossy as AAC) is pointless to saving disk space.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/ffmpeg-can-produce-pseudo-corrupt-audio-when-copying-to-an-mp4-container/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4181</post-id>	</item>
		<item>
		<title>Adobe Photoshop Lightroom doesn&#8217;t support Adobe Photoshop files</title>
		<link>https://wadetregaskis.com/adobe-photoshop-lightroom-doesnt-support-adobe-photoshop-files/</link>
					<comments>https://wadetregaskis.com/adobe-photoshop-lightroom-doesnt-support-adobe-photoshop-files/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Mon, 29 May 2017 05:20:31 +0000</pubDate>
				<category><![CDATA[Creative]]></category>
		<category><![CDATA[Ramblings]]></category>
		<category><![CDATA[Image editing]]></category>
		<category><![CDATA[Lightroom]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[Snafu]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=3927</guid>

					<description><![CDATA[I need a &#8216;facepalm&#8217; category apparently. &#160;There&#8217;s been a lot of that lately. You cannot import, let-alone work on, Photoshop files (&#8220;PSB&#8221; file extension) in Lightroom. &#160;It flat-out doesn&#8217;t support them. &#160;That&#8217;s such a weird limitation &#38; oversight, for a program that Adobe now officially names &#8216;Adobe Photoshop Lightroom&#8217;. See also this six year old&#8230; <a class="read-more-link" href="https://wadetregaskis.com/adobe-photoshop-lightroom-doesnt-support-adobe-photoshop-files/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>I need a &#8216;facepalm&#8217; category apparently. &nbsp;There&#8217;s been a lot of that lately.</p>



<p>You cannot import, let-alone work on, Photoshop files (&#8220;PSB&#8221; file extension) in Lightroom. &nbsp;It flat-out doesn&#8217;t support them. &nbsp;That&#8217;s such a weird limitation &amp; oversight, for a program that Adobe now officially names &#8216;Adobe Photoshop Lightroom&#8217;.</p>



<p>See also <a href="https://web.archive.org/web/20150617111830/https://feedback.photoshop.com/photoshop_family/topics/lightroom_support_cataloging_psb_files" data-wpel-link="external" target="_blank" rel="external noopener">this six year old thread on Adobe&#8217;s website</a>, asking for this feature.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/adobe-photoshop-lightroom-doesnt-support-adobe-photoshop-files/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">3927</post-id>	</item>
		<item>
		<title>iOS Family Sharing users cannot mix authentication schemes</title>
		<link>https://wadetregaskis.com/ios-family-sharing-users-cannot-mix-authentication-schemes/</link>
					<comments>https://wadetregaskis.com/ios-family-sharing-users-cannot-mix-authentication-schemes/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Sat, 15 Apr 2017 19:51:04 +0000</pubDate>
				<category><![CDATA[Ramblings]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[AppleID]]></category>
		<category><![CDATA[authentication]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[Family Sharing]]></category>
		<category><![CDATA[iOS]]></category>
		<category><![CDATA[iPad]]></category>
		<category><![CDATA[Snafu]]></category>
		<category><![CDATA[Two-factor]]></category>
		<category><![CDATA[Two-step]]></category>
		<category><![CDATA[Undocumented]]></category>
		<category><![CDATA[What do you want?]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=3898</guid>

					<description><![CDATA[Apple supports two styles of two-factor authentication,&#160;that they call (and distinguish as) &#8220;two-step&#8221; vs &#8220;two-factor&#8221;. &#160;&#8220;Two-step&#8221; is their older method, though functionally they&#8217;re basically equivalent. If you have multiple accounts on a Family Sharing arrangement, and some use &#8220;two-factor&#8221; while others use &#8220;two-step&#8221;, you&#8217;re in for&#160;a bag of hurt. For example, any time you change&#8230; <a class="read-more-link" href="https://wadetregaskis.com/ios-family-sharing-users-cannot-mix-authentication-schemes/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>Apple supports two styles of two-factor authentication,&nbsp;that they call (and distinguish as) &#8220;two-step&#8221; vs &#8220;two-factor&#8221;. &nbsp;&#8220;Two-step&#8221; is their older method, though functionally they&#8217;re basically equivalent.</p>



<p>If you have multiple accounts on a Family Sharing arrangement, and some use &#8220;two-factor&#8221; while others use &#8220;two-step&#8221;, you&#8217;re in for&nbsp;a bag of hurt.</p>



<p>For example, any time you change the password on any of the non-master accounts, you&#8217;ll have to reauthorise all devices on that account with the master purchaser. &nbsp;You&#8217;ll be prompted, when trying to download apps or purchase anything etc, with a dialog saying &#8220;Your Family Organizer, [foo], must enter the security code for their payment method&#8221;, asking for some kind of input. &nbsp;There is literally nothing you can enter there that will make it work. &nbsp;Not the password for any of the relevant Apple IDs, not any security codes for any credit cards, nada.</p>



<p>The problem is that it&#8217;s asking for a verification code that you can only create on a device which has &#8220;two-factor&#8221; authentication enabled. &nbsp;Compare for example what you see with &#8220;two-factor&#8221; authentication enabled on your iDevice:</p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><a href="https://wadetregaskis.com/wp-content/uploads/2017/04/Two-factor.webp" data-wpel-link="internal"><img loading="lazy" decoding="async" width="1536" height="2048" src="https://wadetregaskis.com/wp-content/uploads/2017/04/Two-factor.webp" alt="Screenshot of two-factor authentication enabled in iOS account settings" class="wp-image-3899" style="width:768px" srcset="https://wadetregaskis.com/wp-content/uploads/2017/04/Two-factor-768x1024@2x.webp 1536w, https://wadetregaskis.com/wp-content/uploads/2017/04/Two-factor-384x512@2x.webp 768w, https://wadetregaskis.com/wp-content/uploads/2017/04/Two-factor-192x256.webp 192w, https://wadetregaskis.com/wp-content/uploads/2017/04/Two-factor-384x512.webp 384w" sizes="auto, (max-width: 1536px) 100vw, 1536px" /></a></figure>
</div>


<p>Versus what you see with &#8220;two-step&#8221;:</p>


<div class="wp-block-image">
<figure class="aligncenter is-resized"><a href="https://wadetregaskis.com/wp-content/uploads/2017/04/Two-step.webp" data-wpel-link="internal"><img loading="lazy" decoding="async" width="1536" height="2048" src="https://wadetregaskis.com/wp-content/uploads/2017/04/Two-step.webp" alt="Screenshot of two-step authentication enabled in iOS account settings" class="wp-image-3900" style="width:768px" srcset="https://wadetregaskis.com/wp-content/uploads/2017/04/Two-step-768x1024@2x.webp 1536w, https://wadetregaskis.com/wp-content/uploads/2017/04/Two-step-384x512@2x.webp 768w, https://wadetregaskis.com/wp-content/uploads/2017/04/Two-step-192x256.webp 192w, https://wadetregaskis.com/wp-content/uploads/2017/04/Two-step-384x512.webp 384w" sizes="auto, (max-width: 1536px) 100vw, 1536px" /></a></figure>
</div>


<p>That &#8220;Get Verification Code&#8221; &#8220;button&#8221; is what you&#8217;re looking for. &nbsp;As you can see, it simply doesn&#8217;t exist with &#8220;two-step&#8221; authentication&nbsp;enabled.</p>



<p>The only solution &#8211; to allow your family members to download apps, purchase music / videos / books / etc, or pretty much do anything else on their iDevices &#8211; is to force the master account over to &#8220;two-factor&#8221; authentication.</p>



<p>To do this, you have to go to&nbsp;<a href="https://appleid.apple.com/" data-wpel-link="external" target="_blank" rel="external noopener">https://appleid.apple.com/</a>&nbsp;and turn off &#8220;two-step&#8221; authentication (which will require you to complete some stupid &#8216;security&#8217; questions). &nbsp;You cannot turn off &#8220;two-step&#8221; authentication from any of your actual iDevices&#8217; Settings apps.</p>



<p>Then, stupidly, you can&#8217;t actually enable &#8220;two-factor&#8221; authentication from that same website. &nbsp;That can only be done in the Settings app on one of your iDevices &#8211; by (in iOS 10.3 or later) going into Settings ➜ &lt;your name at the top of the list&gt;&nbsp;➜ Password &amp; Security.</p>



<p>There&#8217;s no way to enable &#8220;two-step&#8221; authentication anymore. &nbsp;And not having any form of two-factor authentication enabled is a very bad idea. &nbsp;So if any of your family&#8217;s accounts have &#8220;two-factor&#8221; authentication enabled, you basically have to switch&nbsp;to &#8220;two-factor&#8221; on <em>all</em> of them.</p>



<p>Which would be broadly fine, if Apple hadn&#8217;t made it so needlessly&nbsp;complicated, and the two systems so&nbsp;incompatible that their own software can&#8217;t figure out what&#8217;s going on.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/ios-family-sharing-users-cannot-mix-authentication-schemes/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">3898</post-id>	</item>
		<item>
		<title>EXIF metadata stores random gibberish for dates &#038; times</title>
		<link>https://wadetregaskis.com/exif-metadata-stores-random-gibberish-for-dates-times/</link>
					<comments>https://wadetregaskis.com/exif-metadata-stores-random-gibberish-for-dates-times/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Sun, 08 Jan 2017 19:06:19 +0000</pubDate>
				<category><![CDATA[Photography]]></category>
		<category><![CDATA[Aperture]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[EXIF]]></category>
		<category><![CDATA[GPS]]></category>
		<category><![CDATA[Lightroom]]></category>
		<category><![CDATA[Nikon]]></category>
		<category><![CDATA[Snafu]]></category>
		<category><![CDATA[SnapBridge]]></category>
		<category><![CDATA[Time]]></category>
		<category><![CDATA[Time zones]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=3847</guid>

					<description><![CDATA[I hadn&#8217;t &#8217;til yesterday realised that EXIF metadata doesn&#8217;t actually store dates &#38; times correctly.  Whoever came up with the spec all those decades ago clearly didn&#8217;t know how to work with dates &#38; times correctly.  This is immensely frustrating since now we have countless images taken with timestamps that are collectively gibberish. The problem is&#8230; <a class="read-more-link" href="https://wadetregaskis.com/exif-metadata-stores-random-gibberish-for-dates-times/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>I hadn&#8217;t &#8217;til yesterday realised that EXIF metadata doesn&#8217;t actually store dates &amp; times correctly.  Whoever came up with the spec all those decades ago clearly didn&#8217;t know how to work with dates &amp; times correctly.  This is immensely frustrating since now we have countless images taken with timestamps that are collectively gibberish.</p>



<p>The problem is that the standard doesn&#8217;t specify time zones in almost all cases (the sole exception being for GPS timestamps, which are in UTC).  Which means if you see the date &amp; time &#8220;2016-02-03T10:36:33.40&#8221; in your photo, that could be any actual time give or take ~25 hours to either side of that.</p>



<p>I realise now, in hindsight, that programs like Aperture &amp; Lightroom manage this by implicitly associating a time zone with photos as they&#8217;re imported (and both have controls of varying degrees for &#8216;correcting&#8217; the time of the photos, in cases where the camera&#8217;s clock is set wrong &#8211; including being set to the wrong time zone).  They leave it to the user to ensure the time zone that&#8217;s set for import matches what was on the camera at the time the photos were recorded.</p>



<p>However, if you&#8217;re processing images at scale and don&#8217;t have that explicit information from the user(s), you&#8217;re SOL.</p>



<p>Additionally, I don&#8217;t know anyone with a DSLR who hasn&#8217;t at least occasionally forgotten to change the date &amp; time on their camera to account for changes in daylight savings time, or movement to a new time zone.  If the time zone were recorded, this wouldn&#8217;t really matter since you could reliable change it later.  But since it&#8217;s not, it&#8217;s impossible to tell programatically when and where the time zone changes, in a given series of photos.</p>



<p>Now, you might think that since the GPS timestamp <em>is</em> actually recorded as a real, definitive time, that you could just use that to determine the time zone of other dates &amp; times in the metadata (by simply looking at the difference between them).  Unfortunately, in this case, the GPS timestamp is defined as the time at which the GPS data was recorded, <em>not</em> when the photo was created (or edited, or any of the other types of timestamps recorded in EXIF metadata).  Which means that in practice the GPS timestamp can be an unspecified &amp; unpredictable amount of time older than the other timestamps<sup data-fn="1921026e-92ec-4402-92c6-c7e6e8dc678e" class="fn"><a href="#1921026e-92ec-4402-92c6-c7e6e8dc678e" id="1921026e-92ec-4402-92c6-c7e6e8dc678e-link">1</a></sup>.</p>



<p>If it were just a matter of a few minutes difference then this wouldn&#8217;t be an issue, since the vast majority of the world only acknowledges half hour increments in time zone steps<sup data-fn="8932e4f2-b366-4671-9023-f5b3ee145860" class="fn"><a href="#8932e4f2-b366-4671-9023-f5b3ee145860" id="8932e4f2-b366-4671-9023-f5b3ee145860-link">2</a></sup> and thus you could just round and get things right most of the time.  Unfortunately, at least some notable GPS implementations in popular cameras have potentially huge deltas (hours or more) &#8211; e.g. all of <a href="https://web.archive.org/web/20240404204434/https://www.nikonusa.com/en/nikon-products/snapbridge-app.page" data-wpel-link="external" target="_blank" rel="external noopener">Nikon&#8217;s SnapBridge cameras</a>, including the D500, D5600, &amp; D3400.</p>


<ol class="wp-block-footnotes"><li id="1921026e-92ec-4402-92c6-c7e6e8dc678e">And that&#8217;s assuming the camera&#8217;s clock isn&#8217;t set wrong anyway &#8211; it&#8217;s possible to include GPS data in your photos but <em>not</em> sync the camera&#8217;s clock, in at least some popular cameras like Nikon&#8217;s. <a href="#1921026e-92ec-4402-92c6-c7e6e8dc678e-link" aria-label="Jump to footnote reference 1">↩︎</a></li><li id="8932e4f2-b366-4671-9023-f5b3ee145860"><a href="https://en.wikipedia.org/wiki/Time_zone#List_of_UTC_offsets" data-wpel-link="external" target="_blank" rel="external noopener">Wikipedia reports</a> that there are a couple of small regions of Australia &amp; New Zealand which use 15 minute offsets, and the entirety of Nepal does too, but those are the only exceptions.  And only a small minority use half hour offsets, as opposed to hour offsets, to begin with. <a href="#8932e4f2-b366-4671-9023-f5b3ee145860-link" aria-label="Jump to footnote reference 2">↩︎</a></li></ol>]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/exif-metadata-stores-random-gibberish-for-dates-times/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2017/01/Eternal_clock.avif" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">3847</post-id>	</item>
		<item>
		<title>Building John The Ripper Jumbo for macOS Sierra</title>
		<link>https://wadetregaskis.com/building-john-the-ripper-jumbo-for-macos-sierra/</link>
					<comments>https://wadetregaskis.com/building-john-the-ripper-jumbo-for-macos-sierra/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Sat, 12 Nov 2016 02:42:40 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[John The Ripper]]></category>
		<category><![CDATA[OpenSSL]]></category>
		<category><![CDATA[Snafu]]></category>
		<category><![CDATA[Undocumented]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=3740</guid>

					<description><![CDATA[It&#8217;s quickly apparent that John The Ripper Jumbo&#160;doesn&#8217;t build out of the box on macOS, and probably hasn&#8217;t for a long time, due to its complaint about missing OpenSSL headers. This guide was almost helpful, except it&#8217;s out of date &#8211; e.g. the Makefile.in patch it provides no longer applies cleanly &#8211; and simply doesn&#8217;t&#8230; <a class="read-more-link" href="https://wadetregaskis.com/building-john-the-ripper-jumbo-for-macos-sierra/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>It&#8217;s quickly apparent that <a href="https://github.com/openwall/john/tree/bleeding-jumbo" data-wpel-link="external" target="_blank" rel="external noopener">John The Ripper Jumbo</a>&nbsp;doesn&#8217;t build out of the box on macOS, and probably hasn&#8217;t for a long time, due to its complaint about missing OpenSSL headers.</p>



<p><a href="https://web.archive.org/web/20231219225305/https://3583bytesready.net/2016/02/17/building-john-the-ripper-1-8-0-jumbo-on-mac-os-10-11-el-capitan/" data-wpel-link="external" target="_blank" rel="external noopener">This guide</a> was almost helpful, except it&#8217;s out of date &#8211; e.g. the Makefile.in patch it provides no longer applies cleanly &#8211; and simply doesn&#8217;t work &#8211; once you get the John The Ripper configure script to see the OpenSSL devel headers, it then just complains that it can&#8217;t find a valid libssl anyway.</p>



<p>Even the <a href="https://openwall.com/john/pro/macosx/" data-wpel-link="external" target="_blank" rel="external noopener">Pro version of John The Ripper</a>, which isn&#8217;t cheap, doesn&#8217;t look like a good option since its web page has the hallmarks of something that hasn&#8217;t been updated in many, many years. &nbsp;e.g. talk about support for Mac OS X 10.7 Lion being&nbsp;<em>planned</em>.</p>



<p>And although it&nbsp;<em>appears</em> to support using CommonCrypto instead of the now deprecated OpenSSL, that doesn&#8217;t work &#8211; even when configured that way it still compiles in code that requires OpenSSL, for SHA1. &nbsp;Sigh.</p>



<p>Trying to get it to use a fresh build of OpenSSL (1.1.0) also seems an intractable failure &#8211; OpenSSL 1.1.0 out of the box produces libraries which don&#8217;t contain SSL_library_init, which is of course necessary for any OpenSSL user and foils&nbsp;any attempt to use the built libraries by way of&nbsp;missing symbol errors in the link phase.</p>



<p>And OpenSSL 0.9.8zh&#8217;s build system is just screwy. &nbsp;By default it builds only 32-bit and only static libraries (no matter how hard you tell it to build shared ones). &nbsp;You have to bypass its first layer of configery and do it &#8216;manually&#8217;, like so:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><code>./configure --prefix=&lt;install location> darwin64-x86_64-cc -no-shared enable-camellia</code></p>
</blockquote>



<p>You can then configure JohnTheRipper to point to that version of OpenSSL, like so:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><code>./configure CPPFLAGS='-I &lt;OpenSSL install location>/include' LDFLAGS='-L &lt;OpenSSL install location>/lib' OPENSSL_LIBS="-lcrypto"</code></p>
</blockquote>



<p>Now it&#8217;ll finally get past the OpenSSL issues, and build successfully.</p>



<p><strong>Note:</strong> &nbsp;<em>don&#8217;t</em> use the Makefile.in patch provided in the aforelinked guide. &nbsp;That actually&nbsp;<em>breaks</em> the build now, even if you properly apply it manually.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/building-john-the-ripper-jumbo-for-macos-sierra/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">3740</post-id>	</item>
		<item>
		<title>Silent data corruption</title>
		<link>https://wadetregaskis.com/silent-data-corruption/</link>
					<comments>https://wadetregaskis.com/silent-data-corruption/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Wed, 06 Jul 2016 05:23:36 +0000</pubDate>
				<category><![CDATA[Ramblings]]></category>
		<category><![CDATA[APFS]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[lies]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[Snafu]]></category>
		<category><![CDATA[Time Machine]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=3652</guid>

					<description><![CDATA[Alternate title:  Apple&#8217;s file system engineers are sadly naive. I was quite disappointed to see that APFS isn&#8217;t even trying to provide data integrity.  Data integrity is kind of step 0 of any file system, and checksums or use of ECC is pretty much standard in modern &#38; leading-edge file systems.  APFS doesn&#8217;t want to&#8230; <a class="read-more-link" href="https://wadetregaskis.com/silent-data-corruption/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[<p>Alternate title:  Apple&#8217;s file system engineers are sadly naive.</p>
<p>I was quite disappointed to see that APFS isn&#8217;t even trying to provide data integrity.  Data integrity is kind of step 0 of any file system, and checksums or use of ECC is pretty much standard in modern &amp; leading-edge file systems.  APFS doesn&#8217;t want to be one of those, it seems.</p>
<p>Case in point why this matters:</p>
<p style="padding-left: 30px;">I have a bunch of old backup drives, because drives are cheap and until recently I could just buy a new one once the current one filled, instead of ever deleting a backup.  Periodically I go back through these old backup drives and do some basic integrity checks (S.M.A.R.T. bad block scans, file system checks, etc).</p>
<p style="padding-left: 30px;">I <em>also</em> run a comparison of key data between those backups and the current versions on my computer, for files which generally <em>shouldn&#8217;t</em> change nor disappear &#8211; e.g. photos, videos, key documents, etc.</p>
<p style="padding-left: 30px;">And today I found that at least half a dozen valuable personal videos (and a few photos) were corrupt, in the versions on my computer.  Luckily, the versions in the ancient backups were still good, so I could replace the corrupt ones.</p>
<p style="padding-left: 30px;">This corruption was completely silent, until my &#8216;paranoid&#8217; and time-consuming checks discovered it.</p>
<p>It&#8217;s far from the first time.  A failing drive years back corrupted a huge portion of my music library &#8211; silently, as far as the file system &amp; OS were concerned.  Periodically I&#8217;ve discovered photos (of which I have huge numbers &#8211; the majority of my data) which have become corrupt at some indeterminate point.  And I&#8217;ve of course had file system [metadata] corruption occur many times, sometimes requiring complete erasure of the disk, and recovery or rebuilds from backup (a few times I&#8217;ve had to use data recovery software, where backups weren&#8217;t available).</p>
<p>Most, if not all, of these issues would have been discovered by even the most trivial file integrity protections, in the file system.</p>
<p>The notion that modern disks somehow magically protect against all silent data corruption is abject poppycock.  They&#8217;re <em>more</em> likely to suffer from it than older disks &#8211; a byproduct of higher densities and market demand for cheaper, crappier storage products.</p>
<p>And the implicit assertion that Apple&#8217;s file system driver, and kernel overall, are somehow completely free of bugs… is just batshit crazy.</p>
<p><strong>Addendum</strong></p>
<p>Since Apple aren&#8217;t interested in protecting anyone&#8217;s valuable personal data, I&#8217;m on the look-out for other options.  Manual use of <em>shasum</em> is one, for now, but a more streamlined and fool-proof system would be better.  Alas, none seems to exist[1. There is <a href="https://github.com/laktak/chkbit" data-wpel-link="external" target="_blank" rel="external noopener">chkbit</a>, but it relies on MD5… <em>probably</em> acceptable for this use case, but needless in the face of <em>decades</em> of better hash algorithms.  And it&#8217;s written in JavaScript.  Ew.].  Yet.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/silent-data-corruption/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">3652</post-id>	</item>
		<item>
		<title>Lightroom&#8217;s flaws &#038; limitations</title>
		<link>https://wadetregaskis.com/lightrooms-flaws-limitations/</link>
					<comments>https://wadetregaskis.com/lightrooms-flaws-limitations/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Thu, 11 Feb 2016 18:04:55 +0000</pubDate>
				<category><![CDATA[Photography]]></category>
		<category><![CDATA[Ramblings]]></category>
		<category><![CDATA[Aperture]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[Capture One]]></category>
		<category><![CDATA[Lightroom]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[Snafu]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=3515</guid>

					<description><![CDATA[I&#8217;m attempting to switch to Lightroom, from Aperture, given lack of better alternatives. I&#8217;ve attempted this switch a couple of times before, without success. I&#8217;m finding that it&#8217;s still kind of annoying.  Here&#8217;s the start of my laundry list against it: It&#8217;s very slow.  I am already so very tired of seeing that blasted &#8220;Loading…&#8221;&#8230; <a class="read-more-link" href="https://wadetregaskis.com/lightrooms-flaws-limitations/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[<p>I&#8217;m attempting to switch to Lightroom, from Aperture, given lack of better alternatives. I&#8217;ve attempted this switch a couple of times before, <a href="https://wadetregaskis.com/lightroom/" data-wpel-link="internal">without success</a>.</p>
<p>I&#8217;m finding that it&#8217;s still kind of annoying.  Here&#8217;s the start of my laundry list against it:</p>
<ul>
<li>It&#8217;s very slow.  I am already so very tired of seeing that blasted &#8220;Loading…&#8221; floater.
<ul>
<li>I&#8217;m running it off of a pair of SATA-connected SSDs &#8211; one for the library, one for the originals.  It has enough usable bandwidth to read in dozens of photos a second.  But it still takes three to five seconds to show just one.</li>
</ul>
</li>
<li>It&#8217;s also a bit slow to actually show adjustments as you make them, a lot of the time.  Aperture, despite not being updated for <em>many years</em> to take advantage of all sorts of new technologies, is still much more responsive and fluid, and consequently faster to use.
<ul>
<li>I did notice that Capture One is also a bit sluggish sometimes, in this respect.  Sad panda &#8211; everyone&#8217;s screwing this up, it seems.</li>
</ul>
</li>
<li>When you scroll around in the grid view, it&#8217;s a bit janky &#8211; see prior point on Lightroom just generally being sluggish &#8211; but even worse, it doesn&#8217;t show the photo&#8217;s metadata reliably.  A lot of it is invisible for a while, &#8216;loading in&#8217; some time later.  It&#8217;s really annoying to have to frequently wait for Lightroom to get around to actually showing me the grid view properly.  And it&#8217;s also very distracting to see the metadata popping in at random intervals for random photos.</li>
<li>Its search &amp; filtering tools are kinda piss-weak.  e.g. you can do a text search of metadata for only one thing at a time.  And you can&#8217;t even specify what metadata you want to search, with very few exceptions.</li>
<li>As <a href="https://wadetregaskis.com/raw-converter-comparison/" data-wpel-link="internal">I&#8217;d shown previously</a>, its RAW renderer is not very good.  Image quality out of the box is very poor &#8211; mainly that even bright, ISO 100 shots have unacceptably high noise visible and require strong noise reduction.  This is unnecessary in every other RAW renderer I&#8217;ve ever used (Aperture wasn&#8217;t great either, granted, but at least it erred on the side of <em>not</em> noisy-as-heck by default; it&#8217;s much easier to add sharpening <em>as necessary</em> than to have to noise reduce <em>every time</em>).
<ul>
<li>And its noise reduction tool isn&#8217;t great either &#8211; much like Aperture&#8217;s, it&#8217;s ham-fisted and often requires painstaking brush-work to selectively apply it to each photo.</li>
</ul>
</li>
<li>Its sharpening tool is not very good either.  It introduces grittiness and nasty edge haloes really quickly, relative to how much actual sharpness &amp; detail it&#8217;s revealing.  Now I understand why so many Lightroom users seem to use 3rd-party sharpening (and noise reduction) plug-ins.
<ul>
<li>On the upside, vs Aperture, it does let you set a detail-sensitive &#8216;mask&#8217;, which helps reduce brushwork somewhat.  Unfortunately it lacks the corresponding slider for its noise reduction tool, which reintroduces the need to do painstaking brushwork.</li>
</ul>
</li>
<li>Its system for managing brush masks is surprisingly awkward.  Aperture was much more straightforward (though not without its own foibles, like not letting you share masks between adjustment tools).  e.g. to see an overlay of where the mask actually is, you have to however the mouse over a tiny little dot &#8211; and there may be several on the photo, if you&#8217;ve added several different masks, with no way to distinguish them other than their arbitrary locations.  And worse, you have to wait a couple of seconds, <em>every time</em>, for the mask overlay to actually appear.  And then as soon as you move off the dot &#8211; e.g. to actually modify the mask &#8211; it disappears!  Ridiculous!
<ul>
<li>Capture One has the best implementation of this that I&#8217;ve seen, so far.  There&#8217;s a couple of things about Capture One&#8217;s that I find a little awkward, compared to Aperture&#8217;s, but overall it&#8217;s still the best.</li>
</ul>
</li>
<li>It&#8217;s possible to have filters applied but the filter bar hidden.  Very confusing &#8211; it took me quite some time to figure out why a bunch of photos simply weren&#8217;t showing up.  It&#8217;d make much more sense, IMHO, to have the &#8216;hide the filter bar&#8217; option only apply when there aren&#8217;t any filters actually applied.  Though I do recognise the counter-opinion.</li>
<li>Deleting photos is unnecessarily difficult.  If you&#8217;re viewing a collection, there&#8217;s no way to do it directly.  The least sucky way I&#8217;ve found is to mark target photos as &#8216;Rejected&#8217;, and then go to the &#8220;All Photographs&#8221; view and hit command-delete.  Lots of unnecessary steps and distracting from the workflow.</li>
<li>Its built-in Flickr export forces all photos to sRGB.  Sad, blunt fail.
<ul>
<li>There is a popular third-party plug-in, <a href="https://www.newpproducts.com/lightroom-plug-ins/photo-upload/" data-wpel-link="external" target="_blank" rel="external noopener">Photo Upload</a>, which I&#8217;m using for now, but I keep finding it rubs me a bit the wrong way.  Hopefully just a matter of getting used to it &#8211; FlickrExport for Aperture, which I&#8217;d relied on previously, wasn&#8217;t perfect either.  And on the upside, the author seems very responsive and courteous, which is refreshing and encouraging.</li>
</ul>
</li>
<li>Import from SD cards is a little sluggish.  Not terrible &#8211; I&#8217;m talking ~60 MB/s on a card &amp; reader combo capable of ≥80 MB/s &#8211; but it&#8217;s still a bit disappointing.  Aperture is faster.</li>
<li>Its metadata editing interface is certainly better than Capture One&#8217;s horrible &amp; buggy one, but still a far cry from Aperture&#8217;s.  I miss being able to customise the metadata view, but beyond that, even basic things are a bit awkward, like the &#8216;Title&#8217; and &#8216;Caption&#8217; fields being tiny and thrown arbitrarily down towards the end of the list.
<ul>
<li>Though it does have one handy thing &#8211; clicking on the field name shows a pop-up menu listing recently used values, which is actually a thoughtful touch.  Auto-completion is also available, but it&#8217;s nice to avoid the keyboard &lt;-&gt; mouse shift.</li>
</ul>
</li>
<li>I still don&#8217;t like its modality.  I find I have to shift between &#8216;Library&#8217; and &#8216;Develop&#8217; modes <em>a lot</em>, which&#8217;d be tedious enough even without the fact that a whole bunch of keyboard shortcuts are different between the two.  Super annoying.  I keep hitting keys expecting some predictable result, and instead just get NSBeeps, or totally undesired effects, or worse &#8211; just a flash of a menubar item with no indication of what the #%@! just happened, or how to undo it.  Gah!</li>
</ul>
<p>And I could go on.  And probably will in future &#8211; I need somewhere to vent and mind-bogglingly Adobe offer no way to actually file bug reports &amp; feature requests with them directly.</p>
<p>I really get the impression that Adobe haven&#8217;t paid due attention to their [former] competition, Aperture.  There&#8217;s just <em>so many</em> things that Aperture has done better <em>for nearly a decade now</em>.  If they were holding off &#8216;copying&#8217; it out of some sense of honour, that&#8217;s respectable, but the time for it has long passed, given Aperture&#8217;s demise.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/lightrooms-flaws-limitations/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">3515</post-id>	</item>
		<item>
		<title>Why I cancelled Backblaze</title>
		<link>https://wadetregaskis.com/why-i-cancelled-backblaze/</link>
					<comments>https://wadetregaskis.com/why-i-cancelled-backblaze/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Sun, 24 Jan 2016 02:44:31 +0000</pubDate>
				<category><![CDATA[Ramblings]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[lies]]></category>
		<category><![CDATA[Snafu]]></category>
		<guid isPermaLink="false">http://blog.wadetregaskis.com/?p=3062</guid>

					<description><![CDATA[This is the feedback I sent to Backblaze shortly before I cancelled my account with them. For the additional context &#8211; the restore failure I alluded to was basically that: So, the departing &#8216;support&#8217; ticket I filed with them (#167833): Maybe this&#8217;ll help your future would-be customers. The main reason is that when I tried&#8230; <a class="read-more-link" href="https://wadetregaskis.com/why-i-cancelled-backblaze/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>This is the feedback I sent to Backblaze shortly before I cancelled my account with them.</p>



<p>For the additional context &#8211; the restore failure I alluded to was basically that:</p>



<ol class="wp-block-list">
<li>Over the course of more than a week and repeated attempts, they were unable to restore 99.7% of my data.</li>



<li>They sent me 685 spammy emails telling me the restore failed. &nbsp;Six hundred and eighty five.</li>



<li>Their tech support was at least fairly open, and admitted to the problem without fuss, but were unable to actually&nbsp;<em>do</em> anything to get the data back. &nbsp;Which is, after all, the most important thing.</li>
</ol>



<p>So, the departing &#8216;support&#8217; ticket I filed with them (#167833):</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>Maybe this&#8217;ll help your future would-be customers.</p>



<p>The main reason is that when I tried to actually restore data a month or two ago, I was unable to. Epic fail on your part. (Support request #162743, FYI)</p>



<p>That alone is a deal-breaker. The lacklustre customer support and idiotic email spam bugs add icing on that horrible cake.</p>



<p>There are other reasons too, however:</p>



<ul class="wp-block-list">
<li>There&#8217;s no secure way to restore. You require me to provide my private key password to your web site. So many ways that can go wrong. I want something more akin to Crashplan&#8217;s ability to restore through a local app [once it&#8217;s given the private key password]. I should never, *ever* have to transmit my private key password over the internet.</li>



<li>30 day inactivity window. I recently travelled away from home for nearly 30 days, and realised that if I&#8217;d been gone a little longer, you would have thrown out all my backups. If I&#8217;m still paying you, you should still be retaining my backups. (and since *all* my drives are external, including my boot drive, this applies to *all* my data. Even if my boot drive weren&#8217;t external, the vast majority of my valuable data is on [permanently connected] external drives)</li>



<li>30 day restore window. I&#8217;m somewhat on the fence with this one, but other backup services offer retention horizons much longer, or alternative schemes where you have up to N (typically 30) *versions*, regardless of how old those are. Both are preferable to a fixed time window. Since the vast majority of my data is write-once, or thereabouts, I don&#8217;t actually have multiple versions of most, but for those that do it&#8217;d be comforting to know that I could go back months or years to the prior version(s). I&#8217;d be willing to pay extra for this.</li>



<li>The Backblaze daemon takes an unduly long time to notice new files. Even if I manually tell it to hurry up (i.e. option-click &#8216;Restore options&#8230;&#8217;) it still sometimes doesn&#8217;t notice new files. I see good reason to not be too hyperactive with backups &#8211; it&#8217;s true I don&#8217;t need every minute&#8217;s version of some random file I&#8217;m working in &#8211; but most of my data is photos &amp; videos, which are import-once-and-never-change (or maybe delete, later). I&#8217;d really like to just see Backblaze immediately start backing up newly imported photos &amp; photos as soon as they hit the disk.</li>
</ul>



<p>I&#8217;ve realised that I need all these things, and as it happens Crashplan offers them, so I&#8217;m switching.</p>



<p>To your advantage, uploads are much faster than many of the alternative services I tested (particularly Crashplan), and I otherwise like your native app and it&#8217;s relatively minimal system impact. So I&#8217;m a little sad to see that go. But simple, fast uploads are quite pointless if, when all is said and done, they&#8217;re essentially going to /dev/null.</p>
</div></div>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/why-i-cancelled-backblaze/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">3062</post-id>	</item>
		<item>
		<title>Dealing with Nikon Technical Support (a.k.a. pulling teeth)</title>
		<link>https://wadetregaskis.com/dealing-with-nikon-technical-support-a-k-a-pulling-teeth/</link>
					<comments>https://wadetregaskis.com/dealing-with-nikon-technical-support-a-k-a-pulling-teeth/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Mon, 03 Nov 2014 15:29:34 +0000</pubDate>
				<category><![CDATA[Photography]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[Snafu]]></category>
		<guid isPermaLink="false">http://blog.wadetregaskis.com/?p=3008</guid>

					<description><![CDATA[Me:  Hey, the D7100 with these particular lenses over-exposes by exactly one stop when not wide open. Nikon&#8217;s Support Website:  Cool story bro.  Got any pics? Me:  Sure, here they are&#8230; Nikon&#8217;s Support Website:  Sorry, all attachments have to be 10 MB or less. Me:  U wot mate? &#8230; Me:  Okay, I put them up on&#8230; <a class="read-more-link" href="https://wadetregaskis.com/dealing-with-nikon-technical-support-a-k-a-pulling-teeth/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[<p><strong>Me</strong>:  Hey, the D7100 with these particular lenses over-exposes by exactly one stop when not wide open.<br />
<strong>Nikon&#8217;s Support Website</strong>:  Cool story bro.  Got any pics?<br />
<strong>Me</strong>:  Sure, here they are&#8230;<br />
<strong>Nikon&#8217;s Support Website</strong>:  Sorry, all attachments have to be 10 MB or less.<br />
<strong>Me</strong>:  U wot mate?<br />
&#8230;<br />
<strong>Me</strong>:  Okay, I put them up on a HTTP server.  Here are the links.<br />
<strong>Nikon&#8217;s Support Drone</strong>:  We don&#8217;t accept modified images.  You have to provide the images straight from the camera.<br />
<strong>Me</strong>:  Those were copied directly from the SD card.<br />
&#8230;<br />
<strong>Nikon&#8217;s Support Drone</strong>:  Please email them to us.<br />
<strong>Me</strong>:  Your email gateway doesn&#8217;t accept attachments that large.<br />
&#8230;<br />
<strong>Nikon&#8217;s Support Drone</strong>:  Please retake the images at a smaller size.<br />
<strong>Me</strong>:  I have no words.</p>
<p>&nbsp;</p>
<p><em>Or</em>&#8230; you could update your stupid Support site to accept attachments larger than 10 MB. What Nikon camera produces JPEGs less than 10 MB in size, let alone raws? @#%$!</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/dealing-with-nikon-technical-support-a-k-a-pulling-teeth/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">3008</post-id>	</item>
	</channel>
</rss>
