<?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>Sad &#8211; Wade Tregaskis</title>
	<atom:link href="https://wadetregaskis.com/tags/sad/feed/" rel="self" type="application/rss+xml" />
	<link>https://wadetregaskis.com</link>
	<description></description>
	<lastBuildDate>Thu, 02 Apr 2026 03:30:29 +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>Sad &#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>Studio Display XDR vs Pro Display XDR</title>
		<link>https://wadetregaskis.com/studio-display-xdr-vs-pro-display-xdr/</link>
					<comments>https://wadetregaskis.com/studio-display-xdr-vs-pro-display-xdr/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Tue, 03 Mar 2026 17:58:28 +0000</pubDate>
				<category><![CDATA[News]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[Pro Display XDR]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[Studio Display XDR]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8738</guid>

					<description><![CDATA[Studio Display XDR Pro Display XDR Screen diagonal 69 cm 81 cm (+17%) Resolution 5,120 ⨉ 2,880 6,016 ⨉ 3,384 Pixel count 14,745,600 20,358,144 (+38%) Backlight zones 2,304 (+300%) 576 Pixel density 218 218 Contrast ratio 1,000,000 : 1 1,000,000 : 1 Peak sustained brightness 2,000 (≤25℃) (+25%) 1,600 (≤ 25℃) Display P3 coverage ?&#8230; <a class="read-more-link" href="https://wadetregaskis.com/studio-display-xdr-vs-pro-display-xdr/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-table aligncenter is-style-stripes"><table><thead><tr><th></th><th class="has-text-align-center" data-align="center">Studio Display XDR</th><th class="has-text-align-center" data-align="center">Pro Display XDR</th></tr></thead><tbody><tr><td>Screen diagonal</td><td class="has-text-align-center" data-align="center">69 cm</td><td class="has-text-align-center" data-align="center">81 cm (+17%)</td></tr><tr><td>Resolution</td><td class="has-text-align-center" data-align="center">5,120 ⨉ 2,880</td><td class="has-text-align-center" data-align="center"><strong>6,016 ⨉ 3,384</strong></td></tr><tr><td>Pixel count</td><td class="has-text-align-center" data-align="center">14,745,600</td><td class="has-text-align-center" data-align="center"><strong>20,358,144</strong> (+38%)</td></tr><tr><td>Backlight zones</td><td class="has-text-align-center" data-align="center"><strong>2,304</strong> (+300%)</td><td class="has-text-align-center" data-align="center">576</td></tr><tr><td>Pixel density</td><td class="has-text-align-center" data-align="center">218</td><td class="has-text-align-center" data-align="center">218</td></tr><tr><td>Contrast ratio</td><td class="has-text-align-center" data-align="center">1,000,000 : 1</td><td class="has-text-align-center" data-align="center">1,000,000 : 1</td></tr><tr><td>Peak sustained brightness</td><td class="has-text-align-center" data-align="center"><strong>2,000</strong> (≤25℃) (+25%)</td><td class="has-text-align-center" data-align="center">1,600 (≤ 25℃)</td></tr><tr><td>Display P3 coverage</td><td class="has-text-align-center" data-align="center">?</td><td class="has-text-align-center" data-align="center"><a href="https://www.pcmag.com/reviews/apple-pro-display-xdr" data-wpel-link="external" target="_blank" rel="external noopener">98.7%</a></td></tr><tr><td>Adobe RGB coverage</td><td class="has-text-align-center" data-align="center">?</td><td class="has-text-align-center" data-align="center"><a href="https://www.pcmag.com/reviews/apple-pro-display-xdr" data-wpel-link="external" target="_blank" rel="external noopener">96.7%</a></td></tr><tr><td>sRGB coverage</td><td class="has-text-align-center" data-align="center">?</td><td class="has-text-align-center" data-align="center"><a href="https://www.pcmag.com/reviews/apple-pro-display-xdr" data-wpel-link="external" target="_blank" rel="external noopener">94.3%</a></td></tr><tr><td>Refresh rate</td><td class="has-text-align-center" data-align="center"><strong>47 &#8211; 120 Hz</strong></td><td class="has-text-align-center" data-align="center">47.95 &#8211; 60.00 Hz</td></tr><tr><td>USB Power Delivery</td><td class="has-text-align-center" data-align="center"><strong>140W</strong> (+46%)</td><td class="has-text-align-center" data-align="center">96W</td></tr><tr><td>Connectivity</td><td class="has-text-align-center" data-align="center"><strong>Thunderbolt 5 (1 up, 1 down) + 2 USB-C (10 Gb/s)</strong></td><td class="has-text-align-center" data-align="center">Thunderbolt 3 (1 up) + 3 USB-C (5 Gb/s<sup data-fn="e8feb3fe-feb2-4a81-83f9-5562e245b289" class="fn"><a href="#e8feb3fe-feb2-4a81-83f9-5562e245b289" id="e8feb3fe-feb2-4a81-83f9-5562e245b289-link">1</a></sup>)</td></tr><tr><td>Dimensions (excluding stand)</td><td class="has-text-align-center" data-align="center">36.2 ⨉ 62.3 ⨉ 3.3 cm</td><td class="has-text-align-center" data-align="center">41.2 ⨉ 71.8 ⨉ 2.7 cm</td></tr><tr><td>Volume (excluding stand)</td><td class="has-text-align-center" data-align="center">7,442 cm³</td><td class="has-text-align-center" data-align="center">7,987 cm³ (+7%)</td></tr><tr><td>Weight w/ stand</td><td class="has-text-align-center" data-align="center">8.5 kg</td><td class="has-text-align-center" data-align="center">11.8 kg (+39%)</td></tr><tr><td>Weight w/o stand</td><td class="has-text-align-center" data-align="center">6.3 kg</td><td class="has-text-align-center" data-align="center">7.48 kg (+19%)</td></tr><tr><td>Price w/o stand</td><td class="has-text-align-center" data-align="center"><s>$3,299</s> $2,899 US</td><td class="has-text-align-center" data-align="center">$4,999 US (<s>+52%</s> +72%)</td></tr><tr><td>Price w/o stand w/ nano texture</td><td class="has-text-align-center" data-align="center"><s>$3,599</s> $3,199 US</td><td class="has-text-align-center" data-align="center">$5,999 US (<s>+67%</s> +88%)</td></tr></tbody></table></figure>



<p>All in all… meh.</p>



<p>28% fewer pixels for <s>34%</s> 42% fewer dollars (47% if you&#8217;re talking nano-textured) &#8211; so technically better value, if you don&#8217;t really care about screen real-estate. But that extra real estate is really valuable, <em>and</em> Apple have now apparently ceded the large display market to… well, mostly the tumbleweeds. Sure, <a href="https://wadetregaskis.com/6k-display-comparison/" data-type="post" data-id="8747" data-wpel-link="internal">there&#8217;s <em>technically</em> other 6k displays</a>, like the <a href="https://www.lg.com/us/monitors/lg-32u990a-s-ultrafine-monitor" data-wpel-link="external" target="_blank" rel="external noopener">LG</a>, the <a href="https://www.dell.com/en-us/shop/dell-ultrasharp-32-6k-monitor-u3224kb/apd/210-bhbz/monitors-monitor-accessories" data-wpel-link="external" target="_blank" rel="external noopener">Dell</a>, or the <a href="https://shop.asus.com/us/90lm0bd0-b01kb2-proart-display-6k-pa32qcv.html" data-wpel-link="external" target="_blank" rel="external noopener">Asus</a>, but while they have some advantages &#8211; less than half the price, most notably &#8211; they have real big disadvantages &#8211; like low brightness and poor contrast ratios.</p>



<p>4⨉ the backlight zones is a significant improvement, I&#8217;ll grant Apple that.  But it doesn&#8217;t eliminate the blooming that was problematic with the Pro Display XDR, merely reduces it.  In an era of OLED displays &#8211; hell, my old LG TV from nearly a decade ago has an OLED display; this ain&#8217;t new technology &#8211; a brand new, ostensibly-high-end &#8220;studio&#8221; display still running on LED backlighting is just sad.</p>



<p>The extra brightness of the Studio Display XDR is merely nice &#8211; an extra 25% isn&#8217;t a big difference (certainly nothing like the +200% or so in going from a competing 6k display to the Apple Pro Display XDR).  Props to Apple for the improvement, but it&#8217;s minor.</p>



<p>It&#8217;s interesting that the new, smaller, lower-resolution Studio Display XDR is nearly the same spatial volume as its big sister (and not as much lighter as its reduced resolution and screen dimensions would suggest).  I wonder if that&#8217;s dictated by thermals?</p>



<p>I didn&#8217;t bother including the audio &amp; camera aspects because I&#8217;m genuinely confused as to who, in the market for an expensive display, would care?  If you&#8217;re doing photography there&#8217;s no sound anyway, and if you&#8217;re doing videography in this price range you should be using real speakers or headphones.</p>



<p>And the camera… sigh… I miss the <a href="https://paulstamatiou.com/gear/apple-isight" data-wpel-link="external" target="_blank" rel="external noopener">iSight</a>, which gave you a much better camera &#8211; thanks to physics &#8211; that you could <em>optionally</em> buy and use.  And look at it &#8211; it was a beautiful design that functioned <em>really well</em>, that would have fit in perfectly with the Pro Display XDR!</p>



<p>I&#8217;m also choosing to overlook the firmware, which I assume uses the same weird, bastardised, glitchy version of iOS as the prior Studio Display model.</p>



<p>After <em>more than six years</em>, I was hoping for an improved Pro Display XDR, not merely a small version.</p>


<ol class="wp-block-footnotes"><li id="e8feb3fe-feb2-4a81-83f9-5562e245b289">Only when used with Macs which support DSC (Display Stream Compression), otherwise the USB-C ports are limited to USB 2.0 (400 Mb/s). <a href="#e8feb3fe-feb2-4a81-83f9-5562e245b289-link" aria-label="Jump to footnote reference 1">↩︎</a></li></ol>]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/studio-display-xdr-vs-pro-display-xdr/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">8738</post-id>	</item>
		<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>Extracting embedded images from a PDF</title>
		<link>https://wadetregaskis.com/extracting-embedded-images-from-a-pdf/</link>
					<comments>https://wadetregaskis.com/extracting-embedded-images-from-a-pdf/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Sun, 18 Jan 2026 18:50:33 +0000</pubDate>
				<category><![CDATA[Howto]]></category>
		<category><![CDATA[Acrobat Reader]]></category>
		<category><![CDATA[Apple Preview]]></category>
		<category><![CDATA[AVIF]]></category>
		<category><![CDATA[File Juicer]]></category>
		<category><![CDATA[ImageMagick]]></category>
		<category><![CDATA[PDF]]></category>
		<category><![CDATA[pdfimages]]></category>
		<category><![CDATA[pdftoppm]]></category>
		<category><![CDATA[Poppler]]></category>
		<category><![CDATA[PPM]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[StuffIt]]></category>
		<category><![CDATA[The Unarchiver]]></category>
		<category><![CDATA[Zip]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8651</guid>

					<description><![CDATA[Surprisingly, the best way (that I&#8217;ve found) to do this is to use The Unarchiver, a free app from MacPaw (the folks behind SetApp and many other things). It seems to faithfully extract the images as-is, including ICC profiles (which might technically be separate from the image within the PDF, but nonetheless are crucial to&#8230; <a class="read-more-link" href="https://wadetregaskis.com/extracting-embedded-images-from-a-pdf/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>Surprisingly, the best way (that I&#8217;ve found) to do this is to use <a href="https://theunarchiver.com" data-wpel-link="external" target="_blank" rel="external noopener">The Unarchiver</a>, a free app from <a href="https://macpaw.com" data-wpel-link="external" target="_blank" rel="external noopener">MacPaw</a> (the folks behind <a href="https://setapp.com" data-wpel-link="external" target="_blank" rel="external noopener">SetApp</a> and many other things).  It seems to faithfully extract the images <em>as-is</em>, including ICC profiles (which might technically be separate from the image within the PDF, but nonetheless are crucial to the image being extracted correctly).</p>



<p>The primary reason to extract the images <em>exactly</em> as is, bit-for-bit-identical, is that they&#8217;re typically already lossy-compressed (usually JPEG).  Recompressing them will introduce further losses or increase the file size<sup data-fn="548bb9e0-6fb6-4e03-a88c-0726bfb453fa" class="fn"><a href="#548bb9e0-6fb6-4e03-a88c-0726bfb453fa" id="548bb9e0-6fb6-4e03-a88c-0726bfb453fa-link">1</a></sup>, or both.</p>



<p>Kudos to <a href="https://apple.stackexchange.com/users/97368/josef-habr" data-wpel-link="external" target="_blank" rel="external noopener">Josef Habr</a> for <a href="https://apple.stackexchange.com/a/477392/273188" data-wpel-link="external" target="_blank" rel="external noopener">suggesting The Unarchiver on StackExchange</a> &#8211; I would never have found it on my own, even though I already had it installed and use it occasionally (for more traditional archive file formats, like Zip or StuffIt).</p>



<p>Frustratingly, Josef&#8217;s post aside, none of the recommendations you read online mention The Unarchiver, pointing instead to other options which are harder to install, harder to use, and don&#8217;t extract the images correctly.  Worst of all, many people falsely claim that their suggested approach will extract the images losslessly.  Examples include:</p>



<ul class="wp-block-list">
<li>pdfimages from <a href="https://poppler.freedesktop.org" data-wpel-link="external" target="_blank" rel="external noopener">Poppler</a> &#8211; silently re-encodes images in some cases (contrary to what its documentation and users claim), such as if they have non-sRGB colour profiles, <em>and</em> fails to preserve the embedded ICC profile.  Worse, <a href="https://gitlab.freedesktop.org/poppler/poppler/-/issues/526" data-wpel-link="external" target="_blank" rel="external noopener">the developers have known about this for nearly a decade and refuse to fix it</a>.</li>



<li>pdftoppm (et al) &#8211; explicitly convert the embedded images into another format, which while usually a lossless format (e.g. PNG or PPM) by default, still requires you to then re-encode them for use online etc.  Plus, they typically don&#8217;t preserve ICC profiles.</li>



<li><a href="https://imagemagick.org" data-wpel-link="external" target="_blank" rel="external noopener">ImageMagick</a> &#8211; doesn&#8217;t extract the images, merely renders the whole PDF page(s) as images, requiring further post-processing <em>and</em> inevitably reducing the image quality (due to mismatched output resolution and pixel alignment vs the embedded images&#8217;).</li>



<li>Exporting pages as images from Preview or Acrobat Reader &#8211; obviously doesn&#8217;t preserve the extracted images as-is, requires re-encoding them with additional compression losses, etc.</li>



<li>Screenshots via Preview or Acrobat Reader &#8211; ugh, I can&#8217;t even.</li>



<li>Various websites &#8211; I mean, they <em>might</em> work, but why upload your personal data to some skeezy website when it&#8217;s easy and fast to just use The Unarchiver locally?</li>
</ul>



<p>I saw a recommendation for <a href="https://echoone.com/filejuicer/" data-wpel-link="external" target="_blank" rel="external noopener">File Juicer</a>, but unfortunately the free trial doesn&#8217;t work for me &#8211; it claims it&#8217;s already expired &#8211; so I was unable to check that it actually works.  Plus, it&#8217;s not free (USD$19 at time of writing) so that&#8217;s a strong disincentive compared to The Unarchiver.</p>


<ol class="wp-block-footnotes"><li id="548bb9e0-6fb6-4e03-a88c-0726bfb453fa">It <em>is</em> possible, with some newer formats like AVIF, to recompress a JPEG in a way that arguably <em>improves</em> the image quality while also reducing the file size.  AVIF encoders typically have some built-in smarts to recognise JPEG artefacts specifically, and try to remove them &#8211; a direct benefit visually since the artefacts are ugly and a benefit to [re]compression since the encoder then doesn&#8217;t need to waste time &amp; output bits trying to preserve the artefacts.<br><br>But, utilising this feature can require more care during compression to find the right trade-offs and ensure the result <em>is</em> in fact as good or better than the original &#8211; and in any case, AVIF recompression will work better from the original JPEG than a mangled version. <a href="#548bb9e0-6fb6-4e03-a88c-0726bfb453fa-link" aria-label="Jump to footnote reference 1">↩︎</a></li></ol>]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/extracting-embedded-images-from-a-pdf/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">8651</post-id>	</item>
		<item>
		<title>Backblaze seemingly does not support files greater than 1 TB</title>
		<link>https://wadetregaskis.com/backblaze-seemingly-does-not-support-files-greater-than-1-tb/</link>
					<comments>https://wadetregaskis.com/backblaze-seemingly-does-not-support-files-greater-than-1-tb/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Thu, 02 Jan 2025 23:27:59 +0000</pubDate>
				<category><![CDATA[Ramblings]]></category>
		<category><![CDATA[Backblaze]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[Undocumented]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8475</guid>

					<description><![CDATA[For nearly a month now, Backblaze has been fixated on a particular file of mine, that happens to be over 1 TB in size. Backblaze seemingly uploads it completely, but then on the next backup it uploads it again, even though it has not changed (in eight years!). Ad infinitum. Using their Explainfile tool to&#8230; <a class="read-more-link" href="https://wadetregaskis.com/backblaze-seemingly-does-not-support-files-greater-than-1-tb/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>For nearly a month now, Backblaze has been fixated on a particular file of mine, that happens to be over 1 TB in size.  Backblaze seemingly uploads it completely, but then on the next backup it uploads it again, even though it has not changed (in eight years!).  Ad infinitum.</p>



<p>Using their <a href="https://www.backblaze.com/computer-backup/docs/use-explainfile-to-diagnose-backup-issues-mac" data-wpel-link="external" target="_blank" rel="external noopener">Explainfile</a> tool to dig into the log files, the clue seems to be:</p>



<pre class="wp-block-preformatted">  - line 288 - 2024-12-16 16:16:17 0000000646 - ERROR: UpdateBzDoneRegardingFlsToBeExp - Z_B_TOO_MANY_CHUNKS bz_done_ line chunk related, numBytesInLargeFile=1099512156951, totNumChunks=104858, bz_done_line_is: 5	! …<br>  - line 522 - 2024-12-16 16:18:46 0000000646 - ERROR - bz_done_ INCONSISTENCY_FOUND - 20241216161846 - BadBadBadChunkRecord hexAsciiVal=0x78 - AfterBzdoneLargeFileAnalysis: chunkSeq=100001, highestChunkSeqSeen=104857, fileIdOfLargeFile=00000000002c53cd, dateTimeOfLargeFile=20231217091843, XYXBXXX_FILE_NAME: …</pre>



<p>Admittedly I&#8217;m guessing somewhat, since that&#8217;s a rather reader-hostile log message, but the combination of the <code><strong>Z_B_TOO_MANY_CHUNKS</strong></code> error mnemonic and <code><strong>chunkSeq=100001</strong></code> (because of its proximity to the arbitrary round number 100,000) strongly suggests that Backblaze is imposing a 100,000 chunk limit. Since <a href="https://www.backblaze.com/computer-backup/docs/file-sizes" data-wpel-link="external" target="_blank" rel="external noopener">chunks are 10 MB each</a>, that&#8217;s exactly 1 TB.</p>



<p>This is unequivocally at odds with what they claim repeatedly on their website, on pages like <a href="https://www.backblaze.com/cloud-backup/features/what-gets-backed-up" data-wpel-link="external" target="_blank" rel="external noopener">What Backblaze Backs Up</a> and <a href="https://www.backblaze.com/computer-backup/docs/file-sizes" data-wpel-link="external" target="_blank" rel="external noopener">File Sizes</a>.</p>



<p>It&#8217;s not clear to me why this is suddenly a problem; is this a newly-imposed limit?  It&#8217;s possible that a month ago I removed some exclusion on the file, but I don&#8217;t remembering doing that and I can see no reason why I would have excluded it to begin with.  If it is newly imposed, that would imply it&#8217;s also <em>retroactive</em> &#8211; that Backblaze actually <em>deleted</em> the existing backup of the file from their servers, thus causing the client app to try uploading it again.</p>



<p>I reached out to their technical support, of course, but thus far have only received mindless responses &#8211; restart your computer, reinstall Backblaze, etc.</p>



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



<p>I received no further response from Backblaze&#8217;s technical support.  They asked me to send them the log files, which I did on January 2nd, 2025, and they never responded again.</p>



<p>As of this update (February 4th 2025) their website still falsely advertises support for files of any size.</p>



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



<p>I was surprised to see that many folks <a href="https://news.ycombinator.com/item?id=42930786" data-wpel-link="external" target="_blank" rel="external noopener">on HackerNews</a> were surprised by the idea of a 1 TiB file.  I certainly agree that&#8217;s large, but it doesn&#8217;t seem unusual or inexplicable to me.  In my case, this particular &#8220;problem&#8221; file is an encrypted, compressed disk image of the boot drive of a prior computer, that I saved when I upgraded to my current computer.</p>



<p>It&#8217;s true that I could <em>probably</em> throw it out at this point &#8211; it was just a precaution in case I forgot to migrate something over, so now (eight years later) it seems that either I made no such mistake or whatever I forgot to migrate doesn&#8217;t matter anyway.  For now I&#8217;ve just manually excluded it from the backup, to work around Backblaze&#8217;s bugs.</p>



<p>There are other cases in which I&#8217;ve had files over 1 TiB, though &#8211; e.g. video files:</p>



<ul class="wp-block-list">
<li>With some cameras and recording modes (e.g. documentarian, interviews) it&#8217;s in principle easy to exceed 1 TiB per file.  e.g. the <a href="https://onlinemanual.nikonimglib.com/z9/en/06_video_recording_02.html#:~:text=Approx.%205780%C2%A0Mbps" data-wpel-link="external" target="_blank" rel="external noopener">Nikon Z9 &amp; Z8 record around 700 MB/s for 8k60 N-RAW</a>, which is about 24 minutes per TiB.<br><br>Note that I don&#8217;t recall if I personally have ever actually exceeded 1 TiB this way.  I mention it mainly for illustration.  It&#8217;s also possible that the Z9 &amp; Z8 shard large recordings into multiple files (I don&#8217;t recall seeing this in years &#8211; not since the 4 GiB per-file limit of cameras a decade ago &#8211; but perhaps I&#8217;ve just not had a single recording large enough).</li>



<li>Usually (for me) it&#8217;s output files that are largest, since they can combine many clips.  I use Final Cut Pro and its video compression capabilities aren&#8217;t great, so I export essentially lossless ProRes and then use ffmpeg or Handbrake for the real compression.  <a href="https://www.apple.com/final-cut-pro/docs/Apple_ProRes.pdf#page=21" data-wpel-link="external" target="_blank" rel="external noopener">ProRes 422 HQ is nearly a gigabyte per second for 8k60</a>, so it takes less than twenty minutes of video to exceed 1 TiB.  Fortunately these large intermediaries only have to live as long as the final compression takes (though that can be days, especially with the latest formats like AV1).</li>
</ul>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/backblaze-seemingly-does-not-support-files-greater-than-1-tb/feed/</wfw:commentRss>
			<slash:comments>4</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2025/01/backblazes-marketing-claims.avif" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">8475</post-id>	</item>
		<item>
		<title>&#8220;Import from iPhone or iPad&#8221; doesn&#8217;t work when any view contains a SwiftUI Toggle</title>
		<link>https://wadetregaskis.com/import-from-iphone-or-ipad-doesnt-work-when-any-view-contains-a-swiftui-toggle/</link>
					<comments>https://wadetregaskis.com/import-from-iphone-or-ipad-doesnt-work-when-any-view-contains-a-swiftui-toggle/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Thu, 22 Aug 2024 23:22:42 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[Continuity Camera]]></category>
		<category><![CDATA[Import from iPhone or iPad]]></category>
		<category><![CDATA[importableFromServices]]></category>
		<category><![CDATA[ImportFromDevicesCommands]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[SwiftUI]]></category>
		<category><![CDATA[Toggle]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8371</guid>

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



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


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


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



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



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



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



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



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



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



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



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



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



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

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



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



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



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



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



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

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



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



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



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



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



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



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



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



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



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



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



<p>Even though Apple&#8217;s initial response to this bug report hasn&#8217;t been all that fruitful, I do want to emphasise the fact that they <em>did</em> respond, which was a pleasant surprise and very much appreciated.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/nspasteboard-crashes-due-to-unsafe-internal-concurrent-memory-mutation-when-handling-file-promises/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">8369</post-id>	</item>
		<item>
		<title>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>Red Light</title>
		<link>https://wadetregaskis.com/red-light/</link>
					<comments>https://wadetregaskis.com/red-light/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Fri, 16 Aug 2024 20:26:15 +0000</pubDate>
				<category><![CDATA[Ancient History]]></category>
		<category><![CDATA[News]]></category>
		<category><![CDATA[20% time]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[Green Light]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[traffic lights]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8347</guid>

					<description><![CDATA[Famously, Google used to have a practice dubbed &#8220;20% time&#8221;: about one day a week, engineers were {expected ⊻ encouraged ⊻ permitted ⊻ tolerated ⊻ known} to work on something other than their nominal work; something they themselves chose. Circa 2011, not long after I joined Google, I pitched a 20% project idea to my&#8230; <a class="read-more-link" href="https://wadetregaskis.com/red-light/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>Famously, Google used to have a practice dubbed &#8220;20% time&#8221;:  about one day a week, engineers were {expected ⊻ encouraged ⊻ permitted ⊻ tolerated ⊻ known}<sup data-fn="165dbbbb-8d84-499a-bb2a-deb31021a0b5" class="fn"><a href="#165dbbbb-8d84-499a-bb2a-deb31021a0b5" id="165dbbbb-8d84-499a-bb2a-deb31021a0b5-link">1</a></sup> to work on something other than their nominal work; something they themselves chose.</p>



<p>Circa 2011, not long after I joined Google, I pitched a 20% project idea to my boss.  In a nutshell, I wanted to optimise traffic light systems to improve traffic throughput.  My motivation was to reduce wasted fuel &#8211; and therefore pollution and cost to drivers.  Granted I didn&#8217;t have much more than the goal at that point &#8211; with some ideas about starting with simulations and models, and working with the Google Maps &amp; Streetview teams, if I recall correctly.  In any case, my boss said no.  That wasn&#8217;t relevant to Google nor something Google was interested in, I was told.</p>



<p>Well, apparently <em>now</em> it is &#8211; Google has a publicly disclosed project &#8220;<a href="https://sites.research.google/greenlight/" data-wpel-link="external" target="_blank" rel="external noopener">Green Light</a>&#8221; to do exactly that.  Evidently as far more than merely a 20% project (although I&#8217;m curious if that&#8217;s how it started).</p>



<p>I always felt the denial by management was misguided.  It seemed at odds with official company policy.  But, contrary to what this post might seem to imply, it didn&#8217;t weigh on me.  I was a bit miffed, but largely forgot about it until I happened to <a href="https://www.scientificamerican.com/article/googles-project-green-light-uses-ai-to-take-on-city-traffic/" data-wpel-link="external" target="_blank" rel="external noopener">hear about Green Light</a> and was reminded.  Now it&#8217;s perhaps a <a href="https://en.wikipedia.org/wiki/Sliding_doors_moment" data-wpel-link="external" target="_blank" rel="external noopener">sliding doors</a> thought exercise.</p>



<p>Executive decrees, employee handbooks, even public promises, are all weak against undermining management.</p>



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



<p><a href="https://www.flickr.com/photos/martinrp/376595728/" data-wpel-link="external" target="_blank" rel="external noopener">Canary Wharf traffic lights photo</a> by <a href="https://www.flickr.com/photos/martinrp/" data-wpel-link="external" target="_blank" rel="external noopener">Martin Pearce</a>, <a href="https://creativecommons.org/licenses/by-nd/2.0/" data-wpel-link="external" target="_blank" rel="external noopener">CC BY-ND 2.0</a>.</p>


<ol class="wp-block-footnotes"><li id="165dbbbb-8d84-499a-bb2a-deb31021a0b5">The ambiguity there reflects the wildly differing opinions I encountered while at Google starting in 2010.  Though there was some official guidance about 20% time (e.g. in the employee handbook), asserting in writing that it did in fact exist and was supposedly encouraged, in my first- and second-hand experience management were not at all keen on it.  I met few Googlers who actually worked on a 20% project, and some kept it secret from management. <a href="#165dbbbb-8d84-499a-bb2a-deb31021a0b5-link" aria-label="Jump to footnote reference 1">↩︎</a></li></ol>]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/red-light/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2024/08/canary-wharf-traffic-lights-by-martin-pearce.avif" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">8347</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>NSCopyObject, the griefer that keeps on griefing</title>
		<link>https://wadetregaskis.com/nscopyobject-the-griefer-that-keeps-on-griefing/</link>
					<comments>https://wadetregaskis.com/nscopyobject-the-griefer-that-keeps-on-griefing/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Tue, 16 Jul 2024 00:42:07 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[ARC]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[copy(with:)]]></category>
		<category><![CDATA[fixupCopiedIvars]]></category>
		<category><![CDATA[NeXT]]></category>
		<category><![CDATA[NSAnimation]]></category>
		<category><![CDATA[NSCell]]></category>
		<category><![CDATA[NSCopying]]></category>
		<category><![CDATA[NSImageRep]]></category>
		<category><![CDATA[Objective-C]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[Swift]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8281</guid>

					<description><![CDATA[NSCopyObject is a very old Foundation function - pre-dating Mac OS X entirely; from the NeXT era - that was originally basically just memcpy, but now it's complicated. A lot more complicated… <a class="read-more-link" href="https://wadetregaskis.com/nscopyobject-the-griefer-that-keeps-on-griefing/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p><code><a href="https://developer.apple.com/documentation/foundation/1587928-nscopyobject" data-wpel-link="external" target="_blank" rel="external noopener">NSCopyObject</a></code> is a very old Foundation function &#8211; pre-dating Mac OS X entirely; from the NeXT era &#8211; that was <em>originally</em> basically just <code><a href="https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/memcpy.3.html" data-wpel-link="external" target="_blank" rel="external noopener">memcpy</a></code>, but now it&#8217;s complicated.  A lot more complicated.</p>



<h2 class="wp-block-heading">What <code>NSCopyObject</code> does</h2>



<p>Its implementation <em>currently</em> starts with essentially:</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="id NSCopyObject(id object, NSUInteger extraBytes, NSZone *zone) {
    if (nil == object) {
        return nil;
    }
    
    id copy = object_copy(object, extraBytes);
    object_setClass(copy, objc_opt_class(object));
    return copy;
}" 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: #0000FF">id</span><span style="color: #000000"> </span><span style="color: #795E26">NSCopyObject</span><span style="color: #000000">(</span><span style="color: #0000FF">id</span><span style="color: #000000"> object, </span><span style="color: #267F99">NSUInteger</span><span style="color: #000000"> extraBytes, </span><span style="color: #267F99">NSZone</span><span style="color: #000000"> *zone) {</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">nil</span><span style="color: #000000"> == object) {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #AF00DB">return</span><span style="color: #000000"> </span><span style="color: #0000FF">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">    </span><span style="color: #0000FF">id</span><span style="color: #000000"> copy = </span><span style="color: #795E26">object_copy</span><span style="color: #000000">(object, extraBytes);</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">object_setClass</span><span style="color: #000000">(copy, </span><span style="color: #795E26">objc_opt_class</span><span style="color: #000000">(object));</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #AF00DB">return</span><span style="color: #000000"> copy;</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>…where <a href="https://github.com/apple-oss-distributions/objc4/blob/01edf1705fbc3ff78a423cd21e03dfc21eb4d780/runtime/objc-runtime-new.mm#L9057" data-wpel-link="external" target="_blank" rel="external noopener">object_copy</a> et al are part of <a href="https://github.com/apple-oss-distributions/objc4" data-wpel-link="external" target="_blank" rel="external noopener">the Objective-C runtime</a>.  <code>object_copy</code> and its callees are not trivial, so I won&#8217;t repeat them here.  The key parts are:</p>



<ol class="wp-block-list">
<li>The malloc in <code><a href="https://github.com/apple-oss-distributions/objc4/blob/01edf1705fbc3ff78a423cd21e03dfc21eb4d780/runtime/objc-runtime-new.mm#L8981" data-wpel-link="external" target="_blank" rel="external noopener">_class_createInstance</a></code> (allocate space for the copy).</li>



<li>The <code>memmove</code> in <code>object_copy</code> (naively copy the raw bytes over).</li>



<li>The call from <code>object_copy</code> to <code><a href="https://github.com/apple-oss-distributions/objc4/blob/01edf1705fbc3ff78a423cd21e03dfc21eb4d780/runtime/objc-class.mm#L535" data-wpel-link="external" target="_blank" rel="external noopener">fixupCopiedIvars</a></code> (half-heartedly attempt to fix the damage).</li>
</ol>



<p><code>fixupCopiedIvars</code> is notable.  It was added by necessity when <a href="https://en.wikipedia.org/wiki/Automatic_Reference_Counting" data-wpel-link="external" target="_blank" rel="external noopener">ARC</a> was introduced to the Objective-C runtime, in Mac OS X 10.6 (Snow Leopard) in 2006.  ARC added metadata to Objective-C classes to convey which instance variables were retain-counted object references, so that it could manage them automagically at runtime (not just for copying objects, but more importantly for deallocating them).  <code>fixupCopiedIvars</code> uses that metadata to identify things it has to retain (strongly or weakly) in the new copy.</p>



<p>So that should work great, right?  The copy operation increments the retain count of all shared objects the new copy references, like you&#8217;d expect?</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img decoding="async" width="340" height="266" src="https://wadetregaskis.com/wp-content/uploads/2024/07/grumpy-cat-no.avif" alt="Grumpy Cat frowning, with the caption &quot;NO&quot;." class="wp-image-8282" srcset="https://wadetregaskis.com/wp-content/uploads/2024/07/grumpy-cat-no.avif 340w, https://wadetregaskis.com/wp-content/uploads/2024/07/grumpy-cat-no-256x200.avif 256w, https://wadetregaskis.com/wp-content/uploads/2024/07/grumpy-cat-no@2x.avif 680w" sizes="(max-width: 340px) 100vw, 340px" /></figure>
</div>


<p>That metadata is incomplete.  It only works for Objective-C ivars managed by ARC.  i.e. <em>not</em> C++ ivars or Swift stored properties, nor even Objective-C ivars that aren&#8217;t using ARC<sup data-fn="cf3bfb82-cbf6-40c4-817f-3092a63f4021" class="fn"><a href="#cf3bfb82-cbf6-40c4-817f-3092a63f4021" id="cf3bfb82-cbf6-40c4-817f-3092a63f4021-link">1</a></sup>.</p>



<h2 class="wp-block-heading">But I don&#8217;t use <code>NSCopyObject</code>…?</h2>



<p>Almost nobody <em>intentionally</em> uses <code>NSCopyObject</code>, but your superclass might, and therefore you might.  Ever subclassed <code><a href="https://developer.apple.com/documentation/appkit/nscell" data-wpel-link="external" target="_blank" rel="external noopener">NSCell</a></code> or <code><a href="https://developer.apple.com/documentation/appkit/nsanimation" data-wpel-link="external" target="_blank" rel="external noopener">NSAnimation</a></code>, for example?</p>



<p><a href="https://forums.swift.org/t/why-would-deinit-be-called-when-retain-count-is-non-zero/72924" data-wpel-link="external" target="_blank" rel="external noopener">I happened to hit this</a> when subclassing <code><a href="https://developer.apple.com/documentation/appkit/nsbitmapimagerep" data-wpel-link="external" target="_blank" rel="external noopener">NSBitmapImageRep</a></code> (and I&#8217;m very grateful to <a href="https://forums.swift.org/u/ksluder" data-wpel-link="external" target="_blank" rel="external noopener">Kyle Sluder</a> for so quickly identifying the problem &#8211; it could have taken me forever to figure it out, otherwise).</p>



<p>If your superclass uses <code>NSCopyObject</code>, it&#8217;s now your problem just as much as if you&#8217;d used <code>NSCopyObject</code> directly, whether you like it or not.</p>



<p>And even more problematically, whether you <em>know</em> it or not.  If your superclass is defined by a 3rd party framework / library, or anything that&#8217;s closed source, you might have no idea whether it uses <code>NSCopyObject</code> currently.  Worse, you have no control over whether it will or will not use it in future (though anyone that <em>adds</em> a use of <code>NSCopyObject</code> at this point had better hope the atheists are right).</p>



<h2 class="wp-block-heading">So how do I defend against <code>NSCopyObject</code>?</h2>



<h3 class="wp-block-heading">Objective-C</h3>



<p>Pre-ARC it used to be <em>relatively</em> easy to work around this, in Objective-C.  You &#8220;just&#8221; had to manually <code><a href="https://developer.apple.com/documentation/objectivec/1418956-nsobject/1571946-retain?language=objc" data-wpel-link="external" target="_blank" rel="external noopener">retain</a></code> all your subclasses&#8217; reference ivars &#8211; and manually copy some others, like non-ref-counted mutable or mortal buffers, etc.</p>



<p>But that generally isn&#8217;t possible with ARC &#8211; under which you cannot explicitly call <code>retain</code>.  Worse:</p>



<ul class="wp-block-list">
<li>There&#8217;s still <a href="https://www.mikeash.com/pyblog/friday-qa-2010-08-27-defensive-programming-in-cocoa.html" data-wpel-link="external" target="_blank" rel="external noopener">prominent</a> guides scattered about the web that push you unequivocally to use <code>retain</code>, which is not just impossible to do directly under ARC, but flat-out <em>wrong</em> even if you do figure out one of the &#8220;clever&#8221; ways to do it (you&#8217;ll end up <em>over</em>-retaining your ARC-managed references, causing memory leaks).</li>



<li>There&#8217;s also <a href="https://dohle.wordpress.com/2012/05/21/hello-world/" data-wpel-link="external" target="_blank" rel="external noopener">pages</a> lingering on the web that claim that merely turning on ARC will magically solve the problem (it <em>might</em>, but it&#8217;s not a panacea).</li>
</ul>



<p><a href="https://wiki.herzbube.ch/index.php/LearningObjectiveC#Object_copy" data-wpel-link="external" target="_blank" rel="external noopener">Some</a> <a href="https://robnapier.net/implementing-nscopying" data-wpel-link="external" target="_blank" rel="external noopener">guides</a> specify a better method, which is to manually zero out the copied object&#8217;s ivars and then repopulate them via formal property setters.  That actually works with or without ARC, although it may break &#8211; causing memory leaks &#8211; if the superclass ever stops using <code>NSCopyObject</code> (or if <code>NSCopyObject</code> ever gets upgraded to understand reference-counted ivars that it currently does not).  It&#8217;s also only possible in Objective-C because Swift doesn&#8217;t provide direct access to instance variables.</p>



<p>Keep in mind that any reference-typed ivars which are not strong or weak Objective-C objects managed by ARC will still, always need to be handled manually.  e.g. pointers to manually-managed memory buffers.</p>



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



<p>Ironically, Swift&#8217;s attempts to prevent incorrect code actually make it harder to write correct code in this case.  What you <em>want</em> to do is &#8211; like the Objective-C implementation &#8211; to just zero out the references and re-assign them like normal properties.  Zeroing them out <em>without triggering a release</em> basically undoes the mistaken <code>memcpy</code> that <code>NSCopyObject</code> did.  But Swift won&#8217;t let you.</p>



<p>Worse, <a href="https://github.com/swiftlang/swift/issues/47333" data-wpel-link="external" target="_blank" rel="external noopener">this has been known</a> for most of Swift&#8217;s existence and nothing has been done about it.</p>



<p>Simply setting the property to <code>nil</code> will cause it to be erroneously released, which may immediately deallocate the object and ultimately cause a crash or memory corruption.  Even if it doesn&#8217;t happen to deallocate the object, it&#8217;ll negate the retain you do during the assignment, making all your effort moot.</p>



<figure class="wp-block-pullquote"><blockquote><p>Strictly-speaking, the only safe thing to do is override <code><a href="https://developer.apple.com/documentation/foundation/nscopying/1410311-copy" data-wpel-link="external" target="_blank" rel="external noopener">copy(with:)</a></code> and not call super, but rather create a new instance from scratch.</p></blockquote></figure>



<p>That&#8217;s pretty heavy-handed, though, and not always possible (e.g. <code>NSImageRep</code>, as used by e.g. <code>NSBitmapImageRep</code>, does some special magic in its copy implementation which you cannot practically replicate).</p>



<p>It appears that the best you can do is <em>assume</em> the superclass will always use <code>NSCopyObject</code>, if it does currently, and just manually increment the retain count.  Like Objective-C with ARC, the language &amp; standard library really don&#8217;t want you to actually do this, but at least in Swift it&#8217;s relatively straightforward:</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="override func copy(with zone: NSZone? = nil) -&gt; Any {
    let result = super.copy(with: zone)
    
    if result.myProperty === self.myProperty {
        _ = Unmanaged.passRetained(myProperty)
    } else {
        result.myProperty = self.myProperty
    }
}" 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: #0000FF">override</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">copy</span><span style="color: #000000">(</span><span style="color: #795E26">with</span><span style="color: #000000"> </span><span style="color: #001080">zone</span><span style="color: #000000">: NSZone? = </span><span style="color: #0000FF">nil</span><span style="color: #000000">) -&gt; </span><span style="color: #267F99">Any</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"> result = </span><span style="color: #0000FF">super</span><span style="color: #000000">.</span><span style="color: #795E26">copy</span><span style="color: #000000">(</span><span style="color: #795E26">with</span><span style="color: #000000">: zone)</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"> result.</span><span style="color: #001080">myProperty</span><span style="color: #000000"> === </span><span style="color: #0000FF">self</span><span style="color: #000000">.</span><span style="color: #001080">myProperty</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #001080">_</span><span style="color: #000000"> = </span><span style="color: #267F99">Unmanaged</span><span style="color: #000000">.</span><span style="color: #795E26">passRetained</span><span style="color: #000000">(myProperty)</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">        result.</span><span style="color: #001080">myProperty</span><span style="color: #000000"> = </span><span style="color: #0000FF">self</span><span style="color: #000000">.</span><span style="color: #001080">myProperty</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>The conditional <em>might</em> help protect you if the superclass stops using <code>NSCopyObject</code> in future &#8211; in that case, it&#8217;ll <em>probably</em> cause <code>myProperty</code> to default to nil (or to be assigned to some other instance, which you can discard), in which case you just want to assign to it normally.</p>



<p>In the interim &#8211; while <code>NSCopyObject</code> is in use, at least &#8211; the <code>myProperty</code> pointer will be copied verbatim and you have to assume it requires the extra, manual retain.  It&#8217;s <em>not</em> future-proof &#8211; it&#8217;s possible for the superclass to copy the pointer verbatim <em>and</em> increment the retain count for you &#8211; but at least in that case you &#8220;merely&#8221; get a memory leak, rather than a crash or memory corruption.</p>



<h2 class="wp-block-heading">Do as Apple says, not as Apple does</h2>



<p>The most frustrating part of all of this is that this is entirely Apple&#8217;s fault.  Sure, you can argue it&#8217;s not their fault that NeXT added this vile function to Foundation; that Apple &#8220;merely&#8221; inherited it and were &#8220;forced&#8221; to keep for backwards compatibility.  But it&#8217;s <em>entirely</em> Apple&#8217;s choice to have kept using it all this time, in their core frameworks, even while they&#8217;ve been telling everyone else to never use it.</p>



<p><code>NSCopyObject</code> has been a known problem-maker pretty much forever &#8211; it was a terrible idea right from the outset.  Blindly copying the bytes of an object instance, and just hoping that somehow that works correctly &#8211; in an <em>object-oriented</em> language derived from Smalltalk where <a href="https://developer.apple.com/documentation/foundation/nsnumber" data-wpel-link="external" target="_blank" rel="external noopener">even numbers are often reference types</a> &#8211; is farcical.</p>



<p>The introduction of ARC (in 2008) didn&#8217;t really help anything, as although it changed <code>NSCopyObject</code> to properly retain <em>ARC</em>-managed ivars, it did nothing for non-ARC-managed ivars (remember that ARC can be enabled in one library but not in another, and libraries can subclass each others&#8217; classes).</p>



<p><code>NSCopyObject</code> has been officially deprecated since 2012:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>The NSCopyObject() function has been deprecated. It has always been a dangerous function to use, except in the implementation of copy methods<sup data-fn="19a637fd-3fbb-43d3-a9c0-29896c849e94" class="fn"><a href="#19a637fd-3fbb-43d3-a9c0-29896c849e94" id="19a637fd-3fbb-43d3-a9c0-29896c849e94-link">2</a></sup>, and only then with care.</p>
<cite><a href="https://developer.apple.com/library/archive/releasenotes/Foundation/RN-FoundationOlderNotes/index.html#X10_8Notes" data-wpel-link="external" target="_blank" rel="external noopener">Foundation Release Notes for OS X 10.8 Mountain Lion and iOS 6</a></cite></blockquote>



<p>…though Apple officially told everyone not to use it in 2008:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>This function is dangerous and very difficult to use correctly. It&#8217;s [sic] use as part of -copyWithZone: by any class that can be subclassed, is highly error prone. This function is known to fail for objects with embedded retain count ivars, singletons, and C++ ivars, and other circumstances.</p>
<cite><a href="https://developer.apple.com/library/archive/releasenotes/Foundation/RN-FoundationOlderNotes/index.html#X10_6Notes" data-wpel-link="external" target="_blank" rel="external noopener">Foundation Release Notes for Mac OS X 10.6 Snow Leopard</a></cite></blockquote>



<p>And this was all still a decade or more after it was known that <code>NSCopyObject</code> was fundamentally evil, e.g. <a href="https://www.mulle-kybernetik.com/weblog/2004/argh_wasted_two_hours_on_stupi.html" data-wpel-link="external" target="_blank" rel="external noopener">NSCell</a>, and <a href="https://mail.gnu.org/archive/html/discuss-gnustep/2000-09/msg00097.html" data-wpel-link="external" target="_blank" rel="external noopener">GnuStep&#8217;s broken NSControl</a>.</p>



<p>And yet, Apple <em>still</em> use <code>NSCopyObject</code> themselves <em>to this very day</em>, in their own applications and frameworks &#8211; including major frameworks like AppKit that almost all 3rd party developers rely on.  <code>NSCell</code> is <em>still</em> broken, three decades later, as is <code>NSImage</code> &amp; <code>NSImageRep</code>, and <code>NSAnimation</code>.  Most of those are <em>explicitly designed to be subclassed</em>, despite Apple&#8217;s own very clear instructions to never mix subclassing with <code>NSCopyObject</code>.</p>



<p>Admittedly it&#8217;s not trivial for Apple to remove the <code>NSCopyObject</code> use &#8211; alas, <em>because</em> people have had to code myriad hacky workarounds to it, Apple now has to be careful not to break those workarounds.  That might even preclude fixing the existing code paths; it might require a <em>replacement</em> copy mechanism.  Which leads to…</p>



<h2 class="wp-block-heading">Tangent: NSCopying considered harmful</h2>



<p>The big driver of <code>NSCopyObject</code> use has long been <code><a href="https://developer.apple.com/documentation/foundation/nscopying" data-wpel-link="external" target="_blank" rel="external noopener">NSCopying</a></code>.  Classes that intend to be subclassed &#8211; but also semantically should support copying i.e. <code>NSCopying</code> &#8211; have long been making the mistake of thinking that means using <code>NSCopyObject</code>.  One need only read the NSCopying documentation, even <a href="https://preterhuman.net/macstuff/techpubs/macosx/System/Library/Frameworks/Foundation.framework/Versions/C/Resources/English.lproj/Documentation/Reference/ObjC_classic/Protocols/NSCopying.html" data-wpel-link="external" target="_blank" rel="external noopener">from before Mac OS X was even publicly released</a>, to see how dangerously fragile and error-prone <code>NSCopying</code> has always been.</p>



<p>Compounding the problem is that <code>NSCopying</code> <em>doesn&#8217;t work, by default, on subclasses</em>.  You <em>have</em> to override <code>copy(with:)</code> in every subclass<sup data-fn="712b988a-5cac-484c-9eaf-fc22bc3afc25" class="fn"><a href="#712b988a-5cac-484c-9eaf-fc22bc3afc25" id="712b988a-5cac-484c-9eaf-fc22bc3afc25-link">3</a></sup>, but the compiler does not enforce this, because in Objective-C (and alas Swift) protocol conformance is <em>assumed</em> inherited even when it cannot correctly be without explicit, extra work by the subclass.</p>


<ol class="wp-block-footnotes"><li id="cf3bfb82-cbf6-40c4-817f-3092a63f4021">Yes, it&#8217;s still possible to this day to write Objective-C without using ARC &#8211; <code><a href="https://clang.llvm.org/docs/AutomaticReferenceCounting.html#id8" data-wpel-link="external" target="_blank" rel="external noopener">-fno-objc-arc</a></code> / <code><a href="https://developer.apple.com/documentation/xcode/build-settings-reference#Objective-C-Automatic-Reference-Counting" data-wpel-link="external" target="_blank" rel="external noopener">CLANG_ENABLE_OBJC_ARC</a></code>.  There might even be valid (albeit unfortunate) use-cases for having to do so, such as for performance.<br><br>And even with ARC, it&#8217;s of course possible to have pointers to things which aren&#8217;t <code>NSObject</code>s and therefore aren&#8217;t handled by ARC, such as raw malloc allocations. <a href="#cf3bfb82-cbf6-40c4-817f-3092a63f4021-link" aria-label="Jump to footnote reference 1">↩︎</a></li><li id="19a637fd-3fbb-43d3-a9c0-29896c849e94">This is false and always has been (that it was safe to use in copy methods).  Apple&#8217;s false statements in the deprecation notices may ironically have caused even <em>more</em> instances of people using <code>NSCopyObject</code>. <a href="#19a637fd-3fbb-43d3-a9c0-29896c849e94-link" aria-label="Jump to footnote reference 2">↩︎</a></li><li id="712b988a-5cac-484c-9eaf-fc22bc3afc25">Any and all that add retain-counted ivars, Swift stored properties, or ivars of C++ types that have destructors. <a href="#712b988a-5cac-484c-9eaf-fc22bc3afc25-link" aria-label="Jump to footnote reference 3">↩︎</a></li></ol>]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/nscopyobject-the-griefer-that-keeps-on-griefing/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2024/07/grumpy-cat-no.avif" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">8281</post-id>	</item>
		<item>
		<title>Swift&#8217;s native Clocks are very inefficient</title>
		<link>https://wadetregaskis.com/swifts-native-clocks-are-very-inefficient/</link>
					<comments>https://wadetregaskis.com/swifts-native-clocks-are-very-inefficient/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Fri, 03 May 2024 02:10:07 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Benchmarked]]></category>
		<category><![CDATA[clock_gettime_nsec_np]]></category>
		<category><![CDATA[ContinuousClock]]></category>
		<category><![CDATA[gettimeofday]]></category>
		<category><![CDATA[Inefficient by design]]></category>
		<category><![CDATA[mach_absolute_time]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[SuspendingClock]]></category>
		<category><![CDATA[Swift]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=7990</guid>

					<description><![CDATA[By which I mean, things like ContinuousClock and SuspendingClock. In absolute terms they don&#8217;t have much overhead &#8211; think sub-microsecond for most uses. Which makes them perfectly acceptable when they&#8217;re used sporadically (e.g. only a few times per second). However, if you need to deal with time and timing more frequently, their inefficiency can become&#8230; <a class="read-more-link" href="https://wadetregaskis.com/swifts-native-clocks-are-very-inefficient/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>By which I mean, things like <code><a href="https://developer.apple.com/documentation/swift/continuousclock" data-wpel-link="external" target="_blank" rel="external noopener">ContinuousClock</a></code> and <code><a href="https://developer.apple.com/documentation/swift/suspendingclock" data-wpel-link="external" target="_blank" rel="external noopener">SuspendingClock</a></code>.</p>



<p>In absolute terms they don&#8217;t have much overhead &#8211; think sub-microsecond for most uses. Which makes them perfectly acceptable when they&#8217;re used sporadically (e.g. only a few times per second).</p>



<p>However, if you need to deal with time and timing more frequently, their inefficiency can become a serious bottleneck.</p>



<p>I stumbled into this because of a fairly common and otherwise uninteresting pattern &#8211; throttling UI updates on an I/O operation&#8217;s progress. This might look something 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">Example</span><span style="color: #000000">: View {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">let</span><span style="color: #000000"> bytes: AsyncSequence&lt;</span><span style="color: #267F99">UInt8</span><span style="color: #000000">&gt;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">@State</span><span style="color: #000000"> </span><span style="color: #0000FF">var</span><span style="color: #000000"> byteCount = </span><span style="color: #098658">0</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">        </span><span style="color: #795E26">Text</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;Bytes so far: </span><span style="color: #0000FF">\(</span><span style="color: #000000FF">byteCount.</span><span style="color: #795E26">formatted</span><span style="color: #000000FF">(.</span><span style="color: #795E26">byteCount</span><span style="color: #000000FF">(</span><span style="color: #795E26">style</span><span style="color: #000000FF">: .</span><span style="color: #001080">binary</span><span style="color: #000000FF">))</span><span style="color: #0000FF">)</span><span style="color: #A31515">&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">            .</span><span style="color: #001080">task</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"> unpostedByteCount = </span><span style="color: #098658">0</span></span>
<span class="line"><span style="color: #000000">                </span><span style="color: #0000FF">let</span><span style="color: #000000"> clock = </span><span style="color: #795E26">ContinuousClock</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"> lastUpdate = clock.</span><span style="color: #001080">now</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">                </span><span style="color: #AF00DB">for</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"> byte </span><span style="color: #AF00DB">in</span><span style="color: #000000"> bytes {</span></span>
<span class="line"><span style="color: #000000">                    … </span><span style="color: #008000">// Do something with the byte.</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">                    unpostedByteCount += </span><span style="color: #098658">1</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">                    </span><span style="color: #0000FF">let</span><span style="color: #000000"> now = clock.</span><span style="color: #001080">now</span></span>
<span class="line"><span style="color: #000000">                    </span><span style="color: #0000FF">let</span><span style="color: #000000"> delta = now - lastUpdate</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">                    </span><span style="color: #AF00DB">if</span><span style="color: #000000"> (    delta &gt; .</span><span style="color: #795E26">seconds</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">                         || (    (delta &gt; .</span><span style="color: #795E26">milliseconds</span><span style="color: #000000">(</span><span style="color: #098658">100</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">                              &amp;&amp; </span><span style="color: #098658">1_000_000</span><span style="color: #000000"> &lt;= unpostedByteCount))) {</span></span>
<span class="line"><span style="color: #000000">                        byteCount += unpostedByteCount</span></span>
<span class="line"><span style="color: #000000">                        unpostedByteCount = </span><span style="color: #098658">0</span></span>
<span class="line"><span style="color: #000000">                        lastUpdate = now</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>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>☝️ This isn&#8217;t a complete implementation, as it won&#8217;t update the byte count if the download stalls (since the lack of incoming bytes will mean no iteration on the loop, and therefore no updates even if a full second passes). But it&#8217;s sufficient for demonstration purposes here.</p>



<p>🖐️ Why didn&#8217;t I just use <code><a href="https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Throttle.md" data-wpel-link="external" target="_blank" rel="external noopener">throttle</a></code> from <a href="https://github.com/apple/swift-async-algorithms" data-wpel-link="external" target="_blank" rel="external noopener">swift-async-algorithms</a>? I did, at first, and quickly discovered that its performance is <em>horrible</em>. While I do suspect I can &#8216;optimise&#8217; it to not be atrocious, I haven&#8217;t pursued that as it was easier to just write my own throttling system.</p>
</div></div>



<p>The above seems fairly straightforward, but if you run it and have any non-trivial I/O rate &#8211; even just a few hundred kilobytes per second &#8211; you&#8217;ll find that it saturates an entire CPU core, not just wasting CPU time but limiting the I/O rate severely.</p>



<p>Using a <code>SuspendingClock</code> makes no difference.</p>



<p>In a nutshell, the problem is that Swift&#8217;s <code><a href="https://developer.apple.com/documentation/swift/clock" data-wpel-link="external" target="_blank" rel="external noopener">Clock</a></code> protocol has significant overheads by design<sup data-fn="2f4a7c64-e213-44df-a3da-0e5020545aad" class="fn"><a href="#2f4a7c64-e213-44df-a3da-0e5020545aad" id="2f4a7c64-e213-44df-a3da-0e5020545aad-link">1</a></sup>. If you look at a time profile of code like this, you&#8217;ll see things like:</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img decoding="async" width="900" height="716" src="https://wadetregaskis.com/wp-content/uploads/2024/05/ContinuousClock-overhead.webp" alt="Screenshot of Instruments showing the outline view for a Time Profile, expanded to show dozens of spurious, overhead functions taking up the vast majority of the runtime." class="wp-image-7991" srcset="https://wadetregaskis.com/wp-content/uploads/2024/05/ContinuousClock-overhead.webp 900w, https://wadetregaskis.com/wp-content/uploads/2024/05/ContinuousClock-overhead-256x204.webp 256w, https://wadetregaskis.com/wp-content/uploads/2024/05/ContinuousClock-overhead-768x611.webp 768w, https://wadetregaskis.com/wp-content/uploads/2024/05/ContinuousClock-overhead@2x.webp 1800w, https://wadetregaskis.com/wp-content/uploads/2024/05/ContinuousClock-overhead-256x204@2x.webp 512w" sizes="(max-width: 900px) 100vw, 900px" /></figure>
</div>


<p>That&#8217;s a lot of time wasted in function calls and struct initialisation and type conversion and protocol witnesses and all that guff. The only part that&#8217;s <em>actually</em> retrieving the time is the <code><a href="https://github.com/apple/swift/blob/625436af05b1cf8f1904096530235489daec9dac/stdlib/public/Concurrency/Clock.cpp#L30" data-wpel-link="external" target="_blank" rel="external noopener">swift_get_time</a></code> call (which is just a wrapper over <code><a href="https://www.manpagez.com/man/3/clock_gettime/" data-wpel-link="external" target="_blank" rel="external noopener">clock_gettime</a></code>, which is just a wrapper over <code><a href="https://www.manpagez.com/man/3/clock_gettime_nsec_np/" data-wpel-link="external" target="_blank" rel="external noopener">clock_gettime_nsec_np</a>(CLOCK_UPTIME_RAW)</code>, which is just a wrapper over <code><a href="https://developer.apple.com/documentation/kernel/1462446-mach_absolute_time" data-wpel-link="external" target="_blank" rel="external noopener">mach_absolute_time</a></code>).</p>



<p>I wrote <a href="https://github.com/wadetregaskis/Swift-Benchmarks/blob/main/Benchmarks/Clocks/Clocks.swift" data-wpel-link="external" target="_blank" rel="external noopener">some simple benchmarks of various alternative time-tracking methods</a>, with these results with Swift 5.10 (showing the median runtime of the benchmark, which is a million iterations of checking the time):</p>



<figure class="wp-block-table aligncenter"><table><thead><tr><th class="has-text-align-right" data-align="right">Method</th><th class="has-text-align-center" data-align="center">10-core iMac Pro</th><th class="has-text-align-center" data-align="center">M2 MacBook Air</th></tr></thead><tbody><tr><td class="has-text-align-right" data-align="right"><code><a href="https://developer.apple.com/documentation/swift/continuousclock" data-wpel-link="external" target="_blank" rel="external noopener">ContinuousClock</a></code></td><td class="has-text-align-center" data-align="center">429 ms</td><td class="has-text-align-center" data-align="center">258 ms</td></tr><tr><td class="has-text-align-right" data-align="right"><code><a href="https://developer.apple.com/documentation/swift/suspendingclock" data-wpel-link="external" target="_blank" rel="external noopener">SuspendingClock</a></code></td><td class="has-text-align-center" data-align="center">430 ms</td><td class="has-text-align-center" data-align="center">247 ms</td></tr><tr><td class="has-text-align-right" data-align="right"><code><a href="https://developer.apple.com/documentation/foundation/date" data-wpel-link="external" target="_blank" rel="external noopener">Date</a></code></td><td class="has-text-align-center" data-align="center">30 ms</td><td class="has-text-align-center" data-align="center">19 ms</td></tr><tr><td class="has-text-align-right" data-align="right"><code><a href="https://www.manpagez.com/man/3/clock_gettime_nsec_np/" data-wpel-link="external" target="_blank" rel="external noopener">clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW)</a></code></td><td class="has-text-align-center" data-align="center">32 ms</td><td class="has-text-align-center" data-align="center">10 ms</td></tr><tr><td class="has-text-align-right" data-align="right"><code><a href="https://www.manpagez.com/man/3/clock_gettime_nsec_np/" data-wpel-link="external" target="_blank" rel="external noopener">clock_gettime_nsec_np(CLOCK_UPTIME_RAW)</a></code></td><td class="has-text-align-center" data-align="center">27 ms</td><td class="has-text-align-center" data-align="center">10 ms</td></tr><tr><td class="has-text-align-right" data-align="right"><code><a href="https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/gettimeofday.2.html" data-wpel-link="external" target="_blank" rel="external noopener">gettimeofday</a></code></td><td class="has-text-align-center" data-align="center">24 ms</td><td class="has-text-align-center" data-align="center">12 ms</td></tr><tr><td class="has-text-align-right" data-align="right"><code><a href="https://developer.apple.com/documentation/kernel/1462446-mach_absolute_time" data-wpel-link="external" target="_blank" rel="external noopener">mach_absolute_time</a></code></td><td class="has-text-align-center" data-align="center">15 ms</td><td class="has-text-align-center" data-align="center">6 ms</td></tr></tbody></table></figure>



<p>All these alternative methods are <em>well</em> over an order of magnitude faster than Swift&#8217;s native clock APIs, showing just how dreadfully inefficient the Swift <code>Clock</code> API is.</p>



<h3 class="wp-block-heading">mach_absolute_time for the win</h3>



<p>Unsurprisingly, <code>mach_absolute_time</code> is the fastest. It is what all these other APIs are actually based on; it is the lowest level of the time stack.</p>



<p>The downside to calling <code>mach_absolute_time</code> <em>directly</em>, though, is that <a href="https://developer.apple.com/documentation/kernel/1462446-mach_absolute_time#discussion" data-wpel-link="external" target="_blank" rel="external noopener">it&#8217;s on Apple&#8217;s &#8220;naughty&#8221; list</a> &#8211; apparently it&#8217;s been abused for device fingerprinting, so Apple require you to beg for special permission if you want to use it (even though it&#8217;s used by all these other APIs anyway, as the basis for their implementations, and there&#8217;s nothing you can get from <code>mach_absolute_time</code> that you can&#8217;t get from them too 🤨).</p>



<h3 class="wp-block-heading"><code>Date</code> surprisingly not bad</h3>



<p>I was quite surprised to see good ol&#8217; <code><a href="https://developer.apple.com/documentation/foundation/date" data-wpel-link="external" target="_blank" rel="external noopener">Date</a></code> performing competitively with the traditional C-level APIs, at least on x86-64. Even on arm64 it&#8217;s not bad, at still a third to half the speed of the C APIs. This surprised me because <s>it has the overhead of at least one Objective-C message send (for <code><a href="https://developer.apple.com/documentation/foundation/date/1780473-timeintervalsincenow" data-wpel-link="external" target="_blank" rel="external noopener">timeIntervalSinceNow</a></code>), unless somehow the Swift compiler is optimising that into a static function call, or inlining it entirely…?</s></p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p><strong>Update</strong>: I later looked at the disassembly, and found no message sends, only a plain function call to <code>Foundation.Date.timeIntervalSinceNow.getter</code> (which is only 40 instructions, on arm64, over <code>clock_gettime</code> and <code>__stack_chk_fail</code> &#8211; and the former is hundreds of instructions, so it&#8217;s adding relatively little overhead to the C API).</p>



<p>This isn&#8217;t being done by the compiler, it&#8217;s because <a href="https://github.com/apple/swift-foundation/blob/main/Sources/FoundationEssentials/Date.swift" data-wpel-link="external" target="_blank" rel="external noopener">that&#8217;s <em>actually</em> how it&#8217;s implemented in Foundation</a>. I keep forgetting that Foundation from Swift is no longer just the old Objective-C Foundation, but rather mostly the <em>new</em> Foundation that&#8217;s written in native Swift. So these performance results likely don&#8217;t apply once you go back far enough in Apple OS releases (to when Swift really was calling into the Objective-C code for <code>NSDate</code>) &#8211; but it&#8217;s safe to rely on good <code>Date</code> performance now and in future.</p>
</div></div>



<p>I certainly wouldn&#8217;t be afraid to use <code>Date</code> broadly, going down to lower APIs only when truly necessary &#8211; which is pretty rarely, I&#8217;d wager; we&#8217;re talking a mere 19 to 30 <em>nanoseconds</em> to get the time elapsed since a reference date <em>and</em> compare it to a threshold. If that&#8217;s too slow, it might be an indication that there&#8217;s a bigger problem (like transferring data a single byte at a time, as in the example that started this post &#8211; but more on that in <a href="https://wadetregaskis.com/urlsession-performance-for-reading-a-byte-stream/" data-wpel-link="internal">the next post</a>).</p>



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



<h3 class="wp-block-heading">Follow-up</h3>



<p>This post <a href="https://news.ycombinator.com/item?id=40262897" data-wpel-link="external" target="_blank" rel="external noopener">got some attention on HackerNews</a>. Pleasingly, the comments there were almost all well-intentioned and interesting. It&#8217;s a bit beyond me to try to address all of them, but a few in particular raised good points that I would like to answer / clarify:</p>



<ul class="wp-block-list">
<li>A lot of folks were curious about <code>mach_absolute_time</code> being on Apple&#8217;s naughty list. I don&#8217;t know for sure why it is either, but I think it&#8217;s very likely that it&#8217;s <em>primarily</em> because it essentially provides a reference time point, that&#8217;s very precise and pretty unique between computers. It&#8217;s not the boot time necessarily &#8211; because the timer pauses whenever the system is put to sleep &#8211; but even so it provides a simple way to nearly if not exactly identify an individual machine session (between boots &amp; sleeps). It probably wouldn&#8217;t take many other fingerprinting data points to reliably pin-point a specific machine.<br><br>Secondarily, because it provides very precise timing capabilities (e.g. nanosecond-resolution on x86), it could possibly be a key component of <a href="https://en.wikipedia.org/wiki/Timing_attack" data-wpel-link="external" target="_blank" rel="external noopener">timing attacks</a> and broader device fingerprinting based on timing information (e.g. measuring how long it takes to perform an otherwise innocuous operation).<br><br>That all said, the only difference between it and some of the higher-level APIs wrapping it is their overhead. And it&#8217;s not apparent to me that merely making the &#8220;get-time&#8221; functionality 2x slower is going to magically mitigate all the above concerns, especially when we&#8217;re still talking just a few nanoseconds.</li>



<li>Admittedly my phrasing regarding Apple&#8217;s policies on <code>mach_absolute_time</code> &#8211; &#8220;beg for permission to use it&#8221; &#8211; is a little melodramatic. It&#8217;s revealing something of my personal opinions on certain Apple &#8220;security&#8221; practices. I love that Apple genuinely care about protecting everyone&#8217;s privacy, but sometimes I chaff at what feels like capricious or impractical specific policies.<br><br>In this particular case, it&#8217;s not apparent to me why this sort of protection is needed for <em>native</em> apps. In a web browser, sure, you&#8217;re running untrustworthy, essentially arbitrary code from all over the place, a <em>lot</em> of which is openly malicious (thanks, Google &amp; Facebook, for your pervasive trackers &#8211; fuck you too). But a native app &#8211; or heck, even a dodgy non-native one like an Electron app &#8211; must be explicitly installed by the end user, among other barriers like code signing.</li>



<li>A few folks looked at the example case, of iterating a single byte at a time, and were suspicious of how performant that could possibly be anyway. This is a very fair reaction &#8211; it&#8217;s my ingrained instinct as well, from years of C/C++/Objective-C &#8211; <em>but</em> it&#8217;s relying on a few outdated assumptions. <a href="https://wadetregaskis.com/urlsession-performance-for-reading-a-byte-stream/" data-wpel-link="internal">My next post</a> already covered this for the most part, but in short here:<br><br>Through inlining, that code basically optimises down to an outer loop that fetches a new <em>chunk</em> of data (a pointer &amp; length) plus an inner loop to iterate over that as direct memory access. The chunks are typically tens of kilobytes to megabytes, in my experience (depending on the source, e.g. network vs local storage, and the buffer sizes chosen by Apple&#8217;s framework code). So it actually is quite performant and essentially what you&#8217;d conventionally write in a file descriptor read loop. <em>If and when</em> it happens to optimise correctly. That&#8217;s the major caveat &#8211; sometimes the Swift compiler fails to properly optimise code like this, and then indeed the performance can really suck. But for simple cases like in this post&#8217;s example code, the optimiser has no trouble with it.</li>



<li>Similarly, a few folks questioned the need to check the clock on <em>every</em> byte, as in the example. That&#8217;s a valid critique of this sort of code in many contexts, and I concur that where possible one <em>should</em> try to be smarter about such things &#8211; i.e. use sequences of bunches of bytes, not sequences of individual bytes.  <a href="https://wadetregaskis.com/urlsession-performance-for-reading-a-byte-stream/" data-wpel-link="internal">e.g. with <code>URLSession</code> you can</a>, and indeed it is faster to do it smarter like that.  But, you <em>can</em> get acceptable real-world performance with this code, even in high-throughput cases, and it&#8217;s relatively simple and intuitive to write, so it&#8217;s not uncommon or necessarily unreasonable.<br><br>In addition, sometimes you&#8217;re at the mercy of the APIs available &#8211; e.g. sometimes you can <em>only</em> get an <code>AsyncSequence&lt;UInt8&gt;</code>. If you don&#8217;t care about complete accuracy, you can do things like only considering UI updates every N bytes. You&#8217;ll save CPU time and nobody will notice the difference for small enough N on a fast enough iteration, but if those prerequisites aren&#8217;t met you might read e.g. N-1 bytes and then hit a long pause, during which time you <em>have</em> the extra N-1 bytes in hand but you&#8217;re not showing as such in your UI.</li>



<li>Some folks noted that are a <em>lot</em> of other clock APIs from Apple&#8217;s frameworks, like <code><a href="https://developer.apple.com/documentation/dispatch/dispatchtime" data-wpel-link="external" target="_blank" rel="external noopener">DispatchTime</a></code> and <code><a href="https://developer.apple.com/documentation/quartzcore/1395996-cacurrentmediatime" data-wpel-link="external" target="_blank" rel="external noopener">CACurrentMediaTime</a></code>. I didn&#8217;t include those in the benchmark because I just didn&#8217;t think of them at the time. If anyone wants to send me a pull request adding them to <a href="https://github.com/wadetregaskis/Swift-Benchmarks/blob/main/Benchmarks/Clocks/Clocks.swift" data-wpel-link="external" target="_blank" rel="external noopener">the code</a>, I&#8217;d be very happy to accept it.<br><br>I haven&#8217;t checked all those other APIs specifically, but I can pretty much guarantee they&#8217;re all built on <code>mach_absolute_time</code> too (possibly via one or more of the other C APIs already covered in this post). In fact those two examples just mentioned are explicitly documented as using <code>mach_absolute_time</code>.</li>



<li><a href="https://news.ycombinator.com/user?id=Kallikrates" data-wpel-link="external" target="_blank" rel="external noopener">Kallikrates</a> quietly pointed to a very interesting recent change in Apple&#8217;s Swift standard library code, <a href="https://github.com/apple/swift/pull/73429" data-wpel-link="external" target="_blank" rel="external noopener">Make static [milli/micro/nano]seconds members on Duration inlinable</a>. It&#8217;s paired with <a href="https://github.com/apple/swift/pull/73419" data-wpel-link="external" target="_blank" rel="external noopener">another patch</a> that together seem very specifically aimed at eliminating some of the absurd overhead in Swift&#8217;s <code>ContinuousClock</code> &amp; <code>SuspendingClock</code> implementations. The timing is a bit interesting &#8211; I don&#8217;t know if they were prompted by this post, but it&#8217;d be an unlikely coincidence otherwise.<br><br>In any case, I suspect it is possible to eliminate the overheads &#8211; there&#8217;s no apparent reason why they can&#8217;t be at least as efficient as <code>Date</code> already is &#8211; and so I hope that is what&#8217;s happening. Hopefully I&#8217;ll be able to re-run these benchmarks in a few months, with Swift 6, and see the performance gap eliminated. 🤞</li>
</ul>


<ol class="wp-block-footnotes"><li id="2f4a7c64-e213-44df-a3da-0e5020545aad">One might quibble with the &#8220;by design&#8221; assertion.  What I mean is that because it uses a protocol it&#8217;s susceptible to significant overheads &#8211; as is seen in these benchmarks &#8211; and because its internal implementation (a private <code>_Int128</code> type, inside the standard library) is kept hidden, it limits the compiler&#8217;s ability to inline, which is in turn critical to eliminating what&#8217;s technically a lot of boilerplate.  In contrast, if it were simply a struct using only public types internally, it would have avoided most of these overheads and been more amenable to inlining.<br><br>It&#8217;s not an irredeemable design (I think) &#8211; and that&#8217;s what the <a href="https://github.com/apple/swift/pull/73429" data-wpel-link="external" target="_blank" rel="external noopener">recent</a> <a href="https://github.com/apple/swift/pull/73419" data-wpel-link="external" target="_blank" rel="external noopener">patches</a> seem to be banking on, by tweaking the design in order to allow inlining and thus hopefully eliminate almost all the overhead. <a href="#2f4a7c64-e213-44df-a3da-0e5020545aad-link" aria-label="Jump to footnote reference 1">↩︎</a></li></ol>]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/swifts-native-clocks-are-very-inefficient/feed/</wfw:commentRss>
			<slash:comments>13</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2024/05/ContinuousClock-overhead.webp" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">7990</post-id>	</item>
		<item>
		<title>getBitmapDataPlanes can break NSImages &#038; NSBitmapImageReps</title>
		<link>https://wadetregaskis.com/getbitmapdataplanes-can-break-nsimages-nsbitmapimagereps/</link>
					<comments>https://wadetregaskis.com/getbitmapdataplanes-can-break-nsimages-nsbitmapimagereps/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Thu, 21 Mar 2024 04:44:26 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[10-bit]]></category>
		<category><![CDATA[bitmapData]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[getBitmapDataPlanes]]></category>
		<category><![CDATA[NSBitmapImageRep]]></category>
		<category><![CDATA[NSImage]]></category>
		<category><![CDATA[Sad]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=7889</guid>

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



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



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



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



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



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



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



<p>FB13693411.</p>



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



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

					<description><![CDATA[I really wish Swift releases would stop having major regressions in the ability to parse non-trivial expressions. The installation of a new version of Xcode &#8211; with a corresponding new Swift version &#38; toolchain &#8211; should be a joyous occasion, not one I increasingly dread.]]></description>
										<content:encoded><![CDATA[
<p>I really wish Swift releases would stop having major regressions in the ability to parse non-trivial expressions.  The installation of a new version of Xcode &#8211; with a corresponding new Swift version &amp; toolchain &#8211; should be a <em>joyous</em> occasion, not one I increasingly dread.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/another-swift-release-another-day-wasted/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2024/03/Sad-Keanu.avif" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">7838</post-id>	</item>
		<item>
		<title>Proactive Peek &#038; Reveal on Edge Hover</title>
		<link>https://wadetregaskis.com/proactive-peek-reveal-on-edge-hover/</link>
					<comments>https://wadetregaskis.com/proactive-peek-reveal-on-edge-hover/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Mon, 12 Feb 2024 22:29:57 +0000</pubDate>
				<category><![CDATA[Howto]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[Proactive Peek]]></category>
		<category><![CDATA[Reveal on Edge Hover]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[User Defaults]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=7692</guid>

					<description><![CDATA[These are two misfeatures that appeared in macOS Sonoma (I believe). They are where a closed sidebar forces its way back into view temporarily, if the mouse comes to rest near the relevant edge of the window. It&#8217;s easy to see how some UI designer thought this was a good idea. Surely if you move&#8230; <a class="read-more-link" href="https://wadetregaskis.com/proactive-peek-reveal-on-edge-hover/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>These are two misfeatures that appeared in macOS Sonoma (I believe).  They are where a closed sidebar forces its way back into view temporarily, if the mouse comes to rest near the relevant edge of the window.</p>



<figure class="wp-block-video aligncenter fucking-wordpress"><video height="282" style="aspect-ratio: 174 / 282;" width="174" autoplay loop preload="auto" src="https://wadetregaskis.com/wp-content/uploads/2024/02/Proactive-Peek.mp4" playsinline></video></figure>



<p>It&#8217;s easy to see how some UI designer thought this was a good idea.  Surely if you move the mouse near the edge of the window (or the screen, in fullscreen mode) and rest it there, it&#8217;s because you&#8217;re looking forlornly for your lost sidebar?  What could be more helpful and delightful than your missing sidebar popping into view?!</p>



<p>Unfortunately, they have ignored that fact that there is usually already other GUI controls at the edge of the window, not the least of which being the window edge itself (for drag-resizing of the window).  Scrollbars are another common inhabitant of window edges.</p>



<p>&#8220;Proactive Peek&#8221; is the worst of these two because not only does it change what&#8217;s under the mouse cursor <em>just</em> as you&#8217;re likely to click, stealing the click away from its true target, but it actually shrinks the window&#8217;s visible contents.  This leads to layout changes and motion noise, particularly in web pages where it can have knock-on effects like mucking with the scroll position or causing major changes by crossing some &#8220;responsive design&#8221; threshold.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>I&#8217;m <a href="https://forums.macrumors.com/threads/annoying-window-resizing-when-cursor-is-on-the-left.2408523/" data-wpel-link="external" target="_blank" rel="external noopener">not</a> <a href="https://www.reddit.com/r/MacOS/comments/12x96sg/disable_window_resize_when_moving_mouse_pointer/?rdt=34945" data-wpel-link="external" target="_blank" rel="external noopener">the</a> <a href="https://www.reddit.com/r/MacOS/comments/17ep9sr/when_i_rest_my_mouse_on_the_left_edge_of_safari/" data-wpel-link="external" target="_blank" rel="external noopener">only</a> <a href="https://mastodon.social/@stroughtonsmith/111914161132274876" data-wpel-link="external" target="_blank" rel="external noopener">one</a> to detest this &#8216;feature&#8217;, although it&#8217;s hard to know the sentiments of the overall Mac community since these &#8216;features&#8217; have no official names &#8211; I deduced them from the private method &amp; category names in AppKit &#8211; so they&#8217;re hard to search for.  FWIW, I could not find a single positive comment about these behaviours.</p>
</div></div>



<p>Unfortunately there&#8217;s no way to turn this poorly-considered &#8216;feature&#8217; off completely, although you can effectively disable the &#8220;Reveal on Edge Hover&#8221; piece:</p>



<figure class="wp-block-pullquote"><blockquote><p><code>defaults write -g NSSplitViewItemFullscreenEdgeRevealDelay -float 1e300</code><br><code>defaults write -g NSSplitViewItemTileEdgeRevealDelay -float 1e300</code></p><cite><a href="https://mastodon.social/@stroughtonsmith/111914177378857069" data-wpel-link="external" target="_blank" rel="external noopener">Steve Troughton-Smith</a></cite></blockquote></figure>



<p>If you&#8217;re an app developer it looks like (I haven&#8217;t tested it) you can disable these &#8216;features&#8217; within your own app, at least, by implementing the private <code>NSSplitView</code> delegate method <code>_splitView:canProactivePeekArrangedView:</code> and setting the <code>NSSplitView</code> <code>revealsOnEdgeHoverInFullscreen</code> property to <code>NO</code> (or the <code>NSSplitViewController</code> private property <code>&nbsp;_hasItemToRevealOnEdgeHover</code> to <code>NO</code>, if you&#8217;re using <code>NSSplitViewController</code>).  Or subclassing <code>NSSplitView</code> and overriding <code>_canDoSidebarProactivePeek</code> and <code>_canDoInspectorProactivePeek</code> to return NO &#8211; though that only applies to Proactive Peek.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/proactive-peek-reveal-on-edge-hover/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		<enclosure url="https://wadetregaskis.com/wp-content/uploads/2024/02/Proactive-Peek.mp4" length="34788" type="video/mp4" />

			<media:content url="https://wadetregaskis.com/wp-content/uploads/2024/02/Proactive-Peek.avif" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">7692</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>Bad API example: FileManager&#8217;s url(for:in:appropriateFor:create:)</title>
		<link>https://wadetregaskis.com/bad-api-example-filemanagers-urlforinappropriateforcreate/</link>
					<comments>https://wadetregaskis.com/bad-api-example-filemanagers-urlforinappropriateforcreate/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Wed, 31 Jan 2024 20:40:07 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[FileManager]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[Swift]]></category>
		<category><![CDATA[Undocumented]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=7631</guid>

					<description><![CDATA[I find FileManager&#8216;s url(for:in:appropriateFor:create:) to be very unintuitive. It seems to have multiple, largely-orthogonal functions. It can provide paths to common folders (albeit badly). It can create temporary folders. It can locate volume-specific bins (Trash folders). It is an example of bad API design. Specifically, regarding cohesion: the principle that an API should have one&#8230; <a class="read-more-link" href="https://wadetregaskis.com/bad-api-example-filemanagers-urlforinappropriateforcreate/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>I find <code><a href="https://developer.apple.com/documentation/foundation/filemanager" data-wpel-link="external" target="_blank" rel="external noopener">FileManager</a></code>&#8216;s <code><a href="https://developer.apple.com/documentation/foundation/filemanager/1407693-url" data-wpel-link="external" target="_blank" rel="external noopener">url(for:in:appropriateFor:create:)</a></code> to be very unintuitive.  It seems to have multiple, largely-orthogonal functions.  It can provide paths to common folders (albeit badly).  It can create temporary folders.  It can locate volume-specific bins (Trash folders).</p>



<p>It is an example of bad API design.  Specifically, regarding cohesion: the principle that an API should have one purpose.  A litmus test for this is whether all the method parameters are always applicable<sup data-fn="dd2453d4-5a23-4b69-983a-8d13728f39f4" class="fn"><a href="#dd2453d4-5a23-4b69-983a-8d13728f39f4" id="dd2453d4-5a23-4b69-983a-8d13728f39f4-link">1</a></sup>.</p>



<p>It wasn&#8217;t until I wrote a test driver which explores its entire parameter space, that I was finally able to grok what the hell it&#8217;s doing and delineate its multiple modes of operation.</p>



<p>I&#8217;ve contrasted it with the results from its sibling <code><a href="https://developer.apple.com/documentation/foundation/filemanager/1407726-urls" data-wpel-link="external" target="_blank" rel="external noopener">urls(for:in:)</a></code>, to better understand what it&#8217;s doing (and expose some more of its flaws).</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary>Test driver</summary>
<p>You can run this in a Swift playground, or as CLI app, but to observe the behaviour inside an <a href="https://developer.apple.com/documentation/security/app_sandbox" data-wpel-link="external" target="_blank" rel="external noopener">App Sandbox</a> it&#8217;s easiest to create a new GUI app in Xcode and just dump this into the @main <code>App</code> struct&#8217;s <code>init</code> method.</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"> fm = FileManager.</span><span style="color: #001080">default</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> searchPathDirectories = [(</span><span style="color: #A31515">&quot;applicationDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">applicationDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;demoApplicationDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">demoApplicationDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;developerApplicationDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">developerApplicationDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;adminApplicationDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">adminApplicationDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;libraryDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">libraryDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;developerDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">developerDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;userDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">userDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;documentationDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">documentationDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;documentDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">documentDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;coreServiceDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">coreServiceDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;autosavedInformationDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">autosavedInformationDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;desktopDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">desktopDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;cachesDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">cachesDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;applicationSupportDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">applicationSupportDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;downloadsDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">downloadsDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;inputMethodsDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">inputMethodsDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;moviesDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">moviesDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;musicDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">musicDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;picturesDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">picturesDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;printerDescriptionDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">printerDescriptionDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;sharedPublicDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">sharedPublicDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;preferencePanesDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">preferencePanesDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;applicationScriptsDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">applicationScriptsDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;itemReplacementDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</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: #A31515">&quot;allApplicationsDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">allApplicationsDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;allLibrariesDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">allLibrariesDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;trashDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">trashDirectory</span><span style="color: #000000">)]</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> searchPathDomainMasks = [(</span><span style="color: #A31515">&quot;userDomainMask&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDomainMask</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: #A31515">&quot;localDomainMask&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDomainMask</span><span style="color: #000000">.</span><span style="color: #001080">localDomainMask</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;systemDomainMask&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDomainMask</span><span style="color: #000000">.</span><span style="color: #001080">systemDomainMask</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;networkDomainMask&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDomainMask</span><span style="color: #000000">.</span><span style="color: #001080">networkDomainMask</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             </span><span style="color: #008000">/*(&quot;allDomainsMask&quot;, FileManager.SearchPathDomainMask.allDomainsMask)*/</span><span style="color: #000000">]</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;urls(for:in:):&quot;</span><span style="color: #000000">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #AF00DB">for</span><span style="color: #000000"> (dirName, dir) </span><span style="color: #AF00DB">in</span><span style="color: #000000"> searchPathDirectories {</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;</span><span style="color: #EE0000">\n</span><span style="color: #0000FF">\(</span><span style="color: #000000FF">dirName</span><span style="color: #0000FF">)</span><span style="color: #A31515">:&quot;</span><span style="color: #000000">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #AF00DB">for</span><span style="color: #000000"> (domainName, domain) </span><span style="color: #AF00DB">in</span><span style="color: #000000"> searchPathDomainMasks {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #0000FF">let</span><span style="color: #000000"> dirs = fm.</span><span style="color: #795E26">urls</span><span style="color: #000000">(</span><span style="color: #795E26">for</span><span style="color: #000000">: dir, </span><span style="color: #795E26">in</span><span style="color: #000000">: domain)</span></span>
<span class="line"><span style="color: #000000">            .</span><span style="color: #795E26">map</span><span style="color: #000000"> { </span><span style="color: #0000FF">$0</span><span style="color: #000000">.</span><span style="color: #795E26">path</span><span style="color: #000000">(</span><span style="color: #795E26">percentEncoded</span><span style="color: #000000">: </span><span style="color: #0000FF">false</span><span style="color: #000000">) }</span></span>
<span class="line"><span style="color: #000000">            .</span><span style="color: #795E26">joined</span><span style="color: #000000">(</span><span style="color: #795E26">separator</span><span style="color: #000000">: </span><span style="color: #A31515">&quot;</span><span style="color: #EE0000">\n</span><span style="color: #A31515">&quot;</span><span style="color: #000000"> + </span><span style="color: #267F99">String</span><span style="color: #000000">(</span><span style="color: #795E26">repeating</span><span style="color: #000000">: </span><span style="color: #A31515">&quot; &quot;</span><span style="color: #000000">, </span><span style="color: #795E26">count</span><span style="color: #000000">: </span><span style="color: #098658">23</span><span style="color: #000000">))</span></span>
<span class="line"></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;    </span><span style="color: #0000FF">\(</span><span style="color: #000000FF">domainName</span><span style="color: #0000FF">)</span><span style="color: #A31515">: </span><span style="color: #0000FF">\(</span><span style="color: #267F99">String</span><span style="color: #000000FF">(</span><span style="color: #795E26">repeating</span><span style="color: #000000FF">: </span><span style="color: #A31515">&quot; &quot;</span><span style="color: #000000FF">, </span><span style="color: #795E26">count</span><span style="color: #000000FF">: </span><span style="color: #098658">17</span><span style="color: #000000FF"> </span><span style="color: #000000">-</span><span style="color: #000000FF"> domainName.</span><span style="color: #001080">count</span><span style="color: #000000FF">)</span><span style="color: #0000FF">)\(</span><span style="color: #000000FF">dirs</span><span style="color: #0000FF">)</span><span style="color: #A31515">&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>
<span class="line"><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;</span><span style="color: #EE0000">\n\n</span><span style="color: #A31515">url(for:in:appropriateFor:create:):&quot;</span><span style="color: #000000">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> paths = [</span><span style="color: #0000FF">nil</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">             </span><span style="color: #795E26">URL</span><span style="color: #000000">(</span><span style="color: #795E26">filePath</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">             FileManager.</span><span style="color: #001080">default</span><span style="color: #000000">.</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">URL</span><span style="color: #000000">(</span><span style="color: #795E26">filePath</span><span style="color: #000000">: </span><span style="color: #A31515">&quot;/Volumes/Flash/&quot;</span><span style="color: #000000">)]</span></span>
<span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> pathDesc: (URL?) -&gt; </span><span style="color: #267F99">String</span><span style="color: #000000"> = { </span><span style="color: #0000FF">$0</span><span style="color: #000000">?.</span><span style="color: #795E26">path</span><span style="color: #000000">(</span><span style="color: #795E26">percentEncoded</span><span style="color: #000000">: </span><span style="color: #0000FF">false</span><span style="color: #000000">) ?? </span><span style="color: #A31515">&quot;nil&quot;</span><span style="color: #000000"> }</span></span>
<span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> maxPathLength = paths.</span><span style="color: #795E26">map</span><span style="color: #000000"> { </span><span style="color: #795E26">pathDesc</span><span style="color: #000000">(</span><span style="color: #0000FF">$0</span><span style="color: #000000">).</span><span style="color: #001080">count</span><span style="color: #000000"> }.</span><span style="color: #795E26">max</span><span style="color: #000000">() ?? </span><span style="color: #098658">0</span></span>
<span class="line"></span>
<span class="line"><span style="color: #AF00DB">for</span><span style="color: #000000"> (dirName, dir) </span><span style="color: #AF00DB">in</span><span style="color: #000000"> searchPathDirectories {</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;</span><span style="color: #EE0000">\n</span><span style="color: #0000FF">\(</span><span style="color: #000000FF">dirName</span><span style="color: #0000FF">)</span><span style="color: #A31515">:&quot;</span><span style="color: #000000">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #AF00DB">for</span><span style="color: #000000"> (domainName, domain) </span><span style="color: #AF00DB">in</span><span style="color: #000000"> searchPathDomainMasks {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #0000FF">var</span><span style="color: #000000"> results = [</span><span style="color: #267F99">String</span><span style="color: #000000">: </span><span style="color: #267F99">String</span><span style="color: #000000">]()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #AF00DB">for</span><span style="color: #000000"> appropriateForPath </span><span style="color: #AF00DB">in</span><span style="color: #000000"> paths {</span></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #0000FF">let</span><span style="color: #000000"> path: </span><span style="color: #267F99">String</span></span>
<span class="line"></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"> folderURL = </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">: dir,</span></span>
<span class="line"><span style="color: #000000">                                                            </span><span style="color: #795E26">in</span><span style="color: #000000">: domain,</span></span>
<span class="line"><span style="color: #000000">                                                            </span><span style="color: #795E26">appropriateFor</span><span style="color: #000000">: appropriateForPath,</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">false</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">                path = </span><span style="color: #795E26">pathDesc</span><span style="color: #000000">(folderURL)</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">                path = </span><span style="color: #A31515">&quot;ERROR (</span><span style="color: #0000FF">\(</span><span style="color: #000000FF">error</span><span style="color: #0000FF">)</span><span style="color: #A31515">)&quot;</span></span>
<span class="line"><span style="color: #000000">            }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">            results[</span><span style="color: #795E26">pathDesc</span><span style="color: #000000">(appropriateForPath)] = path</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">let</span><span style="color: #000000"> uniquePaths = </span><span style="color: #267F99">Set</span><span style="color: #000000">(results.</span><span style="color: #001080">values</span><span style="color: #000000">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #AF00DB">if</span><span style="color: #000000"> </span><span style="color: #098658">1</span><span style="color: #000000"> == uniquePaths.</span><span style="color: #001080">count</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;    </span><span style="color: #0000FF">\(</span><span style="color: #000000FF">domainName</span><span style="color: #0000FF">)</span><span style="color: #A31515">: </span><span style="color: #0000FF">\(</span><span style="color: #267F99">String</span><span style="color: #000000FF">(</span><span style="color: #795E26">repeating</span><span style="color: #000000FF">: </span><span style="color: #A31515">&quot; &quot;</span><span style="color: #000000FF">, </span><span style="color: #795E26">count</span><span style="color: #000000FF">: </span><span style="color: #098658">17</span><span style="color: #000000FF"> </span><span style="color: #000000">-</span><span style="color: #000000FF"> domainName.</span><span style="color: #001080">count</span><span style="color: #000000FF">)</span><span style="color: #0000FF">)\(</span><span style="color: #000000FF">uniquePaths.</span><span style="color: #001080">first</span><span style="color: #000000">!</span><span style="color: #0000FF">)</span><span style="color: #A31515">&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;    </span><span style="color: #0000FF">\(</span><span style="color: #000000FF">domainName</span><span style="color: #0000FF">)</span><span style="color: #A31515">:&quot;</span><span style="color: #000000">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #AF00DB">for</span><span style="color: #000000"> (appropriateForPath, path) </span><span style="color: #AF00DB">in</span><span style="color: #000000"> results.</span><span style="color: #795E26">sorted</span><span style="color: #000000">(</span><span style="color: #795E26">by</span><span style="color: #000000">: { </span><span style="color: #0000FF">$0</span><span style="color: #000000">.</span><span style="color: #001080">key</span><span style="color: #000000"> &lt; </span><span style="color: #0000FF">$1</span><span style="color: #000000">.</span><span style="color: #001080">key</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;        </span><span style="color: #0000FF">\(</span><span style="color: #000000FF">appropriateForPath</span><span style="color: #0000FF">)</span><span style="color: #A31515">: </span><span style="color: #0000FF">\(</span><span style="color: #267F99">String</span><span style="color: #000000FF">(</span><span style="color: #795E26">repeating</span><span style="color: #000000FF">: </span><span style="color: #A31515">&quot; &quot;</span><span style="color: #000000FF">, </span><span style="color: #795E26">count</span><span style="color: #000000FF">: maxPathLength </span><span style="color: #000000">-</span><span style="color: #000000FF"> appropriateForPath.</span><span style="color: #001080">count</span><span style="color: #000000FF">)</span><span style="color: #0000FF">)\(</span><span style="color: #000000FF">path</span><span style="color: #0000FF">)</span><span style="color: #A31515">&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>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>
</details>



<p>And here&#8217;s the output:</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow" open><summary>When running inside an <a href="https://developer.apple.com/documentation/security/app_sandbox" data-wpel-link="external" target="_blank" rel="external noopener">App Sandbox</a>:</summary>
<pre class="wp-block-preformatted">urls(for:in:):

applicationDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Applications/
    localDomainMask:   /Applications/
    systemDomainMask:  /System/Applications/
                       /System/Cryptexes/App/System/Applications/
    networkDomainMask: /Network/Applications/

demoApplicationDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Applications/Demos/
    localDomainMask:   /Applications/Demos/
    systemDomainMask:  /Applications/Demos/
    networkDomainMask: /Network/Applications/Demos/

developerApplicationDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Developer/Applications/
    localDomainMask:   /Developer/Applications/
    systemDomainMask:  /Developer/Applications/
    networkDomainMask: /Network/Developer/Applications/

adminApplicationDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Applications/Utilities/
    localDomainMask:   /Applications/Utilities/
    systemDomainMask:  /System/Applications/Utilities/
                       /System/Cryptexes/App/System/Applications/Utilities/
    networkDomainMask: /Network/Applications/Utilities/

libraryDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Library/
    localDomainMask:   /Library/
    systemDomainMask:  /System/Library/
                       /System/Cryptexes/App/System/Library/
    networkDomainMask: /Network/Library/

developerDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Developer/
    localDomainMask:   /Developer/
    systemDomainMask:  /Developer/
    networkDomainMask: /Network/Developer/

userDirectory:
    userDomainMask:    
    localDomainMask:   /Users/
    systemDomainMask:  
    networkDomainMask: /Network/Users/

documentationDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Library/Documentation/
    localDomainMask:   /Library/Documentation/
    systemDomainMask:  /System/Library/Documentation/
                       /System/Cryptexes/App/System/Library/Documentation/
    networkDomainMask: /Network/Library/Documentation/

documentDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Documents/
    localDomainMask:   
    systemDomainMask:  
    networkDomainMask: 

coreServiceDirectory:
    userDomainMask:    
    localDomainMask:   
    systemDomainMask:  /System/Library/CoreServices/
                       /System/Cryptexes/App/System/Library/CoreServices/
    networkDomainMask: 

autosavedInformationDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Library/Autosave Information/
    localDomainMask:   
    systemDomainMask:  
    networkDomainMask: 

desktopDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Desktop/
    localDomainMask:   
    systemDomainMask:  
    networkDomainMask: 

cachesDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Library/Caches/
    localDomainMask:   /Library/Caches/
    systemDomainMask:  /System/Library/Caches/
    networkDomainMask: 

applicationSupportDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Library/Application Support/
    localDomainMask:   /Library/Application Support/
    systemDomainMask:  /Library/Application Support/
    networkDomainMask: /Network/Library/Application Support/

downloadsDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Downloads/
    localDomainMask:   
    systemDomainMask:  
    networkDomainMask: 

inputMethodsDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Library/Input Methods/
    localDomainMask:   /Library/Input Methods/
    systemDomainMask:  /System/Library/Input Methods/
                       /System/Cryptexes/App/System/Library/Input Methods/
    networkDomainMask: /Network/Library/Input Methods/

moviesDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Movies/
    localDomainMask:   
    systemDomainMask:  
    networkDomainMask: 

musicDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Music/
    localDomainMask:   
    systemDomainMask:  
    networkDomainMask: 

picturesDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Pictures/
    localDomainMask:   
    systemDomainMask:  
    networkDomainMask: 

printerDescriptionDirectory:
    userDomainMask:    
    localDomainMask:   
    systemDomainMask:  /System/Library/Printers/PPDs/
                       /System/Cryptexes/App/System/Library/Printers/PPDs/
    networkDomainMask: 

sharedPublicDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Public/
    localDomainMask:   
    systemDomainMask:  
    networkDomainMask: 

preferencePanesDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Library/PreferencePanes/
    localDomainMask:   /Library/PreferencePanes/
    systemDomainMask:  /System/Library/PreferencePanes/
                       /System/Cryptexes/App/System/Library/PreferencePanes/
    networkDomainMask: 

applicationScriptsDirectory:
    userDomainMask:    /Users/SadPanda/Library/Application Scripts/com.SadPanda.MyApp/
    localDomainMask:   
    systemDomainMask:  
    networkDomainMask: 

itemReplacementDirectory:
    userDomainMask:    
    localDomainMask:   
    systemDomainMask:  
    networkDomainMask: 

allApplicationsDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Applications/
                       /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Applications/Utilities/
                       /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Developer/Applications/
                       /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Applications/Demos/
    localDomainMask:   /Applications/
                       /Applications/Utilities/
                       /Developer/Applications/
                       /Applications/Demos/
    systemDomainMask:  /System/Applications/
                       /System/Applications/Utilities/
                       /System/Developer/Applications/
                       /System/Applications/Demos/
                       /System/Cryptexes/App/System/Applications/
                       /System/Cryptexes/App/System/Applications/Utilities/
                       /System/Cryptexes/App/System/Developer/Applications/
                       /System/Cryptexes/App/System/Applications/Demos/
    networkDomainMask: /Network/Applications/
                       /Network/Applications/Utilities/
                       /Network/Developer/Applications/
                       /Network/Applications/Demos/

allLibrariesDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Library/
                       /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Developer/
    localDomainMask:   /Library/
                       /Developer/
    systemDomainMask:  /System/Library/
                       /Developer/
                       /System/Cryptexes/App/System/Library/
                       /System/Cryptexes/App/System/Developer/
    networkDomainMask: /Network/Library/
                       /Network/Developer/

trashDirectory:
    userDomainMask:    /Users/SadPanda/.Trash/
    localDomainMask:   /Users/SadPanda/.Trash/
    systemDomainMask:  
    networkDomainMask: 


url(for:in:appropriateFor:create:):

applicationDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Applications/
    localDomainMask:   /Applications/
    systemDomainMask:  /System/Applications/
    networkDomainMask: /Network/Applications/

demoApplicationDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Applications/Demos/
    localDomainMask:   /Applications/Demos/
    systemDomainMask:  /Applications/Demos/
    networkDomainMask: /Network/Applications/Demos/

developerApplicationDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Developer/Applications/
    localDomainMask:   /Developer/Applications/
    systemDomainMask:  /Developer/Applications/
    networkDomainMask: /Network/Developer/Applications/

adminApplicationDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Applications/Utilities/
    localDomainMask:   /Applications/Utilities/
    systemDomainMask:  /System/Applications/Utilities/
    networkDomainMask: /Network/Applications/Utilities/

libraryDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Library/
    localDomainMask:   /Library/
    systemDomainMask:  /System/Library/
    networkDomainMask: /Network/Library/

developerDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Developer/
    localDomainMask:   /Developer/
    systemDomainMask:  /Developer/
    networkDomainMask: /Network/Developer/

userDirectory:
    userDomainMask:    ERROR (nilError)
    localDomainMask:   /Users/
    systemDomainMask:  ERROR (nilError)
    networkDomainMask: /Network/Users/

documentationDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Library/Documentation/
    localDomainMask:   /Library/Documentation/
    systemDomainMask:  /System/Library/Documentation/
    networkDomainMask: /Network/Library/Documentation/

documentDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Documents/
    localDomainMask:   ERROR (nilError)
    systemDomainMask:  ERROR (nilError)
    networkDomainMask: ERROR (nilError)

coreServiceDirectory:
    userDomainMask:    ERROR (nilError)
    localDomainMask:   ERROR (nilError)
    systemDomainMask:  /System/Library/CoreServices/
    networkDomainMask: ERROR (nilError)

autosavedInformationDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Library/Autosave Information/
    localDomainMask:   ERROR (nilError)
    systemDomainMask:  ERROR (nilError)
    networkDomainMask: ERROR (nilError)

desktopDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Desktop/
    localDomainMask:   ERROR (nilError)
    systemDomainMask:  ERROR (nilError)
    networkDomainMask: ERROR (nilError)

cachesDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Library/Caches/
    localDomainMask:   /Library/Caches/
    systemDomainMask:  /System/Library/Caches/
    networkDomainMask: ERROR (nilError)

applicationSupportDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Library/Application Support/
    localDomainMask:   /Library/Application Support/
    systemDomainMask:  /Library/Application Support/
    networkDomainMask: /Network/Library/Application Support/

downloadsDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Downloads/
    localDomainMask:   ERROR (nilError)
    systemDomainMask:  ERROR (nilError)
    networkDomainMask: ERROR (nilError)

inputMethodsDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Library/Input Methods/
    localDomainMask:   /Library/Input Methods/
    systemDomainMask:  /System/Library/Input Methods/
    networkDomainMask: /Network/Library/Input Methods/

moviesDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Movies/
    localDomainMask:   ERROR (nilError)
    systemDomainMask:  ERROR (nilError)
    networkDomainMask: ERROR (nilError)

musicDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Music/
    localDomainMask:   ERROR (nilError)
    systemDomainMask:  ERROR (nilError)
    networkDomainMask: ERROR (nilError)

picturesDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Pictures/
    localDomainMask:   ERROR (nilError)
    systemDomainMask:  ERROR (nilError)
    networkDomainMask: ERROR (nilError)

printerDescriptionDirectory:
    userDomainMask:    ERROR (nilError)
    localDomainMask:   ERROR (nilError)
    systemDomainMask:  /System/Library/Printers/PPDs/
    networkDomainMask: ERROR (nilError)

sharedPublicDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Public/
    localDomainMask:   ERROR (nilError)
    systemDomainMask:  ERROR (nilError)
    networkDomainMask: ERROR (nilError)

preferencePanesDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Library/PreferencePanes/
    localDomainMask:   /Library/PreferencePanes/
    systemDomainMask:  /System/Library/PreferencePanes/
    networkDomainMask: ERROR (nilError)

applicationScriptsDirectory:
    userDomainMask:    /Users/SadPanda/Library/Application Scripts/com.SadPanda.MyApp/
    localDomainMask:   ERROR (nilError)
    systemDomainMask:  ERROR (nilError)
    networkDomainMask: ERROR (nilError)

itemReplacementDirectory:
    userDomainMask:
        /:                                                               /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/tmp/TemporaryItems/NSIRD_MyApp_oflT6r/
        /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/tmp/: /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/tmp/TemporaryItems/NSIRD_MyApp_xkFfky/
        /Volumes/Other/:                                                 /Volumes/Other/.TemporaryItems/folders.501/TemporaryItems/NSIRD_MyApp_bLj7gn/
        nil:                                                             ERROR (nilError)
    localDomainMask:   ERROR (nilError)
    systemDomainMask:  ERROR (nilError)
    networkDomainMask: ERROR (nilError)

allApplicationsDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Applications/
    localDomainMask:   /Applications/
    systemDomainMask:  /System/Applications/Demos/
    networkDomainMask: /Network/Applications/

allLibrariesDirectory:
    userDomainMask:    /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/Library/
    localDomainMask:   /Library/
    systemDomainMask:  /Developer/
    networkDomainMask: /Network/Library/

trashDirectory:
    userDomainMask:
        /:                                                               /Users/SadPanda/.Trash/
        /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/tmp/: /Users/SadPanda/.Trash/
        /Volumes/Other/:                                                 /Volumes/Other/.Trashes/501/
        nil:                                                             /Users/SadPanda/.Trash/
    localDomainMask:
        /:                                                               /Users/SadPanda/.Trash/
        /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/tmp/: /Users/SadPanda/.Trash/
        /Volumes/Other/:                                                 /Volumes/Other/.Trashes/501/
        nil:                                                             /Users/SadPanda/.Trash/
    systemDomainMask:
        /:                                                               /Users/SadPanda/.Trash/
        /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/tmp/: /Users/SadPanda/.Trash/
        /Volumes/Other/:                                                 /Volumes/Other/.Trashes/501/
        nil:                                                             ERROR (nilError)
    networkDomainMask:
        /:                                                               /Users/SadPanda/.Trash/
        /Users/SadPanda/Library/Containers/com.SadPanda.MyApp/Data/tmp/: /Users/SadPanda/.Trash/
        /Volumes/Other/:                                                 /Volumes/Other/.Trashes/501/
        nil:                                                             ERROR (nilError)</pre>
</details>



<p>The output outside of an App Sandbox is pretty similar, just different paths in some cases as you&#8217;d expect.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary>When running without App Sandboxing</summary>
<pre class="wp-block-preformatted">urls(for:in:):

applicationDirectory:
    userDomainMask:    /Users/SadPanda/Applications/
    localDomainMask:   /Applications/
    systemDomainMask:  /System/Applications/
                       /System/Cryptexes/App/System/Applications/
    networkDomainMask: /Network/Applications/

demoApplicationDirectory:
    userDomainMask:    /Users/SadPanda/Applications/Demos/
    localDomainMask:   /Applications/Demos/
    systemDomainMask:  /Applications/Demos/
    networkDomainMask: /Network/Applications/Demos/

developerApplicationDirectory:
    userDomainMask:    /Users/SadPanda/Developer/Applications/
    localDomainMask:   /Developer/Applications/
    systemDomainMask:  /Developer/Applications/
    networkDomainMask: /Network/Developer/Applications/

adminApplicationDirectory:
    userDomainMask:    /Users/SadPanda/Applications/Utilities/
    localDomainMask:   /Applications/Utilities/
    systemDomainMask:  /System/Applications/Utilities/
                       /System/Cryptexes/App/System/Applications/Utilities/
    networkDomainMask: /Network/Applications/Utilities/

libraryDirectory:
    userDomainMask:    /Users/SadPanda/Library/
    localDomainMask:   /Library/
    systemDomainMask:  /System/Library/
                       /System/Cryptexes/App/System/Library/
    networkDomainMask: /Network/Library/

developerDirectory:
    userDomainMask:    /Users/SadPanda/Developer/
    localDomainMask:   /Developer/
    systemDomainMask:  /Developer/
    networkDomainMask: /Network/Developer/

userDirectory:
    userDomainMask:    
    localDomainMask:   /Users/
    systemDomainMask:  
    networkDomainMask: /Network/Users/

documentationDirectory:
    userDomainMask:    /Users/SadPanda/Library/Documentation/
    localDomainMask:   /Library/Documentation/
    systemDomainMask:  /System/Library/Documentation/
                       /System/Cryptexes/App/System/Library/Documentation/
    networkDomainMask: /Network/Library/Documentation/

documentDirectory:
    userDomainMask:    /Users/SadPanda/Documents/
    localDomainMask:   
    systemDomainMask:  
    networkDomainMask: 

coreServiceDirectory:
    userDomainMask:    
    localDomainMask:   
    systemDomainMask:  /System/Library/CoreServices/
                       /System/Cryptexes/App/System/Library/CoreServices/
    networkDomainMask: 

autosavedInformationDirectory:
    userDomainMask:    /Users/SadPanda/Library/Autosave Information/
    localDomainMask:   
    systemDomainMask:  
    networkDomainMask: 

desktopDirectory:
    userDomainMask:    /Users/SadPanda/Desktop/
    localDomainMask:   
    systemDomainMask:  
    networkDomainMask: 

cachesDirectory:
    userDomainMask:    /Users/SadPanda/Library/Caches/
    localDomainMask:   /Library/Caches/
    systemDomainMask:  /System/Library/Caches/
    networkDomainMask: 

applicationSupportDirectory:
    userDomainMask:    /Users/SadPanda/Library/Application Support/
    localDomainMask:   /Library/Application Support/
    systemDomainMask:  /Library/Application Support/
    networkDomainMask: /Network/Library/Application Support/

downloadsDirectory:
    userDomainMask:    /Users/SadPanda/Downloads/
    localDomainMask:   
    systemDomainMask:  
    networkDomainMask: 

inputMethodsDirectory:
    userDomainMask:    /Users/SadPanda/Library/Input Methods/
    localDomainMask:   /Library/Input Methods/
    systemDomainMask:  /System/Library/Input Methods/
                       /System/Cryptexes/App/System/Library/Input Methods/
    networkDomainMask: /Network/Library/Input Methods/

moviesDirectory:
    userDomainMask:    /Users/SadPanda/Movies/
    localDomainMask:   
    systemDomainMask:  
    networkDomainMask: 

musicDirectory:
    userDomainMask:    /Users/SadPanda/Music/
    localDomainMask:   
    systemDomainMask:  
    networkDomainMask: 

picturesDirectory:
    userDomainMask:    /Users/SadPanda/Pictures/
    localDomainMask:   
    systemDomainMask:  
    networkDomainMask: 

printerDescriptionDirectory:
    userDomainMask:    
    localDomainMask:   
    systemDomainMask:  /System/Library/Printers/PPDs/
                       /System/Cryptexes/App/System/Library/Printers/PPDs/
    networkDomainMask: 

sharedPublicDirectory:
    userDomainMask:    /Users/SadPanda/Public/
    localDomainMask:   
    systemDomainMask:  
    networkDomainMask: 

preferencePanesDirectory:
    userDomainMask:    /Users/SadPanda/Library/PreferencePanes/
    localDomainMask:   /Library/PreferencePanes/
    systemDomainMask:  /System/Library/PreferencePanes/
                       /System/Cryptexes/App/System/Library/PreferencePanes/
    networkDomainMask: 

applicationScriptsDirectory:
    userDomainMask:    /Users/SadPanda/Library/Application Scripts/com.SadPanda.MyApp/
    localDomainMask:   
    systemDomainMask:  
    networkDomainMask: 

itemReplacementDirectory:
    userDomainMask:    
    localDomainMask:   
    systemDomainMask:  
    networkDomainMask: 

allApplicationsDirectory:
    userDomainMask:    /Users/SadPanda/Applications/
                       /Users/SadPanda/Applications/Utilities/
                       /Users/SadPanda/Developer/Applications/
                       /Users/SadPanda/Applications/Demos/
    localDomainMask:   /Applications/
                       /Applications/Utilities/
                       /Developer/Applications/
                       /Applications/Demos/
    systemDomainMask:  /System/Applications/
                       /System/Applications/Utilities/
                       /System/Developer/Applications/
                       /System/Applications/Demos/
                       /System/Cryptexes/App/System/Applications/
                       /System/Cryptexes/App/System/Applications/Utilities/
                       /System/Cryptexes/App/System/Developer/Applications/
                       /System/Cryptexes/App/System/Applications/Demos/
    networkDomainMask: /Network/Applications/
                       /Network/Applications/Utilities/
                       /Network/Developer/Applications/
                       /Network/Applications/Demos/

allLibrariesDirectory:
    userDomainMask:    /Users/SadPanda/Library/
                       /Users/SadPanda/Developer/
    localDomainMask:   /Library/
                       /Developer/
    systemDomainMask:  /System/Library/
                       /Developer/
                       /System/Cryptexes/App/System/Library/
                       /System/Cryptexes/App/System/Developer/
    networkDomainMask: /Network/Library/
                       /Network/Developer/

trashDirectory:
    userDomainMask:    /Users/SadPanda/.Trash/
    localDomainMask:   /Users/SadPanda/.Trash/
    systemDomainMask:  
    networkDomainMask: 


url(for:in:appropriateFor:create:):

applicationDirectory:
    userDomainMask:    /Users/SadPanda/Applications/
    localDomainMask:   /Applications/
    systemDomainMask:  /System/Applications/
    networkDomainMask: /Network/Applications/

demoApplicationDirectory:
    userDomainMask:    /Users/SadPanda/Applications/Demos/
    localDomainMask:   /Applications/Demos/
    systemDomainMask:  /Applications/Demos/
    networkDomainMask: /Network/Applications/Demos/

developerApplicationDirectory:
    userDomainMask:    /Users/SadPanda/Developer/Applications/
    localDomainMask:   /Developer/Applications/
    systemDomainMask:  /Developer/Applications/
    networkDomainMask: /Network/Developer/Applications/

adminApplicationDirectory:
    userDomainMask:    /Users/SadPanda/Applications/Utilities/
    localDomainMask:   /Applications/Utilities/
    systemDomainMask:  /System/Applications/Utilities/
    networkDomainMask: /Network/Applications/Utilities/

libraryDirectory:
    userDomainMask:    /Users/SadPanda/Library/
    localDomainMask:   /Library/
    systemDomainMask:  /System/Library/
    networkDomainMask: /Network/Library/

developerDirectory:
    userDomainMask:    /Users/SadPanda/Developer/
    localDomainMask:   /Developer/
    systemDomainMask:  /Developer/
    networkDomainMask: /Network/Developer/

userDirectory:
    userDomainMask:    ERROR (nilError)
    localDomainMask:   /Users/
    systemDomainMask:  ERROR (nilError)
    networkDomainMask: /Network/Users/

documentationDirectory:
    userDomainMask:    /Users/SadPanda/Library/Documentation/
    localDomainMask:   /Library/Documentation/
    systemDomainMask:  /System/Library/Documentation/
    networkDomainMask: /Network/Library/Documentation/

documentDirectory:
    userDomainMask:    /Users/SadPanda/Documents/
    localDomainMask:   ERROR (nilError)
    systemDomainMask:  ERROR (nilError)
    networkDomainMask: ERROR (nilError)

coreServiceDirectory:
    userDomainMask:    ERROR (nilError)
    localDomainMask:   ERROR (nilError)
    systemDomainMask:  /System/Library/CoreServices/
    networkDomainMask: ERROR (nilError)

autosavedInformationDirectory:
    userDomainMask:    /Users/SadPanda/Library/Autosave Information/
    localDomainMask:   ERROR (nilError)
    systemDomainMask:  ERROR (nilError)
    networkDomainMask: ERROR (nilError)

desktopDirectory:
    userDomainMask:    /Users/SadPanda/Desktop/
    localDomainMask:   ERROR (nilError)
    systemDomainMask:  ERROR (nilError)
    networkDomainMask: ERROR (nilError)

cachesDirectory:
    userDomainMask:    /Users/SadPanda/Library/Caches/
    localDomainMask:   /Library/Caches/
    systemDomainMask:  /System/Library/Caches/
    networkDomainMask: ERROR (nilError)

applicationSupportDirectory:
    userDomainMask:    /Users/SadPanda/Library/Application Support/
    localDomainMask:   /Library/Application Support/
    systemDomainMask:  /Library/Application Support/
    networkDomainMask: /Network/Library/Application Support/

downloadsDirectory:
    userDomainMask:    /Users/SadPanda/Downloads/
    localDomainMask:   ERROR (nilError)
    systemDomainMask:  ERROR (nilError)
    networkDomainMask: ERROR (nilError)

inputMethodsDirectory:
    userDomainMask:    /Users/SadPanda/Library/Input Methods/
    localDomainMask:   /Library/Input Methods/
    systemDomainMask:  /System/Library/Input Methods/
    networkDomainMask: /Network/Library/Input Methods/

moviesDirectory:
    userDomainMask:    /Users/SadPanda/Movies/
    localDomainMask:   ERROR (nilError)
    systemDomainMask:  ERROR (nilError)
    networkDomainMask: ERROR (nilError)

musicDirectory:
    userDomainMask:    /Users/SadPanda/Music/
    localDomainMask:   ERROR (nilError)
    systemDomainMask:  ERROR (nilError)
    networkDomainMask: ERROR (nilError)

picturesDirectory:
    userDomainMask:    /Users/SadPanda/Pictures/
    localDomainMask:   ERROR (nilError)
    systemDomainMask:  ERROR (nilError)
    networkDomainMask: ERROR (nilError)

printerDescriptionDirectory:
    userDomainMask:    ERROR (nilError)
    localDomainMask:   ERROR (nilError)
    systemDomainMask:  /System/Library/Printers/PPDs/
    networkDomainMask: ERROR (nilError)

sharedPublicDirectory:
    userDomainMask:    /Users/SadPanda/Public/
    localDomainMask:   ERROR (nilError)
    systemDomainMask:  ERROR (nilError)
    networkDomainMask: ERROR (nilError)

preferencePanesDirectory:
    userDomainMask:    /Users/SadPanda/Library/PreferencePanes/
    localDomainMask:   /Library/PreferencePanes/
    systemDomainMask:  /System/Library/PreferencePanes/
    networkDomainMask: ERROR (nilError)

applicationScriptsDirectory:
    userDomainMask:    /Users/SadPanda/Library/Application Scripts/com.SadPanda.MyApp/
    localDomainMask:   ERROR (nilError)
    systemDomainMask:  ERROR (nilError)
    networkDomainMask: ERROR (nilError)

itemReplacementDirectory:
    userDomainMask:
        /:                                                 /var/folders/v3/8anb56f64adf3_35gj346jg13000xa/T/TemporaryItems/NSIRD_MyApp_6D4CLt/
        /Volumes/Other/:                                   /Volumes/Other/.TemporaryItems/folders.501/TemporaryItems/NSIRD_MyApp_fdKnpT/
        /var/folders/v3/8anb56f64adf3_35gj346jg13000xa/T/: /var/folders/v3/8anb56f64adf3_35gj346jg13000xa/T/TemporaryItems/NSIRD_MyApp_bcHf11/
        nil:                                               ERROR (nilError)
    localDomainMask:   ERROR (nilError)
    systemDomainMask:  ERROR (nilError)
    networkDomainMask: ERROR (nilError)

allApplicationsDirectory:
    userDomainMask:    /Users/SadPanda/Applications/
    localDomainMask:   /Applications/
    systemDomainMask:  /System/Applications/Demos/
    networkDomainMask: /Network/Applications/

allLibrariesDirectory:
    userDomainMask:    /Users/SadPanda/Library/
    localDomainMask:   /Library/
    systemDomainMask:  /Developer/
    networkDomainMask: /Network/Library/

trashDirectory:
    userDomainMask:
        /:                                                 /Users/SadPanda/.Trash/
        /Volumes/Other/:                                   /Volumes/Other/.Trashes/501/
        /var/folders/v3/8anb56f64adf3_35gj346jg13000xa/T/: /Users/SadPanda/.Trash/
        nil:                                               /Users/SadPanda/.Trash/
    localDomainMask:
        /:                                                 /Users/SadPanda/.Trash/
        /Volumes/Other/:                                   /Volumes/Other/.Trashes/501/
        /var/folders/v3/8anb56f64adf3_35gj346jg13000xa/T/: /Users/SadPanda/.Trash/
        nil:                                               /Users/SadPanda/.Trash/
    systemDomainMask:
        /:                                                 /Users/SadPanda/.Trash/
        /Volumes/Other/:                                   /Volumes/Other/.Trashes/501/
        /var/folders/v3/8anb56f64adf3_35gj346jg13000xa/T/: /Users/SadPanda/.Trash/
        nil:                                               ERROR (nilError)
    networkDomainMask:
        /:                                                 /Users/SadPanda/.Trash/
        /Volumes/Other/:                                   /Volumes/Other/.Trashes/501/
        /var/folders/v3/8anb56f64adf3_35gj346jg13000xa/T/: /Users/SadPanda/.Trash/
        nil:                                               ERROR (nilError)
</pre>
</details>



<p>Note how <code>urls(for:in:)</code> works for every <code><a href="https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory" data-wpel-link="external" target="_blank" rel="external noopener">SearchPathDirectory</a></code> <em>except</em> <code><a href="https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/itemreplacementdirectory" data-wpel-link="external" target="_blank" rel="external noopener">itemReplacementDirectory</a></code>.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>Also, something is broken regarding <code>applicationScriptsDirectory</code>.  When used with <code>urls(for:in:)</code> you get this output to stdout:</p>



<pre class="wp-block-preformatted">cannot open file at line 49259 of [1b37c146ee]
os_unix.c:49259: (0) open(/private/var/db/DetachedSignatures) - Undefined error: 0</pre>



<p>And if you use it with <code>url(for:in:appropriateFor:create:)</code> you get <em>eight</em> lines of this output to stderr:</p>



<pre class="wp-block-preformatted">This method should not be called on the main thread as it may lead to UI unresponsiveness.</pre>



<p>I haven&#8217;t delved into the implementation to figure out what&#8217;s going on &#8211; apparently that particular <code>SearchPathDirectory</code> has some kind of special code path all of its own, which is doing something it seemingly should not be doing.</p>
</div></div>



<p>Some observations about <code>urls(for:in:appropriateFor:create:)</code>:</p>



<ul class="wp-block-list">
<li>It returns a <em>single</em> URL, even though there are often multiple folders for a given search path.  Note how it <em>never</em> returns a path to a Cryptex folder, for example.<br><br>If you&#8217;re looking for <em>search paths</em> &#8211; the set of folders to search for a resource &#8211; use <code>urls(for:in:)</code> only.</li>



<li>The <code>appropriateFor</code> argument is completely irrelevant to every <code>SearchPathDirectory</code> <em>except</em> <code>itemReplacementDirectory</code> and <code><a href="https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/trashdirectory" data-wpel-link="external" target="_blank" rel="external noopener">trashDirectory</a></code>.</li>



<li>It will only ever return a path inside the App Sandbox for <code><a href="https://developer.apple.com/documentation/foundation/filemanager/searchpathdomainmask/1408037-userdomainmask" data-wpel-link="external" target="_blank" rel="external noopener">userDomainMask</a></code> (if it doesn&#8217;t fail by throwing an exception).</li>



<li>It won&#8217;t allow <code>nil</code> as an argument for <code>appropriateFor</code> when targeting <code>itemReplacementDirectory</code>, yet it will allow <code>nil</code> when targeting <code>trashDirectory</code>, but <em>only</em> for <code>userDomainMask</code> and <code>localDomainMask</code>.  I cannot think of any explanation for this inconsistency.<br><br>If you ignore that inconsistency, the <code>SearchPathDomainMask</code> is irrelevant to <code>trashDirectory</code> (as it logically should be &#8211; bin folders are tied to <em>volumes</em>, not apps, users, or computers.</li>



<li>It always throws an exception for <code>itemReplacementDirectory</code> if the <code><a href="https://developer.apple.com/documentation/foundation/filemanager/searchpathdomainmask" data-wpel-link="external" target="_blank" rel="external noopener">SearchPathDomainMask</a></code> is not <code>userDomainMask</code>.  This might actually be explicable, even if a bit unintuitive and misguided &#8211; it seems to be presuming that, because you&#8217;re running inside an App Sandbox, you cannot modify any files except the current user&#8217;s.  I&#8217;m not sure that&#8217;s not accurate &#8211; there&#8217;s the possibility of special entitlements and exclusions &#8211; although nor do I know that it&#8217;s not.</li>



<li>Whenever it fails, it throws a <code>nilError</code> exception which is largely useless.  Even the name doesn&#8217;t provide any real insight into what its problem is.<br><br>It&#8217;s unsurprising to me that it does such a poor job, given the rest of the API &#8211; good error messages are a hallmark of good API design, or put conversely, happy path programming produces bad code.</li>
</ul>



<p>I did not explore use of the <code>create</code> argument (I always left it as <code>false</code>).  I didn&#8217;t need to in order to know it has its own problems, too &#8211; the documentation states that it <em>also</em> has inconsistent behaviour, in that it is completely ignored for <code>itemReplacementDirectory</code> (and <em>only</em> that <code>SearchPathDirectory</code>).</p>



<p>The root of all these problems is the API&#8217;s bad design:</p>



<ul class="wp-block-list">
<li><code>itemReplacementDirectory</code> and <code>trashDirectory</code> should not be part of <code>SearchPathDirectory</code>.  They do not behave like any of the other cases; they do not have broadly correct generic values, requiring instead some context regarding the items being replaced or trashed.</li>



<li><code>url(for:in:appropriateFor:create)</code> should be three independent methods &#8211; one of which already exists, in the form of <code>urls(for:in:)</code>.  The other two should handle each of the special cases of <code>itemReplacementDirectory</code> and <code>trashDirectory</code>.  i.e.:</li>
</ul>



<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: #008000">// Existing method</span></span>
<span class="line"><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">urls</span><span style="color: #000000">(</span><span style="color: #795E26">for</span><span style="color: #000000"> </span><span style="color: #001080">directory</span><span style="color: #000000">: FileManager.SearchPathDirectory,</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">domainMask</span><span style="color: #000000">: FileManager.SearchPathDomainMask) -&gt; [URL]</span></span>
<span class="line"></span>
<span class="line"><span style="color: #008000">// New methods</span></span>
<span class="line"><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">replacementItemFolder</span><span style="color: #000000">(</span><span style="color: #795E26">appropriateFor</span><span style="color: #000000"> </span><span style="color: #001080">url</span><span style="color: #000000">: URL) </span><span style="color: #AF00DB">throws</span><span style="color: #000000">(InvalidURLError) -&gt; URL</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">trashFolder</span><span style="color: #000000">(</span><span style="color: #795E26">appropriateFor</span><span style="color: #000000"> </span><span style="color: #001080">url</span><span style="color: #000000">: URL) </span><span style="color: #AF00DB">throws</span><span style="color: #000000">(InvalidURLError) -&gt; URL</span></span></code></pre></div>



<p><em>So</em> much clearer and easier to use than what we have now.</p>



<p>The two specialised methods still need to throw, because they take <code>URL</code>s which could be invalid, but that&#8217;s now the <em>only</em> reason they might throw (I&#8217;ve used a hypothetical <code>InvalidURLError</code> type to express that formally).</p>



<p>They could arguably be combined into one method (taking an enum that delineates the two cases), but I see little practical value in that &#8211; it&#8217;s simpler and easier to read if they&#8217;re simply distinct methods, even though their shape is similar.  I can&#8217;t imagine any reasonable scenario where you only decide at runtime if you&#8217;re going to replace something or delete it.</p>



<p>☝️ I&#8217;ve removed the <code>create</code> parameter entirely, as I think it presumes too much (e.g. what if you want to create a <em>file</em> with the replacement item URL, not a folder?), though arguably it could be added back in (though if it were, it should <em>actually</em> be obeyed).</p>


<ol class="wp-block-footnotes"><li id="dd2453d4-5a23-4b69-983a-8d13728f39f4">Note that I wrote <em>applicable</em>, not necessarily <em>used</em>.  The distinction is important.  e.g. it makes sense to supply the request headers for a HTTP request to an API which makes such requests, even if the request might be served from a local cache in which case those headers aren&#8217;t actually used, <em>in that instance</em>.<br><br>Another way to frame this is that it&#8217;s a design flaw to have parameters whose utility depends only on the value of other parameters.<br><br>If you can know at compile time that some parameters are unnecessary, then you shouldn&#8217;t be forced to provide them. <a href="#dd2453d4-5a23-4b69-983a-8d13728f39f4-link" aria-label="Jump to footnote reference 1">↩︎</a></li></ol>]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/bad-api-example-filemanagers-urlforinappropriateforcreate/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">7631</post-id>	</item>
		<item>
		<title>NSImage is dangerous</title>
		<link>https://wadetregaskis.com/nsimage-is-dangerous/</link>
					<comments>https://wadetregaskis.com/nsimage-is-dangerous/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Tue, 23 Jan 2024 21:53:57 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[AppKit]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[Concurrency]]></category>
		<category><![CDATA[NSBitmapImageRep]]></category>
		<category><![CDATA[NSImage]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[SwiftUI]]></category>
		<category><![CDATA[Undocumented]]></category>
		<category><![CDATA[unsafe]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=7501</guid>

					<description><![CDATA[NSImage is formally documented as largely not thread-safe: The following classes and functions are generally not thread-safe. In most cases, you can use these classes from any thread as long as you use them from only one thread at a time. Check the class documentation for additional details. Apple&#8217;s Threading Programming Guide &#62; Appendix A:&#8230; <a class="read-more-link" href="https://wadetregaskis.com/nsimage-is-dangerous/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p><code><a href="https://developer.apple.com/documentation/appkit/nsimage" data-wpel-link="external" target="_blank" rel="external noopener">NSImage</a></code> is <a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/ThreadSafetySummary/ThreadSafetySummary.html#//apple_ref/doc/uid/10000057i-CH12-123351-BBCFIIEB" data-wpel-link="external" target="_blank" rel="external noopener">formally documented</a> as largely <em>not</em> thread-safe:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>The following classes and functions are generally not thread-safe. In most cases, you can use these classes from any thread as long as you use them from only one thread at a time. Check the class documentation for additional details.</p>
<cite>Apple&#8217;s <a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/Introduction/Introduction.html#//apple_ref/doc/uid/10000057i-CH1-SW1" data-wpel-link="external" target="_blank" rel="external noopener">Threading Programming Guide</a> &gt; <a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/ThreadSafetySummary/ThreadSafetySummary.html#//apple_ref/doc/uid/10000057i-CH12-SW1" data-wpel-link="external" target="_blank" rel="external noopener">Appendix A: Thread Safety Summary</a>, subsection <a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/ThreadSafetySummary/ThreadSafetySummary.html#//apple_ref/doc/uid/10000057i-CH12-123351-BBCFIIEB" data-wpel-link="external" target="_blank" rel="external noopener">Application Kit Framework Thread Safety</a></cite></blockquote>



<p>What &#8220;in most cases&#8221; means is left to the reader&#8217;s imagination.  Apple adds a little addendum for <code>NSImage</code> specifically:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>One thread can create an <code><a href="https://developer.apple.com/documentation/appkit/nsimage" data-wpel-link="external" target="_blank" rel="external noopener">NSImage</a></code> object, draw to the image buffer, and pass it off to the main thread for drawing. The underlying image cache is shared among all threads. For more information about images and how caching works, see <a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CocoaDrawingGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40003290" data-wpel-link="external" target="_blank" rel="external noopener">Cocoa Drawing Guide</a>. </p>
<cite><a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/ThreadSafetySummary/ThreadSafetySummary.html#//apple_ref/doc/uid/10000057i-CH12-126728" data-wpel-link="external" target="_blank" rel="external noopener">NSImage Restrictions</a></cite></blockquote>



<p>For a start, it&#8217;s talking only about <em>creating an NSImage from scratch</em>, not loading it from serialised form (e.g. a file, a pasteboard, etc).  It doesn&#8217;t even deign to mention those other, much more common cases.</p>



<p>And even for that one mentioned use case, what does it mean, exactly?  What &#8220;image cache&#8221; is it referring to?</p>



<p>I don&#8217;t have authoritative answers.  The documentation is so infuriatingly vague that Apple could do basically anything to the implementation, between macOS updates, and claim to have broken no promises.</p>



<p>What I do have is some empirical data and the results of some reverse engineering (shout out to <a href="https://www.hopperapp.com" data-wpel-link="external" target="_blank" rel="external noopener">Hopper</a>), from macOS 14.2 Sonoma.</p>



<h2 class="wp-block-heading">NSImage 101</h2>



<p><code>NSImage</code>s can represent a wide range of imagery.  Most uses of them are probably for bitmap data (i.e. what you find in common image formats like <a href="https://en.wikipedia.org/wiki/WebP" data-wpel-link="external" target="_blank" rel="external noopener">WebP</a> &amp; <a href="https://en.wikipedia.org/wiki/AVIF" data-wpel-link="external" target="_blank" rel="external noopener">AVIF</a>), but <code>NSImage</code> also supports &#8216;raw&#8217; images (e.g. <a href="https://www.nikonusa.com/learn-and-explore/c/products-and-innovation/nikon-electronic-format-nef" data-wpel-link="external" target="_blank" rel="external noopener">Nikon NEF</a>) as well as vector data (e.g. <a href="https://en.wikipedia.org/wiki/SVG" data-wpel-link="external" target="_blank" rel="external noopener">SVG</a> &amp; <a href="https://en.wikipedia.org/wiki/PDF" data-wpel-link="external" target="_blank" rel="external noopener">PDF</a>).  It also has a plug-in mechanism of sorts, so the supported image formats can be extended dynamically at runtime.</p>



<p>You can fetch the full list of supported types from <a href="https://developer.apple.com/documentation/appkit/nsimage/1519988-imagetypes" data-wpel-link="external" target="_blank" rel="external noopener">NSImage.imageTypes</a> &#8211; although it doesn&#8217;t distinguish between those it can read vs those it can write.  Fortunately, <code>sips --formats</code> in Terminal gives you the same list with additional metadata.  On my machine that list happens to be:</p>



<pre class="wp-block-preformatted">Supported Formats:
-------------------------------------------
com.adobe.pdf                pdf   Writable
com.adobe.photoshop-image    psd   Writable
com.adobe.raw-image          dng   
com.apple.atx                --    Writable
com.apple.icns               icns  Writable
com.apple.pict               pict  
com.canon.cr2-raw-image      cr2   
com.canon.cr3-raw-image      cr3   
com.canon.crw-raw-image      crw   
com.canon.tif-raw-image      tif   
com.compuserve.gif           gif   Writable
com.dxo.raw-image            dxo   
com.epson.raw-image          erf   
com.fuji.raw-image           raf   
com.hasselblad.3fr-raw-image 3fr   
com.hasselblad.fff-raw-image fff   
com.ilm.openexr-image        exr   Writable
com.kodak.raw-image          dcr   
com.konicaminolta.raw-image  mrw   
com.leafamerica.raw-image    mos   
com.leica.raw-image          raw   
com.leica.rwl-raw-image      rwl   
com.microsoft.bmp            bmp   Writable
com.microsoft.cur            --    
com.microsoft.dds            dds   Writable
com.microsoft.ico            ico   Writable
com.nikon.nrw-raw-image      nrw   
com.nikon.raw-image          nef   
com.olympus.or-raw-image     orf   
com.olympus.raw-image        orf   
com.olympus.sr-raw-image     orf   
com.panasonic.raw-image      raw   
com.panasonic.rw2-raw-image  rw2   
com.pentax.raw-image         pef   
com.phaseone.raw-image       iiq   
com.samsung.raw-image        srw   
com.sgi.sgi-image            sgi   
com.sony.arw-raw-image       arw   
com.sony.raw-image           srf   
com.sony.sr2-raw-image       sr2   
com.truevision.tga-image     tga   Writable
org.khronos.astc             astc  Writable
org.khronos.ktx              ktx   Writable
org.khronos.ktx2             --    Writable
org.webmproject.webp         webp  
public.avci                  avci  
public.avif                  avif  
public.avis                  --    
public.heic                  heic  Writable
public.heics                 heics Writable
public.heif                  heif  
public.jpeg                  jpeg  Writable
public.jpeg-2000             jp2   Writable
public.jpeg-xl               jxl   
public.mpo-image             mpo   
public.pbm                   pbm   Writable
public.png                   png   Writable
public.pvr                   pvr   Writable
public.radiance              pic   
public.svg-image             svg   
public.tiff                  tiff  Writable</pre>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>Sidenote:  notice how it supports modern formats like WebP, AVIF, and JPEG-XL, but <em>only</em> for reading, not writing. 😕</p>



<p>It used to be that <code>NSImage</code> was pretty much a one-stop-shop for typical app image I/O needs, but Apple have for some reason crippled it over the years.  I feel like this is part of a larger trend of Apple providing increasingly less functionality, more complexity, and less coherence in their frameworks.</p>
</div></div>



<p>An <code>NSImage</code> can have multiple &#8216;<a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CocoaDrawingGuide/Images/Images.html#//apple_ref/doc/uid/TP40003290-CH208-SW9" data-wpel-link="external" target="_blank" rel="external noopener">representations</a>&#8216;.  These are essentially just different versions of the image.  One representation might be the original vector form (e.g. an <code><a href="https://developer.apple.com/documentation/appkit/nspdfimagerep" data-wpel-link="external" target="_blank" rel="external noopener">NSPDFImageRep</a></code>).  Another might be a rasterisation of that at a certain resolution.  Yet another might be a rasterisation at a different resolution.</p>



<p>It gets more complicated, however, because some representations are merely proxies for <em>other</em> frameworks&#8217; representations, like <a href="https://developer.apple.com/documentation/coregraphics/cgimage/" data-wpel-link="external" target="_blank" rel="external noopener"><code>CGImage</code></a>, <a href="https://developer.apple.com/documentation/coreimage/ciimage/" data-wpel-link="external" target="_blank" rel="external noopener"><code>CIImage</code></a> or somesuch.</p>



<p>Nonetheless, for the simple case of a static bitmap image read from a file, generally <code>NSImage</code> produces just one representation, which is the full bitmap (as an <code><a href="https://developer.apple.com/documentation/appkit/nsbitmapimagerep" data-wpel-link="external" target="_blank" rel="external noopener">NSBitmapImageRep</a></code>).  That&#8217;s the only case I&#8217;m going to discuss here, for simplicity&#8217;s sake (though likely the lessons apply to the other cases too).</p>



<h2 class="wp-block-heading">Accessing / using an NSImage</h2>



<p>Generally to use an <code>NSImage</code> you need a bitmap representation.  e.g. to actually draw it to the screen.</p>



<p>Unless you manually create an <code>NSImage</code> from an in-memory bitmap, the bitmap representation is not loaded initially, but rather only when first needed.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>☝️ You can usually obtain the <code>NSBitmapImageRep</code> (via e.g. <code><a href="https://developer.apple.com/documentation/appkit/nsimage/1519961-bestrepresentation" data-wpel-link="external" target="_blank" rel="external noopener">bestRepresentation(for: .infinity, context: nil, hints: nil)</a></code>) even before it&#8217;s actually been loaded &#8211; initially it&#8217;s largely just a shell, that knows its metadata (e.g. dimensions and colour space) but not yet its actual imagery.</p>
</div></div>



<p>Ultimately the load is triggered when something asks for the raw bytes of the bitmap (either you, directly in your code, or indirectly when you e.g. ask AppKit to draw the image).</p>



<p>Fetching the raw bytes of a bitmap from <code>NSBitmapImageRep</code> is not a trivial exercise.  I&#8217;m going to gloss over the complexities (like planar data formats) and just talk about the common case of single-plane (&#8220;interleaved channels&#8221;) bitmaps.</p>



<p>For those, you can access the raw bytes via the <a href="https://developer.apple.com/documentation/appkit/nsbitmapimagerep/1395421-bitmapdata" data-wpel-link="external" target="_blank" rel="external noopener"><code>bitmapData</code></a> property.</p>



<p>Nominally, <code>bitmapData</code> just returns a <code>uint8_t*</code>.</p>



<p>In fact, <code>bitmapData</code> calls a <em>lot</em> of internal methods in a complicated fashion, with many possible code paths.  What exactly it does depends on the underlying source of data, but in a nutshell it checks if the desired data has already been created / loaded (it&#8217;s cached in an instance variable) and if not it loads it, e.g. by calling <code><a href="https://developer.apple.com/documentation/imageio/1465011-cgimagesourcecreateimageatindex" data-wpel-link="external" target="_blank" rel="external noopener">CGImageSourceCreateImageAtIndex</a></code>.</p>



<p>That&#8217;s what makes <code>NSImage</code> dangerous to use from multiple threads simultaneously.</p>



<figure class="wp-block-pullquote"><blockquote><p>There is no mutual exclusion protecting any of these code paths.</p></blockquote></figure>



<p>Reading the <em>cached</em> bitmap data is essentially read-only (just some retain/autorelease traffic) so it&#8217;s safe to call from multiple threads concurrently (as long as something ensures the <code>NSBitmapImageRep</code> is kept alive the whole time <em>and not mutated</em>)</p>



<p>But loading or modifying it is not.  As such, if you call <code>bitmapData</code> from multiple threads concurrently, and you don&#8217;t know for sure that it&#8217;s already fully loaded, you get a <a href="https://www.avanderlee.com/swift/race-condition-vs-data-race/" data-wpel-link="external" target="_blank" rel="external noopener">data race</a> (also known as a &#8220;WTF does my app crash randomly?!&#8221; condition).</p>



<p>The consequences of that race vary.  Maybe you &#8220;win&#8221; the race &#8211; one thread happens to run virtually to completion of <code>bitmapData</code> first, storing the fresh backing data into the caching instance member, and then all the other threads run and just return that same value &#8211; the ideal situation as everything works as intended.</p>



<p>Maybe you &#8220;lose&#8221; the race: every concurrent thread checks simultaneously and sees there&#8217;s no cached value, so they all &#8211; in parallel and redundant to each other &#8211; load the bitmap data and store it into the cache.  They each return the one they created, even though ultimately only one thread wins &#8211; the <em>last</em> one to write into the cache &#8211; and the duplicate bitmap data that all the earlier threads created is deallocated.  Even though pointers to them have been returned to you.  And you might be in the middle of using them.  Causing you to crash with a memory protection fault (or worse, read from some random other memory allocation that happened to be placed at the same address afterwards, reading essentially garbage).</p>



<p>Hypocritically, this is because Apple don&#8217;t follow <a href="https://developer.apple.com/documentation/swift/calling-functions-with-pointer-parameters#Pass-a-Constant-Pointer-as-a-Parameter" data-wpel-link="external" target="_blank" rel="external noopener">their own rules on escaping pointers</a>:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>The pointer you pass to the function is only guaranteed to be valid for the duration of the function call. Do not persist the pointer and access it after the function has returned.</p>
<cite>Apple&#8217;s <a href="https://developer.apple.com/documentation/swift/swift-standard-library" data-wpel-link="external" target="_blank" rel="external noopener">Swift Standard Library</a> &gt; <a href="https://developer.apple.com/documentation/swift/manual-memory-management" data-wpel-link="external" target="_blank" rel="external noopener">Manual Memory Management</a> &gt; <a href="https://developer.apple.com/documentation/swift/calling-functions-with-pointer-parameters" data-wpel-link="external" target="_blank" rel="external noopener">Calling Functions With Pointer Parameters</a>, subsection <a href="https://developer.apple.com/documentation/swift/calling-functions-with-pointer-parameters#Pass-a-Constant-Pointer-as-a-Parameter" data-wpel-link="external" target="_blank" rel="external noopener">Pass a Constant Pointer as a Parameter</a>.</cite></blockquote>



<p>The source code for the relevant <code>NSBitmapImageRep</code> methods is essentially:</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:4;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">- (</span><span style="color: #0000FF">uint8_t</span><span style="color: #000000">*)bitmapData {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">uint8_t</span><span style="color: #000000"> **result = </span><span style="color: #0000FF">nil</span><span style="color: #000000">;</span></span>
<span class="line"><span style="color: #000000">    [</span><span style="color: #0000FF">self</span><span style="color: #000000"> </span><span style="color: #795E26">getBitmapDataPlanes:</span><span style="color: #000000">&amp;result];</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #AF00DB">return</span><span style="color: #000000"> *result;</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">void</span><span style="color: #000000">)getBitmapDataPlanes:(</span><span style="color: #0000FF">uint8_t</span><span style="color: #000000">***)output {</span></span>
<span class="line"><span style="color: #000000">    [</span><span style="color: #0000FF">self</span><span style="color: #000000"> </span><span style="color: #795E26">_performBlockUsingBackingMutableData:</span><span style="color: #000000">^</span><span style="color: #795E26">void</span><span style="color: #000000"> (</span><span style="color: #0000FF">uint8_t</span><span style="color: #000000">* </span><span style="color: #001080">dataPlanes</span><span style="color: #000000">[</span><span style="color: #098658">5</span><span style="color: #000000">]) {</span></span>
<span class="line"><span style="color: #000000">        *output = dataPlanes;</span></span>
<span class="line"><span style="color: #000000">    }];</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p class="has-text-align-center has-x-large-font-size">🤦‍♂️</p>



<figure class="wp-block-pullquote"><blockquote><p>The <code>bitmapData</code> property and <code>getBitmapDataPlanes</code> methods are fundamentally unsafe.</p></blockquote></figure>



<p>Unfortunately, while <code>_performBlockUsingBackingMutableData</code> and its many similar siblings <em>can</em> be made safe, they are (a) all private and (b) not currently enforcing the necessary mutual exclusion.  This could be corrected by Apple in future (although I wouldn&#8217;t hold your breath).</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>Nominally the <code><a href="https://developer.apple.com/documentation/appkit/nsbitmapimagerep/1395583-colorat" data-wpel-link="external" target="_blank" rel="external noopener">colorAt(x:y:)</a></code> method could also be safe, but it currently lacks the same underlying mutual exclusion &#8211; and even if it didn&#8217;t, the performance when using it is atrocious due to the Objective-C method call overhead.  Even utilising <a href="https://developer.apple.com/library/archive/documentation/Performance/Conceptual/CodeSpeed/Articles/CriticalCode.html#//apple_ref/doc/uid/20001871-98344" data-wpel-link="external" target="_blank" rel="external noopener">IMP caching</a>, the performance is still terrible compared to directly accessing the bitmap byte buffer.</p>
</div></div>



<h2 class="wp-block-heading">Why does it matter that NSImage is not thread-safe?</h2>



<p>The crux of the problem is that you often have no good choice about it, because <code>NSImage</code> is a <a href="https://forums.swift.org/t/what-does-currency-type-mean/41065" data-wpel-link="external" target="_blank" rel="external noopener">currency type</a> used widely throughout Apple&#8217;s own frameworks, and you often have no control over what thread it&#8217;s created or used on.</p>



<p>For example, even using the very latest Apple APIs such as SwiftUI&#8217;s <code><a href="https://developer.apple.com/documentation/swiftui/view/dropdestination(for:action:istargeted:)" data-wpel-link="external" target="_blank" rel="external noopener">dropDestination(for:action:isTargeted:)</a></code>, you cannot control which thread the <code>NSImage</code>s are created on (it <em>appears</em> to be the main thread, although that API provides no guarantees).</p>



<p>Similarly you have no control over where those images are used if you pass them to 3rd party code &#8211; including Apple&#8217;s.  e.g. <code><a href="https://developer.apple.com/documentation/swiftui/image/init(nsimage:)" data-wpel-link="external" target="_blank" rel="external noopener">Image(nsImage:)</a></code>; <em>possibly</em> that only uses them on the main thread, but it <em>might</em> be pre-rendered the image a separate thread for better performance (to avoid blocking the main thread and causing the app to hang).  In fact it <em>should</em>, in principle.</p>



<p>Loading an image &#8211; actually reading it from a file or URL and decompressing it into a raw bitmap suitable for drawing to the screen &#8211; should never be done on the main thread, because it can take a long time.  A 1 GiB TIFF takes nearly 30 seconds on my iMac Pro, for example (and TIFF uses very lightweight compression, so it&#8217;s a relatively fast-to-read format).  Anything involving the network could take an unbounded amount of time.  Even small files &#8211; like a 20 MiB NEF &#8211; can take seconds to render because they are non-trivial to decode and/or decompress.</p>



<p>So you&#8217;re screwed on multiple levels:  not only can you generally not guarantee what thread <code>NSImage</code> is born on nor used on, you <em>can&#8217;t</em> use it exclusively on the main thread because that will cause a terrible user experience.</p>



<h2 class="wp-block-heading">What can you do?</h2>



<p>In simple terms, the best you can realistically do is try ensure that all modifications to the <code>NSImage</code> (including implicit ones, such as loading a bitmap representation upon first use) happen exclusively in one thread [at a time].  For example, in my experiments, putting a lock around otherwise concurrent calls to <code>bitmapData</code> is enough to prevent any data races.  Although I am mystified as to how I can still let the main thread draw the image concurrently without any apparent problems. 🤔</p>



<p>If you want to play it <em>really</em> safe, you have to create &amp; pre-load each <code>NSImage</code> on a single thread (<em>not</em> the main thread), before ever sending it to another isolation domain.  That means manually reimplementing things like drag and drop of images (because you can no longer work directly with <code>NSImage</code> with any drag &amp; drop APIs, you have to instead use only &#8211; and <em>all</em> &#8211; the things that <em>could potentially be</em> images, like URLs or data blobs, and then translate those to &amp; from <code>NSImage</code>s manually).</p>



<p>Complicating all this is that <code>NSImage</code> is unclear about how <a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CocoaDrawingGuide/Images/Images.html#//apple_ref/doc/uid/TP40003290-CH208-SW11" data-wpel-link="external" target="_blank" rel="external noopener">its caching</a> works.  For starters, does this caching apply to the representations or is it a hidden, orthogonal system?  Is it thread-safe?  Etc.</p>



<p>You can supposedly modify the caching behaviour via the <code><a href="https://developer.apple.com/documentation/appkit/nsimage/1519850-cachemode" data-wpel-link="external" target="_blank" rel="external noopener">cacheMode</a></code> property, but in my experience there is no apparent effect no matter what it is set it to (not on the image&#8217;s representations nor on render performance in the GUI).</p>



<p>It&#8217;s a shame that <code>NSImage</code> has been so neglected, and has so many glaring problems.  Over the years Apple have seemingly tried to replace it, introducing new image types <a href="https://developer.apple.com/documentation/coreimage/ciimage/" data-wpel-link="external" target="_blank" rel="external noopener">over</a> and <a href="https://developer.apple.com/documentation/uikit/uiimage/" data-wpel-link="external" target="_blank" rel="external noopener">over</a> again, but all that&#8217;s done is <a href="https://xkcd.com/927/" data-wpel-link="external" target="_blank" rel="external noopener">made everything more complicated</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/nsimage-is-dangerous/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2024/01/NSImage-thread-unsafety-caught-by-Address-Sanitizer-MallocScribble.webp" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">7501</post-id>	</item>
		<item>
		<title>Reminder: macOS system frameworks binaries are hidden (since Big Sur)</title>
		<link>https://wadetregaskis.com/reminder-macos-system-frameworks-binaries-are-hidden-since-big-sur/</link>
					<comments>https://wadetregaskis.com/reminder-macos-system-frameworks-binaries-are-hidden-since-big-sur/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Tue, 23 Jan 2024 18:59:12 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Howto]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[dyld]]></category>
		<category><![CDATA[dyld-shared-cache-extractor]]></category>
		<category><![CDATA[Hopper]]></category>
		<category><![CDATA[macOS]]></category>
		<category><![CDATA[macOS Big Sur]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[Undocumented]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=7503</guid>

					<description><![CDATA[Every now and again I&#8217;ll go to do something really innocuous with an Apple framework, like disassemble it in Hopper or check the link headers. And every. single. time. I forget that Apple did some really weird shit in Big Sur, and removed the binaries. $ ls -lh /System/Library/Frameworks/AppKit.framework/Versions/Current/AppKit ls: /System/Library/Frameworks/AppKit.framework/Versions/Current/AppKit: No such file or&#8230; <a class="read-more-link" href="https://wadetregaskis.com/reminder-macos-system-frameworks-binaries-are-hidden-since-big-sur/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>Every now and again I&#8217;ll go to do something really innocuous with an Apple framework, like disassemble it in <a href="https://www.hopperapp.com" data-wpel-link="external" target="_blank" rel="external noopener">Hopper</a> or check the link headers.  And <em>every. single. time</em>. I forget that Apple did some really weird shit in Big Sur, and removed the binaries.</p>



<pre class="wp-block-preformatted">$ ls -lh /System/Library/Frameworks/AppKit.framework/Versions/Current/AppKit
ls: /System/Library/Frameworks/AppKit.framework/Versions/Current/AppKit: No such file or directory</pre>



<p>WTF, mate?</p>



<p>Invariably I spend half an hour websearching around to try to figure out how the hell my system got so broken, and how it&#8217;s possible to even boot macOS in such a corrupt state, until <em>finally</em> I chance upon <a href="https://mjtsai.com/blog/2020/06/26/reverse-engineering-macos-11-0/" data-wpel-link="external" target="_blank" rel="external noopener">Michael Tsai&#8217;s excellent summary of how Apple broke their frameworks starting in Big Sur</a>.</p>



<p>The good news for Hopper is that it has since been updated to work around this &#8211; you can access the Apple framework binaries through <em>File</em> > <em>Read File from DYLD Cache…</em>  There&#8217;s also tools like <a href="https://github.com/keith/dyld-shared-cache-extractor" data-wpel-link="external" target="_blank" rel="external noopener">dyld-shared-cache-extractor</a> which can resurrect the binaries from the cache.</p>



<p>Note also that in Sonoma, at least, the cache lives at <code>/System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/</code> (in previous macOS releases it was apparently in <code>/System/Library/dyld/</code>).</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/reminder-macos-system-frameworks-binaries-are-hidden-since-big-sur/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">7503</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>No more Dark Mode support</title>
		<link>https://wadetregaskis.com/no-more-dark-mode-support/</link>
					<comments>https://wadetregaskis.com/no-more-dark-mode-support/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Mon, 01 Jan 2024 03:45:56 +0000</pubDate>
				<category><![CDATA[Meta]]></category>
		<category><![CDATA[Bloated]]></category>
		<category><![CDATA[Dark mode]]></category>
		<category><![CDATA[Dark Mode for Safari]]></category>
		<category><![CDATA[Dark Reader]]></category>
		<category><![CDATA[Dracula Dark Mode]]></category>
		<category><![CDATA[Noir]]></category>
		<category><![CDATA[Perfect Images]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[Wordpress]]></category>
		<category><![CDATA[WP Dark Mode]]></category>
		<category><![CDATA[WP Fastest Cache]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=7255</guid>

					<description><![CDATA[For about a month I was using WP Dark Mode. I don&#8217;t really recall how I thought upon it to begin with, but I vaguely recall choosing to install it because it seemed kind to support dark mode. Even though I don&#8217;t use it myself, it does seem to have a non-trivial following. And I&#8230; <a class="read-more-link" href="https://wadetregaskis.com/no-more-dark-mode-support/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>For about a month I was using <a href="https://wordpress.org/plugins/wp-dark-mode/" data-wpel-link="external" target="_blank" rel="external noopener">WP Dark Mode</a>.  I don&#8217;t really recall how I thought upon it to begin with, but I vaguely recall choosing to install it because it seemed kind to support dark mode.  Even though I don&#8217;t use it myself, it does seem to have a non-trivial following.  And I remember being obsessively into it &#8211; and OS theming in general &#8211; when I was a child.</p>



<p>WP Dark Mode had &#8216;flashing&#8217; problems, though &#8211; where the page loads first in normal, &#8216;light&#8217; mode and only then re-renders into dark mode.  Possibly just a result of interference by <a href="https://www.wpfastestcache.com" data-wpel-link="external" target="_blank" rel="external noopener">WP Fastest Cache</a>, which I later realised was probably deferring WP Dark Mode&#8217;s JavaScript files past initial page load.  Irrespective, I found that <a href="https://wordpress.org/plugins/dracula-dark-mode/" data-wpel-link="external" target="_blank" rel="external noopener">Dracula Dark Mode</a> worked better &#8211; though still not flawlessly in this regard &#8211; so I switched to that.</p>



<p>However, I eventually noticed that Dracula Dark Mode adds <em>415 KiB</em> to every single page load, with its JavaScript and CSS files.  That <em>doubles</em> the total size for some pages, even with WordPress &amp; Jetpack bloating the baseline.  Its own analytics suggested that basically nobody uses dark mode, at least amongst people that browse my website.  So that does not seem like a reasonable trade-off.</p>



<p>Thus, with a bit of sadness, I&#8217;ve disabled the plug-in.  Maybe in future the plug-in&#8217;s authors can find a way to optimise things such that its JS &amp; CSS is only loaded if dark mode is actually in use.</p>



<p>Turns out, though, that almost no websites support dark mode anyway &#8211; I discovered as much while having dark mode enabled during my own testing.  So it doesn&#8217;t seem like I&#8217;m an odd one out, and my tiny website is not going to start any trends.  From the looks of it the way most people actually tackle the web in dark mode is through browser plug-ins which basically do the same thing as the WordPress plug-ins, but for <em>every</em> website.  And only users actually interested in dark mode have to pay their price.</p>



<p>I haven&#8217;t tried any of them, but there&#8217;s numerous Safari plug-ins for dark mode, e.g. <a href="https://darkreader.org" data-wpel-link="external" target="_blank" rel="external noopener">Dark Reader</a>, <a href="https://getnoir.app" data-wpel-link="external" target="_blank" rel="external noopener">Noir</a>, <a href="https://alexdenk.eu/mywork/darkmode.html" data-wpel-link="external" target="_blank" rel="external noopener">Dark Mode for Safari</a>, etc.</p>



<p>It does mean I can&#8217;t do image substitutions for dark mode users, but I couldn&#8217;t find any dark mode WordPress plug-in that works correctly in that regard anyway (they seem to do naive URL substitutions, without accounting for images having various replicas of different sizes per WordPress standard practice, and certainly without supporting <a href="https://meowapps.com/wp-retina-2x/" data-wpel-link="external" target="_blank" rel="external noopener">Perfect Images</a>).  It&#8217;s also rare that I have a dark mode version of an image available &#8211; though I can at least be better about using actual alpha channels rather than fixed white backgrounds, going forward.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/no-more-dark-mode-support/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">7255</post-id>	</item>
		<item>
		<title>WordPress.org doesn&#8217;t allow reviews warning about fatally broken plug-ins</title>
		<link>https://wadetregaskis.com/wordpress-org-doesnt-allow-reviews-warning-about-fatally-broken-plug-ins/</link>
					<comments>https://wadetregaskis.com/wordpress-org-doesnt-allow-reviews-warning-about-fatally-broken-plug-ins/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Tue, 19 Dec 2023 19:59:33 +0000</pubDate>
				<category><![CDATA[Reviews]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[WordPress.org]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=6834</guid>

					<description><![CDATA[I wrote this review of the Archiver WordPress plug-in: ★☆☆☆☆ I love the idea, but unsurprisingly – given it hasn’t been updated in over seven years – this no longer works. With my site, running WordPress 6.4.2, it broke it so badly that I couldn’t even login in recovery mode. I had to SFTP in&#8230; <a class="read-more-link" href="https://wadetregaskis.com/wordpress-org-doesnt-allow-reviews-warning-about-fatally-broken-plug-ins/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>I wrote this review of the <a href="https://wordpress.org/plugins/archiver/" data-wpel-link="external" target="_blank" rel="external noopener">Archiver</a> WordPress plug-in:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>★☆☆☆☆</p>



<p>I love the idea, but unsurprisingly – given it hasn’t been updated in over seven years – this no longer works. With my site, running WordPress 6.4.2, it broke it so badly that I couldn’t even login in recovery mode. I had to SFTP in and manually delete the plug-in to get my site back up.</p>
</blockquote>



<p>It was immediately removed with the statement:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>I have removed your review. Please do not use the review section for support.</p>
<cite>Anonymous WordPress.org moderator</cite></blockquote>



<p>I think merely noting that an abandoned, seven-years-out-of-date plug-in is broken &#8211; and <em>completely</em> breaks WordPress if it&#8217;s installed &amp; activated &#8211; is not unreasonable to note on the plug-in&#8217;s page.  Putting that information in the Support forums is pointless since (a) nobody&#8217;s going to check those before installing the plug-in, and (b) it&#8217;s <em>abandoned</em>; there is no support.</p>



<p>Quite a disappointing move by the WordPress.org folks. 😕</p>



<p>Curiously, although the review itself was removed, it seems to still be counted in the stars histogram.  Which is ironic since the 1-star rating is really <em>not</em> the important aspect of the review.</p>



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



<p>Another review of mine was just removed by a different moderator for basically the same purported reason:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>…you are 100% doing it wrong. Do not use a review to get support again.</p>
<cite>Jan Dembowski</cite></blockquote>



<p>Seems a bit aggressive.  I was, of course, <em>not</em> &#8220;getting support&#8221;, I was merely noting that another old, abandoned plug-in (<a href="https://wordpress.org/plugins/lh-wayback-machine/" data-wpel-link="external" target="_blank" rel="external noopener">LH Wayback Machine</a>) only partially works now.</p>



<p>The first instance of this was disappointing but conceivably random, but this second removal suggests a systematic problem with WordPress.org&#8217;s review system.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/wordpress-org-doesnt-allow-reviews-warning-about-fatally-broken-plug-ins/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">6834</post-id>	</item>
		<item>
		<title>Apple&#8217;s timing problem</title>
		<link>https://wadetregaskis.com/apples-timing-problem/</link>
					<comments>https://wadetregaskis.com/apples-timing-problem/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Sun, 19 Nov 2023 06:34:48 +0000</pubDate>
				<category><![CDATA[Ramblings]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[M1 Ultra]]></category>
		<category><![CDATA[M2 Ultra]]></category>
		<category><![CDATA[M3 Ultra]]></category>
		<category><![CDATA[Sad]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=5586</guid>

					<description><![CDATA[The M1 was announced in November 2020. The M1 Ultra wasn&#8217;t announced for another sixteen months, in March 2022. It was clearly late. Very late. The M2 was presumed right around the corner (and it was, released just four months later). Why would anyone buy an M1 Ultra, using a two year old CPU architecture&#8230; <a class="read-more-link" href="https://wadetregaskis.com/apples-timing-problem/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>The M1 was announced in November 2020.  The M1 Ultra wasn&#8217;t announced for another <em>sixteen months</em>, in March 2022.  It was clearly late.  <em>Very</em> late.  The M2 was presumed right around the corner (and it was, released just four months later).  Why would anyone buy an M1 Ultra, using a <em>two year old</em> CPU architecture designed for <em>phones</em>, when its successor was due any day (and would surely be a big leap again in performance)?</p>



<p>So I didn&#8217;t buy an M1 Ultra.</p>



<p>The M2 was announced in July 2022.  The M2 Ultra wasn&#8217;t announced for another <em>eleven</em> months, in June 2023, <em>and</em> offered only a small performance gain over its predecessor.  There were rumours that the M3 was right around the corner (and it was, released just four months later).  Worse, the M3 <em>Pro</em> and <em>Max</em> were released simultaneous to the base M3, and the M3 Max actually outperforms the M2 Ultra in many workloads.</p>



<p>So I didn&#8217;t buy an M2 Ultra.</p>



<p>So now the question is:  when will the M3 Ultra come out?  If it takes eleven to sixteen months, like its predecessors, then we&#8217;ll be expecting the M4 by then, and this sad cycle will have repeated yet again.</p>



<p>Will I buy an M3 Ultra?</p>



<p>I hope so, but it&#8217;s up to Apple.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/apples-timing-problem/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5586</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>Apple Watch Ultra is a poor dive computer</title>
		<link>https://wadetregaskis.com/apple-watch-ultra-is-a-poor-dive-computer/</link>
					<comments>https://wadetregaskis.com/apple-watch-ultra-is-a-poor-dive-computer/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Tue, 06 Dec 2022 04:59:28 +0000</pubDate>
				<category><![CDATA[Reviews]]></category>
		<category><![CDATA[Apple Watch Ultra]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[Oceanic+]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[scuba diving]]></category>
		<category><![CDATA[Shearwater Peregrine]]></category>
		<category><![CDATA[Tested]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=5191</guid>

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



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



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



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



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



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



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



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



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


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


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



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



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



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



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



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



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


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


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



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



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



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



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


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

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

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

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

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


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



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



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



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



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



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



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



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



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


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


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



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



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



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



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



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



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



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


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


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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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

			<media:content url="https://wadetregaskis.com/wp-content/uploads/2022/12/Oceanic-Apple-Watch-app.webp" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">5191</post-id>	</item>
		<item>
		<title>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>
	</channel>
</rss>
