<?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>Howto &#8211; Wade Tregaskis</title>
	<atom:link href="https://wadetregaskis.com/categories/howto/feed/" rel="self" type="application/rss+xml" />
	<link>https://wadetregaskis.com</link>
	<description></description>
	<lastBuildDate>Fri, 23 Jan 2026 02:51:13 +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>Howto &#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>Cross Dissolve only the video, not the audio, in Final Cut Pro</title>
		<link>https://wadetregaskis.com/cross-dissolve-only-the-video-not-the-audio-in-final-cut-pro/</link>
					<comments>https://wadetregaskis.com/cross-dissolve-only-the-video-not-the-audio-in-final-cut-pro/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Fri, 23 Jan 2026 02:51:10 +0000</pubDate>
				<category><![CDATA[Howto]]></category>
		<category><![CDATA[Cross Dissolve]]></category>
		<category><![CDATA[Final Cut Pro]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8689</guid>

					<description><![CDATA[When you drop a Cross Dissolve transition onto a clip in Final Cut Pro it applies the transition to both video and audio. That&#8217;s great &#8211; typically that&#8217;s what you want &#8211; but sometimes you don&#8217;t want that. Contrary to what you might read online or what the dumb robots might tell you (because they&#8230; <a class="read-more-link" href="https://wadetregaskis.com/cross-dissolve-only-the-video-not-the-audio-in-final-cut-pro/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>When you drop a Cross Dissolve transition onto a clip in Final Cut Pro it applies the transition to both video and audio.  That&#8217;s great &#8211; typically that&#8217;s what you want &#8211; but sometimes you <em>don&#8217;t</em> want that.</p>



<p>Contrary to what you might <a href="https://www.reddit.com/r/finalcutpro/comments/q9fwlr/how_do_i_cross_dissolve_the_image_but_not_the/" data-wpel-link="external" target="_blank" rel="external noopener">read online</a> or what the dumb robots might tell you (because they just plagiarise it from those same Reddit threads and YouTube videos), you can&#8217;t simply &#8220;Expand Audio&#8221; and apply the Cross Dissolve to just the video lane<sup data-fn="fe74553d-2443-42af-bf51-d9dc058a400d" class="fn"><a href="#fe74553d-2443-42af-bf51-d9dc058a400d" id="fe74553d-2443-42af-bf51-d9dc058a400d-link">1</a></sup>.  You <em>can</em> detach the audio entirely, but that&#8217;s heavy-handed and may make your life much harder downstream (since Final Cut Pro will then treat the separated audio and video as completely independent clips).</p>



<p>Actually, there&#8217;s a simple &#8216;hack&#8217; which works:</p>



<ol class="wp-block-list">
<li><strong>Drag the Cross Dissolve (or similar) onto the clip</strong>.</li>



<li><strong>Select the clip and &#8220;Expand Audio&#8221;</strong> (from the Clip menu, contextual pop-up menu, or by pressing ⌃S).  This is really just to get access to the audio track that&#8217;s otherwise hidden under the Cross Dissolve.</li>



<li><strong>Carefully drag the audio fade-in handle to the edge of the clip</strong>, to <em>exactly</em> where the tooltip says the offset is zero.  Don&#8217;t go too far &#8211; if you go over the end of the clip it&#8217;ll re-install the audio fade!</li>
</ol>



<p>It <em>feels</em> like that shouldn&#8217;t work &#8211; like it&#8217;s actually a subtle bug where Final Cut Pro actually intends to ignore your zero-duration fade-in and reset it back to the full length, like it does if you drag just a pixel too far.  But hey, thank goodness for some bugs!</p>



<figure class="wp-block-video"><video height="248" style="aspect-ratio: 774 / 248;" width="774" autoplay loop muted preload="auto" src="https://wadetregaskis.com/wp-content/uploads/2026/01/How-to-apply-a-Cross-Dissolve-to-only-the-video-of-a-clip-not-the-audio-in-Final-Cut-Pro-AV1.mp4" playsinline></video></figure>


<ol class="wp-block-footnotes"><li id="fe74553d-2443-42af-bf51-d9dc058a400d">Maybe this did work in an earlier version of Final Cut Pro, but it definitely does not in 11.2. <a href="#fe74553d-2443-42af-bf51-d9dc058a400d-link" aria-label="Jump to footnote reference 1">↩︎</a></li></ol>]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/cross-dissolve-only-the-video-not-the-audio-in-final-cut-pro/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">8689</post-id>	</item>
		<item>
		<title>Recording audio to an iPhone via a Tascam Portacapture X8</title>
		<link>https://wadetregaskis.com/recording-audio-to-an-iphone-via-a-tascam-portacapture-x8/</link>
					<comments>https://wadetregaskis.com/recording-audio-to-an-iphone-via-a-tascam-portacapture-x8/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Thu, 22 Jan 2026 06:04:04 +0000</pubDate>
				<category><![CDATA[Howto]]></category>
		<category><![CDATA[audio recording]]></category>
		<category><![CDATA[iPhone]]></category>
		<category><![CDATA[Snafu]]></category>
		<category><![CDATA[Tascam Portacapture X8]]></category>
		<category><![CDATA[Undocumented]]></category>
		<category><![CDATA[USB audio]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8672</guid>

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



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



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



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



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



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



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


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


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



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


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


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



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



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>Note: I vaguely recall having some issues with this <em>not</em> happening if you change some settings… it&#8217;s possible that things like &#8220;Pre Rec&#8221;, &#8220;Auto Rec&#8221;, or &#8220;Rec Pause&#8221; affect this.  I have all those off and it works as I&#8217;ve described.</p>
</div></div>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/recording-audio-to-an-iphone-via-a-tascam-portacapture-x8/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2026/01/tascam-portacapture-x8-error-dialog-usb-fs-mismatch-1536x2048.webp" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">8672</post-id>	</item>
		<item>
		<title>Fixing sudden, random iPhone disconnects from Image Capture</title>
		<link>https://wadetregaskis.com/fixing-sudden-random-iphone-disconnects-from-image-capture/</link>
					<comments>https://wadetregaskis.com/fixing-sudden-random-iphone-disconnects-from-image-capture/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Wed, 21 Jan 2026 18:33:09 +0000</pubDate>
				<category><![CDATA[Howto]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[Image Capture]]></category>
		<category><![CDATA[iPhone]]></category>
		<category><![CDATA[Tethering]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8666</guid>

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



<p>Thankfully the workaround is simple &#8211; disable tethering, or enable Airplane mode, while you&#8217;re using Image Capture.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/fixing-sudden-random-iphone-disconnects-from-image-capture/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2026/01/camera-disconnected-while-importing-error-dialog.webp" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">8666</post-id>	</item>
		<item>
		<title>Better image stabilisation in Final Cut Pro using Object Tracking</title>
		<link>https://wadetregaskis.com/better-image-stabilisation-in-final-cut-pro-using-object-tracking/</link>
					<comments>https://wadetregaskis.com/better-image-stabilisation-in-final-cut-pro-using-object-tracking/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Sun, 18 Jan 2026 20:22:54 +0000</pubDate>
				<category><![CDATA[Howto]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[Final Cut Pro]]></category>
		<category><![CDATA[Object Tracking]]></category>
		<category><![CDATA[stabilisation]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8656</guid>

					<description><![CDATA[This is basically a short written set of instructions derived from Cody Wanner&#8216;s YouTube video on the topic, refined a little for simplicity and updated for the newer GUI in more recent versions of Final Cut Pro. So feel free to view that video if you prefer that medium. I just find I need to&#8230; <a class="read-more-link" href="https://wadetregaskis.com/better-image-stabilisation-in-final-cut-pro-using-object-tracking/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>This is basically a short written set of instructions derived from <a href="https://www.youtube.com/@RCodyWanner" data-wpel-link="external" target="_blank" rel="external noopener">Cody Wanner</a>&#8216;s <a href="https://www.youtube.com/watch?v=JstYyYT4OzM" data-wpel-link="external" target="_blank" rel="external noopener">YouTube video on the topic</a>, refined a little for simplicity and updated for the newer GUI in more recent versions of Final Cut Pro.  So feel free to view that video if you prefer that medium.  I just find I need to reference this sporadically and it&#8217;s easier to just re-read written instructions that re-watch a whole video.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p><strong>Background</strong>: Final Cut Pro&#8217;s built-in image stabilisation is a bit unreliable.  Sometimes it works perfectly, just like you&#8217;d expect.  Most of the time it requires manual tweaking and futzing in order to get good-enough results.  And sometimes it just <em>does not work</em>, no matter what you do, for reasons that are beyond me.  The technique shown here is annoyingly laborious to execute, but it works not just more reliably but also often just better (if your objective is <em>complete</em> stabilisation, at least).</p>
</div></div>



<p>Steps:</p>



<ol class="wp-block-list">
<li>Duplicate the clip (option-drag it in the timeline view) and place the duplicate above<sup data-fn="6883a475-8404-4bfe-a857-098915a99cc4" class="fn"><a href="#6883a475-8404-4bfe-a857-098915a99cc4" id="6883a475-8404-4bfe-a857-098915a99cc4-link">1</a></sup> the original.  Ensure it&#8217;s perfectly aligned on the timeline&#8217;s X (time) axis, otherwise it won&#8217;t show up as a tracking source in step 3.2.</li>



<li>On the duplicate clip (make sure it&#8217;s selected and your playback position is within it so that you can see what you&#8217;re doing!):
<ol class="wp-block-list">
<li>Invert the scale (e.g. make it -100% instead of the default 100%).</li>



<li>Invert the X &amp; Y offsets (if necessary &#8211; if they were non-zero beforehand).</li>



<li>Add an Object Tracker (the + icon to the right of the &#8220;Tracker&#8221; titlebar at the bottom of the Video Inspector (right-hand pane)).</li>



<li>A white grid &#8211; your anchor section &#8211; should appear over your clip.  Move and resize it to have it cover an appropriate part of the clip (a subsection that&#8217;s contrasty and contains object(s) that are stable &#8211; in appearance and position &#8211; within the world space of the scene, and ideally are never obscured by anything during the clip).
<ul class="wp-block-list">
<li>☝️ You can adjust the timeline position of the clip to find the optimum frame in which to identify your anchor section.</li>
</ul>
</li>



<li>Click Analyze.
<ul class="wp-block-list">
<li>⚠️ This will sometimes not work if certain other operations are outstanding, such as dominant motion analysis for the clip &#8211; you have to either cancel those background tasks or wait for them to finish. One of Final Cut Pro&#8217;s many irritating bugs.</li>



<li>⚠️ Watch carefully as it works through the clip (forwards from your starting point, then backwards, as necessary), for:
<ul class="wp-block-list">
<li>Your anchor section being intruded upon by any moving objects within the scene.  If that happens, try to go back and refine your anchor section placement so that it won&#8217;t be intruded upon.  If that&#8217;s impossible, you can proceed but be aware that the results may be subpar.</li>



<li>The anchor section changing in position and size &#8211; the more it &#8216;wobbles&#8217;, the worse the final results are likely to be.  Consider different anchor section placement, or try a different analysis method (in the Video Inspector, for your Object Track, change the Analysis Method from the default, &#8220;Automatic&#8221;, to another option &#8211; note that you must click Analyze again after changing this, for it to take effect).</li>
</ul>
</li>
</ul>
</li>
</ol>
</li>



<li>On the original clip:
<ol class="wp-block-list">
<li>Open the Transform viewer (the rectangle icon in the &#8220;Transform&#8221; titlebar in the Video Inspector).</li>



<li>Click the downward chevron next to &#8220;Tracker&#8221; text on the Tracker tab button (at the top of the video preview view), to bring up the configuration pop-up:
<ul class="wp-block-list">
<li>Set Tracker Source to your duplicate clip.</li>



<li>Set Tracker to the Object Tracker (or whatever you renamed it to).</li>



<li>Set the axes you want stabilisation to apply to (the Apply Tracker To checkboxes).</li>
</ul>
</li>
</ol>
</li>



<li>Disable the duplicate (e.g. V key while it&#8217;s selected).
<ul class="wp-block-list">
<li>☝️ You cannot have it disabled before you select it as the Tracker Source, as it won&#8217;t show up in the pop-up menu while disabled.</li>
</ul>
</li>



<li>On the original clip:
<ol class="wp-block-list">
<li>Set the scale to -100%.</li>



<li>Adjust the X &amp; Y positions to correct the framing, if necessary (if your X &amp; Y position were both 0 on the original clip, this shouldn&#8217;t be necessary).  Note that this usually isn&#8217;t a simple sign inversion as in similar previous steps.</li>
</ol>
</li>
</ol>



<p>Optional sixth step:  file a bug report or suggestion with Apple asking for them to make their built-in image stabilisation work better, and/or make the object tracker GUI easier to use (it could be just one or two clicks, to enable it and say &#8220;lock this part of the scene in place&#8221;).</p>


<ol class="wp-block-footnotes"><li id="6883a475-8404-4bfe-a857-098915a99cc4">Technically it doesn&#8217;t matter if it&#8217;s above or below, I just find it slightly more convenient when it&#8217;s above as usually it&#8217;ll be disabled (showing the original, now-stabilised version from below) but I access it easily by simply enabling it (V key), such as if I want to adjust the tracking. <a href="#6883a475-8404-4bfe-a857-098915a99cc4-link" aria-label="Jump to footnote reference 1">↩︎</a></li></ol>]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/better-image-stabilisation-in-final-cut-pro-using-object-tracking/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">8656</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>Fading audio with AVPlayer</title>
		<link>https://wadetregaskis.com/fading-audio-with-avplayer/</link>
					<comments>https://wadetregaskis.com/fading-audio-with-avplayer/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Wed, 10 Dec 2025 16:29:00 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Howto]]></category>
		<category><![CDATA[audio]]></category>
		<category><![CDATA[AVAudioMix]]></category>
		<category><![CDATA[AVAudioMixInputParameters]]></category>
		<category><![CDATA[AVMutableAudioMix]]></category>
		<category><![CDATA[AVMutableAudioMixInputParameters]]></category>
		<category><![CDATA[AVPlayer]]></category>
		<category><![CDATA[fade]]></category>
		<category><![CDATA[Swift]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8630</guid>

					<description><![CDATA[AVPlayer doesn&#8217;t provide a built-in way to fade in or out. I previously described how you achieve a video fade-in (or out) using general CoreAnimation layer animation, as part of making a macOS screen saver. Now let&#8217;s tackle the audio. I&#8217;m not certain what curve this implements, but to my ears it doesn&#8217;t sound quite&#8230; <a class="read-more-link" href="https://wadetregaskis.com/fading-audio-with-avplayer/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p><code>AVPlayer</code> doesn&#8217;t provide a built-in way to fade in or out.  I previously described how you achieve a <em>video</em> fade-in (or out) using general CoreAnimation layer animation, as <a href="https://wadetregaskis.com/how-to-make-a-macos-screen-saver/#Bonus_topic_fading_in" data-wpel-link="internal">part of making a macOS screen saver</a>.  Now let&#8217;s tackle the audio.</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">extension</span><span style="color: #000000"> </span><span style="color: #267F99">AVPlayer</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">fadeAudio</span><span style="color: #000000">(</span><span style="color: #795E26">from</span><span style="color: #000000"> </span><span style="color: #001080">startVolume</span><span style="color: #000000">: </span><span style="color: #267F99">Float</span><span style="color: #000000">, </span><span style="color: #795E26">to</span><span style="color: #000000"> </span><span style="color: #001080">endVolume</span><span style="color: #000000">: </span><span style="color: #267F99">Float</span><span style="color: #000000">, </span><span style="color: #795E26">duration</span><span style="color: #000000">: </span><span style="color: #267F99">Double</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"> audioMix = </span><span style="color: #795E26">AVMutableAudioMix</span><span style="color: #000000">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        audioMix.</span><span style="color: #001080">inputParameters</span><span style="color: #000000"> = (player.</span><span style="color: #001080">currentItem</span><span style="color: #000000">?.</span><span style="color: #001080">tracks</span><span style="color: #000000"> ?? [])</span></span>
<span class="line"><span style="color: #000000">                                    .</span><span style="color: #795E26">compactMap</span><span style="color: #000000">(\.</span><span style="color: #001080">assetTrack</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">                                    .</span><span style="color: #795E26">filter</span><span style="color: #000000">({ </span><span style="color: #0000FF">$0</span><span style="color: #000000">.</span><span style="color: #001080">mediaType</span><span style="color: #000000"> == .</span><span style="color: #001080">audio</span><span style="color: #000000"> })</span></span>
<span class="line"><span style="color: #000000">                                    .</span><span style="color: #795E26">map</span><span style="color: #000000"> { track </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"> currentTime = player.</span><span style="color: #795E26">currentTime</span><span style="color: #000000">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #0000FF">let</span><span style="color: #000000"> parameters = </span><span style="color: #795E26">AVMutableAudioMixInputParameters</span><span style="color: #000000">(</span><span style="color: #795E26">track</span><span style="color: #000000">: track)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">            parameters.</span><span style="color: #795E26">setVolumeRamp</span><span style="color: #000000">(</span><span style="color: #795E26">fromStartVolume</span><span style="color: #000000">: startVolume,</span></span>
<span class="line"><span style="color: #000000">                                     </span><span style="color: #795E26">toEndVolume</span><span style="color: #000000">: endVolume,</span></span>
<span class="line"><span style="color: #000000">                                     </span><span style="color: #795E26">timeRange</span><span style="color: #000000">: </span><span style="color: #795E26">CMTimeRange</span><span style="color: #000000">(</span><span style="color: #795E26">start</span><span style="color: #000000">: currentTime,</span></span>
<span class="line"><span style="color: #000000">                                                            </span><span style="color: #795E26">duration</span><span style="color: #000000">: </span><span style="color: #795E26">CMTime</span><span style="color: #000000">(</span><span style="color: #795E26">seconds</span><span style="color: #000000">: duration,</span></span>
<span class="line"><span style="color: #000000">                                                                             </span><span style="color: #795E26">preferredTimescale</span><span style="color: #000000">: currentTime.</span><span style="color: #001080">timeScale</span><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"> parameters</span></span>
<span class="line"><span style="color: #000000">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        player.</span><span style="color: #001080">currentItem</span><span style="color: #000000">?.</span><span style="color: #001080">audioMix</span><span style="color: #000000"> = audioMix</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>I&#8217;m not certain what curve this implements, but to my ears it doesn&#8217;t sound quite as harsh as a naive linear ramp, so perhaps it&#8217;s an S-curve or similar.</p>



<h2 class="wp-block-heading">Edge cases not handled</h2>



<h3 class="wp-block-heading">Where there&#8217;s less than <code>duration</code> time left in the track(s).</h3>



<p>How you want to handle that might vary depending on context. e.g. you could clamp the duration to the remaining duration (but you have to think about whether your individual tracks all have the same duration, whether they match the duration of the overall playback item, and whether they&#8217;re all aligned within the playback sequence), or wrap it around to the beginning (if you&#8217;re looping), or carry the fade through to the next item (if you&#8217;re playing a sequence of items), etc. Alas this must be left as an exercise to you, the reader.</p>



<h3 class="wp-block-heading">Starting a fade while another one is still in progress.</h3>



<p>It will halt the previous fade, immediately jump to <code>startVolume</code>, and perform the new fade. If you know that a prior fade is in progress you could potentially extrapolate the current volume and start from there instead (though beware of non-linear ramping).</p>



<h3 class="wp-block-heading">If you move the playhead backwards (e.g. skimming, or looped playback).</h3>



<p>The mix will stay in place, and result in wonky volume levels on the subsequent plays through. To work around that, you can add:</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">player.</span><span style="color: #001080">currentItem</span><span style="color: #000000">?.</span><span style="color: #001080">audioMix</span><span style="color: #000000"> = </span><span style="color: #0000FF">nil</span></span>
<span class="line"><span style="color: #000000">player.</span><span style="color: #001080">volume</span><span style="color: #000000"> = endVolume</span></span></code></pre></div>



<p>…wherever you restart playback at the beginning or move playback earlier than the end of the fade.</p>



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



<p>You <em>can</em> work around these limitations by doing a timer-based fade (i.e. <code>player.volume += smallIncrement</code> at regular, short intervals). However, the problem with that approach is that it&#8217;s not synchronised to actual playback &#8211; e.g. if the audio is paused, stutters, or faces an initial loading delay, your fade won&#8217;t wait for it, potentially resulting in no fade at all (e.g. it takes five seconds to buffer the audio before playback starts, at which point your five second &#8220;fade&#8221; has run to completion, so your audio starts playing abruptly at full volume).</p>



<p>There&#8217;s very likely a third option that addresses <em>all</em> these shortcomings, but I explored that a bit and concluded that it&#8217;d be a lot more work.  If someone wants to explore that all the way, I&#8217;d be interested to see the result.  But for many purposes the above code is quite sufficient.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/fading-audio-with-avplayer/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">8630</post-id>	</item>
		<item>
		<title>How to loop video in AVPlayer</title>
		<link>https://wadetregaskis.com/how-to-loop-video-in-avplayer/</link>
					<comments>https://wadetregaskis.com/how-to-loop-video-in-avplayer/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Tue, 09 Dec 2025 17:42:00 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Howto]]></category>
		<category><![CDATA[AVPlayer]]></category>
		<category><![CDATA[AVPlayerItem]]></category>
		<category><![CDATA[AVPlayerItemDidPlayToEndTime]]></category>
		<category><![CDATA[AVPlayerLooper]]></category>
		<category><![CDATA[AVQueuePlayer]]></category>
		<category><![CDATA[Swift]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8625</guid>

					<description><![CDATA[This is pretty rudimentary, but apparently our robot overlords need me to write this post because many of them suggested some truly bizarre approaches, some of which don&#8217;t work at all. If you&#8217;re using AVQueuePlayer, then just use AVPlayerLooper. Easy. But if for some reason you want to use AVPlayer specifically (e.g. you need to&#8230; <a class="read-more-link" href="https://wadetregaskis.com/how-to-loop-video-in-avplayer/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>This is pretty rudimentary, but apparently our robot overlords need me to write this post because many of them suggested some truly bizarre approaches, some of which don&#8217;t work at all.</p>



<p>If you&#8217;re using <code><a href="https://developer.apple.com/documentation/avfoundation/avqueueplayer" data-wpel-link="external" target="_blank" rel="external noopener">AVQueuePlayer</a></code>, then just use <code><a href="https://developer.apple.com/documentation/avfoundation/avplayerlooper" data-wpel-link="external" target="_blank" rel="external noopener">AVPlayerLooper</a></code>.  Easy.  But if for some reason you want to use <code>AVPlayer</code> specifically (e.g. you need to do additional things anyway when playback loops back around), read on.</p>



<p><code>AVPlayer</code> itself doesn&#8217;t really help you here &#8211; it&#8217;s <code>AVPlayerItem</code> that you need to look at, as it has several notifications associated with it that can be very useful &#8211; most relevant here is<a href="https://developer.apple.com/documentation/avfoundation/avplayeritem/didplaytoendtimenotification" data-wpel-link="external" target="_blank" rel="external noopener"> </a>the <a href="https://developer.apple.com/documentation/avfoundation/avplayeritem/didplaytoendtimenotification" data-wpel-link="external" target="_blank" rel="external noopener">didPlayToEndTimeNotification</a>.  Simply observe that and restart the player explicitly, like so:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><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"> player = </span><span style="color: #795E26">AVPlayer</span><span style="color: #000000">(…)  </span><span style="color: #008000">// You&#39;ll need to keep a reference to this somewhere, for use in the notification handler, as the notification doesn&#39;t provide it.</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">void </span><span style="color: #795E26">startPlayer</span><span style="color: #000000">() {  </span><span style="color: #008000">// Or wherever / however you start playback.</span></span>
<span class="line"><span style="color: #000000">    NotificationCenter.</span><span style="color: #001080">default</span><span style="color: #000000">.</span><span style="color: #795E26">addObserver</span><span style="color: #000000">(</span><span style="color: #0000FF">self</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                           </span><span style="color: #795E26">selector</span><span style="color: #000000">: </span><span style="color: #795E26">#selector</span><span style="color: #000000">(</span><span style="color: #795E26">playedToEnd</span><span style="color: #000000">(_:)),</span></span>
<span class="line"><span style="color: #000000">                                           </span><span style="color: #795E26">name</span><span style="color: #000000">: .</span><span style="color: #001080">AVPlayerItemDidPlayToEndTime</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                           </span><span style="color: #795E26">object</span><span style="color: #000000">: player.</span><span style="color: #001080">currentItem</span><span style="color: #000000">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    player.</span><span style="color: #795E26">play</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: #0000FF">@objc</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">playedToEnd</span><span style="color: #000000">(</span><span style="color: #795E26">_</span><span style="color: #000000"> </span><span style="color: #001080">notification</span><span style="color: #000000">: Notification) {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">assert</span><span style="color: #000000">(player.</span><span style="color: #001080">currentItem</span><span style="color: #000000"> == notification.</span><span style="color: #001080">object</span><span style="color: #000000"> as? AVPlayerItem, </span><span style="color: #A31515">&quot;AVPlayer </span><span style="color: #0000FF">\(</span><span style="color: #000000FF">player</span><span style="color: #0000FF">)</span><span style="color: #A31515">&#39;s current item (</span><span style="color: #0000FF">\(</span><span style="color: #000000FF">player.</span><span style="color: #001080">currentItem</span><span style="color: #0000FF">)</span><span style="color: #A31515">) doesn&#39;t match the one in the notification object (</span><span style="color: #0000FF">\(</span><span style="color: #000000FF">notification.</span><span style="color: #001080">object</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">    player.</span><span style="color: #795E26">seek</span><span style="color: #000000">(</span><span style="color: #795E26">to</span><span style="color: #000000">: .</span><span style="color: #001080">zero</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">    player.</span><span style="color: #795E26">play</span><span style="color: #000000">()</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>Again, considering using <code>AVQueuePlayer</code> if possible, but the above works too.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/how-to-loop-video-in-avplayer/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">8625</post-id>	</item>
		<item>
		<title>How to make a macOS screen saver</title>
		<link>https://wadetregaskis.com/how-to-make-a-macos-screen-saver/</link>
					<comments>https://wadetregaskis.com/how-to-make-a-macos-screen-saver/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Sat, 06 Dec 2025 22:46:22 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Howto]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[com.apple.screensaver.willstop]]></category>
		<category><![CDATA[SaveHollywood]]></category>
		<category><![CDATA[Screen saver]]></category>
		<category><![CDATA[ScreenSaverView]]></category>
		<category><![CDATA[Snafu]]></category>
		<category><![CDATA[Swift]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8580</guid>

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



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



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



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



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



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



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



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

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

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

    private func setup() {
        // TODO
    }

    override func startAnimation() {
        super.startAnimation()

        // TODO
    }

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

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

        super.stopAnimation()
    }

    override var hasConfigureSheet: Bool {
        true
    }

    private var configureSheetController: ConfigureSheetController?

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

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

    override func windowDidLoad() {
        super.windowDidLoad()

        // TODO
    }

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

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



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



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



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



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



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



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



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



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



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



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



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



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



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



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

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

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

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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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

var lameDuck = false

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

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

    // Further setup…
}

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

    stopAnimation()

    self.removeFromSuperview()

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

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



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



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



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



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



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

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

        idleTimeoutWorkItem = nil
    }
    
    // Other code…
}

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

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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



<p>It&#8217;s that recurring theme of <em>not caring</em> that&#8217;s most disappointing in today&#8217;s Apple.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/how-to-make-a-macos-screen-saver/feed/</wfw:commentRss>
			<slash:comments>3</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2025/12/screen-saver-system-settings-panel-with-my-screen-saver-selected.webp" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">8580</post-id>	</item>
		<item>
		<title>How to disable automatic project backups in Final Cut Pro</title>
		<link>https://wadetregaskis.com/how-to-disable-automatic-project-backups-in-final-cut-pro/</link>
					<comments>https://wadetregaskis.com/how-to-disable-automatic-project-backups-in-final-cut-pro/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Wed, 23 Jul 2025 23:22:17 +0000</pubDate>
				<category><![CDATA[Howto]]></category>
		<category><![CDATA[backup]]></category>
		<category><![CDATA[Final Cut Pro]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8517</guid>

					<description><![CDATA[Apple does provide these instructions, although they were annoyingly hard to find (in no small part because the page title is only incidentally related to the task). In short: My projects are already covered by Time Machine and other backup mechanisms, and I tried to tolerate Final Cut Pros built-in backup system, but the damn&#8230; <a class="read-more-link" href="https://wadetregaskis.com/how-to-disable-automatic-project-backups-in-final-cut-pro/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>Apple does provide <a href="https://support.apple.com/en-au/guide/final-cut-pro/ver7db6ffe77/11.1/mac/14.6" data-wpel-link="external" target="_blank" rel="external noopener">these instructions</a>, although they were annoyingly hard to find (in no small part because the page title is only incidentally related to the task).</p>



<p>In short:</p>



<ol class="wp-block-list">
<li>File &gt; Library Properties (⌃⌘J).</li>



<li>Click &#8220;Modify Settings&#8221; next to &#8220;Storage Locations&#8221;.</li>



<li>Set the &#8220;Backups&#8221; option to &#8220;Do Not Save&#8221;.</li>
</ol>



<p>My projects are already covered by Time Machine and other backup mechanisms, and I tried to tolerate Final Cut Pros built-in backup system, but the damn thing runs <em>so</em> often, which I know about very well because whenever it runs Final Cut Pro becomes completely unusable for several minutes.  It&#8217;s immensely irritating when it kicks in while I&#8217;m in the middle of editing.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/how-to-disable-automatic-project-backups-in-final-cut-pro/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">8517</post-id>	</item>
		<item>
		<title>Lightroom could not import this catalog because of an unknown error</title>
		<link>https://wadetregaskis.com/lightroom-could-not-import-this-catalog-because-of-an-unknown-error/</link>
					<comments>https://wadetregaskis.com/lightroom-could-not-import-this-catalog-because-of-an-unknown-error/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Fri, 07 Jun 2024 22:23:07 +0000</pubDate>
				<category><![CDATA[Howto]]></category>
		<category><![CDATA[Photography]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[Lightroom]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8219</guid>

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



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



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



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



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



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



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



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



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



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


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

					<description><![CDATA[Five years ago installing Swift on a Raspberry Pi &#8211; or really any non-Apple platform &#8211; was fairly involved. Compared to getting a Raspberry Pi working to begin with it was easy, but still a far cry from apt install swift. Sadly it&#8217;s still not quite that easy (and some Python package is squatting on&#8230; <a class="read-more-link" href="https://wadetregaskis.com/swift-on-a-raspberry-pi-in-2024/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>Five years ago installing Swift on a Raspberry Pi &#8211; or really <em>any</em> non-Apple platform &#8211; was <a href="https://wadetregaskis.com/swift-on-raspberry-pi/" data-wpel-link="internal">fairly involved</a>.  Compared to getting a Raspberry Pi working to begin with it was easy, but still a far cry from <code>apt install swift</code>.</p>



<p>Sadly it&#8217;s still not <em>quite</em> that easy (and some Python package is squatting on the <code>swift</code> package name 🤨).</p>



<p>You can still install from source, in principle, although I&#8217;m not aware of any <em>current</em> instructions on how to do so.  The <a href="https://github.com/uraimo/buildSwiftOnARM" data-wpel-link="external" target="_blank" rel="external noopener">buildSwiftOnARM</a> project has seemingly been abandoned.  I haven&#8217;t tested if their now-years-old instructions still work for the latest versions of Swift (e.g. 5.10, 6.0).</p>



<p>But, you shouldn&#8217;t need to build from source anymore &#8211; now you have many better options:</p>



<h2 class="wp-block-heading">Swift.org (Apple-provided options)</h2>



<ul class="wp-block-list">
<li>If you happen to be using a <code>yum</code>-centric platform, like RHEL / Amazon Linux / CentOS, you can (finally!) just <a href="https://www.swift.org/install/linux/#installation-via-rpm" data-wpel-link="external" target="_blank" rel="external noopener">install RPMs</a>.</li>



<li>If you happen to use one of <a href="https://www.swift.org/platform-support/" data-wpel-link="external" target="_blank" rel="external noopener">the officially supported Linux distributions</a>, you can <a href="https://www.swift.org/install/linux/#installation-via-docker" data-wpel-link="external" target="_blank" rel="external noopener">install via Docker</a>.  Alas many major distros are notably absent from that list, like Debian<sup data-fn="bf7d2063-1944-4bd3-8081-5ce94b1243ed" class="fn"><a href="#bf7d2063-1944-4bd3-8081-5ce94b1243ed" id="bf7d2063-1944-4bd3-8081-5ce94b1243ed-link">1</a></sup> (and therefore Raspbian). 😔<br><br>It&#8217;s unfortunate that <a href="https://www.swift.org/install/linux/" data-wpel-link="external" target="_blank" rel="external noopener">Swift.org pushes this as the preferred way</a> to install Swift on Linux.  I avoid Docker because it tends to just make everything more complex (and error-prone), and encourage bad software practices (&#8220;it works in my docker image 🖕&#8221;).
<ul class="wp-block-list">
<li>You <em>can</em> bypass the Docker requirement via the relatively primitive method of <a href="https://www.swift.org/install/linux/#installation-via-tarball" data-wpel-link="external" target="_blank" rel="external noopener">splatting a tarball into your system</a>.  It works, it&#8217;s pretty quick, but it&#8217;s a maintenance hassle (no version management, no reliable update method, etc).</li>
</ul>
</li>
</ul>



<h2 class="wp-block-heading">Swift Community Apt Repository</h2>



<p>Thanks to the efforts of community volunteers &#8211; in particular the folks at <a href="https://futurejones.com" data-wpel-link="external" target="_blank" rel="external noopener">FutureJones</a> &#8211; there is the <a href="https://swiftlang.xyz" data-wpel-link="external" target="_blank" rel="external noopener">Swift Community Apt Repository</a>, which fills the massive void left by Apple&#8217;s apparent hatred for <code>apt</code>-based distros:</p>



<ol class="wp-block-list">
<li><code>curl -s https://archive.swiftlang.xyz/install.sh | sudo bash</code></li>



<li><code>sudo apt install swiftlang</code></li>



<li><a href="https://www.youtube.com/watch?v=c3pa3HCNHGI" data-wpel-link="external" target="_blank" rel="external noopener">There is no step 3!</a></li>
</ol>



<p><a href="https://swiftlang.xyz/user-guide" data-wpel-link="external" target="_blank" rel="external noopener">More details</a> (including the list of supported distros).</p>



<p>It&#8217;s a shame that it falls to third parties to make Swift on Linux easy (on major distros, at least).  Don&#8217;t forget to <a href="https://ko-fi.com/futurejones" data-wpel-link="external" target="_blank" rel="external noopener">support them</a> if you benefit from their work!</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>☝️ FutureJones also run <a href="https://swift-arm.com" data-wpel-link="external" target="_blank" rel="external noopener">Swift-Arm</a>, <a href="https://swift-arm.com/category/news" data-wpel-link="external" target="_blank" rel="external noopener">which implies</a> that only Swift 5.8 (and older) are supported.  But in fact 5.10 is available &#8211; it seems they&#8217;ve just forgotten / chosen to stop posting about newly supported Swift versions.</p>
</div></div>


<ol class="wp-block-footnotes"><li id="bf7d2063-1944-4bd3-8081-5ce94b1243ed">Though it appears <a href="https://github.com/apple/swift-docker/commit/1263ae112a1fd4b3445215c1f86994f161775b5c" data-wpel-link="external" target="_blank" rel="external noopener">Debian support is in the works</a>. <a href="#bf7d2063-1944-4bd3-8081-5ce94b1243ed-link" aria-label="Jump to footnote reference 1">↩︎</a></li></ol>]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/swift-on-a-raspberry-pi-in-2024/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2019/10/Swift-on-Raspberry-Pi.webp" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">8202</post-id>	</item>
		<item>
		<title>Copy-on-write on APFS</title>
		<link>https://wadetregaskis.com/copy-on-write-on-apfs/</link>
					<comments>https://wadetregaskis.com/copy-on-write-on-apfs/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Thu, 16 May 2024 21:14:10 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Education]]></category>
		<category><![CDATA[Howto]]></category>
		<category><![CDATA[APFS]]></category>
		<category><![CDATA[clonefile]]></category>
		<category><![CDATA[copy-on-write]]></category>
		<category><![CDATA[copyfile]]></category>
		<category><![CDATA[FileManager]]></category>
		<category><![CDATA[Foundation]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8138</guid>

					<description><![CDATA[APFS (like many modern file systems but unlike its predecessor HFS+) supports copy-on-write. This means you can logically copy a file &#8211; it looks and behaves like a distinct file &#8211; but it doesn&#8217;t immediately copy the file&#8217;s contents on disk &#8211; it merely shares them with the original. Only if and as you modify&#8230; <a class="read-more-link" href="https://wadetregaskis.com/copy-on-write-on-apfs/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p><a href="https://en.wikipedia.org/wiki/Apple_File_System" data-wpel-link="external" target="_blank" rel="external noopener">APFS</a> (like many modern file systems but unlike its predecessor <a href="https://en.wikipedia.org/wiki/HFS_Plus" data-wpel-link="external" target="_blank" rel="external noopener">HFS+</a>) supports <a href="https://eclecticlight.co/2017/06/23/what-is-copy-on-write-and-how-is-it-good/" data-wpel-link="external" target="_blank" rel="external noopener">copy-on-write</a>.  This means you can <em>logically</em> copy a file &#8211; it <em>looks</em> and <em>behaves</em> like a distinct file &#8211; but it doesn&#8217;t <em>immediately</em> copy the file&#8217;s contents on disk &#8211; it merely shares them with the original.  Only if and as you modify either version do they start to diverge on disk, with APFS dynamically allocating new storage for the modified parts<sup data-fn="fd9542d5-ca23-49bc-ae00-3d2b57caf906" class="fn"><a href="#fd9542d5-ca23-49bc-ae00-3d2b57caf906" id="fd9542d5-ca23-49bc-ae00-3d2b57caf906-link">1</a></sup>.</p>



<p>This is kind of a sister function to hard links, which similarly avoid copying the file&#8217;s contents <em>but</em> where modifications apply to <em>all</em> copies.  See also <a href="https://eclecticlight.co/2019/01/05/aliases-hard-links-symlinks-and-copies-in-mojaves-apfs/" data-wpel-link="external" target="_blank" rel="external noopener">this article on the differences</a>, including versus aliases and symlinks.</p>



<p>Copy-on-write is beneficial for several reasons:</p>



<ul class="wp-block-list">
<li>Copies don&#8217;t take up any significant space (just whatever tiny amount is necessary for their metadata).</li>



<li>The initial copy operation is practically instantaneous (just a few small metadata writes &amp; updates).</li>



<li>Deferring (if not entirely avoiding) the actual disk I/O reduces wear on the disk.</li>



<li>The copies can share common segments, saving disk space even when they&#8217;re not ultimately identical copies.</li>
</ul>



<p>It does have some potential downsides:</p>



<ul class="wp-block-list">
<li>You don&#8217;t get the increased data redundancy and error resilience that <em>actual</em> copies provide (although if you&#8217;re aiming for data redundancy or backup, you should be using separate physical disks anyway).</li>



<li>It can make subsequent modifications of the file slower, as even just modifying a single byte can trigger the actual copy to be performed.</li>
</ul>



<p>And some basic limitations:</p>



<ul class="wp-block-list">
<li>It&#8217;s only supported on APFS (and <em>maybe</em> additional file systems added by 3rd party extensions, but I haven&#8217;t tested nor can I find any accounts of this).</li>



<li>It only works within individual volumes (it doesn&#8217;t work even between two volumes in the same APFS container, or sharing the same physical disk).</li>
</ul>



<p>Contrary to what I saw online in a few places, copy-on-write works on <em>all</em> APFS volumes, irrespective of whether they are backed by SSDs, HDDs, or some other type of storage.</p>



<h2 class="wp-block-heading">Should I use it?</h2>



<p>Yes!</p>



<p>For most purposes those downsides aren&#8217;t an issue, and the limitations merely mean that it&#8217;s wise to have a fallback option (of just copying the actual file contents) whenever copy-on-write isn&#8217;t available.  And many of the tools &amp; APIs fallback automatically (unless you explicitly require them not to, such as with <code>COPYFILE_CLONE_FORCE</code> to <code>copyfile</code>).</p>



<h2 class="wp-block-heading">How do I use it?</h2>



<figure class="wp-block-table aligncenter"><table><thead><tr><th>Method</th><th class="has-text-align-center" data-align="center">Uses copy-on-write<br>(where possible)</th><th class="has-text-align-center" data-align="center">Does actual copy<br>if copy-on-write<br> isn&#8217;t available</th></tr></thead><tbody><tr><td>cp</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">N/A</td></tr><tr><td>cp -c</td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">❌</td></tr><tr><td>ditto</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">N/A</td></tr><tr><td>ditto &#8211;clone</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">N/A</td></tr><tr><td>dd</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">N/A</td></tr><tr><td>scp</td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">N/A</td></tr><tr><td>Finder Copy then Paste</td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">✅</td></tr><tr><td>Finder Duplicate</td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">✅</td></tr><tr><td>Finder ⌥-drag</td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">✅</td></tr><tr><td><code><a href="https://www.manpagez.com/man/2/clonefile/" data-wpel-link="external" target="_blank" rel="external noopener">clonefile</a></code><sup data-fn="1bebdb6f-7e69-4ebb-bca6-692795e4e8f7" class="fn"><a href="#1bebdb6f-7e69-4ebb-bca6-692795e4e8f7" id="1bebdb6f-7e69-4ebb-bca6-692795e4e8f7-link">2</a></sup></td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">❌</td></tr><tr><td><code><a href="https://keith.github.io/xcode-man-pages/copyfile.3.html" data-wpel-link="external" target="_blank" rel="external noopener">copyfile</a>(…, …, …, COPYFILE_DATA)</code></td><td class="has-text-align-center" data-align="center">❌</td><td class="has-text-align-center" data-align="center">N/A</td></tr><tr><td><code><a href="https://keith.github.io/xcode-man-pages/copyfile.3.html" data-wpel-link="external" target="_blank" rel="external noopener">copyfile</a>(…, …, …, COPYFILE_CLONE)</code></td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">✅</td></tr><tr><td><code><a href="https://keith.github.io/xcode-man-pages/copyfile.3.html" data-wpel-link="external" target="_blank" rel="external noopener">copyfile</a>(…, …, …, COPYFILE_CLONE_FORCE)</code></td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">❌</td></tr><tr><td><code><a href="https://developer.apple.com/documentation/foundation/filemanager/1412957-copyitem" data-wpel-link="external" target="_blank" rel="external noopener">FileManager.copyItem(at:to:)</a></code></td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">✅</td></tr><tr><td><code><a href="https://developer.apple.com/documentation/foundation/filemanager/1407903-copyitem" data-wpel-link="external" target="_blank" rel="external noopener">FileManager.copyItem(atPath:toPath:)</a></code></td><td class="has-text-align-center" data-align="center">✅</td><td class="has-text-align-center" data-align="center">✅</td></tr></tbody></table><figcaption class="wp-element-caption">This is accurate for macOS 14.5 (23F79).  The behaviour might vary across OS releases.</figcaption></figure>



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



<h3 class="wp-block-heading">Testing method</h3>



<p>Not that this is interesting, just for posterity and to show my work a bit, in case I made a mistake.</p>



<p>I created large-enough files on each of my test volumes (APFS on SSD, APFS on HDD, HFS+ on SSD) that an actual copy would take tens of seconds at least, using <code>dd</code> e.g.:</p>



<figure class="wp-block-pullquote"><blockquote><p><code>dd if=/dev/random of=/tmp/bigfile oflag=direct status=progress bs=1k count=104857600</code></p></blockquote></figure>



<p>I then ran the various command line tools on these files, attempting to clone the file to a different name in the same folder, and observed disk I/O activity with Activity Monitor and iStat Menus.</p>



<p>For actual copy-on-writes I would observe that the program successfully concluded practically instantly, and there&#8217;d be at most a small blip of disk writes (for metadata modifications).</p>



<p>For failed copy-on-writes I would observe that the program would not exit promptly and I&#8217;d see voluminous disk writes (hundreds of megabytes to gigabytes per second, depending on the disk, sustained for many seconds until I was satisfied with the results and killed the test).</p>



<p>For API tests the overall approach was the same, but the APIs were invoked from inside <code>swift repl</code> where possible, and from a throw-away Swift script otherwise<sup data-fn="4f593842-632c-4279-83ba-435dde24c411" class="fn"><a href="#4f593842-632c-4279-83ba-435dde24c411" id="4f593842-632c-4279-83ba-435dde24c411-link">3</a></sup>.</p>


<ol class="wp-block-footnotes"><li id="fd9542d5-ca23-49bc-ae00-3d2b57caf906">I haven&#8217;t tested it, but as far as I&#8217;ve heard APFS does <em>not</em> actually check if the modifications actually diverge the files.  e.g. if you &#8220;modify&#8221; a byte of the file to the value it already has &#8211; a pointless but technically possible operation that leaves both copies still identical &#8211; APFS <em>will still copy the modified block</em>.<br><br>Furthermore, APFS &amp; Apple&#8217;s operating systems appear to have no tools (nor APIs) to deduplicate files &#8211; e.g. to detect full or partial copies and deduplicate their actual storage on disk.  Not even tools that you could invoke manually if you do the hard work of first determining that two files&#8217; contents are identical.<br><br>APFS / Apple&#8217;s operating systems rely entirely on user applications using copy-on-write explicitly and upfront. <a href="#fd9542d5-ca23-49bc-ae00-3d2b57caf906-link" aria-label="Jump to footnote reference 1">↩︎</a></li><li id="1bebdb6f-7e69-4ebb-bca6-692795e4e8f7">Curiously, <code>clonefile</code> first shipped in macOS 10.12 (Sierra), <a href="https://www.manpagez.com/man/2/clonefile/" data-wpel-link="external" target="_blank" rel="external noopener">according to its man page</a>, which is the release <em>preceding</em> the introduction of APFS in macOS 10.13 (High Sierra).  Yet, as far as I can tell HFS+ doesn&#8217;t and never did support cloning &#8211; nor do any of the other file systems supported by macOS 10.12 (e.g. FAT16 &amp; FAT32, exFAT, NFS).  It&#8217;s possible it was just convenient for Apple to include it in the prior release as they were probably testing APFS with it internally, during APFS&#8217;s development.<br><br><strong>Update</strong>: <a href="https://mjtsai.com" data-wpel-link="external" target="_blank" rel="external noopener">Michael Tsai</a> <a href="https://mastodon.social/@mjtsai/112456863768132668" data-wpel-link="external" target="_blank" rel="external noopener">pointed out</a> that <a href="https://www.pcmag.com/news/what-macos-sierras-new-apfs-file-system-means-to-you" data-wpel-link="external" target="_blank" rel="external noopener">Sierra included APFS support in beta form</a> (e.g. you could create disk images with it, but not use it for a boot disk).  Thus why <code>clonefile</code> (and other APFS-related tools) were included in Sierra.  <a href="#1bebdb6f-7e69-4ebb-bca6-692795e4e8f7-link" aria-label="Jump to footnote reference 2">↩︎</a></li><li id="4f593842-632c-4279-83ba-435dde24c411">While the C APIs worked just fine inside <code>swift repl</code> irrespective of what volumes I was targeting, the Foundation APIs oddly refused to work whenever the files were not on the boot volume, throwing &#8220;Operation not permitted&#8221; errors (NSCocoaErrorDomain 513, with no user info).  If it weren&#8217;t for the C APIs working just fine I&#8217;d think it&#8217;s a sandboxing issue, but clearly it&#8217;s not (and <code>swift repl -disable-sandbox</code> makes no difference). 🤔 <a href="#4f593842-632c-4279-83ba-435dde24c411-link" aria-label="Jump to footnote reference 3">↩︎</a></li></ol>]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/copy-on-write-on-apfs/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">8138</post-id>	</item>
		<item>
		<title>Matching prefixes in Swift strings</title>
		<link>https://wadetregaskis.com/matching-prefixes-in-swift-strings/</link>
					<comments>https://wadetregaskis.com/matching-prefixes-in-swift-strings/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Thu, 02 May 2024 01:46:02 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Howto]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[NSString]]></category>
		<category><![CDATA[Snafu]]></category>
		<category><![CDATA[String]]></category>
		<category><![CDATA[Swift]]></category>
		<category><![CDATA[Undocumented]]></category>
		<category><![CDATA[Unicode]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=7953</guid>

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



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



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



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



<p>Wot?</p>



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



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



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



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



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



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



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



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



<p>Well, shit.</p>



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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


<ol class="wp-block-footnotes"><li id="0a002391-051b-43f5-90c2-52da80012369">Yes, really.  I have no idea why <code>commonPrefix(with:options:)</code> doesn&#8217;t work correctly with the <code>numeric</code> option, given it presumably uses <code>compare(_:options:range:locale:)</code> or <code>range(of:options:range:locale:)</code> under the hood.  Possibly some bad interaction with locale-specific settings, given it doesn&#8217;t let you specify the locale and it doesn&#8217;t document what it hard-codes it to. <a href="#0a002391-051b-43f5-90c2-52da80012369-link" aria-label="Jump to footnote reference 1">↩︎</a></li><li id="6ac6f3c3-cfbc-4d7e-a4ec-b02e3304d489">Yes, really.  I don&#8217;t know why using the current locale (en_AU in this case) turns <em>off</em> baseline insensitivity when the <code>numeric</code> option is used, whereas it turns it <em>on</em> otherwise.  Seems like a bug in Apple&#8217;s framework. <a href="#6ac6f3c3-cfbc-4d7e-a4ec-b02e3304d489-link" aria-label="Jump to footnote reference 2">↩︎</a></li></ol>]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/matching-prefixes-in-swift-strings/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">7953</post-id>	</item>
		<item>
		<title>Including Services in contextual menus in SwiftUI</title>
		<link>https://wadetregaskis.com/including-services-in-contextual-menus-in-swiftui/</link>
					<comments>https://wadetregaskis.com/including-services-in-contextual-menus-in-swiftui/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Tue, 19 Mar 2024 01:35:39 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Howto]]></category>
		<category><![CDATA[AppKit]]></category>
		<category><![CDATA[Contextual Menus]]></category>
		<category><![CDATA[Electron]]></category>
		<category><![CDATA[NSHostingView]]></category>
		<category><![CDATA[NSMenu]]></category>
		<category><![CDATA[NSMenuItem]]></category>
		<category><![CDATA[NSServicesMenuRequestor]]></category>
		<category><![CDATA[NSViewRepresentable]]></category>
		<category><![CDATA[Swift]]></category>
		<category><![CDATA[SwiftUI]]></category>
		<category><![CDATA[Undocumented]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=7861</guid>

					<description><![CDATA[SwiftUI provides a way to provide a contextual menu for a view, with contextMenu(menuItems:) and friends, but it requires you to manually specify the entire contents of the contextual menu. That means it does not include the standard Services submenu. A brief history of Contextual Menus Contextual menus were introduced [to the Mac] in 1997&#8230; <a class="read-more-link" href="https://wadetregaskis.com/including-services-in-contextual-menus-in-swiftui/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>SwiftUI provides a way to provide a contextual menu for a view, with <code><a href="https://developer.apple.com/documentation/swiftui/view/contextmenu(menuitems:)" data-wpel-link="external" target="_blank" rel="external noopener">contextMenu(menuItems:)</a></code> and friends, but it requires you to manually specify the <em>entire</em> contents of the contextual menu.  That means it does not include the standard Services submenu.</p>



<h2 class="wp-block-heading">A brief history of Contextual Menus</h2>



<p>Contextual menus were introduced [to the Mac] in 1997 with <a href="https://infinitemac.org/1997/Mac%20OS%208.0" data-wpel-link="external" target="_blank" rel="external noopener">Mac OS 8</a>, with the new Contextual Menu Manager system extension and associated <a href="http://preserve.mactech.com/articles/mactech/Vol.14/14.02/ContextualMenuModules/index.html" data-wpel-link="external" target="_blank" rel="external noopener">Contextual Menu Modules</a>.  See also <a href="https://preterhuman.net/macstuff/techpubs/mac/pdf/HIGOS8Guidelines.pdf" data-wpel-link="external" target="_blank" rel="external noopener">the Mac OS 8 edition of the HIG</a> (page 93).</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img decoding="async" width="354" height="436" src="https://wadetregaskis.com/wp-content/uploads/2024/03/Mac-OS-8-Finder-file-contextual-menu.webp" alt="" class="wp-image-7862" srcset="https://wadetregaskis.com/wp-content/uploads/2024/03/Mac-OS-8-Finder-file-contextual-menu.webp 354w, https://wadetregaskis.com/wp-content/uploads/2024/03/Mac-OS-8-Finder-file-contextual-menu-208x256.webp 208w" sizes="(max-width: 354px) 100vw, 354px" /></figure>
</div>


<p>Support varied a lot in the early days, though, and some user-interface conventions didn&#8217;t solidify until a few years later.  e.g. SimpleText didn&#8217;t support contextual menus at all in Mac OS 8.0, and while 3rd party apps like BBEdit eventually did, it took longer still for now-standard items to become commonplace, like Cut / Copy / Paste.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="820" height="404" src="https://wadetregaskis.com/wp-content/uploads/2024/03/Mac-OS-8-BBEdit-5-1-1-contextual-menu.webp" alt="" class="wp-image-7863" srcset="https://wadetregaskis.com/wp-content/uploads/2024/03/Mac-OS-8-BBEdit-5-1-1-contextual-menu.webp 820w, https://wadetregaskis.com/wp-content/uploads/2024/03/Mac-OS-8-BBEdit-5-1-1-contextual-menu-256x126.webp 256w, https://wadetregaskis.com/wp-content/uploads/2024/03/Mac-OS-8-BBEdit-5-1-1-contextual-menu-768x378.webp 768w, https://wadetregaskis.com/wp-content/uploads/2024/03/Mac-OS-8-BBEdit-5-1-1-contextual-menu-256x126@2x.webp 512w" sizes="auto, (max-width: 820px) 100vw, 820px" /></figure>
</div>


<p>Broadly-speaking, contextual menus have changed very little over the decades.  At some point the app-specific commands were separated from the system-wide commands, the latter becoming relegated to the current &#8220;Services&#8221; submenu.  And the specific menu items have of course evolved over time, but the basic idea has persisted: to provide app-specific or built-in commands followed by commands proffered by system-wide extensions.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="728" height="656" src="https://wadetregaskis.com/wp-content/uploads/2024/03/macOS-14-3-1-Sonoma-TextEdit-contextual-menu.webp" alt="Screenshot from macOS 14.3.1 Sonoma showing an untitled TextEdit window containing the text &quot;Old Macs never die, they just fade away&quot;. &quot;Fade&quot; is selected, and a contextual menu is popped up from it. The Services menu item is highlighted, with the submenu also open, listing various Apple and 3rd party service extensions." class="wp-image-7864" srcset="https://wadetregaskis.com/wp-content/uploads/2024/03/macOS-14-3-1-Sonoma-TextEdit-contextual-menu.webp 728w, https://wadetregaskis.com/wp-content/uploads/2024/03/macOS-14-3-1-Sonoma-TextEdit-contextual-menu-256x231.webp 256w, https://wadetregaskis.com/wp-content/uploads/2024/03/macOS-14-3-1-Sonoma-TextEdit-contextual-menu@2x.webp 1456w, https://wadetregaskis.com/wp-content/uploads/2024/03/macOS-14-3-1-Sonoma-TextEdit-contextual-menu-256x231@2x.webp 512w" sizes="auto, (max-width: 728px) 100vw, 728px" /></figure>
</div>


<p>It&#8217;s chuckle-worthy to remember that when Mac OS 8, with Contextual Menus, was introduced, Macs still had one-button mice.  I don&#8217;t recall but assume it was <em>possible</em> to connect a mouse with multiple buttons, to a Mac, but remember that no Macs had USB at this time (that didn&#8217;t arrive until the iMac in August 1998, nearly a year later).  Prior to USB, Macs used different I/O ports than PCs (<a href="https://en.wikipedia.org/wiki/Apple_Desktop_Bus" data-wpel-link="external" target="_blank" rel="external noopener">ADB</a> vs <a href="https://en.wikipedia.org/wiki/PS/2_port" data-wpel-link="external" target="_blank" rel="external noopener">PS/2</a>, for the most part), so there were a lot fewer mouse (and keyboard) options for Macs.  Thus, contextual menus were originally &#8211; and to this day still also &#8211; invoked by control-clicking.  You might have encountered the term &#8220;secondary-click[ing]&#8221;, which arose in that era to abstract over whether it was a physically distinct mouse button or just a modified left click<sup data-fn="0c0ed089-5fc2-434e-8cfb-699487865d79" class="fn"><a href="#0c0ed089-5fc2-434e-8cfb-699487865d79" id="0c0ed089-5fc2-434e-8cfb-699487865d79-link">1</a></sup>.</p>



<p>It <a href="https://www.macobserver.com/tips/how-to/how-to-right-click-on-a-mac/#:~:text=With%20the%20proliferation%20of%20Microsoft%20Windows%2C%20which%20embraced,with%20Mac%20OS%208%2C%20which%20debuted%20in%201997." data-wpel-link="external" target="_blank" rel="external noopener">apparently</a> wasn&#8217;t until the Mighty Mouse in 2005 that Apple actually shipped a multi-button mouse.  (I say apparently because I don&#8217;t personally recall, and I had switched to 3rd party mice long before then anyway so I probably didn&#8217;t even notice at the time)</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>🤔 I don&#8217;t know why SwiftUI gets the terminology wrong, calling it the &#8220;context menu&#8221; rather than contextual menu.  It&#8217;s not just SwiftUI &#8211; <a href="https://developer.apple.com/design/human-interface-guidelines/context-menus" data-wpel-link="external" target="_blank" rel="external noopener">Apple&#8217;s latest Human Interface Guidelines</a> have the same spelling error.</p>



<p>The correct term (in both English and from precedence) is <em>contextual</em>.  The menu provides <em>contextual</em> commands (or context-sensitive, if you prefer).  It is not a &#8220;context&#8221; menu.  That doesn&#8217;t even make sense.  It doesn&#8217;t provide context, it is contextual.</p>
</div></div>



<h2 class="wp-block-heading">Are Services still relevant?</h2>



<p>I assume that the Services submenu &#8211; whether through contextual menus or the app menu &#8211; is not used by most Mac users today, if only because the Mac user-base has expanded massively and most people barely leave their web browsers.  I&#8217;ve heard Services derided or overlooked as a &#8220;power-user&#8221; or &#8220;niche&#8221; feature.  Which is sad, because they can be very handy.</p>



<p>More importantly, some of your app&#8217;s users might rely heavily on Services as part of their personal workflow and choice, and it&#8217;s really not our place &#8211; as native Mac application developers &#8211; to tell them they shouldn&#8217;t use a standard system feature.</p>



<p>It&#8217;s frustrating for end-users to encounter applications which don&#8217;t support standard Mac features, like Services, and makes such applications stand out as non-native or otherwise broken.</p>



<p>It is sad that SwiftUI is in the general company of Electron and its ilk, here.</p>



<h2 class="wp-block-heading">So how&#8217;s it done?</h2>



<p>There&#8217;s <em>probably</em> at least two ways to do this, one of them being to manually insert the Services menu item into an otherwise vanilla SwiftUI menu.  But I quickly ran into non-trivial challenges in pursuing that avenue, as you can&#8217;t just ask AppKit for the Services menu item, or its contents.  Alas.</p>



<p>Ultimately I found it easier &#8211; and more in keeping with the grain &#8211; to instead just not use SwiftUI for contextual menus at all.  But fortunately that doesn&#8217;t mean abandoning SwiftUI entirely, merely intermingling some AppKit into it.</p>



<p>The basic design is the standard AppKit sandwich<sup data-fn="2d6f612a-7fb7-498e-bdd6-68f54de88f78" class="fn"><a href="#2d6f612a-7fb7-498e-bdd6-68f54de88f78" id="2d6f612a-7fb7-498e-bdd6-68f54de88f78-link">2</a></sup>:  an <code><a href="https://developer.apple.com/documentation/swiftui/nsviewrepresentable" data-wpel-link="external" target="_blank" rel="external noopener">NSViewRepresentable</a></code> containing an <code><a href="https://developer.apple.com/documentation/swiftui/nshostingview" data-wpel-link="external" target="_blank" rel="external noopener">NSHostingView</a></code>.</p>



<p>The first part is particularly easy, and just the usual annoying boilerplate:</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">ContextualMenuView</span><span style="color: #000000">&lt;</span><span style="color: #0000FF">Content</span><span style="color: #000000">: </span><span style="color: #267F99">View</span><span style="color: #000000">&gt;: NSViewRepresentable {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">let</span><span style="color: #000000"> viewContent: () -&gt; Content</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">let</span><span style="color: #000000"> textProvider: </span><span style="color: #0000FF">@MainActor</span><span style="color: #000000"> () -&gt; </span><span style="color: #267F99">String</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">init</span><span style="color: #000000">(@</span><span style="color: #795E26">ViewBuilder</span><span style="color: #000000"> </span><span style="color: #001080">viewContent</span><span style="color: #000000">: </span><span style="color: #0000FF">@escaping</span><span style="color: #000000"> () -&gt; Content,</span></span>
<span class="line"><span style="color: #000000">         </span><span style="color: #795E26">text</span><span style="color: #000000">: </span><span style="color: #0000FF">@autoclosure</span><span style="color: #000000"> </span><span style="color: #0000FF">@escaping</span><span style="color: #000000"> </span><span style="color: #0000FF">@MainActor</span><span style="color: #000000"> () -&gt; </span><span style="color: #267F99">String</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: #001080">viewContent</span><span style="color: #000000"> = viewContent</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #0000FF">self</span><span style="color: #000000">.</span><span style="color: #001080">textProvider</span><span style="color: #000000"> = text</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">func</span><span style="color: #000000"> </span><span style="color: #795E26">updateNSView</span><span style="color: #000000">(</span><span style="color: #795E26">_</span><span style="color: #000000"> </span><span style="color: #001080">nsView</span><span style="color: #000000">: ContextualMenuViewImplementation&lt;Content&gt;,</span></span>
<span class="line"><span style="color: #000000">                      </span><span style="color: #795E26">context</span><span style="color: #000000">: NSViewRepresentableContext&lt;ContextualMenuView&gt;) {</span></span>
<span class="line"><span style="color: #000000">        nsView.</span><span style="color: #001080">rootView</span><span style="color: #000000"> = </span><span style="color: #0000FF">self</span><span style="color: #000000">.</span><span style="color: #795E26">viewContent</span><span style="color: #000000">()</span></span>
<span class="line"><span style="color: #000000">        nsView.</span><span style="color: #001080">textProvider</span><span style="color: #000000"> = </span><span style="color: #0000FF">self</span><span style="color: #000000">.</span><span style="color: #001080">textProvider</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">func</span><span style="color: #000000"> </span><span style="color: #795E26">makeNSView</span><span style="color: #000000">(</span><span style="color: #795E26">context</span><span style="color: #000000">: Context) -&gt; ContextualMenuViewImplementation&lt;Content&gt; {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #795E26">ContextualMenuViewImplementation</span><span style="color: #000000">(</span><span style="color: #795E26">rootView</span><span style="color: #000000">: </span><span style="color: #795E26">viewContent</span><span style="color: #000000">(),</span></span>
<span class="line"><span style="color: #000000">                                         </span><span style="color: #795E26">textProvider</span><span style="color: #000000">: textProvider)</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">func</span><span style="color: #000000"> </span><span style="color: #795E26">sizeThatFits</span><span style="color: #000000">(</span><span style="color: #795E26">_</span><span style="color: #000000"> </span><span style="color: #001080">proposal</span><span style="color: #000000">: ProposedViewSize,</span></span>
<span class="line"><span style="color: #000000">                      </span><span style="color: #795E26">nsView</span><span style="color: #000000">: ContextualMenuViewImplementation&lt;Content&gt;,</span></span>
<span class="line"><span style="color: #000000">                      </span><span style="color: #795E26">context</span><span style="color: #000000">: Context) -&gt; CGSize? {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #AF00DB">return</span><span style="color: #000000"> nsView.</span><span style="color: #001080">fittingSize</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>I&#8217;ve hard-coded it for text in this example, for simplicity, but you can adjust that to suite your needs.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>⚠️ The <code>sizeThatFits(…)</code> implementation is a bit arbitrary.  SwiftUI&#8217;s view sizing methodology is mildly infuriating, in the sense that it&#8217;s both very limited in its capabilities and very confusing for what little it does.  It took me a <em>lot</em> of trial-and-error and reverse engineering to figure out what value I needed to return.  But I suspect it&#8217;s context-sensitive, based on what the subview does.  So feel free to adjust it as necessary for your own use.</p>
</div></div>



<p>For convenience it&#8217;s nice to add a view modifier for this too:</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">extension</span><span style="color: #000000"> </span><span style="color: #267F99">View</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">contextualMenu</span><span style="color: #000000">(</span><span style="color: #795E26">for</span><span style="color: #000000"> </span><span style="color: #001080">textProvider</span><span style="color: #000000">: </span><span style="color: #0000FF">@autoclosure</span><span style="color: #000000"> </span><span style="color: #0000FF">@MainActor</span><span style="color: #000000"> () -&gt; </span><span style="color: #267F99">String</span><span style="color: #000000">) -&gt; ContextualMenuView&lt;</span><span style="color: #0000FF">Self</span><span style="color: #000000">&gt; {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #795E26">ContextualMenuView</span><span style="color: #000000">(</span><span style="color: #795E26">viewContent</span><span style="color: #000000">: { </span><span style="color: #0000FF">self</span><span style="color: #000000"> },</span></span>
<span class="line"><span style="color: #000000">                           </span><span style="color: #795E26">text</span><span style="color: #000000">: textProvider)</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>And now on to the real guts of all this, the custom <code>NSView</code> subclass that will define the contextual menu.  Fortunately, <code>NSView</code> has very straightforward built-in support for contextual menus, so you don&#8217;t need to worry about mouse-event handling &#8211; you just provide it a non-nil <code>NSMenu</code> and it does the rest.</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">class</span><span style="color: #000000"> </span><span style="color: #267F99">ContextualMenuViewImplementation</span><span style="color: #000000">&lt;</span><span style="color: #0000FF">Content</span><span style="color: #000000">: </span><span style="color: #267F99">View</span><span style="color: #000000">&gt;: NSHostingView&lt;Content&gt;,</span></span>
<span class="line"><span style="color: #000000">                                                       NSServicesMenuRequestor {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">@MainActor</span><span style="color: #000000"> </span><span style="color: #0000FF">fileprivate</span><span style="color: #000000"> </span><span style="color: #0000FF">var</span><span style="color: #000000"> textProvider: </span><span style="color: #0000FF">@MainActor</span><span style="color: #000000"> () -&gt; </span><span style="color: #267F99">String</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">@MainActor</span><span style="color: #000000"> </span><span style="color: #0000FF">required</span><span style="color: #000000"> </span><span style="color: #0000FF">init</span><span style="color: #000000">(</span><span style="color: #795E26">rootView</span><span style="color: #000000">: Content,</span></span>
<span class="line"><span style="color: #000000">                             </span><span style="color: #795E26">text</span><span style="color: #000000">: </span><span style="color: #0000FF">@autoclosure</span><span style="color: #000000"> </span><span style="color: #0000FF">@MainActor</span><span style="color: #000000"> () -&gt; </span><span style="color: #267F99">String</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: #001080">textProvider</span><span style="color: #000000"> = text</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #0000FF">super</span><span style="color: #000000">.</span><span style="color: #0000FF">init</span><span style="color: #000000">(</span><span style="color: #795E26">rootView</span><span style="color: #000000">: rootView)</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">    </span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #008000">// Mandated by NSHostingView, but not actually necessary for our purposes here.  But feel free to give this a real implementation, if that makes sense for your use and needs.</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">@MainActor</span><span style="color: #000000"> </span><span style="color: #0000FF">@objc</span><span style="color: #000000"> </span><span style="color: #0000FF">required</span><span style="color: #000000"> </span><span style="color: #0000FF">dynamic</span><span style="color: #000000"> </span><span style="color: #0000FF">init?</span><span style="color: #000000">(</span><span style="color: #795E26">coder</span><span style="color: #000000"> </span><span style="color: #001080">aDecoder</span><span style="color: #000000">: NSCoder) {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #795E26">fatalError</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;init(coder:) has not been implemented for ContextualMenuView&quot;</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: #008000">// As above.</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">@MainActor</span><span style="color: #000000"> </span><span style="color: #0000FF">required</span><span style="color: #000000"> </span><span style="color: #0000FF">init</span><span style="color: #000000">(</span><span style="color: #795E26">rootView</span><span style="color: #000000">: Content) {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #795E26">fatalError</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;init(rootView:) has not been implemented for ContextualMenuView&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">@objc</span><span style="color: #000000"> </span><span style="color: #0000FF">override</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">validRequestor</span><span style="color: #000000">(</span><span style="color: #795E26">forSendType</span><span style="color: #000000"> </span><span style="color: #001080">sendType</span><span style="color: #000000">: NSPasteboard.PasteboardType?,</span></span>
<span class="line"><span style="color: #000000">                                       </span><span style="color: #795E26">returnType</span><span style="color: #000000">: NSPasteboard.PasteboardType?) -&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: #AF00DB">guard</span><span style="color: #000000"> sendType == .</span><span style="color: #001080">string</span><span style="color: #000000"> || sendType == .</span><span style="color: #0000FF">init</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;NSStringPboardType&quot;</span><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: #AF00DB">return</span><span style="color: #000000"> </span><span style="color: #0000FF">super</span><span style="color: #000000">.</span><span style="color: #795E26">validRequestor</span><span style="color: #000000">(</span><span style="color: #795E26">forSendType</span><span style="color: #000000">: sendType, </span><span style="color: #795E26">returnType</span><span style="color: #000000">: returnType)</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">self</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">@objc</span><span style="color: #000000"> </span><span style="color: #0000FF">nonisolated</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">writeSelection</span><span style="color: #000000">(</span><span style="color: #795E26">to</span><span style="color: #000000"> </span><span style="color: #001080">pboard</span><span style="color: #000000">: NSPasteboard,</span></span>
<span class="line"><span style="color: #000000">                                          </span><span style="color: #795E26">types</span><span style="color: #000000">: [NSPasteboard.PasteboardType]) -&gt; </span><span style="color: #267F99">Bool</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #AF00DB">guard</span><span style="color: #000000"> types.</span><span style="color: #795E26">contains</span><span style="color: #000000">(.</span><span style="color: #001080">string</span><span style="color: #000000">) || types.</span><span style="color: #795E26">contains</span><span style="color: #000000">(.</span><span style="color: #0000FF">init</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;NSStringPboardType&quot;</span><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: #AF00DB">return</span><span style="color: #000000"> </span><span style="color: #0000FF">false</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">let</span><span style="color: #000000"> text = </span><span style="color: #AF00DB">if</span><span style="color: #000000"> Thread.</span><span style="color: #001080">isMainThread</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">textProvider</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">            DispatchQueue.</span><span style="color: #001080">main</span><span style="color: #000000">.</span><span style="color: #001080">sync</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">textProvider</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: #000000">        pboard.</span><span style="color: #795E26">setString</span><span style="color: #000000">(text, </span><span style="color: #795E26">forType</span><span style="color: #000000">: .</span><span style="color: #001080">string</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>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">@objc</span><span style="color: #000000"> </span><span style="color: #0000FF">override</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">menu</span><span style="color: #000000">(</span><span style="color: #795E26">for</span><span style="color: #000000"> </span><span style="color: #001080">event</span><span style="color: #000000">: NSEvent) -&gt; NSMenu? {</span></span>
<span class="line"><span style="color: #000000">        NSApplication.</span><span style="color: #001080">shared</span><span style="color: #000000">.</span><span style="color: #795E26">registerServicesMenuSendTypes</span><span style="color: #000000">([.</span><span style="color: #001080">string</span><span style="color: #000000">, .</span><span style="color: #0000FF">init</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;NSStringPboardType&quot;</span><span style="color: #000000">)],</span></span>
<span class="line"><span style="color: #000000">                                                           </span><span style="color: #795E26">returnTypes</span><span style="color: #000000">: [])</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #0000FF">let</span><span style="color: #000000"> menu = </span><span style="color: #795E26">NSMenu</span><span style="color: #000000">()</span></span>
<span class="line"><span style="color: #000000">        menu.</span><span style="color: #001080">allowsContextMenuPlugIns</span><span style="color: #000000"> = </span><span style="color: #0000FF">true</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #008000">// Insert other menu items here.</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #AF00DB">return</span><span style="color: #000000"> menu</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>☝️ <code><a href="https://developer.apple.com/documentation/appkit/nsapplication/1428751-registerservicesmenusendtypes" data-wpel-link="external" target="_blank" rel="external noopener">registerServicesMenuSendTypes(_:returnTypes:)</a></code> is normally called in <code><a href="https://developer.apple.com/documentation/objectivec/nsobject/1418639-initialize" data-wpel-link="external" target="_blank" rel="external noopener">+initialize</a></code>, but Swift doesn&#8217;t provide any way to do that (it <em>explicitly</em> bans declaring the method on your <code>NSObject</code> subclasses, for no apparent reason &#8211; perhaps a limitation of Swift&#8217;s Objective-C interoperability).</p>



<p>So, in the example above I&#8217;ve called it (every time!) in <code><a href="https://developer.apple.com/documentation/appkit/nsview/1483231-menu" data-wpel-link="external" target="_blank" rel="external noopener">menu(for:)</a></code>.  That works, but it is inefficient &#8211; you only need to call it once per app session.  If your application has a natural, better place to put it, e.g. during app launch, move it there.</p>
</div></div>



<p>The only real complexity is in the <code><a href="https://developer.apple.com/documentation/appkit/nsservicesmenurequestor/" data-wpel-link="external" target="_blank" rel="external noopener">NSServicesMenuRequestor</a></code> delegate method <code><a href="https://developer.apple.com/documentation/appkit/nsservicesmenurequestor/1428477-writeselection" data-wpel-link="external" target="_blank" rel="external noopener">writeSelection(to:types:)</a></code>.  Because it&#8217;s declared <code>nonisolated</code> by the <code>NSServicesMenuRequestor</code> protocol, you&#8217;re forced to assume it can be called in <em>any</em> isolation context; from any thread.  Unfortunately, at runtime it&#8217;s sometimes &#8211; but not always! &#8211; called from the <em>main</em> thread.  Swift doesn&#8217;t have an elegant way to say &#8220;run this synchronously on the main thread / actor&#8221; &#8211; if you naively call <code><a href="https://developer.apple.com/documentation/dispatch/dispatchqueue/" data-wpel-link="external" target="_blank" rel="external noopener">DispatchQueue</a>.<a href="https://developer.apple.com/documentation/dispatch/dispatchqueue/1781006-main" data-wpel-link="external" target="_blank" rel="external noopener">main</a>.<a href="https://developer.apple.com/documentation/dispatch/dispatchqueue#3119600" data-wpel-link="external" target="_blank" rel="external noopener">sync(…)</a></code> and you&#8217;re <em>already</em> on the main queue, it crashes your application!  So you must manually check for being on the main thread, and handle that specially. 😒</p>



<p>The other thing you might want to consider, not shown in the simple example above, is whether you want to support Services sending data <em>back</em> to your view.  e.g. they might transform the text and return the new text to you, or generate / source text from somewhere else entirely for you.  If you wish to support that, you need to populate the <code>returnTypes</code> argument to <code><a href="https://developer.apple.com/documentation/appkit/nsapplication/1428751-registerservicesmenusendtypes" data-wpel-link="external" target="_blank" rel="external noopener">registerServicesMenuSendTypes(_:returnTypes:)</a></code> <em>and</em> implement the <code><a href="https://developer.apple.com/documentation/appkit/nsservicesmenurequestor/1428481-readselection" data-wpel-link="external" target="_blank" rel="external noopener">readSelection(from:)</a></code> delegate method.</p>



<h2 class="wp-block-heading">How&#8217;s it done <em>better</em>?</h2>



<p>The above works &#8211; and is the canonical way to do it.  It results in the same plain Services submenu as you&#8217;ll see throughout Mac apps.</p>



<p>But that menu&#8217;s not great.  It just dumps everything it a big amorphous list, with no delineation and merely alphabetical ordering.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="334" height="496" src="https://wadetregaskis.com/wp-content/uploads/2024/03/Dull-Services-submenu.webp" alt="Screenshot of the default Services menu for contextual menus, showing a plain list of items ordered alphabetically." class="wp-image-7866" style="width:334px;height:auto" srcset="https://wadetregaskis.com/wp-content/uploads/2024/03/Dull-Services-submenu.webp 334w, https://wadetregaskis.com/wp-content/uploads/2024/03/Dull-Services-submenu-172x256.webp 172w, https://wadetregaskis.com/wp-content/uploads/2024/03/Dull-Services-submenu@2x.webp 668w" sizes="auto, (max-width: 334px) 100vw, 334px" /></figure>
</div>


<p>Whereas if you look the Services submenu in the application menu:</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="442" height="815" src="https://wadetregaskis.com/wp-content/uploads/2024/03/Good-Services-submenu.webp" alt="Screenshot of the good Services submenu (as found in the application menu), with more options, app icons, and better grouping." class="wp-image-7867" srcset="https://wadetregaskis.com/wp-content/uploads/2024/03/Good-Services-submenu.webp 442w, https://wadetregaskis.com/wp-content/uploads/2024/03/Good-Services-submenu-139x256.webp 139w, https://wadetregaskis.com/wp-content/uploads/2024/03/Good-Services-submenu@2x.webp 884w, https://wadetregaskis.com/wp-content/uploads/2024/03/Good-Services-submenu-139x256@2x.webp 278w" sizes="auto, (max-width: 442px) 100vw, 442px" /></figure>
</div>


<p>…it is superior in many ways:</p>



<ul class="wp-block-list">
<li>There are more items.</li>



<li>There&#8217;s a link to System Preferences / Settings to adjust which services are shown.</li>



<li>There&#8217;s dividers in appropriate places, with subheadings, to help visually organise everything.</li>



<li>App icons are shown to better visually distinguish each service.</li>



<li>The grouping is somewhat alphabetical but with all services from a given app shown contiguously.</li>
</ul>



<p>Some of the items shown aren&#8217;t actually context-specific &#8211; or at least, to no more detail than merely which application is targeted &#8211; but they&#8217;re intentionally relegated to the bottom, and could be handy anyway.  e.g. if you can see any right-clickable area of any window of an application, you can quickly start profiling that application in Instruments.</p>



<p>It&#8217;s not <em>hard</em>, per se, to get the better menu.  But it&#8217;s not documented, not <em>officially</em> supported, and has some broken edge cases.  It <em>is</em> how <em>many</em> applications insert the Services menu into their menus (in fact it was <a href="https://github.com/electron/electron/blob/193e162ec6efbb688de2a0fd533c87ab0666d133/shell/browser/ui/cocoa/electron_menu_controller.mm#L332" data-wpel-link="external" target="_blank" rel="external noopener">the Electron source</a> which helped me figure out how to do this in the first place), so it&#8217;s highly unlikely Apple will break it in the foreseeable future.  Nonetheless, be warned.</p>



<p>The main edge case / bug that I&#8217;ve encountered is that this better Services submenu only works if the window is key.  And there&#8217;s no way to <em>force</em> the window to be key (all of the AppKit APIs which seem <em>specifically</em> for that purpose flat-out do not work, such as <code><a href="https://developer.apple.com/documentation/uikit/uiwindow/1621610-makekeywindow" data-wpel-link="external" target="_blank" rel="external noopener">makeKeyWindow</a></code>).  So you have to fall back to the lesser version of the Services menu in those cases.  Which is basically whenever the view&#8217;s window is not the active window when the right-click occurs (or, whenever that macOS bug hits whereby the window is <em>shown</em> as if it&#8217;s the key window but actually isn&#8217;t 😤).</p>



<p>Anyway, with all that in mind and apparently not having dissuaded you, here&#8217;s the code to go in <code>menu(for:)</code>:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #000000">menu.</span><span style="color: #001080">allowsContextMenuPlugIns</span><span style="color: #000000"> = !(</span><span style="color: #0000FF">self</span><span style="color: #000000">.</span><span style="color: #001080">window</span><span style="color: #000000">?.</span><span style="color: #001080">isKeyWindow</span><span style="color: #000000"> ?? </span><span style="color: #0000FF">false</span><span style="color: #000000">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #AF00DB">if</span><span style="color: #000000"> !menu.</span><span style="color: #001080">allowsContextMenuPlugIns</span><span style="color: #000000"> &amp;&amp; !(</span><span style="color: #0000FF">self</span><span style="color: #000000">.</span><span style="color: #001080">window</span><span style="color: #000000">?.</span><span style="color: #795E26">makeFirstResponder</span><span style="color: #000000">(</span><span style="color: #0000FF">self</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">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;Unable to make self the first responder - reverting to built-in Services submenu.&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">    menu.</span><span style="color: #001080">allowsContextMenuPlugIns</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>
<span class="line"><span style="color: #008000">// Add all your other items here.</span></span>
<span class="line"></span>
<span class="line"><span style="color: #AF00DB">if</span><span style="color: #000000"> !menu.</span><span style="color: #001080">allowsContextMenuPlugIns</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">    menu.</span><span style="color: #795E26">addItem</span><span style="color: #000000">(NSMenuItem.</span><span style="color: #795E26">separator</span><span style="color: #000000">())</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">let</span><span style="color: #000000"> services = </span><span style="color: #795E26">NSMenuItem</span><span style="color: #000000">(</span><span style="color: #795E26">title</span><span style="color: #000000">: </span><span style="color: #A31515">&quot;Services&quot;</span><span style="color: #000000">, </span><span style="color: #795E26">action</span><span style="color: #000000">: </span><span style="color: #0000FF">nil</span><span style="color: #000000">, </span><span style="color: #795E26">keyEquivalent</span><span style="color: #000000">: </span><span style="color: #A31515">&quot;&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">let</span><span style="color: #000000"> serviceSubmenu = </span><span style="color: #795E26">NSMenu</span><span style="color: #000000">()</span></span>
<span class="line"><span style="color: #000000">    services.</span><span style="color: #001080">submenu</span><span style="color: #000000"> = serviceSubmenu</span></span>
<span class="line"><span style="color: #000000">    services.</span><span style="color: #001080">representedObject</span><span style="color: #000000"> = textItem.</span><span style="color: #001080">provider</span></span>
<span class="line"><span style="color: #000000">    NSApplication.</span><span style="color: #001080">shared</span><span style="color: #000000">.</span><span style="color: #001080">servicesMenu</span><span style="color: #000000"> = serviceSubmenu</span></span>
<span class="line"><span style="color: #000000">    menu.</span><span style="color: #795E26">addItem</span><span style="color: #000000">(services)</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>Fortunately &#8211; and thanks to the deep elegance of AppKit&#8217;s design &#8211; that&#8217;s <em>it</em>!  It otherwise uses all the same machinery as before (like <code>writeSelection(to:returnType:)</code>).</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>☝️ Making the view into the first responder is necessary to ensure it&#8217;s the one that gets called [first] about what data is available to the Services etc.  And it&#8217;s also in principle the correct thing to do &#8211; any view that responds to your interactions should generally be [made] first responder as a result.  And it seems to work perfectly in my use-cases.</p>



<p><em>But</em>, be aware that it <em>could</em> cause issues in some applications, if it interacts poorly with whatever other view(s) lose first responder status as a result.  I can only leave that as a warning and potential exercise for each reader, to figure out how to adapt the above to their circumstances as necessary.</p>



<p>(this isn&#8217;t a concern unique to this code, by any means, more the general warning for whenever you manually change the first responder)</p>
</div></div>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>⚠️ macOS 14 Sonoma has a rendering bug when first opening the Services submenu within a given application session, as shown below.  It&#8217;s not a <em>big</em> deal insofar as most of the items still work, and dismissing the menu and re-opening it fixes the rendering.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="367" height="815" src="https://wadetregaskis.com/wp-content/uploads/2024/03/Broken-Services-submenu-rendering-in-macOS-14-Sonoma.webp" alt="Screenshot of the Services menu in macOS 14 Sonoma showing the rendering glitch bug, whereby the bottom third of the menu is mangled, with many items missing entirely and others vertically truncated or displayed atop each other." class="wp-image-7874" srcset="https://wadetregaskis.com/wp-content/uploads/2024/03/Broken-Services-submenu-rendering-in-macOS-14-Sonoma.webp 367w, https://wadetregaskis.com/wp-content/uploads/2024/03/Broken-Services-submenu-rendering-in-macOS-14-Sonoma-115x256.webp 115w, https://wadetregaskis.com/wp-content/uploads/2024/03/Broken-Services-submenu-rendering-in-macOS-14-Sonoma@2x.webp 734w, https://wadetregaskis.com/wp-content/uploads/2024/03/Broken-Services-submenu-rendering-in-macOS-14-Sonoma-115x256@2x.webp 230w" sizes="auto, (max-width: 367px) 100vw, 367px" /></figure>
</div></div></div>



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



<h3 class="wp-block-heading">Adding a Copy menu item</h3>



<p>Since the contextual menu is empty except for the default Services menu, you&#8217;ll probably want to add in some of the other options that are typically found in the contextual menu &#8211; Copy being perhaps the most prominent and often-used.</p>



<p>Fortunately, it&#8217;s trivial:</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">@MainActor</span><span style="color: #000000"> </span><span style="color: #0000FF">@objc</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">doCopy</span><span style="color: #000000">(</span><span style="color: #795E26">_</span><span style="color: #000000"> </span><span style="color: #001080">menuItem</span><span style="color: #000000">: NSMenuItem) {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">let</span><span style="color: #000000"> pb = NSPasteboard.</span><span style="color: #001080">general</span></span>
<span class="line"><span style="color: #000000">    pb.</span><span style="color: #795E26">prepareForNewContents</span><span style="color: #000000">(</span><span style="color: #795E26">with</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">    pb.</span><span style="color: #795E26">setString</span><span style="color: #000000">(</span><span style="color: #0000FF">self</span><span style="color: #000000">.</span><span style="color: #795E26">textProvider</span><span style="color: #000000">(), </span><span style="color: #795E26">forType</span><span style="color: #000000">: .</span><span style="color: #001080">string</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: #008000">// Then, in your `menu(for:)` method:</span></span>
<span class="line"><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"> copyMenuItem = </span><span style="color: #795E26">NSMenuItem</span><span style="color: #000000">(</span><span style="color: #795E26">title</span><span style="color: #000000">: </span><span style="color: #A31515">&quot;Copy&quot;</span><span style="color: #000000">, </span><span style="color: #795E26">action</span><span style="color: #000000">: </span><span style="color: #795E26">#selector</span><span style="color: #000000">(</span><span style="color: #0000FF">Self</span><span style="color: #000000">.</span><span style="color: #795E26">doCopy</span><span style="color: #000000">(_:)), </span><span style="color: #795E26">keyEquivalent</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">    copyMenuItem.</span><span style="color: #001080">target</span><span style="color: #000000"> = </span><span style="color: #0000FF">self</span></span>
<span class="line"><span style="color: #000000">    copyMenuItem.</span><span style="color: #001080">isEnabled</span><span style="color: #000000"> = </span><span style="color: #0000FF">true</span></span>
<span class="line"><span style="color: #000000">    menu.</span><span style="color: #795E26">addItem</span><span style="color: #000000">(copyMenuItem)</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>One obvious missing piece is localisation of the &#8220;Copy&#8221; label; left as an exercise for the reader.</p>



<h3 class="wp-block-heading">Adding a Share menu item</h3>



<p>It&#8217;s similarly simple to add a standard Share menu item:</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">@MainActor</span><span style="color: #000000"> </span><span style="color: #0000FF">@objc</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">showSharePopup</span><span style="color: #000000">(</span><span style="color: #795E26">_</span><span style="color: #000000"> </span><span style="color: #001080">menuItem</span><span style="color: #000000">: NSMenuItem) {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">let</span><span style="color: #000000"> picker = </span><span style="color: #795E26">NSSharingServicePicker</span><span style="color: #000000">(</span><span style="color: #795E26">items</span><span style="color: #000000">: [</span><span style="color: #0000FF">self</span><span style="color: #000000">.</span><span style="color: #795E26">textProvider</span><span style="color: #000000">()])</span></span>
<span class="line"><span style="color: #000000">    picker.</span><span style="color: #795E26">show</span><span style="color: #000000">(</span><span style="color: #795E26">relativeTo</span><span style="color: #000000">: </span><span style="color: #0000FF">self</span><span style="color: #000000">.</span><span style="color: #001080">bounds</span><span style="color: #000000">, </span><span style="color: #795E26">of</span><span style="color: #000000">: </span><span style="color: #0000FF">self</span><span style="color: #000000">, </span><span style="color: #795E26">preferredEdge</span><span style="color: #000000">: .</span><span style="color: #001080">maxY</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: #008000">// Then, in your `menu(for:)` method:</span></span>
<span class="line"><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"> picker = </span><span style="color: #795E26">NSSharingServicePicker</span><span style="color: #000000">(</span><span style="color: #795E26">items</span><span style="color: #000000">: [</span><span style="color: #A31515">&quot;🐞&quot;</span><span style="color: #000000">])</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">let</span><span style="color: #000000"> pickerMenuItem = picker.</span><span style="color: #001080">standardShareMenuItem</span></span>
<span class="line"><span style="color: #000000">    pickerMenuItem.</span><span style="color: #001080">target</span><span style="color: #000000"> = </span><span style="color: #0000FF">self</span></span>
<span class="line"><span style="color: #000000">    pickerMenuItem.</span><span style="color: #001080">action</span><span style="color: #000000"> = </span><span style="color: #795E26">#selector</span><span style="color: #000000">(</span><span style="color: #0000FF">Self</span><span style="color: #000000">.</span><span style="color: #795E26">showSharePopup</span><span style="color: #000000">(_:))</span></span>
<span class="line"><span style="color: #000000">    pickerMenuItem.</span><span style="color: #001080">isEnabled</span><span style="color: #000000"> = </span><span style="color: #0000FF">true</span></span>
<span class="line"><span style="color: #000000">    menu.</span><span style="color: #795E26">addItem</span><span style="color: #000000">(pickerMenuItem)</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>This one benefits from using the standard, AppKit-provided menu item, so you don&#8217;t need to handle localising its label.</p>



<p>The 🐞 is there for two reasons:</p>



<ol class="wp-block-list">
<li>You have to provide <code><a href="https://developer.apple.com/documentation/appkit/nssharingservicepicker" data-wpel-link="external" target="_blank" rel="external noopener">NSSharingServicePicker</a></code> the nominal item(s) to share up front, in order to initialise it (and so it can tailor its display to the content).  <code><a href="https://developer.apple.com/documentation/appkit/nssharingservicepicker/4031316-standardsharemenuitem" data-wpel-link="external" target="_blank" rel="external noopener">standardShareMenuItem</a></code> should actually be a class member variable, not an instance member variable &#8211; a design flaw in AppKit.<br><br>You don&#8217;t want to invoke the <code>textProvider</code> closure unless you really need to (it could be time-consuming to run, so you don&#8217;t want to run it unnecessarily nor while the user is trying to navigate the contextual menu lest it cause GUI hiccups).  So an equivalent placeholder value &#8211; any other string, in this case &#8211; is better, and suffices.</li>



<li>I use the ladybug emoji so that it stands out if the value ever somehow gets shown to the user (it&#8217;s a bug, get it? 😄).</li>
</ol>



<h3 class="wp-block-heading">Adding a Look Up menu item</h3>



<p>This one&#8217;s a little bit more iffy.  If you think you genuinely have need of a Look Up item, consider whether you should be instead making the text selectable and simply utilising the built-in contextual menu support that selectable text views have in SwiftUI.</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">@MainActor</span><span style="color: #000000"> </span><span style="color: #0000FF">@objc</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">lookUp</span><span style="color: #000000">(</span><span style="color: #795E26">_</span><span style="color: #000000"> </span><span style="color: #001080">menuItem</span><span style="color: #000000">: NSMenuItem) {</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">showDefinition</span><span style="color: #000000">(</span><span style="color: #795E26">for</span><span style="color: #000000">: </span><span style="color: #795E26">NSAttributedString</span><span style="color: #000000">(</span><span style="color: #795E26">string</span><span style="color: #000000">: </span><span style="color: #0000FF">self</span><span style="color: #000000">.</span><span style="color: #795E26">textProvider</span><span style="color: #000000">()),</span></span>
<span class="line"><span style="color: #000000">                        </span><span style="color: #795E26">at</span><span style="color: #000000">: </span><span style="color: #795E26">NSPoint</span><span style="color: #000000">(</span><span style="color: #795E26">x</span><span style="color: #000000">: CGFloat.</span><span style="color: #001080">infinity</span><span style="color: #000000">, </span><span style="color: #795E26">y</span><span style="color: #000000">: CGFloat.</span><span style="color: #001080">infinity</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: #008000">// Then, in your `menu(for:)` method:</span></span>
<span class="line"><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"> lookupItem = </span><span style="color: #795E26">NSMenuItem</span><span style="color: #000000">(</span><span style="color: #795E26">title</span><span style="color: #000000">: submenu == menu ? </span><span style="color: #A31515">&quot;Look Up “</span><span style="color: #0000FF">\(self</span><span style="color: #000000FF">.</span><span style="color: #795E26">textProvider</span><span style="color: #000000FF">()</span><span style="color: #0000FF">)</span><span style="color: #A31515">”&quot;</span><span style="color: #000000"> : </span><span style="color: #A31515">&quot;Look Up&quot;</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                </span><span style="color: #795E26">action</span><span style="color: #000000">: </span><span style="color: #795E26">#selector</span><span style="color: #000000">(</span><span style="color: #0000FF">Self</span><span style="color: #000000">.</span><span style="color: #795E26">lookUp</span><span style="color: #000000">(_:)),</span></span>
<span class="line"><span style="color: #000000">                                </span><span style="color: #795E26">keyEquivalent</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">    lookupItem.</span><span style="color: #001080">target</span><span style="color: #000000"> = </span><span style="color: #0000FF">self</span></span>
<span class="line"><span style="color: #000000">    lookupItem.</span><span style="color: #001080">isEnabled</span><span style="color: #000000"> = </span><span style="color: #0000FF">true</span></span>
<span class="line"><span style="color: #000000">    menu.</span><span style="color: #795E26">addItem</span><span style="color: #000000">(lookupItem)</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>This doesn&#8217;t display perfectly.  The use of infinities for the <code>at</code> argument causes it to draw a small yellow box at the top left of the view, layered underneath the contextual menu but usually still visible.  That&#8217;s a hack for cases where you can&#8217;t determine where the text is, or the text being looked up doesn&#8217;t exactly match what&#8217;s shown in the view.</p>



<p>If you know the <em>actual</em> location of the selected text, you can specify that instead to get the yellow text drawn in that location.  But beware: the text from <code>textProvider</code> must <em>exactly</em> match what&#8217;s rendered in the view, otherwise the yellow overlaid box &#8211; intended to look like a highlight effect &#8211; will look weird, because it [re]renders the text based on what <code>textProvider</code> returned.  It also might not correctly match the font, in any case.</p>



<p>Thus why I caution against using the reinvention of this particular wheel.</p>


<ol class="wp-block-footnotes"><li id="0c0ed089-5fc2-434e-8cfb-699487865d79">Possibly borrowed from earlier computers that had multi-mouse-button support, such as some Unixes, but I suspect just coincidence.  I vaguely recall that they were the philosophical antithesis of Apple w.r.t. mouse buttons, with some *nix GUIs <em>requiring</em> at least three mouse buttons for their basic function.  I seem to recall some of them labelling the buttons primary, secondary, and tertiary. <a href="#0c0ed089-5fc2-434e-8cfb-699487865d79-link" aria-label="Jump to footnote reference 1">↩︎</a></li><li id="2d6f612a-7fb7-498e-bdd6-68f54de88f78">I doubt I came up with this metaphor &#8211; although it&#8217;s pretty obvious in any case &#8211; but it&#8217;s worth considering if it&#8217;s more than just a cute superficial analogy.  AppKit forms the meaningful contents of the sandwich, providing its flavour, substance, and value, while SwiftUI serves only as the bread, there mainly just to convey the contents. 🤔 <a href="#2d6f612a-7fb7-498e-bdd6-68f54de88f78-link" aria-label="Jump to footnote reference 2">↩︎</a></li></ol>]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/including-services-in-contextual-menus-in-swiftui/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2024/03/Good-Services-submenu.webp" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">7861</post-id>	</item>
		<item>
		<title>Copying whole folders in an Xcode Copy Files Build Phase</title>
		<link>https://wadetregaskis.com/copying-whole-folders-in-an-xcode-copy-files-build-phase/</link>
					<comments>https://wadetregaskis.com/copying-whole-folders-in-an-xcode-copy-files-build-phase/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Mon, 26 Feb 2024 00:00:26 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Howto]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[Build Phases]]></category>
		<category><![CDATA[Copy Files]]></category>
		<category><![CDATA[Undocumented]]></category>
		<category><![CDATA[Xcode]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=7783</guid>

					<description><![CDATA[If you try to copy a regular folder (what Xcode calls a &#8220;Group&#8221;) into a file list for any Build Phase, Xcode refuses. But it does work if you use a folder reference . I have been unable to deduce any reason for this.]]></description>
										<content:encoded><![CDATA[
<p>If you try to copy a regular folder <img loading="lazy" decoding="async" width="16" height="13" class="wp-image-7784" style="width: 16px;" src="https://wadetregaskis.com/wp-content/uploads/2024/02/Xcode-group.webp" alt="Xcode Group icon, a grey folder." srcset="https://wadetregaskis.com/wp-content/uploads/2024/02/Xcode-group.webp 16w, https://wadetregaskis.com/wp-content/uploads/2024/02/Xcode-group@2x.webp 32w" sizes="auto, (max-width: 16px) 100vw, 16px" /> (what Xcode calls a &#8220;Group&#8221;) into a file list for any Build Phase, Xcode refuses.  But it <em>does</em> work if you use a folder reference <img loading="lazy" decoding="async" width="16" height="13" class="wp-image-7785" style="width: 16px;" src="https://wadetregaskis.com/wp-content/uploads/2024/02/Xcode-folder-reference.webp" alt="Xcode Folder Reference icon, a blue folder." srcset="https://wadetregaskis.com/wp-content/uploads/2024/02/Xcode-folder-reference.webp 16w, https://wadetregaskis.com/wp-content/uploads/2024/02/Xcode-folder-reference@2x.webp 32w" sizes="auto, (max-width: 16px) 100vw, 16px" />.</p>



<p>I have been unable to deduce any reason for this.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/copying-whole-folders-in-an-xcode-copy-files-build-phase/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">7783</post-id>	</item>
		<item>
		<title>Hiding SwiftUI views</title>
		<link>https://wadetregaskis.com/hiding-swiftui-views/</link>
					<comments>https://wadetregaskis.com/hiding-swiftui-views/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Thu, 15 Feb 2024 01:42:38 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Howto]]></category>
		<category><![CDATA[AnyView]]></category>
		<category><![CDATA[EmptyView]]></category>
		<category><![CDATA[Swift]]></category>
		<category><![CDATA[SwiftUI]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=7711</guid>

					<description><![CDATA[There are several ways to hide a SwiftUI view, although they don&#8217;t all agree on what it means to hide the view. Do you want it to be invisible, or actually not there? To make it invisible you need only set its opacity to zero or use the hidden modifier. But the view will still&#8230; <a class="read-more-link" href="https://wadetregaskis.com/hiding-swiftui-views/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>There are several ways to hide a SwiftUI view, although they don&#8217;t all agree on what it <em>means</em> to hide the view.  Do you want it to be invisible, or actually not there?</p>



<p>To make it invisible you need only set its <a href="https://developer.apple.com/documentation/swiftui/view/opacity(_:)" data-wpel-link="external" target="_blank" rel="external noopener">opacity</a> to zero or use the <code><a href="https://developer.apple.com/documentation/swiftui/view/hidden()" data-wpel-link="external" target="_blank" rel="external noopener">hidden</a></code> modifier.  But the view will still be laid out just the same, and take up space in the GUI.</p>



<p>If you want to <em>actually</em> hide the view, so that it disappears completely, you can either not emit the view at all (e.g. conditionalise its existence with <code>if</code> or <code>switch</code>) or you can replace it with <code><a href="https://developer.apple.com/documentation/swiftui/emptyview/" data-wpel-link="external" target="_blank" rel="external noopener">EmptyView</a></code>.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="654" height="545" src="https://wadetregaskis.com/wp-content/uploads/2024/02/Wolf-amongst-the-sheep.avif" alt="" class="wp-image-7715" srcset="https://wadetregaskis.com/wp-content/uploads/2024/02/Wolf-amongst-the-sheep.avif 654w, https://wadetregaskis.com/wp-content/uploads/2024/02/Wolf-amongst-the-sheep-256x213.avif 256w, https://wadetregaskis.com/wp-content/uploads/2024/02/Wolf-amongst-the-sheep@2x.avif 1308w, https://wadetregaskis.com/wp-content/uploads/2024/02/Wolf-amongst-the-sheep-256x213@2x.avif 512w" sizes="auto, (max-width: 654px) 100vw, 654px" /></figure>
</div>


<p><code>EmptyView</code> is pretty marvellous in this respect.  Notice how even though it&#8217;s a real view &#8211; an actual value that you emit from a view builder &#8211; layout collections (e.g. <code>HStack</code>) don&#8217;t &#8220;see&#8221; it &#8211; there&#8217;s no visible gap, from doubling up on the cell padding, like there would be if they simply treated <code>EmptyView</code>s like 0 wide ⨉ 0 high views.</p>



<p>You might ask, when would you actually <em>need</em> EmptyView?  Wouldn&#8217;t you&#8217;d just use conditional code to hide a view, e.g.:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">struct</span><span style="color: #000000"> </span><span style="color: #267F99">MyView</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"> model: Model?</span></span>
<span class="line"><span style="color: #000000">    </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: #AF00DB">if</span><span style="color: #000000"> </span><span style="color: #0000FF">let</span><span style="color: #000000"> model {</span></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #795E26">Text</span><span style="color: #000000">(model.</span><span style="color: #001080">text</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>
<span class="line"><span style="color: #008000">// Now you can just use MyView(model: someOptional),</span></span>
<span class="line"><span style="color: #008000">// without having to unwrap it before every use.</span></span></code></pre></div>



<p>The above is making use of the &#8220;ViewBuilder mode&#8221;, to implicitly yield zero or more views which are automagically aggregated into a <code><a href="https://developer.apple.com/documentation/swiftui/tupleview" data-wpel-link="external" target="_blank" rel="external noopener">TupleView</a></code> (if you emit more than one), an optional view (if you dynamically emit zero or one), etc.  That&#8217;s very convenient, in simple cases like that.</p>



<p>But, &#8220;ViewBuilder mode&#8221; comes with a <em>lot</em> of limitations, including:</p>



<ul class="wp-block-list">
<li>Compiler diagnostics are far inferior to regular Swift code.</li>



<li>You cannot return early (i.e. use <code>return</code> statements) except by throwing an exception, which is not always semantically appropriate.</li>



<li>You cannot have nested functions.</li>



<li>You sometimes cannot use full Swift control flow syntax (e.g. switch statements).</li>
</ul>



<p>Fortunately, you can opt out of &#8220;ViewBuilder mode&#8221; by simply using an explicit <code>return</code> statement, but then you have to follow the usual rules for opaque return types, i.e. that the value be of the same type for every <code>return</code> statement.  You can use <code>AnyView</code> in concert with <code>EmptyView</code> to straighten the compiler&#8217;s knickers regarding the return type<sup data-fn="d83a6861-4b9d-4634-a63e-e5247eff8def" class="fn"><a href="#d83a6861-4b9d-4634-a63e-e5247eff8def" id="d83a6861-4b9d-4634-a63e-e5247eff8def-link">1</a></sup>, e.g.:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">struct</span><span style="color: #000000"> </span><span style="color: #267F99">MyView</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"> model: Model?</span></span>
<span class="line"><span style="color: #000000">    </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: #AF00DB">guard</span><span style="color: #000000"> </span><span style="color: #0000FF">let</span><span style="color: #000000"> model </span><span style="color: #AF00DB">else</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: #795E26">AnyView</span><span style="color: #000000">(</span><span style="color: #795E26">EmptyView</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: #AF00DB">return</span><span style="color: #000000"> </span><span style="color: #795E26">AnyView</span><span style="color: #000000">(&lt;actual view contents&gt;)</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>…or &#8211; if you don&#8217;t mind the return type being <code>Optional</code>, which SwiftUI itself doesn&#8217;t &#8211; you can use a partially opaque return type:</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">MyView</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"> model: Model?</span></span>
<span class="line"><span style="color: #000000">    </span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">var</span><span style="color: #000000"> body: </span><span style="color: #267F99">Optional</span><span style="color: #000000">&lt;some View&gt; {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #AF00DB">guard</span><span style="color: #000000"> </span><span style="color: #0000FF">let</span><span style="color: #000000"> model </span><span style="color: #AF00DB">else</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"> Body.</span><span style="color: #001080">none</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"> &lt;actual view contents&gt;</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>Thanks to <a href="https://forums.swift.org/u/dmt/summary" data-wpel-link="external" target="_blank" rel="external noopener">Dima Galimzianov</a> for <a href="https://forums.swift.org/t/cannot-use-nil-or-none-with-optional-some-t/70098/2" data-wpel-link="external" target="_blank" rel="external noopener">helping me figure out how to do that</a>.</p>



<p>Note that in the trivial cases shown above there&#8217;s no <em>particularly</em> compelling reason to use these forms instead of the &#8220;ViewBuilder mode&#8221;, but you could hopefully imagine how, as the conditional logic gets more complicated, it becomes increasingly impractical to avoid using guard, or early returns otherwise.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>Note that while <code>AnyView</code> has a lot of FUD<sup data-fn="41306d14-0450-4ed5-a469-b2a06045dc54" class="fn"><a href="#41306d14-0450-4ed5-a469-b2a06045dc54" id="41306d14-0450-4ed5-a469-b2a06045dc54-link">2</a></sup> associated with it, as far as I can tell there&#8217;s really nothing wrong with it.  Most often it&#8217;s claimed to cause performance problems, but I&#8217;ve never detected that in my use, nor seen anyone provide a real example case of such.  And multiple people have gone looking for performance problems with <code>AnyView</code> and found nothing.  e.g. <a href="https://www.linkedin.com/in/nalexn/" data-wpel-link="external" target="_blank" rel="external noopener">Alexey Naumov</a>&#8216;s <a href="https://nalexn.github.io/anyview-vs-group/" data-wpel-link="external" target="_blank" rel="external noopener">Performance Battle: AnyView vs Group</a>.</p>
</div></div>



<p>In case you&#8217;re wondering, there&#8217;s no difference in show/hide behaviour either &#8211; it just works!</p>



<figure class="wp-block-video aligncenter fix-wolf-video-size"><video height="1086" style="aspect-ratio: 1304 / 1086;" width="1304" autoplay loop preload="auto" src="https://wadetregaskis.com/wp-content/uploads/2024/02/The-Big-Wolfie-Reveal.mp4" playsinline></video></figure>



<p>It&#8217;s possible that there&#8217;s some situations in which using <code>EmptyView</code> might cause animation issues, by confusing SwiftUI as to what the relationship is between view hierarchies over time, but I haven&#8217;t found that to be the case in practice.  And if it ever does crop up, you can always just explicitly <a href="https://developer.apple.com/documentation/swiftui/view/id(_:)" data-wpel-link="external" target="_blank" rel="external noopener">id</a> your views.</p>



<p><code>EmptyView</code> is one of those little sleeper features that seems irrelevant until you stumble upon a need for it, and then it&#8217;s <em>really</em> nice to have.</p>


<ol class="wp-block-footnotes"><li id="d83a6861-4b9d-4634-a63e-e5247eff8def">There&#8217;s technically another option: use a concrete return type instead.  But, that&#8217;s usually impractical &#8211; SwiftUI uses opaque return types <em>a lot</em>, such as for virtually all view modifiers, which force you to use opaque return types in turn.  And even when <em>that</em> isn&#8217;t an issue, good luck deducing the correct fully-qualified type name even for something as simple as a <code>LabeledContent</code>. <a href="#d83a6861-4b9d-4634-a63e-e5247eff8def-link" aria-label="Jump to footnote reference 1">↩︎</a></li><li id="41306d14-0450-4ed5-a469-b2a06045dc54">Literally, &#8220;Fear, Uncertainty, and Doubt&#8221;.  Used here in that face-value sense, not to suggest any malice on anyone&#8217;s part. <a href="#41306d14-0450-4ed5-a469-b2a06045dc54-link" aria-label="Jump to footnote reference 2">↩︎</a></li></ol>]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/hiding-swiftui-views/feed/</wfw:commentRss>
			<slash:comments>7</slash:comments>
		
		<enclosure url="https://wadetregaskis.com/wp-content/uploads/2024/02/The-Big-Wolfie-Reveal.mp4" length="186396" type="video/mp4" />

			<media:content url="https://wadetregaskis.com/wp-content/uploads/2024/02/Wolf-amongst-the-sheep.avif" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">7711</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>Creating files safely in Mac apps</title>
		<link>https://wadetregaskis.com/creating-temporary-files-safely-in-mac-apps/</link>
					<comments>https://wadetregaskis.com/creating-temporary-files-safely-in-mac-apps/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Wed, 31 Jan 2024 02:48:27 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Howto]]></category>
		<category><![CDATA[App sandboxing]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[File system]]></category>
		<category><![CDATA[Files]]></category>
		<category><![CDATA[Insecure]]></category>
		<category><![CDATA[mkstemp]]></category>
		<category><![CDATA[mktemp]]></category>
		<category><![CDATA[NSString]]></category>
		<category><![CDATA[O_CREAT]]></category>
		<category><![CDATA[O_EXCL]]></category>
		<category><![CDATA[O_TRUNC]]></category>
		<category><![CDATA[open]]></category>
		<category><![CDATA[OutputStream]]></category>
		<category><![CDATA[Swift]]></category>
		<category><![CDATA[Tested]]></category>
		<category><![CDATA[tmp files]]></category>
		<category><![CDATA[umask]]></category>
		<category><![CDATA[Undocumented]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=7603</guid>

					<description><![CDATA[Creating a file is a pretty basic and conceptually simple task, that many applications do (whether they realise it or not &#8211; library code often does this too, at least for temporary files such as caches or for communicating between programs). So you&#8217;d think it&#8217;d be trivial to do correctly. Alas, it is not. ☝️&#8230; <a class="read-more-link" href="https://wadetregaskis.com/creating-temporary-files-safely-in-mac-apps/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>Creating a file is a pretty basic and conceptually simple task, that many applications do (whether they realise it or not &#8211; library code often does this too, at least for temporary files such as caches or for communicating between programs).</p>



<p>So you&#8217;d think it&#8217;d be trivial to do correctly.  Alas, it is not.</p>



<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 challenging topic, and I&#8217;ve done my best to research thoroughly and check everything experimentally.  Still, it&#8217;s certainly possible I&#8217;ve made a mistake or overlooked something.  Please let me know of any errors, in the comments at the bottom.<br><br>Also, it&#8217;s a dense topic, so I&#8217;ve tried to highlight (in bold) the most important points.  In case of TL;DR. 🙂</p>
</div></div>



<h1 class="wp-block-heading">What is the danger?</h1>



<p>There are two key things to watch out for when creating files:</p>



<ol class="wp-block-list">
<li><strong>Security flaws due to incorrect use of, or badly designed, file system APIs</strong>.  This is especially a concern for privileged applications (e.g. setuid) or those that ever run with elevated privileges (e.g. via sudo or by admin users).  Unintentional reuse of existing files can be (and <a href="https://nvd.nist.gov/vuln/detail/CVE-2011-4119" data-wpel-link="external" target="_blank" rel="external noopener">has</a> <a href="https://nvd.nist.gov/vuln/detail/CVE-2020-28407" data-wpel-link="external" target="_blank" rel="external noopener">been</a>) the cause of major security vulnerabilities.  <a href="#file-system-races-in-more-detail">See the appendix for more details</a>.<br><br>Another security concern is leaking sensitive information to other programs (or users, on a shared computer).  This can easily happen if files are created in places other programs or users can access, such as shared folders.  The files may be inadvertently created with inappropriately broad permissions (e.g. world-readable) or the parent folder&#8217;s permissions might permit others to change the permissions of the file after the fact even if they don&#8217;t own it (e.g. a world-writable folder <em>without</em> the sticky bit set).</li>



<li><strong>Data loss risks due to inadvertent overwrites or modifications of existing files</strong>.  If you&#8217;re not <em>certain</em> you know what file is already at a given path on disk, <em>and</em> that you should be allowed to overwrite it, then you should not.  Usually, the user has to give <em>explicit</em> permission (e.g. Save dialogs that explicitly ask the user if they intend to overwrite an existing file).<br><br>This risk is greatest for persistent files, e.g. files in your Documents folder.  Those are usually where the user stores their most important data.<br><br>For temporary files the level of danger is generally lower but not zero.  If you&#8217;re using a system-designated temporaries folder, then in principle anything in there is unimportant anyway.  However, randomly mucking with temporary files can still cause data corruption or loss, depending on how those files are used by applications (including your own).  e.g. they might store autosaves of the current document in temporary files, and directly copy / move those files when the user formally saves.  Thus, modifying the temporary file might end up modifying the user&#8217;s actual save file.</li>
</ol>



<h1 class="wp-block-heading">How are these dangers mitigated?</h1>



<h2 class="wp-block-heading">App Sandboxing helps</h2>



<p>As annoying &amp; limiting as <a href="https://developer.apple.com/documentation/security/app_sandbox/" data-wpel-link="external" target="_blank" rel="external noopener">App Sandboxing</a> can be in other regards, it is one of the best single steps an application author can take to improve file security.  For the most part, your application is the only (non-system &amp; non-privileged<sup data-fn="77cbc356-ca3e-4dff-892d-ddf0ca3c31c9" class="fn"><a href="#77cbc356-ca3e-4dff-892d-ddf0ca3c31c9" id="77cbc356-ca3e-4dff-892d-ddf0ca3c31c9-link">1</a></sup>) application that can write within its sandbox, and you&#8217;ll usually be creating files only within your own sandbox.</p>



<h2 class="wp-block-heading">Avoid /private/tmp (a.k.a. /tmp)</h2>



<p><code>/tmp</code> is merely a symlink to <code>/private/tmp</code>.</p>



<p><code>/private/tmp</code> is <em>world-readable</em>: every program on the computer can access its contents.  Thankfully, it&#8217;s not <em>as</em> bad it first appears &#8211; <code>/private/tmp</code> is special in that it has the &#8220;<a href="https://en.wikipedia.org/wiki/Sticky_bit" data-wpel-link="external" target="_blank" rel="external noopener">sticky bit</a>&#8221; set, which imposes some key restrictions on what users may do to each other&#8217;s files (most importantly, that they can&#8217;t delete them even though <code>/tmp</code> is world-writable).</p>



<p>Still, it&#8217;s better to use the tmp folder designated via <code><a href="https://developer.apple.com/documentation/foundation/filemanager" data-wpel-link="external" target="_blank" rel="external noopener">FileManager</a>.<a href="https://developer.apple.com/documentation/foundation/filemanager/1409234-default" data-wpel-link="external" target="_blank" rel="external noopener">default</a>.<a href="https://developer.apple.com/documentation/foundation/filemanager/1642996-temporarydirectory" data-wpel-link="external" target="_blank" rel="external noopener">temporaryDirectory</a></code> or <code><a href="https://developer.apple.com/documentation/foundation/url" data-wpel-link="external" target="_blank" rel="external noopener">URL</a>.<a href="https://developer.apple.com/documentation/foundation/url/3988477-temporarydirectory" data-wpel-link="external" target="_blank" rel="external noopener">temporaryDirectory</a></code>.  Though the location and security properties of that folder varies:</p>



<ul class="wp-block-list">
<li>If App Sandboxing is in use, it&#8217;s an application-specific folder like <code>/Users/SadPanda/Library/Containers/com.sadpanda.MyApp/Data/tmp/</code>.  No other (unprivileged) application can access that folder.</li>



<li>If App Sandboxing is not in use, it&#8217;s a user-specific folder like <code>/var/folders/v3/8anbad56df64adf3_35gj346jg13v19a/T/</code> (the exact path varies between user accounts and computers, by design for security).  That folder is still insecure &#8211; it is accessible to all programs of the same user &#8211; but at least you don&#8217;t have to worry about other [unprivileged] users.</li>
</ul>



<p>If used correctly, <code>/private/tmp</code> has essentially the same properties as the user-specific temporary folders.  Using it correctly starts with ensuring files are created with no group or &#8216;other&#8217; privileges, but the full complexities are beyond the scope of this post.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>☝️ <code>/private/tmp</code> is not accessible when App Sandboxing is enabled.</p>
</div></div>



<h2 class="wp-block-heading" id="consider-setting-a-restrictive-umask">Consider setting a restrictive umask</h2>



<p>When you create a file on any Linux or Unix system, such as macOS, its permissions are set based on a combination of the specific API used and the process-wide <code><a href="https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/umask.2.html" data-wpel-link="external" target="_blank" rel="external noopener">umask</a></code>.  Good APIs will require you to specify the initial privileges of the file, but some do not (e.g. <code>fopen</code>) and instead use some arbitrary default, such as creating files as readable and writable <em>by anyone</em> (<a href="https://en.wikipedia.org/wiki/File-system_permissions#Numeric_notation" data-wpel-link="external" target="_blank" rel="external noopener">0666</a>).  That is bad &#8211; usually you <em>don&#8217;t</em> want your files readable by any other users.</p>



<p>While you should generally avoid APIs that don&#8217;t provide proper control over file permissions, you realistically may not even <em>know</em> if you&#8217;re using such APIs because they might be employed by library code you don&#8217;t control (including Apple&#8217;s).</p>



<p>While it&#8217;s possible that the parent folder(s) will protect a given file (by preventing access to their contents by other users), it&#8217;s safest to not assume that.</p>



<p>The umask can help mitigate the dangers of bad APIs by specifying which privileges are <em>not</em> to be granted by default<sup data-fn="afb3f9ba-f914-4dd9-803c-2aad08a6800a" class="fn"><a href="#afb3f9ba-f914-4dd9-803c-2aad08a6800a" id="afb3f9ba-f914-4dd9-803c-2aad08a6800a-link">2</a></sup>.</p>



<p>The umask defaults to denying write access to groups and other users (0022), which is a start but still not good &#8211; read access to private user data is still a concern.</p>



<p>However &#8211; beyond the overly permissive default setting &#8211; there at two problems with umask:</p>



<ol class="wp-block-list">
<li>It is a process-wide global.  Modifications to it apply to all threads in your process, which makes it dangerous to modify.  e.g. you might be creating a particularly sensitive file and need the umask to be 0177, so you set it to that, but before you actually get to execute the file creation another thread sets the umask to 0000 because <em>it</em> wants to create an otherwise unrelated file that&#8217;s world-writable.<br><br>So unfortunately the only safe way to use umask is to set it very early in process launch before any additional threads are created<sup data-fn="9dedd292-995e-479b-ba90-8d2d787e436e" class="fn"><a href="#9dedd292-995e-479b-ba90-8d2d787e436e" id="9dedd292-995e-479b-ba90-8d2d787e436e-link">3</a></sup>.<br><br>You can <em>try</em> to enforce your own mutual exclusion around umask, e.g. with a global lock, but beware of 3rd party code (including Apple&#8217;s) that might modify umask without following your mutual exclusion protocol.  <em>Generally</em> umask isn&#8217;t modified often, so this arguably <em>is</em> possible to achieve in practice, but <em>proving</em> that there are no missed calls to <code>umask</code> can be practically impossible.</li>



<li>Lots of existing code, that you might be unwittingly using via libraries or frameworks, assumes the umask remains at its default.  Thus, making it more restrictive might break things in ways &amp; places that are difficult to foresee.<br><br>In general the more complicated your program, that more of a concern this is.  e.g. most command-line programs can modify umask without causing issues, but GUI programs pull in a <em>lot</em> of framework code and functionality, some of which might implicitly rely on certain umask bits.  Unfortunately the only way to find out is experimentally.</li>
</ol>



<p>So umask is not a panacea.  Still, setting a restrictive umask improves security if you can get away with it.</p>



<h2 class="wp-block-heading">Correctly use the right file creation APIs</h2>



<p>In short, creation of files needs to (generally) not extend nor replace existing files, and ensure correct initial file permissions.</p>



<p>There are quite a lot of APIs for creating a file in macOS.  I&#8217;m going to enumerate only the most common ones provided by the system libraries &amp; frameworks.</p>



<h3 class="wp-block-heading"><code>⚠️</code> <code><a href="https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/open.2.html" data-wpel-link="external" target="_blank" rel="external noopener">open</a></code></h3>



<p>Nominally this is <em>the</em> low-level API for opening (existing or new) files, although as you&#8217;ll see later it&#8217;s not actually the only one.</p>



<p>It has a flags parameter, which is how you tell it what to <em>actually</em> do, between opening existing files, creating new ones, etc.  Two of the most important flags are <code>O_CREAT</code> and <code>O_EXCL</code>.  <code>O_CREAT</code> tells <code>open</code> to create the file.  If <code>O_EXCL</code> is specified, <code>open</code> will fail if a file already exists at the target location.  If <code>O_EXCL</code> is <em>not</em> specified, <code>O_CREAT</code> is interperted as &#8220;create the file <em>if necessary</em>&#8221; &#8211; meaning, it will actually open the existing file if it exists, and <em>not</em> create a new file.</p>



<p><strong>You should almost never use <code>O_CREAT</code> without <code>O_EXCL</code>.</strong>  If you really do intend to overwrite the existing file, then it&#8217;s safer to remove the existing file first (e.g. with <code><a href="https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/unlink.2.html" data-wpel-link="external" target="_blank" rel="external noopener">unlink</a></code>), and <em>then</em> create your new file (with <code>O_EXCL</code> to ensure no other file appears at the target location in the interim).  That way you ensure the new file has your expected location (not a symlink) and attributes (e.g. file permissions).</p>



<p>Note that <code>O_EXCL</code> will fail if the target is a symlink, so you don&#8217;t need to specify <code>O_NOFOLLOW</code> (although it doesn&#8217;t hurt).</p>



<p><code>open</code> requires you to specify the new file&#8217;s permissions if you use the <code>O_CREAT</code> option (and respects the umask), which is good as it makes you think about what the permissions should be, and lets you set them to something suitable for each use case.  These permissions are <em>only</em> applied to <em>new</em> files, so in a nutshell <strong>you cannot rely on them if you don&#8217;t use <code>O_EXCL</code>.</strong></p>



<p>For that reason also, <strong>you typically should not use <code>O_TRUNC</code>.</strong>  If you don&#8217;t need the contents of the existing file, <em>delete the file first</em>.  &#8220;Reusing it&#8221; via <code>O_TRUNC</code> <em>also</em> reuses its permissions and other attributes, which might not be set correctly for your intentions (e.g. a malicious program might have pre-created the file as world-readable, even though you intend it to be readable only by the current user and have otherwise done the right things such as set the umask to ensure that).</p>



<p>One reason you <em>might</em> validly use <code>O_TRUNC</code> is if you anticipate there being multiple <em>hard</em> links to the file, and you want to modify the file as seen by the other links too.  This is very rare.  Be <em>sure</em> that&#8217;s necessary before you use <code>O_TRUNC</code>, and consider putting validations in place to ensure the file you&#8217;ve just opened for reuse has the expected permissions and attributes (e.g. via <code><a href="https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/fstat.2.html" data-wpel-link="external" target="_blank" rel="external noopener">fstat64</a></code> and similar APIs that operate on the file descriptor &#8211; do <em>not</em> use <code>lstat</code> or any other path-based APIs).</p>



<h3 class="wp-block-heading">⚠️ <code><a href="https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/fopen.3.html" data-wpel-link="external" target="_blank" rel="external noopener">fopen</a></code></h3>



<p>This is essentially just a wrapper atop <code>open</code>, with the &#8220;w&#8221; and &#8220;a&#8221; flags mapping to <code>O_CREAT</code> (essentially) and the &#8220;x&#8221; flag mapping to <code>O_EXCL</code>.  The same rules apply, so <strong>you should generally never use &#8220;w&#8221; without the &#8220;x&#8221; flag as well, nor &#8220;a&#8221; without &#8220;+&#8221;.</strong></p>



<p><code>fopen</code> does <em>not</em> let you specify the permissions of the created file, instead defaulting to 0666 (readable &amp; writable by <em>everyone</em>) which is a terrible default.  It does respect umask, so by default it will create files as 0644 which is marginally better.  But, <a href="#consider-setting-a-restrictive-umask">as discussed previously</a>, it is difficult to guarantee what the umask actually is at any particular point in time.  So <strong>in general you should prefer <code>open</code> instead of <code>fopen</code></strong>.</p>



<h3 class="wp-block-heading">❌ <code><a href="https://developer.apple.com/documentation/foundation/outputstream/1416367-init" data-wpel-link="external" target="_blank" rel="external noopener">OutputStream(toFileAtPath:append:)</a></code> &amp; friends</h3>



<p>This ultimately (when you call the <code>open</code> method) calls <code>open</code> with the flags <code>O_WRONLY | O_CREAT</code> (plus <code>O_TRUNC</code> if the append argument is false).  As such it will <em>always</em> modify an existing file if present.</p>



<p>It also does not let you specify the permissions of the new file, instead defaulting arbitrarily to 0666 (but respecting umask, at least).</p>



<p><strong>It is an unsafe API and should not be used.</strong></p>



<h3 class="wp-block-heading">⚠️ <code><a href="https://developer.apple.com/documentation/foundation/nsdata" data-wpel-link="external" target="_blank" rel="external noopener">NSData</a>.<a href="https://developer.apple.com/documentation/foundation/nsdata/1414800-write" data-wpel-link="external" target="_blank" rel="external noopener">write(toFile:options:)</a></code> &amp; friends</h3>



<p>This family of methods all ultimately call <code><a href="https://opensource.apple.com/source/xnu/xnu-2050.9.2/libsyscall/wrappers/open_dprotected_np.c.auto.html" data-wpel-link="external" target="_blank" rel="external noopener">open_dprotected_np</a></code> (<a href="https://github.com/apple/darwin-xnu/blob/2ff845c2e033bd0ff64b5b6aa6063a1f8f65aa32/bsd/vfs/vfs_syscalls.c#L4517" data-wpel-link="external" target="_blank" rel="external noopener">implementation</a>), a variant of <code>open</code> specific to Apple platforms which adds Apple-specific functionality regarding file encryption and isolation (see e.g. the protection-related flags within <code><a href="https://developer.apple.com/documentation/foundation/nsdata/writingoptions" data-wpel-link="external" target="_blank" rel="external noopener">NSData.WritingOptions</a></code>).  It takes the same flags as the regular <code>open</code>, and <code>write(toFile:options:)</code> by default uses <code>O_CREAT | O_TRUNC</code>.  If you use the <code>withoutOverwriting</code> option, it adds <code>O_EXCL</code>.  <strong>So you should usually use <code>withoutOverwriting</code>.</strong></p>



<p>If you use the <code>atomic</code> flag, it creates the file using a private function <code>_NSCreateTemporaryFile_Protected</code> which obtains a file path using <a href="#mktemp"><code>mktemp</code> ⚠️</a> and calls <code>open_dprotected_np</code> with the flags <code>O_CREAT | O_EXCL | O_RDWR</code><sup data-fn="1d64125f-81d0-4bb5-8090-ac0b5d942d7d" class="fn"><a href="#1d64125f-81d0-4bb5-8090-ac0b5d942d7d" id="1d64125f-81d0-4bb5-8090-ac0b5d942d7d-link">4</a></sup>.  Once it has created &amp; written to that temporary file, it uses <code><a href="https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/rename.2.html" data-wpel-link="external" target="_blank" rel="external noopener">rename</a></code> to move it into place.  <code>rename</code> just silently deletes any existing file at the destination path.  So it&#8217;s safe against race attacks, but susceptible to data loss bugs from unintentionally overwriting existing files.  As such, <strong>use <code>atomic</code> only with caution</strong>.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>⚠️ If you add <code>withoutOverwriting</code> on top of <code>atomic</code>, the call crashes your program!  It throws an Objective-C exception &#8211; <code>NSInvalidArgumentException</code>.  Swift does not support Objective-C exceptions (<a href="https://forums.swift.org/t/pitch-a-swift-representation-for-thrown-and-caught-exceptions/54583/3" data-wpel-link="external" target="_blank" rel="external noopener">it&#8217;s fundamentally unsafe to pass Objective-C exceptions up to Swift functions</a>) so you have to <a href="https://stackoverflow.com/questions/32758811/catching-nsexception-in-swift/36454808#36454808" data-wpel-link="external" target="_blank" rel="external noopener">use an Objective-C helper function as an intermediary</a>.</p>



<p>This is a particularly unfortunate limitation &#8211; even aside from the crashiness &#8211; because using both options together is highly desirable and it <em>should</em> in principle work &#8211; the implementation can simply use <code><a href="https://www.manpagez.com/man/2/renamex_np/osx-10.12.3.php" data-wpel-link="external" target="_blank" rel="external noopener">renamex_np</a></code> instead with the flag <code>RENAME_EXCL</code>.</p>



<p>FB13568491.</p>
</div></div>



<p>It also does not let you specify the permissions of the new file, instead defaulting arbitrarily to 0666 (but respecting umask, at least).  <strong>For files containing sensitive data (such as private user data), this API should generally not be used.</strong></p>



<h3 class="wp-block-heading">❌ <code><a href="https://developer.apple.com/documentation/foundation/nsstring" data-wpel-link="external" target="_blank" rel="external noopener">NSString</a>.<a href="https://developer.apple.com/documentation/foundation/nsstring/1407654-write" data-wpel-link="external" target="_blank" rel="external noopener">write(toFile:atomically:encoding:)</a></code> &amp; friends</h3>



<p>These are essentially just wrappers over <code>NSData.write(toFile:options:)</code> &amp; friends, where the <code>atomically</code> argument maps to the <code>atomic</code> option.  They provide no way to use the <code>withoutOverwriting</code> option, so they <strong>should not be used in most cases.</strong></p>



<h2 class="wp-block-heading">Use randomised names for transient files</h2>



<p>Whenever the file name doesn&#8217;t actually matter &#8211; i.e. it&#8217;s not chosen by the user and isn&#8217;t pre-defined by some system requirement &#8211; it&#8217;s best to use a randomised name.  This serves two purposes:</p>



<ol class="wp-block-list">
<li>If your code has any bugs that allow it to erroneously overwrite existing files, using random names at least makes it a lot less likely that you&#8217;ll trigger those bugs, by greatly reducing the odds of using the same name twice.</li>



<li>It makes it harder (if not impossible) for attackers to predict the file names, and therefore to attack them.</li>
</ol>



<p>There are many ways to obtain a randomised name, but it&#8217;s wise to use one of the canonical methods detailed below.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>☝️ If you do care about the file name, but not its location, you can use these APIs to create a randomly-named temporary <em>folder</em>, and then create your file within there.</p>



<p>This can be handy for e.g. preparing a file URL to be dragged from your app, where you want the file to have a proper name but don&#8217;t care (per se) where it lives.</p>
</div></div>



<h3 class="wp-block-heading" id="mktemp">⚠️ <code><code><a href="https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/mktemp.3.html" data-wpel-link="external" target="_blank" rel="external noopener">mktemp</a></code></code></h3>



<p><code>mktemp</code> is infamously a source of security vulnerabilities, because it doesn&#8217;t actually create the file (or folder) but merely returns a path.  The caller is responsible for securely creating the file (or folder).  This <em>can</em> be done safely &#8211; by following the guidance earlier in this post, in particular around the <code>O_EXCL</code> <code>open</code> flag &#8211; but it&#8217;s easy to screw up.</p>



<p><strong>Generally it&#8217;s preferable to use <code>mkstemp</code> / <code>mkdtemp</code> &amp; friends</strong>.  Unfortunately, if you want to use higher-level file APIs on Apple&#8217;s platforms that&#8217;s a problem because most of those APIs don&#8217;t support initialisation from a file descriptor, only a path (or equivalently, URL).  So you may find that you still need to use <code>mktemp</code>.  If so, be very careful about how you actually create the files &amp; folders.</p>



<h3 class="wp-block-heading">✅ <code><a href="https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/mkstemp.3.html" data-wpel-link="external" target="_blank" rel="external noopener">mkstemp</a></code> / <code><a href="https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/mkstemp.3.html" data-wpel-link="external" target="_blank" rel="external noopener">mkdtemp</a></code> &amp; friends</h3>



<p>These replacements for <code>mktemp</code> actually create the file / folder (respectively), in a safe way.</p>



<p><code>mkstemp</code> creates the file and returns the corresponding open file descriptor, instead of merely returning a path and leaving it to the caller to get the creation step right.  It will never overwrite (nor open) an existing file.  It also sets the file&#8217;s initial permissions to 0600 (i.e. read-writable but only by the current user), which is a pretty safe default.</p>



<p><code>mkdtemp</code> still returns a path (not a file descriptor), like <code>mktemp</code>, but it ensures the folder was actually created (and not previously existent) with the permissions set to 0700 (i.e. usable only by the current user).</p>



<p>Neither make any guarantees regarding security &#8211; or lack thereof &#8211; due to parent folder permissions.  The caller still needs to ensure necessary security protections for those (whether by choosing a suitable system-provided folder, or manually checking permissions and symlinks in the path).  <code><code><code><a href="https://developer.apple.com/documentation/foundation/url" data-wpel-link="external" target="_blank" rel="external noopener">URL</a>.<a href="https://developer.apple.com/documentation/foundation/url/3988477-temporarydirectory" data-wpel-link="external" target="_blank" rel="external noopener">temporaryDirectory</a></code></code></code> is a good starting point.</p>



<p>Using these from Swift is a little awkward because they mutate their primary argument (the path template), but <a href="https://github.com/apple/swift-corelibs-foundation/blob/dbca8c7ddcfd19f7f6f6e1b60fd3ee3f748e263c/Sources/Foundation/NSPathUtilities.swift#L774" data-wpel-link="external" target="_blank" rel="external noopener">here&#8217;s an example</a>.  Once you have the file descriptor (in the <code>mkstemp</code> case) you can wrap it in e.g. a <code><a href="https://developer.apple.com/documentation/foundation/filehandle" data-wpel-link="external" target="_blank" rel="external noopener">FileHandle</a></code> and work with it at a slightly higher level.</p>



<h3 class="wp-block-heading"><code><a href="https://developer.apple.com/documentation/foundation/filemanager" data-wpel-link="external" target="_blank" rel="external noopener">⚠️</a></code> <code><a href="https://developer.apple.com/documentation/foundation/filemanager" data-wpel-link="external" target="_blank" rel="external noopener">FileManager</a>.<a href="https://developer.apple.com/documentation/foundation/filemanager/1409234-default" data-wpel-link="external" target="_blank" rel="external noopener">default</a>.<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></h3>



<p>In its special mode where &#8216;for&#8217; is <code><a href="https://developer.apple.com/documentation/foundation/filemanager/searchpathdirectory/itemreplacementdirectory" data-wpel-link="external" target="_blank" rel="external noopener">.itemReplacementDirectory</a></code> and &#8216;in&#8217; is <a href="https://developer.apple.com/documentation/foundation/filemanager/searchpathdomainmask/1408037-userdomainmask" data-wpel-link="external" target="_blank" rel="external noopener">.<code>userDomainMask</code></a>, this behaves like <code>mkdtemp</code>; it creates a randomly-named folder and returns the URL to it (note that it completely ignores the &#8216;create&#8217; argument in this case &#8211; there is no way to have it <em>not</em> create the temporary folder).</p>



<p>This API seems to presume the use of App Sandboxing to mitigate its problems &#8211; and when App Sandboxing is enabled, it&#8217;s the best way to obtain a temporary file or folder path, though only if you use a suitable value for the &#8216;appropriateFor&#8217; parameter, such as <code><code><a href="https://developer.apple.com/documentation/foundation/url" data-wpel-link="external" target="_blank" rel="external noopener">URL</a>.<a href="https://developer.apple.com/documentation/foundation/url/3988477-temporarydirectory" data-wpel-link="external" target="_blank" rel="external noopener">temporaryDirectory</a></code></code><sup data-fn="a6f54a24-4596-4700-8d50-786942903c54" class="fn"><a href="#a6f54a24-4596-4700-8d50-786942903c54" id="a6f54a24-4596-4700-8d50-786942903c54-link">5</a></sup>.  When App Sandboxing is enabled, that will result in a path <em>inside</em> the sandbox, which is the most secure place an unprivileged application can use.</p>



<p>However, if instead a URL is provided which points to a different volume, it returns a path to a user-specific temporary folder on that volume, e.g. <code>/Volumes/Example/.TemporaryItems/folders.501/TemporaryItems/</code>.  While that does exclude (unprivileged) other users, it&#8217;s still a big step down from a location inside the app&#8217;s sandbox.</p>



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



<h1 class="wp-block-heading" id="file-system-races-in-more-detail">Appendix: File system races in more detail</h1>



<p>Generally-speaking, the file system is a shared resource.  Multiple programs can access it simultaneously with no coordination required<sup data-fn="41b5a8b5-6e90-4a7b-b4c2-8f687acd6914" class="fn"><a href="#41b5a8b5-6e90-4a7b-b4c2-8f687acd6914" id="41b5a8b5-6e90-4a7b-b4c2-8f687acd6914-link">6</a></sup> between them.  That opens the door for races &#8211; where the state of the world changes in-between file system operations that a program might mistakenly assume are atomic.</p>



<p>In general, any <em>single</em> call to a low-level file system API &#8211; e.g. <code>open</code> &#8211; is atomic.  Most such APIs correspond to a single syscall into the kernel, and the operation inside the kernel is wrapped inside a lock (conceptually if not also literally).</p>



<p>Conversely, any operation that takes multiple calls to a file system API is <em>never</em> atomic.</p>



<p>A textbook security vulnerability arises when you do something like:</p>



<ol class="wp-block-list">
<li>Make up some random file name (e.g. with <code>mktemp</code>).</li>



<li>Check that it doesn&#8217;t exist (<em>one</em> syscall), and see that it doesn&#8217;t.</li>



<li>Write to that file (<em>a separate syscall</em>).</li>
</ol>



<p>A malicious program could inject its own file in-between steps two and three &#8211; or even more dangerously, a symlink &#8211; and cause your program to overwrite something (many file APIs will automatically follow symlinks and open existing files, if not used correctly as detailed in this post).</p>



<p>The classic concern in this regard is with privileged programs that have the ability to overwrite sensitive files, e.g. <code>/etc/passwd</code>.  Tricking them into doing so can cause major damage to the system (e.g. nobody can login anymore!) in the <em>best</em> case, and in the worst case &#8211; where the attacker can also influence the contents of the file, or those contents are conveniently just what the attacker wants &#8211; they might be able to implement a more subtle attack that doesn&#8217;t merely break the system but instead e.g. changes the root password, giving them superuser access to the computer.</p>



<p>Even for unprivileged applications, it can still be a concern.  e.g. they might be tricked into writing a bunch of private user data into a shared location from where the attacker can exfiltrate it.</p>



<h1 class="wp-block-heading">Appendix: File writing APIs that cannot create new files</h1>



<p>These APIs are nominally irrelevant since they can&#8217;t be used to create new files, but it can be useful to <em>know</em> that fact, for use in converse scenarios where you do <em>not</em> want to create a file.</p>



<h3 class="wp-block-heading"><code><a href="https://developer.apple.com/documentation/foundation/filehandle/1414405-init" data-wpel-link="external" target="_blank" rel="external noopener">FileHandle(forWritingAtPath:)</a></code></h3>



<p>…ultimately calls <code>[[NSConcreteFileHandle alloc] initWithPath:… flags:0x1 createMode:0 error:nil]</code>, which turns the path string into a URL using <code>-[NSURL fileURLWithPath:]</code> and calls <code>-[NSConcreteFileHandle initWithURL:flags:createMode:error:]</code>, which calls <code>_NSOpenFileDescriptor</code> to do the actual file system calls.  That calls <code>open</code> with <em>only</em> the flag <code>O_WRONLY</code>; it does not pass <code>O_CREAT</code> nor <code>O_EXCL</code>.  So <code>FileHandle</code> cannot create new files (which I find a bit unintuitive, as nothing in the name really suggests that limitation).</p>



<h3 class="wp-block-heading"><code><a href="https://developer.apple.com/documentation/foundation/nsdata/1411145-init" data-wpel-link="external" target="_blank" rel="external noopener">NSData(contentsOfFile:options:)</a></code> &amp; friends</h3>



<p>…ultimately call <code>open</code> with no flags (meaning they can only <em>read</em> existing files, not even modify them).  So again, cannot create new files.  Which is perhaps implied and obvious from the name, but it&#8217;s good to be certain.</p>


<ol class="wp-block-footnotes"><li id="77cbc356-ca3e-4dff-892d-ddf0ca3c31c9">In principle system programs &amp; privileged (e.g. root &amp; admin) programs should be defended against too, but it&#8217;s often impractically difficult to do so, and beyond the scope of this post to try to explain how.<br><br>Root is of course the most impractical to defend against &#8211; even though the root user is <em>not</em> a traditional &#8220;God&#8221; user on macOS, Apple&#8217;s nerfing of root is designed to protect <em>Apple&#8217;s</em> programs, not yours.  Root (and admin users) can ultimately still access any files your application(s) create and there&#8217;s nothing you can do about it (other than potentially through orthogonal protections, such as encryption). <a href="#77cbc356-ca3e-4dff-892d-ddf0ca3c31c9-link" aria-label="Jump to footnote reference 1">↩︎</a></li><li id="afb3f9ba-f914-4dd9-803c-2aad08a6800a">It is of course possible for libraries to override the umask, but that would be <em>particularly</em> foul of them and none that I&#8217;ve surveyed do that, thankfully. <a href="#afb3f9ba-f914-4dd9-803c-2aad08a6800a-link" aria-label="Jump to footnote reference 2">↩︎</a></li><li id="9dedd292-995e-479b-ba90-8d2d787e436e">This can be hard to guarantee, in a non-trivial program.  You can use an assertion or precondition on the return value of <code><a href="https://github.com/apple-oss-distributions/libpthread/blob/d8c4e3c212553d3e0f5d76bb7d45a8acd61302dc/src/pthread.c#L943" data-wpel-link="external" target="_blank" rel="external noopener">pthread_is_threaded_np</a></code> to help ensure you&#8217;re modifying umask before additional threads are created. <a href="#9dedd292-995e-479b-ba90-8d2d787e436e-link" aria-label="Jump to footnote reference 3">↩︎</a></li><li id="1d64125f-81d0-4bb5-8090-ac0b5d942d7d">It&#8217;s not apparent to me why it needs <em>read</em> access to the file as well &#8211; that appears to be a bug. <a href="#1d64125f-81d0-4bb5-8090-ac0b5d942d7d-link" aria-label="Jump to footnote reference 4">↩︎</a></li><li id="a6f54a24-4596-4700-8d50-786942903c54">I&#8217;m not sure what happens if the App Sandbox container is not on the boot volume. <a href="#a6f54a24-4596-4700-8d50-786942903c54-link" aria-label="Jump to footnote reference 5">↩︎</a></li><li id="41b5a8b5-6e90-4a7b-b4c2-8f687acd6914">There are various mechanism for <em>voluntary</em> coordination, such as <code><a href="https://developer.apple.com/documentation/foundation/nsfilecoordinator" data-wpel-link="external" target="_blank" rel="external noopener">NSFileCoordinator</a></code> and <code><a href="https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/flock.2.html" data-wpel-link="external" target="_blank" rel="external noopener">flock</a></code>, but programs are not required to use them (and malicious programs happily won&#8217;t). <a href="#41b5a8b5-6e90-4a7b-b4c2-8f687acd6914-link" aria-label="Jump to footnote reference 6">↩︎</a></li></ol>]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/creating-temporary-files-safely-in-mac-apps/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">7603</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>cloudinit breaks connections from localhost to Plesk-operated web servers</title>
		<link>https://wadetregaskis.com/cloudinit-breaks-connections-from-localhost-to-plesk-operated-web-servers/</link>
					<comments>https://wadetregaskis.com/cloudinit-breaks-connections-from-localhost-to-plesk-operated-web-servers/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Tue, 23 Jan 2024 01:03:08 +0000</pubDate>
				<category><![CDATA[Howto]]></category>
		<category><![CDATA[Meta]]></category>
		<category><![CDATA[Apache]]></category>
		<category><![CDATA[Broken Link Checker]]></category>
		<category><![CDATA[CDN]]></category>
		<category><![CDATA[cloud-init]]></category>
		<category><![CDATA[curl]]></category>
		<category><![CDATA[Nginx]]></category>
		<category><![CDATA[Plesk]]></category>
		<category><![CDATA[Plesk Obsidian]]></category>
		<category><![CDATA[Wordpress]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=7491</guid>

					<description><![CDATA[I don&#8217;t know how or why it is even installed &#8211; perhaps part of the provided Ubuntu image from my VPS host &#8211; but cloud-init (a.k.a &#8220;cloud-config&#8221;) is a bit obnoxious. At [re]boot time (best I can tell), it resets /etc/hosts based on some random template it has (/etc/cloud/templates/hosts.debian.tmpl in my case, even though that&#8230; <a class="read-more-link" href="https://wadetregaskis.com/cloudinit-breaks-connections-from-localhost-to-plesk-operated-web-servers/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>I don&#8217;t know how or why it is even installed &#8211; perhaps part of the provided Ubuntu image from my VPS host &#8211; but <a href="https://github.com/canonical/cloud-init" data-wpel-link="external" target="_blank" rel="external noopener">cloud-init</a> (a.k.a &#8220;cloud-config&#8221;) is a bit obnoxious.  At [re]boot time (best I can tell), it resets <code>/etc/hosts</code> based on some random template it has (<code>/etc/cloud/templates/hosts.debian.tmpl</code> in my case, even though that makes no sense as I&#8217;m not using Debian).  That template has a very weird entry:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><code>127.0.1.1 {{fqdn}} {{hostname}}</code></p>
</blockquote>



<p>I have no idea what it&#8217;s even trying to do with that.</p>



<p>What it <em>does</em> do is break a lot of things, because while 127.0.1.1 is a valid address (albeit weird to use instead of the canonical 127.0.0.1), in a standard <a href="https://www.plesk.com" data-wpel-link="external" target="_blank" rel="external noopener">Plesk</a> system <a href="https://nginx.org" data-wpel-link="external" target="_blank" rel="external noopener">Nginx</a> (that sits in front of <a href="https://httpd.apache.org" data-wpel-link="external" target="_blank" rel="external noopener">Apache</a> typically) is configured to listen only on the network interface(s) associated with your web server&#8217;s public IP address(es), not localhost<sup data-fn="f02da440-1755-49a0-b849-7989472e1805" class="fn"><a href="#f02da440-1755-49a0-b849-7989472e1805" id="f02da440-1755-49a0-b849-7989472e1805-link">1</a></sup>.  So if you try e.g. <code>curl mydomain.com</code> from inside your server hosting that very domain, you get a connection refused error (or it just times out with no response, depending on firewall settings).</p>



<p>The way <a href="https://talk.plesk.com/threads/can-no-longer-connect-to-nginx-from-localhost.372659/" data-wpel-link="external" target="_blank" rel="external noopener">I found out about this</a> was when WordPress&#8217;s &#8220;cron&#8221; system silently stopped running tasks (because I use a real cron job to trigger it periodically, rather than relying on WordPress&#8217;s flaky built-in system).  I subsequently also noticed that <a href="https://wordpress.org/plugins/broken-link-checker/" data-wpel-link="external" target="_blank" rel="external noopener">Broken Link Checker</a> mistakenly reported <em>every</em> link within my own site as broken.</p>



<h2 class="wp-block-heading">The solution</h2>



<p>…is fairly simple &#8211; modify the template to remove the offending line.  You then have to either reboot or similarly manually modify <code>/etc/hosts</code> to actually apply the fix.</p>



<p>If for some reason you cannot do that, there are potentially workarounds.  For example, for <code>curl</code> commands you can make <code>curl</code> navigate around the problem this like:</p>



<figure class="wp-block-pullquote"><blockquote><p><code>curl --header 'Host: mydomain.com' https://&lt;your server's external IP></code></p></blockquote></figure>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>☝️ If you have a CDN or similar in front of your server, e.g. <a href="https://www.cloudflare.com/" data-wpel-link="external" target="_blank" rel="external noopener">Cloudflare</a>, this will potentially behave differently to connecting from another host; it&#8217;ll bypass the CDN and connect directly to your web server (effectively the same as if you did connect to 127.0.0.1).</p>



<p>Depending on your needs, this could be desirable (e.g. no need to send WordPress cron invocations through a CDN) or undesirable (broken link checking probably should go through the CDN, just like all real traffic, to test that the CDN doesn&#8217;t cause a problem).</p>
</div></div>



<p>In a non-Plesk environment you might also be able to just change the Nginx configuration (somewhere under <code>/etc/nginx</code>).  That&#8217;s ill-advised for Plesk-operated servers as Plesk will inevitably revert your changes (and as far as I can tell there is no configuration option, or workaround otherwise, to make Plesk configure Nginx to listen on localhost as well 😕).</p>


<ol class="wp-block-footnotes"><li id="f02da440-1755-49a0-b849-7989472e1805">I suspect this is more a side-effect than intentional.  Plesk chooses the interface only implicitly, when it specifies that it listens on specific addresses &#8211; those assigned to the website in question (Nginx can host multiple otherwise unrelated websites, and each is configured independently within its settings).  Since Plesk supports hosting multiple websites, each with potentially <em>different</em> IP addresses, it makes sense that it would want to keep them separate.  Otherwise, in a shared hosting situation you could connect to the IP address for somehost.com yet issue a HTTP request for unrelatedhost.com and actually get a response, which is weird, at least. <a href="#f02da440-1755-49a0-b849-7989472e1805-link" aria-label="Jump to footnote reference 1">↩︎</a></li></ol>]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/cloudinit-breaks-connections-from-localhost-to-plesk-operated-web-servers/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">7491</post-id>	</item>
		<item>
		<title>How to install ImageMagick 7 for WordPress under Plesk Obsidian on Ubuntu 22.04</title>
		<link>https://wadetregaskis.com/how-to-install-imagemagick-7-for-wordpress-under-plesk-obsidian-on-ubuntu-22-04/</link>
					<comments>https://wadetregaskis.com/how-to-install-imagemagick-7-for-wordpress-under-plesk-obsidian-on-ubuntu-22-04/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Mon, 22 Jan 2024 02:29:08 +0000</pubDate>
				<category><![CDATA[Howto]]></category>
		<category><![CDATA[Meta]]></category>
		<category><![CDATA[AVIF]]></category>
		<category><![CDATA[ImageMagick]]></category>
		<category><![CDATA[Imagick]]></category>
		<category><![CDATA[JXL]]></category>
		<category><![CDATA[Plesk]]></category>
		<category><![CDATA[Plesk Obsidian]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[Wordpress]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=7457</guid>

					<description><![CDATA[Why should I do this? WordPress relies on either ImageMagick or GD for its ability to read &#38; write images. It prefers ImageMagick, as ImageMagick supports a much wider range of files, tends to be faster, and some assert that it produces higher quality files. ImageMagick 6 was superseded by ImageMagick 7 nearly a decade&#8230; <a class="read-more-link" href="https://wadetregaskis.com/how-to-install-imagemagick-7-for-wordpress-under-plesk-obsidian-on-ubuntu-22-04/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading no-clear-thanks">Why should I do this?</h2>



<p><a href="https://wordpress.org" data-wpel-link="external" target="_blank" rel="external noopener">WordPress</a> relies on either <a href="https://imagemagick.org" data-wpel-link="external" target="_blank" rel="external noopener">ImageMagick</a> or <a href="https://libgd.github.io" data-wpel-link="external" target="_blank" rel="external noopener">GD</a> for its ability to read &amp; write images.  It prefers ImageMagick, as ImageMagick supports a much wider range of files, tends to be faster, and some assert that it produces higher quality files.</p>



<p>ImageMagick 6 was superseded by ImageMagick 7 nearly a decade ago.  <a href="https://legacy.imagemagick.org" data-wpel-link="external" target="_blank" rel="external noopener">Version 6 is officially in legacy mode</a> and while still technically supported (in a <a href="https://en.wikipedia.org/wiki/Long-term_support" data-wpel-link="external" target="_blank" rel="external noopener">long term support</a> sense) it generally receives only bug fixes &#8211; it fundamentally lacks many features, and most crucially it doesn&#8217;t support modern image formats correctly, such as AVIF<sup data-fn="39baf781-8a25-40c7-8128-a093b1008184" class="fn"><a href="#39baf781-8a25-40c7-8128-a093b1008184" id="39baf781-8a25-40c7-8128-a093b1008184-link">1</a></sup>.</p>



<p>Additionally, <a href="https://www.cvedetails.com/vulnerability-list/vendor_id-1749/Imagemagick.html?page=1&amp;cvssscoremin=9&amp;order=7&amp;trc=646&amp;sha=65fdcf32523abd445121e3eeede4072f9b2e0164" data-wpel-link="external" target="_blank" rel="external noopener">ImageMagick is a common source of serious security exploits</a>.  It&#8217;s very important to run the latest version (or very close to) at all times, to have as many critical bug fixes as possible.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>I recall when I was at Google there was &#8211; very begrudging &#8211; use of ImageMagick &#8211; it <em>is</em> the premier library for converting images when you really need to accept just about anything &#8211; but there were heroic efforts taken to isolate it and compartmentalise it, because it was taken as a given that it would be successfully exploited all the time<sup data-fn="dd7bbb45-83b3-427f-b78e-2e7899711d0f" class="fn"><a href="#dd7bbb45-83b3-427f-b78e-2e7899711d0f" id="dd7bbb45-83b3-427f-b78e-2e7899711d0f-link">2</a></sup>.  Buffer overflows, memory corruption, etc &#8211; ImageMagick is written in plain old C &amp; C++, and as such is inherently very prone to these kinds of problems.</p>
</div></div>



<p>Note that, depending on how you operate your WordPress site(s), you may or may not be seriously exposed to ImageMagick security problems.  For simple single-author sites, where only the admin can upload anything to the site (images or otherwise), it&#8217;s arguably not a huge concern.  But for more complex sites with multiple contributors &#8211; or worse, sites which accept images from general users or the public &#8211; using an outdated ImageMagick release is a serious risk.</p>



<h2 class="wp-block-heading">Why isn&#8217;t this easy?</h2>



<p>On some Linux distros, and when Plesk isn&#8217;t involved, it <em>is</em> easy &#8211; you just <code>apt install imagemagick</code> or <code>yum install ImageMagick</code> or at worst <a href="https://imagemagick.org/script/download.php" data-wpel-link="external" target="_blank" rel="external noopener">download the RPM</a> and install that.  Generally that installs ImageMagick 7 (and if not the very latest release, at least a recent one).  You subsequently just run <code>update</code> on your package manager periodically and ImageMagick is kept reasonably up to date.</p>



<p>Ubuntu does not like ImageMagick 7, for some reason.  No version of Ubuntu supports it officially, in the sense that it is not in any of the official package repos.  If you <code>apt install imagemagick</code>, you&#8217;ll get version 6 &#8211; and not even a recent version 6, but 6.9.11-60 which is over three years old!  That&#8217;s <em>three years</em> of well-known bugs &#8211; some of them potential security exploits.</p>



<p>So, unfortunately, the only way to install a modern version of ImageMagick on Ubuntu is manually.</p>



<p>Consequently, you have to manually install a suitable version of <a href="https://github.com/Imagick/imagick" data-wpel-link="external" target="_blank" rel="external noopener">Imagick</a> as well, and you have to do it into Plesk&#8217;s special PHP installation(s).</p>



<p>It took me several hours to figure all of this out, which is why I&#8217;ve written this post &#8211; so that nobody else ever has to.</p>



<h2 class="wp-block-heading">The procedure</h2>



<p>If you run into any issues, please let me know in the comments section at the bottom of the page.  I endeavour to correct any mistakes, oversights, or confusion.</p>



<p>Note that most of these commands must be run as <em>root</em>.  Either run them inside a root shell (e.g. <code>su</code>, or <code>sudo bash</code>) or prefix them with <code>sudo</code>.</p>



<h3 class="wp-block-heading">Install ImageMagick 7</h3>



<p>Fortunately, <a href="https://github.com/SoftCreatR/imei" data-wpel-link="external" target="_blank" rel="external noopener">ImageMagick Easy Install (IMEI)</a> exists to greatly simplify this step.</p>



<p>First, as a precaution IMEI insists you remove any existing ImageMagick installation(s).  Manually installing a build of ImageMagick over the formally-packaged Ubuntu version could cause problems.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<div class="wp-block-group warning is-horizontal is-content-justification-center is-layout-flex wp-container-core-group-is-layout-00ace616 wp-block-group-is-layout-flex">
<p style="font-size:100px">⚠️</p>



<p class="wp-container-content-9cfa9a5a">Be mindful that as soon as you run this command, things on your server that rely on ImageMagick will no longer work!  Fortunately this whole process takes less than half an hour, so it might go unnoticed (WordPress itself will continue working fine, serving up all existing media, but you probably won&#8217;t be able to upload new images until you finish this procedure).<br><br>If you do not finish the installation successfully, your server will likely be left in a bad, partly-broken state.  In principle you can revert back to the standard system versions of things, with e.g. <code>apt install imagemagick</code> or perhaps through Plesk, but I haven&#8217;t tested it and I make no promises.</p>
</div>
</div></div>



<figure class="wp-block-pullquote"><blockquote><p><code>apt remove "*imagemagick*" --purge -y &amp;&amp; apt autoremove --purge -y</code></p></blockquote></figure>



<p>Next, download and install ImageMagick:</p>



<figure class="wp-block-pullquote"><blockquote><p><code>pushd /tmp &amp;&amp; git clone https://github.com/SoftCreatR/imei &amp;&amp; cd imei &amp;&amp; chmod +x imei.sh &amp;&amp; ./imei.sh</code></p></blockquote></figure>



<p>By default it&#8217;ll install a very recent version &#8211; usually the very latest release &#8211; but you can customise it if you wish with the <code>--imagemagick-version</code> argument to <code>imei.sh</code>.</p>



<p>Note: I had to use the <code>--force</code> argument to get <code>imei.sh</code> to actually build &amp; install <em>all</em> the components &#8211; for some reason it skipped the dependent libraries (for AVIF &amp; JXL support) and just installed ImageMagick, <em>without</em> AVIF and JXL support.  Try it without first, since that&#8217;s supposed to be the happy path, but check that it says <code>[<span style="color: green">OK</span>]</code> for every item; that nothing is <code>[<span style="color: green">SKIPPED</span>]</code>.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>Building libaom (for AVIF support) requires CMake 3.6 or later.  Ubuntu 22.04 comes with a suitable version, so you shouldn&#8217;t have any issues.  But if you&#8217;re installing on an older Ubuntu, a different distro, or you&#8217;ve messed with CMake otherwise, check that you have a suitable version with <code>cmake --version</code>.</p>
</div></div>



<p>Once it&#8217;s completed successfully &#8211; which can take tens of minutes if you have a wimpy server such as is often used for WordPress hosting &#8211; double-check that you have a good version installed:</p>



<figure class="wp-block-pullquote"><blockquote><p><code>identify --version</code></p></blockquote></figure>



<p>At time of writing that says 7.1.1-26 for me, but of course for you it&#8217;ll probably be something newer.  You can consult <a href="https://imagemagick.org/script/download.php" data-wpel-link="external" target="_blank" rel="external noopener">ImageMagick&#8217;s download page</a> to find out what the newest version is.</p>



<h3 class="wp-block-heading">Install Imagick</h3>



<p>First, download Imagick:</p>



<figure class="wp-block-pullquote"><blockquote><p><code>pushd /tmp &amp;&amp; git clone https://github.com/Imagick/imagick &amp;&amp; cd imagick</code></p></blockquote></figure>



<p>Next, you need to set the version string to the actual version &#8211; otherwise, at the very end of this process, you&#8217;ll find that WordPress quietly refuses to use ImageMagick / Imagick!  Do that by editing the <code>php_imagick.h</code> file.  You should see, somewhere near the top of the file, 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: #AF00DB">#define</span><span style="color: #0000FF"> PHP_IMAGICK_VERSION    </span><span style="color: #A31515">&quot;@PACKAGE_VERSION@&quot;</span></span>
<span class="line"><span style="color: #AF00DB">#define</span><span style="color: #0000FF"> PHP_IMAGICK_EXTNUM     </span><span style="color: #098658">30700</span></span></code></pre></div>



<p>You need to change that first string to the version declared on that second line.  30700 means version 3.7.0.  So in my case I replaced <code>"@PACKAGE_VERSION@"</code> with <code>"3.7.0"</code>.</p>



<p>Now you need to determine where the relevant PHP installation is &#8211; the version that Plesk is using to run WordPress for your website(s).  You can find that out in various ways, perhaps the simplest being to log in to your Plesk dashboard, open the &#8220;Websites &amp; Domains&#8221; section, and look at the PHP version listed for each website of interest.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>☝️If you have multiple sites and they&#8217;re not all running the same version of PHP, you&#8217;ll need to repeat all the following steps for each PHP version.</p>
</div></div>



<p>In my case I&#8217;m using PHP 8.2. So now I know that the PHP install of interest is located at <code>/opt/plesk/php/8.2/</code> (Plesk installs all its versions of PHP in <code>/opt/plesk/php</code>). I&#8217;ll use that in the subsequent commands shown here, but make sure to adjust that to suit whatever version of PHP you&#8217;re using.</p>



<p>To build Imagick, you need the PHP developer tools &#8211; for your particular Plesk PHP installation(s).  e.g. in my case I&#8217;m using PHP 8.2, so I need to run:</p>



<figure class="wp-block-pullquote"><blockquote><p><code>apt install plesk-php82-dev</code></p></blockquote></figure>



<p>Now you need to configure the Imagick build:</p>



<figure class="wp-block-pullquote"><blockquote><p><code>/opt/plesk/php/8.2/bin/phpize &amp;&amp; ./configure --prefix=/opt/plesk/php/8.2/ --exec-prefix=/opt/plesk/php/8.2/ --datadir=/opt/plesk/php/8.2/ --with-php-config=/opt/plesk/php/8.2/bin/php-config &amp;&amp; make -j `nproc --all` install</code></p></blockquote></figure>



<p>Don&#8217;t forget to change &#8220;8.2&#8221; to your version of PHP, if it&#8217;s different!</p>



<p>It should only take ten seconds or so to configure, build, &amp; install Imagick.</p>



<p>Now, rename the built library so that Plesk updates won&#8217;t clobber it:</p>



<figure class="wp-block-pullquote"><blockquote><p><code>mv /opt/plesk/php/8.2/lib/php/modules/imagick{,_custom}.so</code></p></blockquote></figure>



<p>Lastly, although it&#8217;s technically optional, it&#8217;s nice to fix the permissions of the installed library:</p>



<figure class="wp-block-pullquote"><blockquote><p><code>chmod 644 /opt/plesk/php/8.2/lib/php/modules/imagick_custom.so</code></p></blockquote></figure>



<h3 class="wp-block-heading">Load Imagick into PHP</h3>



<p>Annoyingly, PHP won&#8217;t automatically notice Imagick is installed &#8211; you have to tell it so, manually.  Though this is pretty easy to do.  There are a few ways to do it, the easiest being through Plesk:  go to the &#8220;Tools &amp; Settings&#8221; section of your Plesk dashboard, click &#8220;PHP Settings&#8221; (from the &#8220;General Settings&#8221; category), then click on the relevant handler in the list (e.g. &#8220;FPM application&#8221;).  If you&#8217;re not sure which is relevant, look for a non-zero number in the &#8220;Domains&#8221; column.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>☝️If you use different handlers for different websites &#8211; as will be shown in the &#8220;Domains&#8221; column by a number greater than zero for multiple rows &#8211; then you might need to repeat the following steps for each one of them.  Start with any one of them, then when editing the <code>php.ini</code> file (as explained below) check which handlers are listed as also covered by that same file.  If any are missing, those are the ones you&#8217;ll need to repeat these steps on.</p>
</div></div>



<p>Click on &#8220;php.ini&#8221; to switch to that tab, and you should see a text field containing the contents of the relevant <code>php.ini</code> file.  Somewhere in this file &#8211; technically anywhere, but I like to scroll down to the section where all the other extensions are listed &#8211; you need to add the line:</p>



<pre class="wp-block-preformatted">extension=imagick_custom.so</pre>



<p>Then, click the &#8220;OK&#8221; button to save your changes.</p>



<p>In <em>theory</em> Plesk will now restart the relevant server daemons, to have them pick up the change, but if you subsequently find that WordPress doesn&#8217;t seem to see the new ImageMagick installation (or you edited <code>php.ini</code> directly from the command line), you can give it a more forceful shove:</p>



<figure class="wp-block-pullquote"><blockquote><p><code>service plesk-php82-fpm restart</code></p></blockquote></figure>



<p>You should now be set &#8211; you can test your upgrade in a variety of ways, such as:</p>



<ul class="wp-block-list">
<li>Try uploading an image in a format, or using features such as animation or transparency, that wasn&#8217;t previously supported.  e.g. AVIFs.  Note that you might need additional WordPress plug-ins to enable use of the new formats, e.g. <a href="https://wordpress.org/plugins/avif-support/" data-wpel-link="external" target="_blank" rel="external noopener">AVIF Support</a> or any of the <a href="https://wordpress.org/plugins/search/svg/" data-wpel-link="external" target="_blank" rel="external noopener">numerous SVG support plug-ins</a>.<br><br>See also <a href="https://wadetregaskis.com/image-workflow-for-wordpress/" data-wpel-link="internal">my earlier post on my Image workflow for WordPress</a> &#8211; particularly <a href="https://wadetregaskis.com/image-workflow-for-wordpress/#AVIF_Support" data-wpel-link="internal">the section on AVIF</a>.</li>



<li>Check phpinfo &#8211; in your Plesk dashboard, go to your website(s) in the &#8220;Websites &amp; Domains&#8221; section, click &#8220;PHP&#8221; (under the &#8220;Dev Tools&#8221; category), then click the subtle &#8220;View the phpinfo() page&#8221; link to the right of the first pop-up menus.  Search for &#8220;imagick&#8221; &#8211; you should find an entry for it, with a table of plug-in information such as its version, the version of ImageMagick being used, and the file formats supported.</li>
</ul>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="946" height="480" src="https://wadetregaskis.com/wp-content/uploads/2024/01/imagick-section-of-phpinfo.webp" alt="Screenshot of the &quot;imagick&quot; section of the phpinfo page." class="wp-image-7465" srcset="https://wadetregaskis.com/wp-content/uploads/2024/01/imagick-section-of-phpinfo.webp 946w, https://wadetregaskis.com/wp-content/uploads/2024/01/imagick-section-of-phpinfo-256x130.webp 256w, https://wadetregaskis.com/wp-content/uploads/2024/01/imagick-section-of-phpinfo-512x260.webp 512w, https://wadetregaskis.com/wp-content/uploads/2024/01/imagick-section-of-phpinfo@2x.webp 1892w" sizes="auto, (max-width: 946px) 100vw, 946px" /></figure>
</div>


<h2 class="wp-block-heading">Addendum: updating ImageMagick &amp; Magick</h2>



<p>You can of course just repeat all the above steps to perform a fresh install, incorporating any updates since you last installed everything.  But you can save a little time by keeping the <code>imei</code> and <code>imagick</code> folders around, and just doing:</p>



<ol class="wp-block-list">
<li><code>git pull --rebase</code> in each, then re-run their respective build commands.  For Imagick you&#8217;ll probably have to <code>git stash</code> first, in order to do the <code>git pull</code>, and then restore your version string patch with <code>git stash apply</code>.<br><br>Don&#8217;t forget to check if you need to update the version string in <code>php_imagick.h</code>.</li>



<li>Rename the built <code>imagick.so</code> again, and fix its permissions.</li>



<li>Restart the PHP daemon.</li>
</ol>


<ol class="wp-block-footnotes"><li id="39baf781-8a25-40c7-8128-a093b1008184">Some versions &amp; builds nominally support AVIF, but this support is almost by accident &#8211; what they actually support formally is <a href="https://en.wikipedia.org/wiki/High_Efficiency_Image_File_Format" data-wpel-link="external" target="_blank" rel="external noopener">HEIF</a>, for which they use <a href="https://github.com/strukturag/libheif" data-wpel-link="external" target="_blank" rel="external noopener">libheif</a>, which <em>can</em> be built with AVIF support as well, but in my experience that support is buggy and incomplete &#8211; e.g. <a href="https://alexwlchan.net/2023/check-for-transparency/" data-wpel-link="external" target="_blank" rel="external noopener">alpha channels are not supported</a>. <a href="#39baf781-8a25-40c7-8128-a093b1008184-link" aria-label="Jump to footnote reference 1">↩︎</a></li><li id="dd7bbb45-83b3-427f-b78e-2e7899711d0f">Alas I don&#8217;t actually know if / how often it was successfully exploited, as far as Google saw.  I have to imagine it was not uncommon, though, given the large attack surface that is ImageMagick&#8217;s APIs and image plug-ins, not to mention how lucrative a target Google is. <a href="#dd7bbb45-83b3-427f-b78e-2e7899711d0f-link" aria-label="Jump to footnote reference 2">↩︎</a></li></ol>]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/how-to-install-imagemagick-7-for-wordpress-under-plesk-obsidian-on-ubuntu-22-04/feed/</wfw:commentRss>
			<slash:comments>12</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2024/01/ImageMagick-logo.avif" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">7457</post-id>	</item>
		<item>
		<title>SwiftUI main thread hang detector</title>
		<link>https://wadetregaskis.com/swiftui-main-thread-hang-detector/</link>
					<comments>https://wadetregaskis.com/swiftui-main-thread-hang-detector/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Mon, 22 Jan 2024 00:27:53 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Howto]]></category>
		<category><![CDATA[Swift]]></category>
		<category><![CDATA[SwiftUI]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=7459</guid>

					<description><![CDATA[This is just a little snippet that is quite useful for reporting when your GUI thread (the main thread / actor) hangs for a significant amount of time. There are numerous heavier-weight tools for analysing this sort of thing, but I&#8217;ve found that this simple monitor does what I need most of the time. You&#8230; <a class="read-more-link" href="https://wadetregaskis.com/swiftui-main-thread-hang-detector/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>This is just a little snippet that is quite useful for reporting when your GUI thread (the main thread / actor) hangs for a significant amount of time.  There are numerous heavier-weight tools for analysing this sort of thing, but I&#8217;ve found that this simple monitor does what I need most of the time.</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="var body: some View {
    SomeRootView {
        …
    }.task {
        let approximateGranularity = Duration.milliseconds(10)
        let threshold = Duration.milliseconds(50)

        let clock = SuspendingClock()
        var lastIteration = clock.now

        while !Task.isCancelled {
            try? await Task.sleep(for: approximateGranularity,
                                  tolerance: approximateGranularity / 2,
                                  clock: clock)

            let now = clock.now

            if now - lastIteration &gt; threshold {
                print(&quot;Main thread hung for &quot;,
                      (now - lastIteration).formatted(.units(width: .wide,
                                                             fractionalPart: .show(length: 2))),
                      &quot;.&quot;,
                      separator: &quot;&quot;)
            }

            lastIteration = now
        }
    }
}" 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">var</span><span style="color: #000000"> body: some View {</span></span>
<span class="line"><span style="color: #000000">    SomeRootView {</span></span>
<span class="line"><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">let</span><span style="color: #000000"> approximateGranularity = Duration.</span><span style="color: #795E26">milliseconds</span><span style="color: #000000">(</span><span style="color: #098658">10</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"> threshold = Duration.</span><span style="color: #795E26">milliseconds</span><span style="color: #000000">(</span><span style="color: #098658">50</span><span style="color: #000000">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #0000FF">let</span><span style="color: #000000"> clock = </span><span style="color: #795E26">SuspendingClock</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"> lastIteration = 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">while</span><span style="color: #000000"> !Task.</span><span style="color: #001080">isCancelled</span><span style="color: #000000"> {</span></span>
<span class="line"><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"> Task.</span><span style="color: #795E26">sleep</span><span style="color: #000000">(</span><span style="color: #795E26">for</span><span style="color: #000000">: approximateGranularity,</span></span>
<span class="line"><span style="color: #000000">                                  </span><span style="color: #795E26">tolerance</span><span style="color: #000000">: approximateGranularity / </span><span style="color: #098658">2</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                  </span><span style="color: #795E26">clock</span><span style="color: #000000">: clock)</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>
<span class="line"><span style="color: #000000">            </span><span style="color: #AF00DB">if</span><span style="color: #000000"> now - lastIteration &gt; threshold {</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;Main thread hung for &quot;</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                      (now - lastIteration).</span><span style="color: #795E26">formatted</span><span style="color: #000000">(.</span><span style="color: #795E26">units</span><span style="color: #000000">(</span><span style="color: #795E26">width</span><span style="color: #000000">: .</span><span style="color: #001080">wide</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                                             </span><span style="color: #795E26">fractionalPart</span><span style="color: #000000">: .</span><span style="color: #795E26">show</span><span style="color: #000000">(</span><span style="color: #795E26">length</span><span style="color: #000000">: </span><span style="color: #098658">2</span><span style="color: #000000">))),</span></span>
<span class="line"><span style="color: #000000">                      </span><span style="color: #A31515">&quot;.&quot;</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                      </span><span style="color: #795E26">separator</span><span style="color: #000000">: </span><span style="color: #A31515">&quot;&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">            }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">            lastIteration = 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></code></pre></div>



<p>You can adjust the two parameters &#8211; <code>approximateGranularity</code> and <code>threshold</code> &#8211; to suit your preferences.  The overhead is quite tiny in CPU-usage terms, although be aware that this will cause the main thread to wake up frequently so it may have a noticeable, detrimental energy-usage impact.  I suggest not deploying this to your users.</p>



<p>Perhaps it goes without saying, but a breakpoint set on the <code>print</code> statement enables you to debug deeper into hangs.  Even without that, though, it can be illuminating just to have the log message &#8211; oftentimes you don&#8217;t notice that your app is hanging, because you don&#8217;t happen to be actively interacting with it in that moment.  But your users might.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/swiftui-main-thread-hang-detector/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2024/01/SPOD.avif" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">7459</post-id>	</item>
		<item>
		<title>Image workflow for WordPress</title>
		<link>https://wadetregaskis.com/image-workflow-for-wordpress/</link>
					<comments>https://wadetregaskis.com/image-workflow-for-wordpress/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Sun, 31 Dec 2023 01:41:49 +0000</pubDate>
				<category><![CDATA[Howto]]></category>
		<category><![CDATA[Meta]]></category>
		<category><![CDATA[AVIF]]></category>
		<category><![CDATA[AVIF Support]]></category>
		<category><![CDATA[Enable Media Replace]]></category>
		<category><![CDATA[Image Regenerate & Select Crop]]></category>
		<category><![CDATA[Image Tool+]]></category>
		<category><![CDATA[ImageOptim]]></category>
		<category><![CDATA[JPEG 2000]]></category>
		<category><![CDATA[JPEG XL]]></category>
		<category><![CDATA[Perfect Images]]></category>
		<category><![CDATA[PNG]]></category>
		<category><![CDATA[WebP]]></category>
		<category><![CDATA[Wordpress]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=7146</guid>

					<description><![CDATA[These are the main tools and procedures I use for publishing images through WordPress. In case it&#8217;s helpful to others. My input images generally fall into three categories: Some of these formats can be published directly into WordPress &#8211; mainly PNG and JPEG &#8211; but not all, and neither PNG nor JPEG are the best&#8230; <a class="read-more-link" href="https://wadetregaskis.com/image-workflow-for-wordpress/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>These are the main tools and procedures I use for publishing images through WordPress.  In case it&#8217;s helpful to others.</p>



<p>My input images generally fall into three categories:</p>



<ol class="wp-block-list">
<li>Screenshots, as PNGs.</li>



<li>Random 3rd party images, typically as JPEGs but sometimes PNGs or other formats.</li>



<li>Photos, as Nikon NEFs.</li>
</ol>



<p>Some of these formats <em>can</em> be published directly into WordPress &#8211; mainly PNG and JPEG &#8211; but not all, and neither PNG nor JPEG are the best file formats for anything these days.  In terms of compression efficiency, there are numerous superior formats &#8211; JPEG 2000, JPEG XL, HEIF<sup data-fn="ce19f9a1-4b3d-427c-b205-ec8684b1e5ae" class="fn"><a href="#ce19f9a1-4b3d-427c-b205-ec8684b1e5ae" id="ce19f9a1-4b3d-427c-b205-ec8684b1e5ae-link">1</a></sup>, WebP, AVIF, etc.</p>



<p>Using an appropriate and efficient format &#8211; and suitable compression quality settings &#8211; is important to me to save disk space, save bandwidth, minimise page load times, and ensure images are seen the way I want them to be (re. lossless vs lossy).</p>



<h1 class="wp-block-heading">Best image codecs</h1>



<h2 class="wp-block-heading">Legacy options</h2>



<p>PNG and JPEG are supported in practically any &amp; every web browser still in real use today.  As is GIF, technically, although it&#8217;s strictly inferior to PNG [for static images] and actual video formats [for moving images].  It certainly <em>is</em> possible to still use them, but it wastes storage space &amp; bandwidth, and hurts page load times.</p>



<p>That said, PNG is not the worst option <em>if</em> you use appropriate tooling to maximise the compression.  The level of compression you achieve &#8216;out of the box&#8217; with most applications &#8211; e.g. Photoshop, Apple&#8217;s apps, etc &#8211; is actually quite poor.  There are numerous specialised tools which can greatly improve the compression.  I recommend <a href="https://imageoptim.com/mac" data-wpel-link="external" target="_blank" rel="external noopener">ImageOptim</a> &#8211; it&#8217;s free and open source.  It typically reduces PNG files sizes by a further 20% to 50% (depending on how much fine detail or noise the image contains) without any loss of image quality.  It actually makes PNG competitive with the modern formats to a degree, although ultimately still not the best.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>⚠️ ImageOptim does not work correctly with <em>animated</em> PNGs &#8211; it silently removes the animation!</p>
</div></div>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="664" height="559" src="https://wadetregaskis.com/wp-content/uploads/2023/12/ImageOptim.webp" alt="" class="wp-image-7150" style="width:664px;height:auto" srcset="https://wadetregaskis.com/wp-content/uploads/2023/12/ImageOptim.webp 664w, https://wadetregaskis.com/wp-content/uploads/2023/12/ImageOptim-256x216.webp 256w, https://wadetregaskis.com/wp-content/uploads/2023/12/ImageOptim-512x431.webp 512w, https://wadetregaskis.com/wp-content/uploads/2023/12/ImageOptim@2x.webp 1328w" sizes="auto, (max-width: 664px) 100vw, 664px" /></figure>
</div>


<p>It&#8217;s quite configurable, including with regard to stripping metadata or not &#8211; although, alas, it offers no nuance in this, such as keeping copyright metadata but removing spurious camera metadata, or geolocations.</p>



<div class="alignnormal"><div id="metaslider-id-7155" style="max-width: 775px; margin: 0 auto;" class="ml-slider-3-107-0 metaslider metaslider-flex metaslider-7155 ml-slider ms-theme-default nav-hidden" role="region" aria-label="ImageOptim Preferences" data-height="510" data-width="775">
    <div id="metaslider_container_7155">
        <div id="metaslider_7155" class="flexslider">
            <ul class='slides'>
                <li style="display: block; width: 100%;" class="slide-7157 ms-image " aria-roledescription="slide" data-date="2023-12-30 12:03:47" data-filename="ImageOptim-Settings-General.webp" data-slide-type="image"><img loading="lazy" decoding="async" width="775" height="510" src="https://wadetregaskis.com/wp-content/uploads/2023/12/ImageOptim-Settings-General.webp" class="slider-7155 slide-7157 msDefaultImage" alt="Screenshot of the ImageOptim Preferences window, showing the General tab" rel="" title="ImageOptim Settings - General" srcset="https://wadetregaskis.com/wp-content/uploads/2023/12/ImageOptim-Settings-General.webp 775w, https://wadetregaskis.com/wp-content/uploads/2023/12/ImageOptim-Settings-General-256x168.webp 256w, https://wadetregaskis.com/wp-content/uploads/2023/12/ImageOptim-Settings-General-512x337.webp 512w, https://wadetregaskis.com/wp-content/uploads/2023/12/ImageOptim-Settings-General@2x.webp 1550w" sizes="auto, (max-width: 775px) 100vw, 775px" /><div class="caption-wrap"><div class="caption">You have full control over which libraries are used.  Not all the libraries are enabled by default - Guetzli in particular is off by default because it can make the recompression process take a particularly long time.  But in my opinion, if you're going to the trouble of using specialised tools to improve your image compression, you might as well use them to their fullest.</div></div></li>
                <li style="display: none; width: 100%;" class="slide-7158 ms-image " aria-roledescription="slide" data-date="2023-12-30 12:03:47" data-filename="ImageOptim-Settings-Quality.webp" data-slide-type="image"><img loading="lazy" decoding="async" width="775" height="510" src="https://wadetregaskis.com/wp-content/uploads/2023/12/ImageOptim-Settings-Quality.webp" class="slider-7155 slide-7158 msDefaultImage" alt="Screenshot of the ImageOptim Preferences window, showing the Quality tab" rel="" title="ImageOptim Settings - Quality" srcset="https://wadetregaskis.com/wp-content/uploads/2023/12/ImageOptim-Settings-Quality.webp 775w, https://wadetregaskis.com/wp-content/uploads/2023/12/ImageOptim-Settings-Quality-256x168.webp 256w, https://wadetregaskis.com/wp-content/uploads/2023/12/ImageOptim-Settings-Quality-512x337.webp 512w, https://wadetregaskis.com/wp-content/uploads/2023/12/ImageOptim-Settings-Quality@2x.webp 1550w" sizes="auto, (max-width: 775px) 100vw, 775px" /><div class="caption-wrap"><div class="caption">By default ImageOptim preserves your images exactly as they appear (lossless recompression), but it does give you the option of using lossy compression.  Just beware of significant and unpredictable compression artefacts, particularly with PNGs.</div></div></li>
                <li style="display: none; width: 100%;" class="slide-7156 ms-image " aria-roledescription="slide" data-date="2023-12-30 12:03:46" data-filename="ImageOptim-Settings-Optimisation-level.webp" data-slide-type="image"><img loading="lazy" decoding="async" width="775" height="510" src="https://wadetregaskis.com/wp-content/uploads/2023/12/ImageOptim-Settings-Optimisation-level.webp" class="slider-7155 slide-7156 msDefaultImage" alt="Screenshot of the ImageOptim Preferences window, showing the Optimisation level tab" rel="" title="ImageOptim Settings - Optimisation level" srcset="https://wadetregaskis.com/wp-content/uploads/2023/12/ImageOptim-Settings-Optimisation-level.webp 775w, https://wadetregaskis.com/wp-content/uploads/2023/12/ImageOptim-Settings-Optimisation-level-256x168.webp 256w, https://wadetregaskis.com/wp-content/uploads/2023/12/ImageOptim-Settings-Optimisation-level-512x337.webp 512w, https://wadetregaskis.com/wp-content/uploads/2023/12/ImageOptim-Settings-Optimisation-level@2x.webp 1550w" sizes="auto, (max-width: 775px) 100vw, 775px" /><div class="caption-wrap"><div class="caption">I haven't really played with this 'Optimisation level' setting, I just set it to maximum.  Why use a tool like this if you're not going to use it to its fullest?</div></div></li>
            </ul>
        </div>
        
    </div>
</div></div>



<p>It&#8217;s also possible to use such specialised tools for <em>lossy</em> PNG compression.  PNG is not inherently lossy, but by manipulating the image before compression it is possible to greatly improve its compressibility.  e.g. by reducing the colour palette, adding or removing dithering, or simply removing fine detail.  Various tools &#8211; including ImageOptim &#8211; will do this for you if you wish, although be warned that the resulting image quality is unpredictable and sometimes very poor (exhibiting obvious colour changes, banding, or visual artefacts otherwise).</p>



<p>The example below is a fairly typical &#8220;good&#8221; result &#8211; the file size is cut by 70%, but the lossy PNG compression has introduced noticeable banding and dithering.  You might consider it worth the trade-off but consider that a lossy WebP version, <em>also</em> saving 70% on file size, doesn&#8217;t have any noticeable image degradation.  And an AVIF version with similar image quality cuts the file size by <em>80%</em>.  So while you might think it&#8217;s neat to use a &#8216;lossy&#8217; PNG and get &#8211; by PNG standards &#8211; dramatically higher compression, you&#8217;re probably better off just using WebP or AVIF.</p>



<div class="alignnormal"><div id="metaslider-id-7178" style="max-width: 1000px; margin: 0 auto;" class="ml-slider-3-107-0 metaslider metaslider-flex metaslider-7178 ml-slider ms-theme-default nav-hidden nav-hidden" role="region" aria-label="Lossy PNG warning" data-height="597" data-width="1000">
    <div id="metaslider_container_7178">
        <div id="metaslider_7178" class="flexslider">
            <ul class='slides'>
                <li style="display: block; width: 100%;" class="slide-7183 ms-image " aria-roledescription="slide" data-date="2023-12-30 16:01:23" data-filename="Swift-on-Raspberry-Pi-lossless.png" data-slide-type="image"><img loading="lazy" decoding="async" width="2000" height="1194" src="https://wadetregaskis.com/wp-content/uploads/2023/12/Swift-on-Raspberry-Pi-lossless.png" class="slider-7178 slide-7183 msDefaultImage" alt="" rel="" title="Swift on Raspberry Pi (lossless)" srcset="https://wadetregaskis.com/wp-content/uploads/2023/12/Swift-on-Raspberry-Pi-lossless.png 2000w, https://wadetregaskis.com/wp-content/uploads/2023/12/Swift-on-Raspberry-Pi-lossless-256x153.png 256w, https://wadetregaskis.com/wp-content/uploads/2023/12/Swift-on-Raspberry-Pi-lossless-512x306@2x.png 1024w, https://wadetregaskis.com/wp-content/uploads/2023/12/Swift-on-Raspberry-Pi-lossless-512x306.png 512w" sizes="auto, (max-width: 2000px) 100vw, 2000px" /><div class="caption-wrap"><div class="caption">Lossless PNG (1,956 KB)</div></div></li>
                <li style="display: none; width: 100%;" class="slide-7184 ms-image " aria-roledescription="slide" data-date="2023-12-30 16:01:23" data-filename="Swift-on-Raspberry-Pi-lossy.png" data-slide-type="image"><img loading="lazy" decoding="async" width="2000" height="1194" src="https://wadetregaskis.com/wp-content/uploads/2023/12/Swift-on-Raspberry-Pi-lossy.png" class="slider-7178 slide-7184 msDefaultImage" alt="" rel="" title="Swift on Raspberry Pi (lossy)" srcset="https://wadetregaskis.com/wp-content/uploads/2023/12/Swift-on-Raspberry-Pi-lossy.png 2000w, https://wadetregaskis.com/wp-content/uploads/2023/12/Swift-on-Raspberry-Pi-lossy-256x153.png 256w, https://wadetregaskis.com/wp-content/uploads/2023/12/Swift-on-Raspberry-Pi-lossy-512x306@2x.png 1024w, https://wadetregaskis.com/wp-content/uploads/2023/12/Swift-on-Raspberry-Pi-lossy-512x306.png 512w" sizes="auto, (max-width: 2000px) 100vw, 2000px" /><div class="caption-wrap"><div class="caption">Lossy PNG (587 KB)</div></div></li>
            </ul>
        </div>
        
    </div>
</div></div>



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



<p>Despite its age JPEG 2000 is surprisingly good in my experience &#8211; I used it many years ago on this website for my <a href="https://wadetregaskis.com/raw-converter-comparison/" data-wpel-link="internal">Raw converter comparison</a> as an alternative to TIFF and PNG, as it yielded file sizes about half that of PNG.  It&#8217;s competitive even with newer formats like WebP and AVIF.  JPEG XL is purportedly even better again.  Alas, no browser other than Safari supports either of them.</p>



<p>HEIF exists and is &#8220;modern&#8221; but isn&#8217;t really in the same ballpark &#8211; while it <em>is</em> better than JPEG, that&#8217;s a low bar; it achieves worse results even than JPEG 2000, a codec that&#8217;s <em>twenty years</em> older.  Its only claim to any fame is that it&#8217;s the native source format for many photos today, thanks to its use on Apple iDevices.  In any case, the only current-version web browser which supports it is Safari.</p>



<h2 class="wp-block-heading">Viable options</h2>



<p>The only modern format which is universally supported<sup data-fn="42530c0c-bed1-4a7a-bff8-2c27f0a337bd" class="fn"><a href="#42530c0c-bed1-4a7a-bff8-2c27f0a337bd" id="42530c0c-bed1-4a7a-bff8-2c27f0a337bd-link">2</a></sup> is WebP.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1158" height="746" src="https://wadetregaskis.com/wp-content/uploads/2023/12/caniuse-com-for-WebP.webp" alt="Screenshot of caniuse.com showing the web browser compatibility matrix for the WebP image format" class="wp-image-7147" srcset="https://wadetregaskis.com/wp-content/uploads/2023/12/caniuse-com-for-WebP.webp 1158w, https://wadetregaskis.com/wp-content/uploads/2023/12/caniuse-com-for-WebP-256x165.webp 256w, https://wadetregaskis.com/wp-content/uploads/2023/12/caniuse-com-for-WebP-512x330@2x.webp 1024w, https://wadetregaskis.com/wp-content/uploads/2023/12/caniuse-com-for-WebP-512x330.webp 512w, https://wadetregaskis.com/wp-content/uploads/2023/12/caniuse-com-for-WebP@2x.webp 2316w" sizes="auto, (max-width: 1158px) 100vw, 1158px" /><figcaption class="wp-element-caption">Source: <a href="https://caniuse.com/webp" data-wpel-link="external" target="_blank" rel="external noopener">caniuse.com</a></figcaption></figure>
</div>


<p>AVIF is <em>nearly</em> universally supported, with the unfortunately notable exception of Edge.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1158" height="879" src="https://wadetregaskis.com/wp-content/uploads/2023/12/caniuse-com-for-AVIF.webp" alt="Screenshot of caniuse.com showing the web browser compatibility matrix for the AVIF image format" class="wp-image-7148" srcset="https://wadetregaskis.com/wp-content/uploads/2023/12/caniuse-com-for-AVIF.webp 1158w, https://wadetregaskis.com/wp-content/uploads/2023/12/caniuse-com-for-AVIF-256x194.webp 256w, https://wadetregaskis.com/wp-content/uploads/2023/12/caniuse-com-for-AVIF-512x389@2x.webp 1024w, https://wadetregaskis.com/wp-content/uploads/2023/12/caniuse-com-for-AVIF-512x389.webp 512w, https://wadetregaskis.com/wp-content/uploads/2023/12/caniuse-com-for-AVIF@2x.webp 2316w" sizes="auto, (max-width: 1158px) 100vw, 1158px" /><figcaption class="wp-element-caption">Source: <a href="https://caniuse.com/avif" data-wpel-link="external" target="_blank" rel="external noopener">caniuse.com</a></figcaption></figure>
</div>


<p>I choose to use AVIF nonetheless, as my audience are unlikely to be using Edge anyway, since they&#8217;re unlikely to be using Windows at all.  I also like to assume that Edge <em>will</em>, eventually, get with the program &#8211; although the fact that AVIF support was apparently incorporated in versions 114 to 117, but then <em>removed</em>, is concerning.</p>



<p>So those are the two options, basically.  For better or worse, neither one is strictly superior to the other.</p>



<figure class="wp-block-pullquote"><blockquote><p>WebP is best for lossless compression, and AVIF is usually the best for lossy compression (especially at lower qualities).</p></blockquote></figure>



<p>It is case-dependent and a little subjective as they have noticeably different methodologies:</p>



<ul class="wp-block-list">
<li>AVIF tends to be heavy-handed with &#8216;noise&#8217; removal, which seems to be a significant part of how it achieves better compression.</li>



<li>WebP will more faithfully preserve noise (and artefacts from earlier lossy compressions), which can be counter-productive.</li>
</ul>



<p>AVIF really excels &#8211; compared to WebP and earlier codecs &#8211; with more aggressive compression settings.  It&#8217;s much better under tight file size constraints.  It also performs particularly well for images which <em>aren&#8217;t</em> so clean to begin with &#8211; where they do have some actual image noise, or visible compression artefacts from an earlier lossy compression stage, or otherwise fine details that aren&#8217;t important to preserve.</p>



<p>WebP behaves more &#8220;traditionally&#8221; &#8211; like JPEG &#8211; with regard to increase blockiness and banding as you crank up the compression.  AVIF does a much better job of degrading image quality more &#8216;smoothly&#8217; across the image &#8211; so that particularly bad, noticeable artefacts don&#8217;t develop as easily.  Even while it preserves more genuine detail.</p>



<div class="alignnormal"><div id="metaslider-id-7171" style="max-width: 1024px; margin: 0 auto;" class="ml-slider-3-107-0 metaslider metaslider-flex metaslider-7171 ml-slider ms-theme-default nav-hidden nav-hidden nav-hidden" role="region" aria-label="Noisy Echo (WebP vs AVIF high-compression comparison)" data-height="1024" data-width="1024">
    <div id="metaslider_container_7171">
        <div id="metaslider_7171" class="flexslider">
            <ul class='slides'>
                <li style="display: block; width: 100%;" class="slide-7174 ms-image " aria-roledescription="slide" data-date="2023-12-30 15:40:08" data-filename="Noisy-Echo-10.avif" data-slide-type="image"><img loading="lazy" decoding="async" width="2048" height="2048" src="https://wadetregaskis.com/wp-content/uploads/2023/12/Noisy-Echo-10.avif" class="slider-7171 slide-7174 msDefaultImage" alt="" rel="" title="Noisy-Echo (10)" srcset="https://wadetregaskis.com/wp-content/uploads/2023/12/Noisy-Echo-10-1024x1024@2x.avif 2048w, https://wadetregaskis.com/wp-content/uploads/2023/12/Noisy-Echo-10-256x256.avif 256w, https://wadetregaskis.com/wp-content/uploads/2023/12/Noisy-Echo-10-512x512@2x.avif 1024w, https://wadetregaskis.com/wp-content/uploads/2023/12/Noisy-Echo-10-512x512.avif 512w" sizes="auto, (max-width: 2048px) 100vw, 2048px" /><div class="caption-wrap"><div class="caption">AVIF (188 KB)</div></div></li>
                <li style="display: none; width: 100%;" class="slide-7175 ms-image " aria-roledescription="slide" data-date="2023-12-30 15:40:09" data-filename="Noisy-Echo-22.webp" data-slide-type="image"><img loading="lazy" decoding="async" width="2048" height="2048" src="https://wadetregaskis.com/wp-content/uploads/2023/12/Noisy-Echo-22.webp" class="slider-7171 slide-7175 msDefaultImage" alt="" rel="" title="Noisy-Echo (22)" srcset="https://wadetregaskis.com/wp-content/uploads/2023/12/Noisy-Echo-22-1024x1024@2x.webp 2048w, https://wadetregaskis.com/wp-content/uploads/2023/12/Noisy-Echo-22-256x256.webp 256w, https://wadetregaskis.com/wp-content/uploads/2023/12/Noisy-Echo-22-512x512@2x.webp 1024w, https://wadetregaskis.com/wp-content/uploads/2023/12/Noisy-Echo-22-512x512.webp 512w" sizes="auto, (max-width: 2048px) 100vw, 2048px" /><div class="caption-wrap"><div class="caption">WebP (185 KB)</div></div></li>
            </ul>
        </div>
        
    </div>
</div></div>



<p>WebP is more competitive with high quality settings or high quality, &#8216;smooth&#8217; images (e.g. screenshots or computer renderings).  In lossless mode WebP universally outperforms AVIF, often by a significant margin.  In fact even PNG typically outperforms AVIF for lossless compression!</p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="1208" height="566" src="https://wadetregaskis.com/wp-content/uploads/2019/03/Dialog-screenshot.webp" alt="" class="wp-image-4352" style="width:604px" srcset="https://wadetregaskis.com/wp-content/uploads/2019/03/Dialog-screenshot.webp 1208w, https://wadetregaskis.com/wp-content/uploads/2019/03/Dialog-screenshot-512x240@2x.webp 1024w, https://wadetregaskis.com/wp-content/uploads/2019/03/Dialog-screenshot-256x120.webp 256w, https://wadetregaskis.com/wp-content/uploads/2019/03/Dialog-screenshot-512x240.webp 512w" sizes="auto, (max-width: 1208px) 100vw, 1208px" /><figcaption class="wp-element-caption">82 KB as WebP, 119 KB as PNG, and a whopping 284 KB as AVIF!</figcaption></figure>
</div>


<p>I&#8217;ve also noticed that WebP is <em>substantially</em> better at compressing repetitive patterns (even if they&#8217;re not perfectly identical).</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1024" height="1024" src="https://wadetregaskis.com/wp-content/uploads/2023/12/WebP-repetitive-tiling-example.webp" alt="" class="wp-image-7151" style="object-fit:cover" srcset="https://wadetregaskis.com/wp-content/uploads/2023/12/WebP-repetitive-tiling-example-512x512@2x.webp 1024w, https://wadetregaskis.com/wp-content/uploads/2023/12/WebP-repetitive-tiling-example-256x256.webp 256w, https://wadetregaskis.com/wp-content/uploads/2023/12/WebP-repetitive-tiling-example-512x512.webp 512w, https://wadetregaskis.com/wp-content/uploads/2023/12/WebP-repetitive-tiling-example@2x.webp 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Using high-quality (but <em>lossy</em>) compression, this conglomerate image comparing four different RAW converters is 2.3 MB as WebP, 3.8 MB as AVIF (and 12.6 MB as an uncompressed TIFF, for reference).  Yet if you compress any one of the quarters individually, AVIF outperforms WebP.  Even though each quarter is significantly different, there&#8217;s enough repetition between them that WebP can extract significant compression gains, that AVIF apparently cannot.  And in cases with more verbatim repetition, WebP&#8217;s advantage increases even further.</figcaption></figure>
</div>


<p>AVIF seems particularly good at reducing visible artefacts resulting from JPEG over-compression, and in avoiding prominent compression artefacts in general &#8211; such as in the example below, where the file sizes are very similar (14 KB for AVIF, 13 KB for WebP) but AVIF does more with it &#8211; preserving more genuine detail such as in the camera lens and IR sensor even while doing a better job of removing the JPEG artefacts (noise, blockiness, and banding).</p>



<div class="alignnormal"><div id="metaslider-id-7188" style="max-width: 849px; margin: 0 auto;" class="ml-slider-3-107-0 metaslider metaslider-flex metaslider-7188 ml-slider ms-theme-default nav-hidden nav-hidden nav-hidden nav-hidden" role="region" aria-label="AVIF vs WebP for reducing JPEG artefacts" data-height="538" data-width="849">
    <div id="metaslider_container_7188">
        <div id="metaslider_7188" class="flexslider">
            <ul class='slides'>
                <li style="display: block; width: 100%;" class="slide-7196 ms-image " aria-roledescription="slide" data-date="2023-12-30 16:52:16" data-filename="Blink-XT.png" data-slide-type="image"><img loading="lazy" decoding="async" width="849" height="538" src="https://wadetregaskis.com/wp-content/uploads/2023/12/Blink-XT.png" class="slider-7188 slide-7196 msDefaultImage" alt="" rel="" title="Blink-XT" srcset="https://wadetregaskis.com/wp-content/uploads/2023/12/Blink-XT.png 849w, https://wadetregaskis.com/wp-content/uploads/2023/12/Blink-XT-256x162.png 256w, https://wadetregaskis.com/wp-content/uploads/2023/12/Blink-XT-512x324.png 512w" sizes="auto, (max-width: 849px) 100vw, 849px" /><div class="caption-wrap"><div class="caption">Original, full of JPEG artefacts</div></div></li>
                <li style="display: none; width: 100%;" class="slide-7195 ms-image " aria-roledescription="slide" data-date="2023-12-30 16:52:16" data-filename="Blink-XT-60.webp" data-slide-type="image"><img loading="lazy" decoding="async" width="849" height="538" src="https://wadetregaskis.com/wp-content/uploads/2023/12/Blink-XT-60.webp" class="slider-7188 slide-7195 msDefaultImage" alt="" rel="" title="Blink-XT (60)" srcset="https://wadetregaskis.com/wp-content/uploads/2023/12/Blink-XT-60.webp 849w, https://wadetregaskis.com/wp-content/uploads/2023/12/Blink-XT-60-256x162.webp 256w, https://wadetregaskis.com/wp-content/uploads/2023/12/Blink-XT-60-512x324.webp 512w" sizes="auto, (max-width: 849px) 100vw, 849px" /><div class="caption-wrap"><div class="caption">WebP</div></div></li>
                <li style="display: none; width: 100%;" class="slide-7194 ms-image " aria-roledescription="slide" data-date="2023-12-30 16:52:15" data-filename="Blink-XT-30.avif" data-slide-type="image"><img loading="lazy" decoding="async" width="849" height="538" src="https://wadetregaskis.com/wp-content/uploads/2023/12/Blink-XT-30.avif" class="slider-7188 slide-7194 msDefaultImage" alt="" rel="" title="Blink-XT (30)" srcset="https://wadetregaskis.com/wp-content/uploads/2023/12/Blink-XT-30.avif 849w, https://wadetregaskis.com/wp-content/uploads/2023/12/Blink-XT-30-256x162.avif 256w, https://wadetregaskis.com/wp-content/uploads/2023/12/Blink-XT-30-512x324.avif 512w" sizes="auto, (max-width: 849px) 100vw, 849px" /><div class="caption-wrap"><div class="caption">AVIF</div></div></li>
            </ul>
        </div>
        
    </div>
</div></div>



<p>While generally I avoid re-compressing from an already lossy source since it tends to just compound the compression artefacts, AVIF can be useful for replacing low-quality JPEGs when you don&#8217;t have a quality original available.  It can actually <em>improve</em> subjective image quality &#8211; by correcting JPEG artefacts &#8211; even while substantially shrinking the file size.</p>



<h1 class="wp-block-heading">Image format conversion</h1>



<p>There&#8217;s basically two schools of thought on converting images to their serving format &#8211; do it in advance of uploading the image to WordPress, or have WordPress do it (via one or more plug-ins).  I prefer the former as:</p>



<ol class="wp-block-list">
<li>I have more control over the images by doing the conversion myself.
<ul class="wp-block-list">
<li>I can pick the best format for each situation.</li>



<li>I can customise compression settings to suit the situation (e.g. use more aggressive compression if the source image isn&#8217;t high quality to begin with) and use (e.g. use lossless for images which are about comparing image formats, RAW converters, or other such situations where viewers need to see the image <em>exactly</em>).</li>
</ul>
</li>



<li>I have more reliably-available computing horsepower locally; I can run pretty much any conversion or optimisation process and it&#8217;ll just work.  In contrast, WordPress-hosted processing is subject to various limits on runtime and RAM usage, which can cause things to fail in all sorts of ways both obvious and nefarious.  Most web servers [used for WordPress sites] have <em>way</em> less RAM available than even an entry-level personal computer.  Processing of &#8220;large&#8221; images, such as photos from a camera, <em>will</em> fail with default WordPress &amp; PHP settings.</li>



<li>Every additional WordPress plug-in increases the possibility of problems, whether due to bugs in the plug-in, the possibility of incompatibilities between plug-ins, etc.</li>



<li>WordPress plug-ins don&#8217;t always age well; each one I adopt is yet another plug-in which I might have to migrate off of in future.</li>



<li>WordPress plug-ins for image format conversion &amp; optimisation tend, in my experience, to be both buggy and money-grubbing.  Many are tied to 3rd party services like CDNs or &#8220;Cloud&#8221;-based image processing, and try to lock you into pricey and unnecessary paid subscriptions.</li>
</ol>



<p>Nominally having WordPress do it means you can just upload any random image without care for its format or compression, <em>and</em> that over time you can easily adopt new image formats just by installing new plug-ins, with all existing images converted retroactively.  In reality, new image formats come along very rarely and popular ones &#8211; like PNG, JPEG and WebP &#8211; are supported basically forever.</p>



<p>You do have to be a little careful, however, to not over-compress your original uploads (or, retain higher-quality versions locally).  Especially if you&#8217;re churning through image conversions &amp; uploads rapidly, you might not immediately notice compression artefacts.  Having the original available in order to redo the compression is valuable.</p>



<h2 class="wp-block-heading">Advance conversion</h2>



<p>There&#8217;s a further choice as to whether you do the processing locally or use a website.  I prefer to just do it locally, for some of the same reasons as not using WordPress, and <em>also</em> because in my experience I can achieve better results &#8211; websites that do image conversion / recompression, <em>especially</em> those that do it for free, use weak compression in order to minimise processing time, and consequently do not achieve the best results.</p>



<p>The best tool I&#8217;ve found for local image processing is <a href="https://apps.apple.com/us/app/image-tool/id1524216218" data-wpel-link="external" target="_blank" rel="external noopener">Image Tool+</a>.  It supports all the formats you&#8217;ll likely need and is designed for easy batch conversions.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>⚠️ While Image Tool+ <em>mostly</em> supports animated images, it does <em>not</em> support animated AVIFs.  It will silently remove animation from any AVIFs it creates.</p>



<p>Beyond that, though, I&#8217;ve found that it does a surprisingly poor job of producing animated images, no matter what format you use &#8211; the resulting files are <em>huge</em>.  You can get <em>much</em> better results with other tools (e.g. <a href="https://ezgif.com/gif-to-avif" data-wpel-link="external" target="_blank" rel="external noopener">EZGIF</a> albeit with no control over the quality).</p>
</div></div>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1277" height="832" src="https://wadetregaskis.com/wp-content/uploads/2023/12/Image-Tool.webp" alt="Screenshot of the Image Tool+ main window, showing a bunch of image format conversions in progress" class="wp-image-7149" srcset="https://wadetregaskis.com/wp-content/uploads/2023/12/Image-Tool.webp 1277w, https://wadetregaskis.com/wp-content/uploads/2023/12/Image-Tool-256x167.webp 256w, https://wadetregaskis.com/wp-content/uploads/2023/12/Image-Tool-512x334@2x.webp 1024w, https://wadetregaskis.com/wp-content/uploads/2023/12/Image-Tool-512x334.webp 512w, https://wadetregaskis.com/wp-content/uploads/2023/12/Image-Tool@2x.webp 2554w" sizes="auto, (max-width: 1277px) 100vw, 1277px" /><figcaption class="wp-element-caption">The user interface is a little clunky &#8211; with some odd but harmless rendering issues occasionally, as seen in the left hand column &#8211; but ultimately it&#8217;s functional, and does a better job than any other apps I&#8217;ve found.</figcaption></figure>
</div>


<p>In years gone by I used <a href="https://www.lemkesoft.de/en/products/graphicconverter/" data-wpel-link="external" target="_blank" rel="external noopener">GraphicConverter</a>, but it doesn&#8217;t support AVIF.</p>



<p>It&#8217;s important to note that the PNG files produced by Image Tool+ are not very efficiently compressed &#8211; you should run them through ImageOptim if you actually intend to use them.</p>



<h1 class="wp-block-heading">Publishing images in WordPress</h1>



<p>WordPress supports <em>some</em> image formats natively, and does a passable job of handling them by default.  So you don&#8217;t <em>have</em> to do anything additional, necessarily.  However, there&#8217;s several plug-ins I recommend which significantly improve its capabilities.</p>



<h2 class="wp-block-heading" id="avif-support"><a href="https://wordpress.org/plugins/avif-support/" data-wpel-link="external" target="_blank" rel="external noopener">AVIF Support</a></h2>



<p>As it says on the tin, it adds support for AVIF to WordPress, which sadly lacks support otherwise.  Without this plug-in, WordPress will simply refuse to accept AVIF uploads.</p>



<p>It&#8217;s free, <a href="https://plugins.trac.wordpress.org/browser/avif-support/" data-wpel-link="external" target="_blank" rel="external noopener">open source</a>, and works pretty seamlessly, as long as your web server and WordPress installation are sufficiently modern.  The easiest way to determine if that&#8217;s the case it to simply install the plug-in &#8211; its Settings page will tell you if AVIF is supported or not.  If it&#8217;s not, only then will you need to do some package upgrading.  In particular, you need (as best I can tell from experience and what I&#8217;ve read online):</p>



<ul class="wp-block-list">
<li><a href="https://grandplugins.com/how-to-upload-avif-images-in-wordpress/" data-wpel-link="external" target="_blank" rel="external noopener">PHP 8.2 (or later)</a>.</li>



<li><a href="https://php.watch/versions/8.1/gd-avif" data-wpel-link="external" target="_blank" rel="external noopener">libavif and suitably-compiled versions of GD and PHP</a>.</li>



<li><a href="https://web.archive.org/web/20240104012918/https://avif.io/blog/tutorials/imagemagick/" data-wpel-link="external" target="_blank" rel="external noopener">ImageMagick 7.0.25 (or later)</a><br><br>…<em>or</em> <a href="https://legacy.imagemagick.org" data-wpel-link="external" target="_blank" rel="external noopener">ImageMagick 6</a> with a suitably new and compiled <a href="https://github.com/strukturag/libheif" data-wpel-link="external" target="_blank" rel="external noopener">libheif</a> w/ AVIF support.  I haven&#8217;t been able to pin down specific version numbers, and it might vary by package repo (as libheif has to be compiled with certain flags to include the AVIF support).  In a nutshell you can tell if your system has it by following the dependency graph from your libmagickcore-6… library (via <code>apt info &lt;package></code> or similar) and seeing if it includes libheifN with in turn an [installed] dependency on svt-av1 or libaomN.  Where &#8216;N&#8217; may be any number.<br><br>Note: ImageMagick 6 doesn&#8217;t support alpha channels in AVIF images (see <a href="#avif-bug-alpha-channels">Bug: Alpha channels</a>, below).  So use ImageMagick 7 if possible.</li>
</ul>



<p>Even though all those versions are quite old, <a href="https://wadetregaskis.com/migrated-hosts-out-of-the-subdomain/" data-wpel-link="internal">I actually had to switch web hosts in order to get them</a>, as you&#8217;d be sadly surprised how many WordPress hosting services are running wildly ancient versions of Linux (kernel &amp; distro) and PHP.  Now that I have my own Linux VPS, however, it was quite trivial &#8211; I basically just used <code>apt</code> and <a href="https://www.plesk.com" data-wpel-link="external" target="_blank" rel="external noopener">Plesk</a> to install the latest versions of all the relevant software (which you should be frequently doing anyway, for security patches and bug fixes).  Although sadly that still only gets me ImageMagick 6 (and an outdated GD that doesn&#8217;t support AVIF) &#8211; Plesk PHP builds are pretty far behind, and Ubuntu doesn&#8217;t officially support ImageMagick 7 at all! 😠</p>



<p>Note that you only need <em>one</em> of GD or ImageMagick to have the necessary AVIF support, which is fortunate in case one of the two is difficult to upgrade with your particular system (such as if you use Plesk).</p>



<p>ImageMagick is apparently superior:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>It&#8217;s recommended though to use Imagick lib. It gives way better results in terms of speed, quality and size.</p>
<cite>AVIF Support author (<a href="https://grandplugins.com" data-wpel-link="external" target="_blank" rel="external noopener">GrandPlugins</a>)</cite></blockquote>



<p>I haven&#8217;t tested that claim myself, but you&#8217;ll find it repeated in numerous places online, and not just in the context of AVIF.  It sounds like the common wisdom is that GD just isn&#8217;t very good<sup data-fn="416556d8-6d2c-4a41-9401-e301838a8bb7" class="fn"><a href="#416556d8-6d2c-4a41-9401-e301838a8bb7" id="416556d8-6d2c-4a41-9401-e301838a8bb7-link">3</a></sup>.</p>



<h3 class="wp-block-heading"><s>Bug: AVIF over-compression</s></h3>



<p><s>At time of writing, it has a bug whereby it doesn&#8217;t respect image quality settings, neither the WordPress defaults nor any customisation you might do with other plug-ins.  Instead, it always uses a 30% quality setting for AVIF compression (e.g. for thumbnails) which often results in noticeable visual artefacts from the over-compression.  I&#8217;ve reported this to the developer and 🤞 they address this soon, as it looks like a trivial one-line patch.</s></p>



<p><em>Update</em>: the plug-in author fixed this in version 1.0.5, January 2nd 2024.  As a bonus they also added plug-in settings for specifying the desired image quality and (for GD) &#8216;speed&#8217; (how hard the AVIF encoder tries, trading off compression speed for effectiveness).</p>



<h3 class="wp-block-heading" id="avif-bug-alpha-channels">Bug: Alpha channels</h3>



<p><a href="https://alexwlchan.net/2023/check-for-transparency/" data-wpel-link="external" target="_blank" rel="external noopener">AVIFs with alpha channels don&#8217;t work with ImageMagick 6</a>, at time of writing &#8211; any time WordPress re-encodes the image, such as for thumbnail generation, it removes the alpha channel.  Typically this results in the transparent sections turning black instead.</p>



<p><s>For now, I use WebP when transparency is involved, as I&#8217;m hamstrung by Plesk&#8217;s lack of support for ImageMagick 7 in their PHP packages. 😕</s></p>



<p>I ultimately upgraded to ImageMagick 7 to fix this bug.  See <a href="https://wadetregaskis.com/how-to-install-imagemagick-7-for-wordpress-under-plesk-obsidian-on-ubuntu-22-04/" data-wpel-link="internal">my upgrade guide</a> if you&#8217;re on Ubuntu or using Plesk, or otherwise can&#8217;t seem to get ImageMagick 7 from your normal package management process.</p>



<h3 class="wp-block-heading">CDN incompatibility</h3>



<p>Note that some CDNs <em>still</em> don&#8217;t support AVIF, and there&#8217;s nothing the AVIF Support plug-in can do about that.  You might be using such a CDN without even realising it &#8211; e.g. the popular <a href="https://wordpress.org/plugins/jetpack/" data-wpel-link="external" target="_blank" rel="external noopener">Jetpack</a> plug-in secretly uses the wp.com CDN, so AVIF images don&#8217;t work in some places, such as Jetpack&#8217;s &#8220;Related posts&#8221; feature.  Alas there&#8217;s not much you can do other than not use such broken CDNs &#8211; nor plug-ins that rely on them, like Jetpack.</p>



<p>In general you don&#8217;t want to use such CDNs anyway, because their lack of support for AVIF &#8211; or any other content format &#8211; is usually only a result of them mucking with your images in some way.  Not all CDNs are bad, though &#8211; e.g. <a href="https://www.cloudflare.com/" data-wpel-link="external" target="_blank" rel="external noopener">Cloudflare</a> has no issues with AVIF because they don&#8217;t try to mess with your files.</p>



<h2 class="wp-block-heading" id="enable-media-replace"><a href="https://wordpress.org/plugins/enable-media-replace/" data-wpel-link="external" target="_blank" rel="external noopener">Enable Media Replace</a></h2>



<p>This lets you replace an existing image without breaking any use of it in existing posts, pages, etc.  <em>Including with a different file format</em>.  That latter detail is something most competing image replacement plug-ins do <em>not</em> support [reliably], but it&#8217;s crucial if you want to upgrade any existing images on your website.</p>



<p>It&#8217;s free and <a href="https://github.com/short-pixel-optimizer/enable-media-replace" data-wpel-link="external" target="_blank" rel="external noopener">open source</a>.</p>



<h2 class="wp-block-heading" id="image-regenerate-and-select-crop"><a href="https://iuliacazan.ro/image-regenerate-select-crop/" data-wpel-link="external" target="_blank" rel="external noopener">Image Regenerate &amp; Select Crop</a></h2>



<p>This lets you perform a bunch of helpful little things, including:</p>



<ul class="wp-block-list">
<li>You can define custom image &#8220;thumbnail&#8221; sizes (and modify WordPress&#8217;s defaults), which can be handy for tailoring your images to your actual presentation sizes.  The WordPress defaults are pretty arbitrary and typically don&#8217;t match what your theme desires, let-alone any other plug-ins&#8217; needs (such as for galleries, slideshows, etc).</li>



<li>You can adjust the default image compression quality setting (for each thumbnail size individually).<br><br>Note however that this doesn&#8217;t always apply, e.g. currently the <a href="#avif-support">AVIF Support</a> plug-in has a bug whereby it ignores this setting.  The setting seems to work for the built-in-supported image formats, though, like JPEG and WebP.</li>



<li>You can manually delete or [re]generate thumbnails in each of the possible sizes, for images both individually or en masse.  This can be handy if you change relevant settings (like compression quality, above) or in debugging situations (such as if you&#8217;re experimenting with other image plug-ins and they muck up your thumbnails).</li>
</ul>



<p>It&#8217;s free (albeit with optional paid &#8216;advanced&#8217; features).</p>



<h2 class="wp-block-heading"><a href="https://meowapps.com/wp-retina-2x/" data-wpel-link="external" target="_blank" rel="external noopener">Perfect Images</a></h2>



<p>It&#8217;s baffling that <em>to this day</em> WordPress doesn&#8217;t support modern displays &#8211; and I use &#8216;modern&#8217; in a very liberal sense, since we&#8217;re talking circa 2010 onwards.  That is, displays designed with a high pixel density that use a non-1-to-1 ratio of points to pixels.  i.e. any smartphone or quality computer display in the last decade.</p>



<p>The result is images that render at a quarter (or worse) of full resolution, looking pixelated or soft.</p>



<p>If you care enough about image quality to even consider what image formats or compression settings you&#8217;re using, you absolutely should be using Perfect Images.</p>



<p>Basically it fixes &amp; extends WordPress&#8217;s image support to &#8211; mostly automagically and invisibly &#8211; just work with full-resolution images.  It&#8217;s particularly noticeable and essential for things like screenshots, or any images containing text, as those are especially obvious if rendered incorrectly.</p>



<p>Its core functionality is free &#8211; and you can get a fair way with just the free version &#8211; but <a href="https://meowapps.com/products/wp-retina-2x-pro/" data-wpel-link="external" target="_blank" rel="external noopener">the pro version</a> is worth it, even if a bit pricey.</p>



<h3 class="wp-block-heading">Usage tips</h3>



<h4 class="wp-block-heading">Fix WordPress&#8217;s image size limit</h4>



<p>Make sure to check the &#8220;Image Threshold … Disable&#8221; checkbox in the General settings.  This fixes a truly obnoxious WordPress bug whereby it mangles uploads of large images.</p>



<h4 class="wp-block-heading">Upload &#8216;Retina&#8217; images specially</h4>



<p>For images which are explicitly &#8220;2x&#8221; &#8211; such as screenshots &#8211; it&#8217;s important to upload them &#8216;manually&#8217; using Media &gt; Add New Media File, as opposed to e.g. just drag-and-dropping them into a post.  The plug-in adds an &#8220;Upload New Retina Image&#8221; option to the dedicated upload page, which registers the uploaded image with WordPress at technically a quarter of its actual size.  That basically tricks WordPress into using the <em>correct</em> dimensions for the image &#8211; otherwise, WordPress mistakes the <em>pixel</em> dimensions for the <em>point</em> dimensions, and will naively try to display the image at four times its natural size.</p>



<p>Don&#8217;t worry, Perfect Images preserves the real, full-resolution version as the &#8220;2x&#8221; version of the upload, and will display that instead to your visitors.</p>



<p>This particular upload method is the <em>only</em> way to get images to render correctly in all cases.  In some places &#8211; e.g. embedded in posts &#8211; you can manually specify the correct image dimensions, but that&#8217;s both tedious and error-prone, and not possible in all uses.</p>



<p>The only caveat is that those images will look ugly and blurry in the post &amp; page editors.  Don&#8217;t worry, they&#8217;ll render correctly in the actual published (and preview) version of your posts &amp; pages.</p>



<p>For images which don&#8217;t have an intrinsic pixel density &#8211; such as photos &#8211; you typically don&#8217;t have to worry about this, as you&#8217;ll usually just let them render at whatever size the viewport &amp; layout permit, up to their natural full size.</p>



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



<p>The core functionality works reliably in my experience (over <em>many</em> years now), but some of the peripheral features are broken or notably buggy:</p>



<ul class="wp-block-list">
<li>Image replacement will <em>not</em> work correctly if the file format changes &#8211; and it won&#8217;t even recognise this case and prevent it, instead leaving you with broken images.  Use <a href="#enable-media-replace">Enable Media Replace</a> for this instead.</li>



<li>Batch regeneration of thumbnails doesn&#8217;t work reliably &#8211; it will silently fail midway if an error occurs, such as it encountering a video file.  Use <a href="#image-regenerate-and-select-crop">Image Regenerate &amp; Select Crop</a> for this instead.</li>



<li>Custom Image Sizes don&#8217;t work correctly &#8211; they <em>always</em> force cropping of the image (&#8220;resize to fill&#8221;), rather than resizing to fit.  This is almost never what you want.  Use <a href="#image-regenerate-and-select-crop">Image Regenerate &amp; Select Crop</a> for this instead.</li>



<li>For the Pro version [only], auto-update doesn&#8217;t work.  You have to periodically check the website for new versions.</li>



<li>It sometimes reports &#8220;issues&#8221; for images, in its special dashboard under the Media section, when there aren&#8217;t any.  This is of course pretty insignificant &#8211; you can just ignore it &#8211; although it might hide any <em>actual</em> issues if they occur.  I did find a way to clear these bogus issues, but it involved manually deleting all thumbnails for all my images and regenerating them, which was a bit of an ordeal due to other, aforementioned bugs.  So I don&#8217;t recommend mucking with it.</li>
</ul>


<ol class="wp-block-footnotes"><li id="ce19f9a1-4b3d-427c-b205-ec8684b1e5ae">Note that <a href="https://nokiatech.github.io/heif/technical.html" data-wpel-link="external" target="_blank" rel="external noopener">HEIF <em>does</em> support lossless encoding</a> but support for this amongst actual encoding applications is poor to non-existent.  As such it&#8217;s generally not a suitable replacement for PNG in practice. <a href="#ce19f9a1-4b3d-427c-b205-ec8684b1e5ae-link" aria-label="Jump to footnote reference 1">↩︎</a></li><li id="42530c0c-bed1-4a7a-bff8-2c27f0a337bd">As the matrix shows, no version of <a href="https://en.wikipedia.org/wiki/Internet_Explorer" data-wpel-link="external" target="_blank" rel="external noopener">Internet Explorer</a> supports WebP.  But &#8211; though there apparently <em>is</em> some non-trivial number of people still using it &#8211; I don&#8217;t consider Internet Explorer a browser of concern anymore, given it has an official and straightforward successor in <a href="https://en.wikipedia.org/wiki/Microsoft_Edge" data-wpel-link="external" target="_blank" rel="external noopener">Edge</a>, and <a href="https://en.wikipedia.org/wiki/Internet_Explorer#Internet_Explorer_11" data-wpel-link="external" target="_blank" rel="external noopener">its last version was released <em>a decade ago</em> in 2013</a>. <a href="#42530c0c-bed1-4a7a-bff8-2c27f0a337bd-link" aria-label="Jump to footnote reference 2">↩︎</a></li><li id="416556d8-6d2c-4a41-9401-e301838a8bb7">Although for image compression, specifically, this is odd, since there&#8217;s only a couple of underlying libraries they could possibly be using, and while <a href="https://github.com/strukturag/libheif/wiki/AVIF-Encoder-Benchmark" data-wpel-link="external" target="_blank" rel="external noopener">they do vary significantly in speed</a> they should produce very similar outputs. <a href="#416556d8-6d2c-4a41-9401-e301838a8bb7-link" aria-label="Jump to footnote reference 3">↩︎</a></li></ol>]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/image-workflow-for-wordpress/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2023/12/Image-Tool.webp" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">7146</post-id>	</item>
		<item>
		<title>Secret Sonoma design lead: Barbie</title>
		<link>https://wadetregaskis.com/secret-sonoma-design-lead-barbie/</link>
					<comments>https://wadetregaskis.com/secret-sonoma-design-lead-barbie/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Tue, 12 Dec 2023 21:13:50 +0000</pubDate>
				<category><![CDATA[Howto]]></category>
		<category><![CDATA[Ramblings]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[CGContextHighlight2xScaledImages]]></category>
		<category><![CDATA[macOS Sonoma]]></category>
		<category><![CDATA[pink]]></category>
		<category><![CDATA[Retina]]></category>
		<category><![CDATA[Snafu]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=6579</guid>

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



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



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



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



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


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

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

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

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


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



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


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


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



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



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



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



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



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



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



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



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



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



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



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



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



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


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


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



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



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



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



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



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



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



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



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



<p>The morale of this is, as always, that filing bug reports with Apple is an infuriating waste of time.  Mea culpa.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/secret-sonoma-design-lead-barbie/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2023/12/Quartz-Debug-1.webp" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">6579</post-id>	</item>
	</channel>
</rss>
