<?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>Broken by design &#8211; Wade Tregaskis</title>
	<atom:link href="https://wadetregaskis.com/tags/broken-by-design/feed/" rel="self" type="application/rss+xml" />
	<link>https://wadetregaskis.com</link>
	<description></description>
	<lastBuildDate>Tue, 04 Feb 2025 16:11:10 +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>Broken by design &#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>Backblaze seemingly does not support files greater than 1 TB</title>
		<link>https://wadetregaskis.com/backblaze-seemingly-does-not-support-files-greater-than-1-tb/</link>
					<comments>https://wadetregaskis.com/backblaze-seemingly-does-not-support-files-greater-than-1-tb/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Thu, 02 Jan 2025 23:27:59 +0000</pubDate>
				<category><![CDATA[Ramblings]]></category>
		<category><![CDATA[Backblaze]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[Undocumented]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8475</guid>

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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/backblaze-seemingly-does-not-support-files-greater-than-1-tb/feed/</wfw:commentRss>
			<slash:comments>4</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2025/01/backblazes-marketing-claims.avif" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">8475</post-id>	</item>
		<item>
		<title>NSCopyObject, the griefer that keeps on griefing</title>
		<link>https://wadetregaskis.com/nscopyobject-the-griefer-that-keeps-on-griefing/</link>
					<comments>https://wadetregaskis.com/nscopyobject-the-griefer-that-keeps-on-griefing/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Tue, 16 Jul 2024 00:42:07 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[ARC]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[copy(with:)]]></category>
		<category><![CDATA[fixupCopiedIvars]]></category>
		<category><![CDATA[NeXT]]></category>
		<category><![CDATA[NSAnimation]]></category>
		<category><![CDATA[NSCell]]></category>
		<category><![CDATA[NSCopying]]></category>
		<category><![CDATA[NSImageRep]]></category>
		<category><![CDATA[Objective-C]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[Swift]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8281</guid>

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



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



<p>Its implementation <em>currently</em> starts with essentially:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="id NSCopyObject(id object, NSUInteger extraBytes, NSZone *zone) {
    if (nil == object) {
        return nil;
    }
    
    id copy = object_copy(object, extraBytes);
    object_setClass(copy, objc_opt_class(object));
    return copy;
}" style="color:#000000;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">id</span><span style="color: #000000"> </span><span style="color: #795E26">NSCopyObject</span><span style="color: #000000">(</span><span style="color: #0000FF">id</span><span style="color: #000000"> object, </span><span style="color: #267F99">NSUInteger</span><span style="color: #000000"> extraBytes, </span><span style="color: #267F99">NSZone</span><span style="color: #000000"> *zone) {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #AF00DB">if</span><span style="color: #000000"> (</span><span style="color: #0000FF">nil</span><span style="color: #000000"> == object) {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #AF00DB">return</span><span style="color: #000000"> </span><span style="color: #0000FF">nil</span><span style="color: #000000">;</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">    </span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">id</span><span style="color: #000000"> copy = </span><span style="color: #795E26">object_copy</span><span style="color: #000000">(object, extraBytes);</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">object_setClass</span><span style="color: #000000">(copy, </span><span style="color: #795E26">objc_opt_class</span><span style="color: #000000">(object));</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #AF00DB">return</span><span style="color: #000000"> copy;</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



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



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



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



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



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



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


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


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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



<p>It appears that the best you can do is <em>assume</em> the superclass will always use <code>NSCopyObject</code>, if it does currently, and just manually increment the retain count.  Like Objective-C with ARC, the language &amp; standard library really don&#8217;t want you to actually do this, but at least in Swift it&#8217;s relatively straightforward:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="override func copy(with zone: NSZone? = nil) -&gt; Any {
    let result = super.copy(with: zone)
    
    if result.myProperty === self.myProperty {
        _ = Unmanaged.passRetained(myProperty)
    } else {
        result.myProperty = self.myProperty
    }
}" style="color:#000000;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">override</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">copy</span><span style="color: #000000">(</span><span style="color: #795E26">with</span><span style="color: #000000"> </span><span style="color: #001080">zone</span><span style="color: #000000">: NSZone? = </span><span style="color: #0000FF">nil</span><span style="color: #000000">) -&gt; </span><span style="color: #267F99">Any</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">let</span><span style="color: #000000"> result = </span><span style="color: #0000FF">super</span><span style="color: #000000">.</span><span style="color: #795E26">copy</span><span style="color: #000000">(</span><span style="color: #795E26">with</span><span style="color: #000000">: zone)</span></span>
<span class="line"><span style="color: #000000">    </span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #AF00DB">if</span><span style="color: #000000"> result.</span><span style="color: #001080">myProperty</span><span style="color: #000000"> === </span><span style="color: #0000FF">self</span><span style="color: #000000">.</span><span style="color: #001080">myProperty</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #001080">_</span><span style="color: #000000"> = </span><span style="color: #267F99">Unmanaged</span><span style="color: #000000">.</span><span style="color: #795E26">passRetained</span><span style="color: #000000">(myProperty)</span></span>
<span class="line"><span style="color: #000000">    } </span><span style="color: #AF00DB">else</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">        result.</span><span style="color: #001080">myProperty</span><span style="color: #000000"> = </span><span style="color: #0000FF">self</span><span style="color: #000000">.</span><span style="color: #001080">myProperty</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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


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

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



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



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



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



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



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



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



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



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

			<media:content url="https://wadetregaskis.com/wp-content/uploads/2024/02/Proactive-Peek.avif" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">7692</post-id>	</item>
		<item>
		<title>SwiftUI drag &#038; drop does not support file promises</title>
		<link>https://wadetregaskis.com/swiftui-drag-drop-does-not-support-file-promises/</link>
					<comments>https://wadetregaskis.com/swiftui-drag-drop-does-not-support-file-promises/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Sun, 04 Feb 2024 19:04:31 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Howto]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[Drag & drop]]></category>
		<category><![CDATA[NSFilePromiseProvider]]></category>
		<category><![CDATA[NSItemProvider]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[Snafu]]></category>
		<category><![CDATA[Swift]]></category>
		<category><![CDATA[SwiftUI]]></category>
		<category><![CDATA[Transferable]]></category>
		<category><![CDATA[Undocumented]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=7646</guid>

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



<p>FB13583826.</p>



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



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



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



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



<p>The closest you can get is to write data into some arbitrary location that you have to choose blindly (not having any idea where the actual destination is), and then rely on the receiving app to move or copy the file from there.  It doesn&#8217;t matter, in this regard, whether you use <a href="https://developer.apple.com/documentation/swiftui/drag-and-drop#moving-transferable-items" data-wpel-link="external" target="_blank" rel="external noopener">the <code>Transferable</code>-based APIs</a> or <a href="https://developer.apple.com/documentation/swiftui/drag-and-drop#moving-items-using-item-providers" data-wpel-link="external" target="_blank" rel="external noopener">the <code>NSItemProvider</code>-based APIs</a>.</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #000000">view.</span><span style="color: #001080">onDrag</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">let</span><span style="color: #000000"> provider = </span><span style="color: #795E26">NSItemProvider</span><span style="color: #000000">()</span></span>
<span class="line"><span style="color: #000000">    </span></span>
<span class="line"><span style="color: #000000">    provider.</span><span style="color: #795E26">registerDataRepresentation</span><span style="color: #000000">(</span><span style="color: #795E26">for</span><span style="color: #000000">: UTType.</span><span style="color: #001080">fileURL</span><span style="color: #000000">) {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #AF00DB">do</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #0000FF">let</span><span style="color: #000000"> tmpDir = </span><span style="color: #AF00DB">try</span><span style="color: #000000"> FileManager.</span><span style="color: #001080">default</span><span style="color: #000000">.</span><span style="color: #001080">url</span><span style="color: #000000">(</span><span style="color: #795E26">for</span><span style="color: #000000">: .</span><span style="color: #001080">itemReplacementDirectory</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                                     </span><span style="color: #795E26">in</span><span style="color: #000000">: .</span><span style="color: #001080">userDomainMask</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                                     </span><span style="color: #795E26">appropriateFor</span><span style="color: #000000">: URL.</span><span style="color: #001080">temporaryDirectory</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                                     </span><span style="color: #795E26">create</span><span style="color: #000000">: </span><span style="color: #0000FF">true</span><span style="color: #000000">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #008000">// You&#39;ll need to provide the `suggestedFileName` based on the particulars of your use-case.</span></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #0000FF">let</span><span style="color: #000000"> file = tmpDir.</span><span style="color: #795E26">appending</span><span style="color: #000000">(</span><span style="color: #795E26">component</span><span style="color: #000000">: suggestedFileName)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #AF00DB">try</span><span style="color: #000000"> bytes.</span><span style="color: #795E26">write</span><span style="color: #000000">(</span><span style="color: #795E26">to</span><span style="color: #000000">: file, </span><span style="color: #795E26">options</span><span style="color: #000000">: .</span><span style="color: #001080">withoutOverwriting</span><span style="color: #000000">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #0000FF">$0</span><span style="color: #000000">(file.</span><span style="color: #001080">dataRepresentation</span><span style="color: #000000">, </span><span style="color: #0000FF">nil</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">        } </span><span style="color: #AF00DB">catch</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #0000FF">$0</span><span style="color: #000000">(</span><span style="color: #0000FF">nil</span><span style="color: #000000">, error)</span></span>
<span class="line"><span style="color: #000000">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #AF00DB">return</span><span style="color: #000000"> </span><span style="color: #0000FF">nil</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">    </span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #AF00DB">return</span><span style="color: #000000"> provider</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



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



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



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



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



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



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



<p>Also, I found that the newer and ostensibly better <code><a href="https://developer.apple.com/documentation/coretransferable/transferable" data-wpel-link="external" target="_blank" rel="external noopener">Transferable</a></code>-based API was harder to understand and harder to use than the <code><a href="https://developer.apple.com/documentation/foundation/nsitemprovider/" data-wpel-link="external" target="_blank" rel="external noopener">NSItemProvider</a></code>-based APIs.  I recommend going straight to, and sticking with, the latter.  Your code will be simpler and more reliable.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/swiftui-drag-drop-does-not-support-file-promises/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">7646</post-id>	</item>
		<item>
		<title>Bad API example: FileManager&#8217;s url(for:in:appropriateFor:create:)</title>
		<link>https://wadetregaskis.com/bad-api-example-filemanagers-urlforinappropriateforcreate/</link>
					<comments>https://wadetregaskis.com/bad-api-example-filemanagers-urlforinappropriateforcreate/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Wed, 31 Jan 2024 20:40:07 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[FileManager]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[Swift]]></category>
		<category><![CDATA[Undocumented]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=7631</guid>

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



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



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



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



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary>Test driver</summary>
<p>You can run this in a Swift playground, or as CLI app, but to observe the behaviour inside an <a href="https://developer.apple.com/documentation/security/app_sandbox" data-wpel-link="external" target="_blank" rel="external noopener">App Sandbox</a> it&#8217;s easiest to create a new GUI app in Xcode and just dump this into the @main <code>App</code> struct&#8217;s <code>init</code> method.</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> fm = FileManager.</span><span style="color: #001080">default</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> searchPathDirectories = [(</span><span style="color: #A31515">&quot;applicationDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">applicationDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;demoApplicationDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">demoApplicationDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;developerApplicationDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">developerApplicationDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;adminApplicationDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">adminApplicationDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;libraryDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">libraryDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;developerDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">developerDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;userDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">userDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;documentationDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">documentationDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;documentDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">documentDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;coreServiceDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">coreServiceDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;autosavedInformationDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">autosavedInformationDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;desktopDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">desktopDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;cachesDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">cachesDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;applicationSupportDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">applicationSupportDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;downloadsDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">downloadsDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;inputMethodsDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">inputMethodsDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;moviesDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">moviesDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;musicDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">musicDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;picturesDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">picturesDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;printerDescriptionDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">printerDescriptionDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;sharedPublicDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">sharedPublicDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;preferencePanesDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">preferencePanesDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;applicationScriptsDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">applicationScriptsDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;itemReplacementDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">itemReplacementDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;allApplicationsDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">allApplicationsDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;allLibrariesDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">allLibrariesDirectory</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;trashDirectory&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDirectory</span><span style="color: #000000">.</span><span style="color: #001080">trashDirectory</span><span style="color: #000000">)]</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> searchPathDomainMasks = [(</span><span style="color: #A31515">&quot;userDomainMask&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDomainMask</span><span style="color: #000000">.</span><span style="color: #001080">userDomainMask</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;localDomainMask&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDomainMask</span><span style="color: #000000">.</span><span style="color: #001080">localDomainMask</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;systemDomainMask&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDomainMask</span><span style="color: #000000">.</span><span style="color: #001080">systemDomainMask</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             (</span><span style="color: #A31515">&quot;networkDomainMask&quot;</span><span style="color: #000000">, FileManager.</span><span style="color: #001080">SearchPathDomainMask</span><span style="color: #000000">.</span><span style="color: #001080">networkDomainMask</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                             </span><span style="color: #008000">/*(&quot;allDomainsMask&quot;, FileManager.SearchPathDomainMask.allDomainsMask)*/</span><span style="color: #000000">]</span></span>
<span class="line"></span>
<span class="line"><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;urls(for:in:):&quot;</span><span style="color: #000000">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #AF00DB">for</span><span style="color: #000000"> (dirName, dir) </span><span style="color: #AF00DB">in</span><span style="color: #000000"> searchPathDirectories {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;</span><span style="color: #EE0000">\n</span><span style="color: #0000FF">\(</span><span style="color: #000000FF">dirName</span><span style="color: #0000FF">)</span><span style="color: #A31515">:&quot;</span><span style="color: #000000">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #AF00DB">for</span><span style="color: #000000"> (domainName, domain) </span><span style="color: #AF00DB">in</span><span style="color: #000000"> searchPathDomainMasks {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #0000FF">let</span><span style="color: #000000"> dirs = fm.</span><span style="color: #795E26">urls</span><span style="color: #000000">(</span><span style="color: #795E26">for</span><span style="color: #000000">: dir, </span><span style="color: #795E26">in</span><span style="color: #000000">: domain)</span></span>
<span class="line"><span style="color: #000000">            .</span><span style="color: #795E26">map</span><span style="color: #000000"> { </span><span style="color: #0000FF">$0</span><span style="color: #000000">.</span><span style="color: #795E26">path</span><span style="color: #000000">(</span><span style="color: #795E26">percentEncoded</span><span style="color: #000000">: </span><span style="color: #0000FF">false</span><span style="color: #000000">) }</span></span>
<span class="line"><span style="color: #000000">            .</span><span style="color: #795E26">joined</span><span style="color: #000000">(</span><span style="color: #795E26">separator</span><span style="color: #000000">: </span><span style="color: #A31515">&quot;</span><span style="color: #EE0000">\n</span><span style="color: #A31515">&quot;</span><span style="color: #000000"> + </span><span style="color: #267F99">String</span><span style="color: #000000">(</span><span style="color: #795E26">repeating</span><span style="color: #000000">: </span><span style="color: #A31515">&quot; &quot;</span><span style="color: #000000">, </span><span style="color: #795E26">count</span><span style="color: #000000">: </span><span style="color: #098658">23</span><span style="color: #000000">))</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;    </span><span style="color: #0000FF">\(</span><span style="color: #000000FF">domainName</span><span style="color: #0000FF">)</span><span style="color: #A31515">: </span><span style="color: #0000FF">\(</span><span style="color: #267F99">String</span><span style="color: #000000FF">(</span><span style="color: #795E26">repeating</span><span style="color: #000000FF">: </span><span style="color: #A31515">&quot; &quot;</span><span style="color: #000000FF">, </span><span style="color: #795E26">count</span><span style="color: #000000FF">: </span><span style="color: #098658">17</span><span style="color: #000000FF"> </span><span style="color: #000000">-</span><span style="color: #000000FF"> domainName.</span><span style="color: #001080">count</span><span style="color: #000000FF">)</span><span style="color: #0000FF">)\(</span><span style="color: #000000FF">dirs</span><span style="color: #0000FF">)</span><span style="color: #A31515">&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;</span><span style="color: #EE0000">\n\n</span><span style="color: #A31515">url(for:in:appropriateFor:create:):&quot;</span><span style="color: #000000">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> paths = [</span><span style="color: #0000FF">nil</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">             </span><span style="color: #795E26">URL</span><span style="color: #000000">(</span><span style="color: #795E26">filePath</span><span style="color: #000000">: </span><span style="color: #A31515">&quot;/&quot;</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">             FileManager.</span><span style="color: #001080">default</span><span style="color: #000000">.</span><span style="color: #001080">temporaryDirectory</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">             </span><span style="color: #795E26">URL</span><span style="color: #000000">(</span><span style="color: #795E26">filePath</span><span style="color: #000000">: </span><span style="color: #A31515">&quot;/Volumes/Flash/&quot;</span><span style="color: #000000">)]</span></span>
<span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> pathDesc: (URL?) -&gt; </span><span style="color: #267F99">String</span><span style="color: #000000"> = { </span><span style="color: #0000FF">$0</span><span style="color: #000000">?.</span><span style="color: #795E26">path</span><span style="color: #000000">(</span><span style="color: #795E26">percentEncoded</span><span style="color: #000000">: </span><span style="color: #0000FF">false</span><span style="color: #000000">) ?? </span><span style="color: #A31515">&quot;nil&quot;</span><span style="color: #000000"> }</span></span>
<span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> maxPathLength = paths.</span><span style="color: #795E26">map</span><span style="color: #000000"> { </span><span style="color: #795E26">pathDesc</span><span style="color: #000000">(</span><span style="color: #0000FF">$0</span><span style="color: #000000">).</span><span style="color: #001080">count</span><span style="color: #000000"> }.</span><span style="color: #795E26">max</span><span style="color: #000000">() ?? </span><span style="color: #098658">0</span></span>
<span class="line"></span>
<span class="line"><span style="color: #AF00DB">for</span><span style="color: #000000"> (dirName, dir) </span><span style="color: #AF00DB">in</span><span style="color: #000000"> searchPathDirectories {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;</span><span style="color: #EE0000">\n</span><span style="color: #0000FF">\(</span><span style="color: #000000FF">dirName</span><span style="color: #0000FF">)</span><span style="color: #A31515">:&quot;</span><span style="color: #000000">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #AF00DB">for</span><span style="color: #000000"> (domainName, domain) </span><span style="color: #AF00DB">in</span><span style="color: #000000"> searchPathDomainMasks {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #0000FF">var</span><span style="color: #000000"> results = [</span><span style="color: #267F99">String</span><span style="color: #000000">: </span><span style="color: #267F99">String</span><span style="color: #000000">]()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #AF00DB">for</span><span style="color: #000000"> appropriateForPath </span><span style="color: #AF00DB">in</span><span style="color: #000000"> paths {</span></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #0000FF">let</span><span style="color: #000000"> path: </span><span style="color: #267F99">String</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #AF00DB">do</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">                </span><span style="color: #0000FF">let</span><span style="color: #000000"> folderURL = </span><span style="color: #AF00DB">try</span><span style="color: #000000"> FileManager.</span><span style="color: #001080">default</span><span style="color: #000000">.</span><span style="color: #001080">url</span><span style="color: #000000">(</span><span style="color: #795E26">for</span><span style="color: #000000">: dir,</span></span>
<span class="line"><span style="color: #000000">                                                            </span><span style="color: #795E26">in</span><span style="color: #000000">: domain,</span></span>
<span class="line"><span style="color: #000000">                                                            </span><span style="color: #795E26">appropriateFor</span><span style="color: #000000">: appropriateForPath,</span></span>
<span class="line"><span style="color: #000000">                                                            </span><span style="color: #795E26">create</span><span style="color: #000000">: </span><span style="color: #0000FF">false</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">                path = </span><span style="color: #795E26">pathDesc</span><span style="color: #000000">(folderURL)</span></span>
<span class="line"><span style="color: #000000">            } </span><span style="color: #AF00DB">catch</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">                path = </span><span style="color: #A31515">&quot;ERROR (</span><span style="color: #0000FF">\(</span><span style="color: #000000FF">error</span><span style="color: #0000FF">)</span><span style="color: #A31515">)&quot;</span></span>
<span class="line"><span style="color: #000000">            }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">            results[</span><span style="color: #795E26">pathDesc</span><span style="color: #000000">(appropriateForPath)] = path</span></span>
<span class="line"><span style="color: #000000">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #0000FF">let</span><span style="color: #000000"> uniquePaths = </span><span style="color: #267F99">Set</span><span style="color: #000000">(results.</span><span style="color: #001080">values</span><span style="color: #000000">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #AF00DB">if</span><span style="color: #000000"> </span><span style="color: #098658">1</span><span style="color: #000000"> == uniquePaths.</span><span style="color: #001080">count</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;    </span><span style="color: #0000FF">\(</span><span style="color: #000000FF">domainName</span><span style="color: #0000FF">)</span><span style="color: #A31515">: </span><span style="color: #0000FF">\(</span><span style="color: #267F99">String</span><span style="color: #000000FF">(</span><span style="color: #795E26">repeating</span><span style="color: #000000FF">: </span><span style="color: #A31515">&quot; &quot;</span><span style="color: #000000FF">, </span><span style="color: #795E26">count</span><span style="color: #000000FF">: </span><span style="color: #098658">17</span><span style="color: #000000FF"> </span><span style="color: #000000">-</span><span style="color: #000000FF"> domainName.</span><span style="color: #001080">count</span><span style="color: #000000FF">)</span><span style="color: #0000FF">)\(</span><span style="color: #000000FF">uniquePaths.</span><span style="color: #001080">first</span><span style="color: #000000">!</span><span style="color: #0000FF">)</span><span style="color: #A31515">&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">        } </span><span style="color: #AF00DB">else</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;    </span><span style="color: #0000FF">\(</span><span style="color: #000000FF">domainName</span><span style="color: #0000FF">)</span><span style="color: #A31515">:&quot;</span><span style="color: #000000">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #AF00DB">for</span><span style="color: #000000"> (appropriateForPath, path) </span><span style="color: #AF00DB">in</span><span style="color: #000000"> results.</span><span style="color: #795E26">sorted</span><span style="color: #000000">(</span><span style="color: #795E26">by</span><span style="color: #000000">: { </span><span style="color: #0000FF">$0</span><span style="color: #000000">.</span><span style="color: #001080">key</span><span style="color: #000000"> &lt; </span><span style="color: #0000FF">$1</span><span style="color: #000000">.</span><span style="color: #001080">key</span><span style="color: #000000"> }) {</span></span>
<span class="line"><span style="color: #000000">                </span><span style="color: #795E26">print</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;        </span><span style="color: #0000FF">\(</span><span style="color: #000000FF">appropriateForPath</span><span style="color: #0000FF">)</span><span style="color: #A31515">: </span><span style="color: #0000FF">\(</span><span style="color: #267F99">String</span><span style="color: #000000FF">(</span><span style="color: #795E26">repeating</span><span style="color: #000000FF">: </span><span style="color: #A31515">&quot; &quot;</span><span style="color: #000000FF">, </span><span style="color: #795E26">count</span><span style="color: #000000FF">: maxPathLength </span><span style="color: #000000">-</span><span style="color: #000000FF"> appropriateForPath.</span><span style="color: #001080">count</span><span style="color: #000000FF">)</span><span style="color: #0000FF">)\(</span><span style="color: #000000FF">path</span><span style="color: #0000FF">)</span><span style="color: #A31515">&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">            }</span></span>
<span class="line"><span style="color: #000000">        }</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>
</details>



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



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

itemReplacementDirectory:
    userDomainMask:    
    localDomainMask:   
    systemDomainMask:  
    networkDomainMask: 

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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



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



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

itemReplacementDirectory:
    userDomainMask:    
    localDomainMask:   
    systemDomainMask:  
    networkDomainMask: 

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #008000">// Existing method</span></span>
<span class="line"><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">urls</span><span style="color: #000000">(</span><span style="color: #795E26">for</span><span style="color: #000000"> </span><span style="color: #001080">directory</span><span style="color: #000000">: FileManager.SearchPathDirectory,</span></span>
<span class="line"><span style="color: #000000">          </span><span style="color: #795E26">in</span><span style="color: #000000"> </span><span style="color: #001080">domainMask</span><span style="color: #000000">: FileManager.SearchPathDomainMask) -&gt; [URL]</span></span>
<span class="line"></span>
<span class="line"><span style="color: #008000">// New methods</span></span>
<span class="line"><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">replacementItemFolder</span><span style="color: #000000">(</span><span style="color: #795E26">appropriateFor</span><span style="color: #000000"> </span><span style="color: #001080">url</span><span style="color: #000000">: URL) </span><span style="color: #AF00DB">throws</span><span style="color: #000000">(InvalidURLError) -&gt; URL</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">trashFolder</span><span style="color: #000000">(</span><span style="color: #795E26">appropriateFor</span><span style="color: #000000"> </span><span style="color: #001080">url</span><span style="color: #000000">: URL) </span><span style="color: #AF00DB">throws</span><span style="color: #000000">(InvalidURLError) -&gt; URL</span></span></code></pre></div>



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



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



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



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


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

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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



<p>The source code for the relevant <code>NSBitmapImageRep</code> methods is essentially:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:4;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #000000">- (</span><span style="color: #0000FF">uint8_t</span><span style="color: #000000">*)bitmapData {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">uint8_t</span><span style="color: #000000"> **result = </span><span style="color: #0000FF">nil</span><span style="color: #000000">;</span></span>
<span class="line"><span style="color: #000000">    [</span><span style="color: #0000FF">self</span><span style="color: #000000"> </span><span style="color: #795E26">getBitmapDataPlanes:</span><span style="color: #000000">&amp;result];</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #AF00DB">return</span><span style="color: #000000"> *result;</span></span>
<span class="line"><span style="color: #000000">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">- (</span><span style="color: #0000FF">void</span><span style="color: #000000">)getBitmapDataPlanes:(</span><span style="color: #0000FF">uint8_t</span><span style="color: #000000">***)output {</span></span>
<span class="line"><span style="color: #000000">    [</span><span style="color: #0000FF">self</span><span style="color: #000000"> </span><span style="color: #795E26">_performBlockUsingBackingMutableData:</span><span style="color: #000000">^</span><span style="color: #795E26">void</span><span style="color: #000000"> (</span><span style="color: #0000FF">uint8_t</span><span style="color: #000000">* </span><span style="color: #001080">dataPlanes</span><span style="color: #000000">[</span><span style="color: #098658">5</span><span style="color: #000000">]) {</span></span>
<span class="line"><span style="color: #000000">        *output = dataPlanes;</span></span>
<span class="line"><span style="color: #000000">    }];</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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

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



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



<p>WTF, mate?</p>



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



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



<p>Note also that in Sonoma, at least, the cache lives at <code>/System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/</code> (in previous macOS releases it was apparently in <code>/System/Library/dyld/</code>).</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/reminder-macos-system-frameworks-binaries-are-hidden-since-big-sur/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">7503</post-id>	</item>
		<item>
		<title>Mac app sandboxing interferes with drag &#038; drop</title>
		<link>https://wadetregaskis.com/mac-app-sandboxing-interferes-with-drag-drop/</link>
					<comments>https://wadetregaskis.com/mac-app-sandboxing-interferes-with-drag-drop/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Sat, 06 Jan 2024 02:06:10 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[App sandboxing]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[Drag & drop]]></category>
		<category><![CDATA[Insecure]]></category>
		<category><![CDATA[NSEvent]]></category>
		<category><![CDATA[NSPasteboard]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[Snafu]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=7375</guid>

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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



<p>The essential code is:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> dragPasteboard = </span><span style="color: #795E26">NSPasteboard</span><span style="color: #000000">(</span><span style="color: #795E26">name</span><span style="color: #000000">: .</span><span style="color: #001080">drag</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #0000FF">var</span><span style="color: #000000"> lastDragPasteboardChangeCount: </span><span style="color: #267F99">Int</span><span style="color: #000000"> = dragPasteboard.</span><span style="color: #001080">changeCount</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> mouseDragWatcher = NSEvent.</span><span style="color: #795E26">addGlobalMonitorForEvents</span><span style="color: #000000">(</span><span style="color: #795E26">matching</span><span style="color: #000000">: [.</span><span style="color: #001080">leftMouseDragged</span><span style="color: #000000">],</span></span>
<span class="line"><span style="color: #000000">                                                         </span><span style="color: #795E26">handler</span><span style="color: #000000">: { event </span><span style="color: #AF00DB">in</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">let</span><span style="color: #000000"> currentChangeCount = dragPasteboard.</span><span style="color: #001080">changeCount</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #AF00DB">if</span><span style="color: #000000"> lastDragPasteboardChangeCount != currentChangeCount {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #008000">// The user very likely just started dragging something.</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    lastDragPasteboardChangeCount = currentChangeCount</span></span>
<span class="line"><span style="color: #000000">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">let</span><span style="color: #000000"> mouseUpWatcher = NSEvent.</span><span style="color: #795E26">addGlobalMonitorForEvents</span><span style="color: #000000">(</span><span style="color: #795E26">matching</span><span style="color: #000000">: [.</span><span style="color: #001080">leftMouseUp</span><span style="color: #000000">],</span></span>
<span class="line"><span style="color: #000000">                                                       </span><span style="color: #795E26">handler</span><span style="color: #000000">: { event </span><span style="color: #AF00DB">in</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #008000">// The drag ended.</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



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



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



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



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



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



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



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



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



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



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



<p>Sigh.</p>



<p>FB13520048.</p>



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



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



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



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



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



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



<p>I disassembled it (thanks <a href="https://www.hopperapp.com" data-wpel-link="external" target="_blank" rel="external noopener">Hopper</a>!) and saw that it&#8217;s basically just doing some UTI pre-checks &#8211; already known to be safe &#8211; to see if there&#8217;s a file URL in the pasteboard, and then it fetches that file URL using <code>readObjects(forClasses: [NSURL.self])</code>, the same method that if you or I call results in this bug surfacing.  The difference is it uses a secret <a href="https://developer.apple.com/documentation/appkit/nspasteboard/readingoptionkey" data-wpel-link="external" target="_blank" rel="external noopener">reading option key</a> &#8211; &#8220;NSPasteboardURLReadingSecurityScopedFileURLsKey&#8221;!  Apparently that&#8217;s the magic sauce which allows you to read the URL <em>without</em> triggering the bug.  You still can&#8217;t actually <em>access</em> the file, but at least you can e.g. examine its file extension (which is essentially what <code>NSImage.canInit(with:)</code> does).</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #AF00DB">if</span><span style="color: #000000"> </span><span style="color: #0000FF">let</span><span style="color: #000000"> URLs = dragPasteboard.</span><span style="color: #795E26">readObjects</span><span style="color: #000000">(</span><span style="color: #795E26">forClasses</span><span style="color: #000000">: [NSURL.self],</span></span>
<span class="line"><span style="color: #000000">                                         </span><span style="color: #795E26">options</span><span style="color: #000000">: [NSPasteboard.</span><span style="color: #795E26">ReadingOptionKey</span><span style="color: #000000">(</span><span style="color: #795E26">rawValue</span><span style="color: #000000">: </span><span style="color: #A31515">&quot;NSPasteboardURLReadingSecurityScopedFileURLsKey&quot;</span><span style="color: #000000">) : kCFBooleanTrue]) {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #AF00DB">for</span><span style="color: #000000"> u </span><span style="color: #AF00DB">in</span><span style="color: #000000"> URLs {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #008000">// Go nuts!</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



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



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



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



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



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



<p>I also now wish I&#8217;d written this post better. 😝</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/mac-app-sandboxing-interferes-with-drag-drop/feed/</wfw:commentRss>
			<slash:comments>3</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">7375</post-id>	</item>
		<item>
		<title>SwiftData pitfalls</title>
		<link>https://wadetregaskis.com/swiftdata-pitfalls/</link>
					<comments>https://wadetregaskis.com/swiftdata-pitfalls/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Wed, 15 Nov 2023 21:04:36 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[Core Data]]></category>
		<category><![CDATA[MySQL Workbench]]></category>
		<category><![CDATA[Snafu]]></category>
		<category><![CDATA[SQL]]></category>
		<category><![CDATA[SQLite]]></category>
		<category><![CDATA[Swift]]></category>
		<category><![CDATA[SwiftData]]></category>
		<category><![CDATA[Undocumented]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=5378</guid>

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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



<p>None, really.</p>



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



<p>Another poignant example:</p>



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



<p>My only notable complaint with it is that it has historically been a tad buggy.  Not dramatically, but consistently.  That said, I only just got back into it as a result of this SwiftData work &#8211; and it&#8217;s worked flawlessly so far &#8211; so it may well have upped its game since I last properly used it a couple of years ago. 🤞</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/swiftdata-pitfalls/feed/</wfw:commentRss>
			<slash:comments>4</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5378</post-id>	</item>
		<item>
		<title>Apple Watch Ultra is a poor dive computer</title>
		<link>https://wadetregaskis.com/apple-watch-ultra-is-a-poor-dive-computer/</link>
					<comments>https://wadetregaskis.com/apple-watch-ultra-is-a-poor-dive-computer/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Tue, 06 Dec 2022 04:59:28 +0000</pubDate>
				<category><![CDATA[Reviews]]></category>
		<category><![CDATA[Apple Watch Ultra]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[Oceanic+]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[scuba diving]]></category>
		<category><![CDATA[Shearwater Peregrine]]></category>
		<category><![CDATA[Tested]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=5191</guid>

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



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



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



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



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



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



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



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



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


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


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



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



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



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



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



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



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


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


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



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



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



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



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


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

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

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

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

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


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



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



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



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



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



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



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



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



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


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


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



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



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



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



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



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



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



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


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


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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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

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

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



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



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



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



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



<p>So never let Final Cut Pro &#8220;copy&#8221; your files &#8211; always copy them yourself first, as needed, and then import them into Final Cut Pro by reference only (the &#8220;Leave files in place&#8221; option).  Thankfully Final Cut Pro doesn&#8217;t mangle the files when you use them that way (underlining the question: why does it force a lossy conversion to MOV to begin with, since it clearly works just fine with MP4 originals).</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/never-import-by-copy-into-final-cut-pro/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5187</post-id>	</item>
		<item>
		<title>Preventing system sleep in Ventura</title>
		<link>https://wadetregaskis.com/preventing-system-sleep-in-ventura/</link>
					<comments>https://wadetregaskis.com/preventing-system-sleep-in-ventura/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Sat, 12 Nov 2022 16:55:49 +0000</pubDate>
				<category><![CDATA[Howto]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[macOS Ventura]]></category>
		<category><![CDATA[pmset]]></category>
		<category><![CDATA[system sleep]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=5180</guid>

					<description><![CDATA[Ventura&#8217;s new System Settings app, replacing System Preferences, removes the ability to control quite a few important things. One of those is whether or not the system goes to sleep automatically. Well, it does let you control this setting but only for when the system is on mains power: By default the system will sleep&#8230; <a class="read-more-link" href="https://wadetregaskis.com/preventing-system-sleep-in-ventura/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>Ventura&#8217;s new System Settings app, replacing System Preferences, removes the ability to control quite a few important things.  One of those is whether or not the system goes to sleep automatically.</p>



<p>Well, it does let you control this setting but <em>only</em> for when the system is on mains power:</p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="900" height="267" src="https://wadetregaskis.com/wp-content/uploads/2022/11/Crippled-System-Settings.webp" alt="" class="wp-image-5182" style="width:450px;height:134px" srcset="https://wadetregaskis.com/wp-content/uploads/2022/11/Crippled-System-Settings.webp 900w, https://wadetregaskis.com/wp-content/uploads/2022/11/Crippled-System-Settings-256x76.webp 256w, https://wadetregaskis.com/wp-content/uploads/2022/11/Crippled-System-Settings-512x152.webp 512w" sizes="auto, (max-width: 900px) 100vw, 900px" /><figcaption class="wp-element-caption">Limited controls over system sleep, in Ventura</figcaption></figure>
</div>


<p>By default the system will sleep on battery power after some undefined amount of idle time (approximately ten minutes, from observation).  System Settings does not allow you to change this.</p>



<p>Fortunately you can use <em>pmset</em> in Terminal to fix this:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="mono"><code>sudo pmset -a sleep 0</code></p>
</blockquote>



<p>This specifies that the system should never sleep automatically &#8211; that&#8217;s the 0 part &#8211; and that it should apply in all situations &#8211; that&#8217;s the -a part.</p>



<p>Alternatively you can modify the setting for just certain power modes, by using different flags:</p>



<ul class="wp-block-list">
<li><em>-b</em> for when on battery.</li>



<li><em>-c</em> for when connected to mains power.</li>



<li><em>-u</em> for when connected to a UPS (and mains power is down).</li>
</ul>



<p>Sidenote:  you can use <em>pmset -g</em> to see the current settings regarding power management, e.g.:</p>



<pre class="wp-block-preformatted mono">System-wide power settings:
Currently in use:
 standby              1
 Sleep On Power Button 1
 hibernatefile        /var/vm/sleepimage
 powernap             1
 networkoversleep     0
 disksleep            10
 sleep                0 (sleep prevented by powerd, sharingd, nsurlsessiond, nsurlsessiond, nsurlsessiond, nsurlsessiond, nsurlsessiond, nsurlsessiond)
 hibernatemode        3
 ttyskeepawake        1
 displaysleep         2
 tcpkeepalive         1
 lowpowermode         0
 womp                 0</pre>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/preventing-system-sleep-in-ventura/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5180</post-id>	</item>
		<item>
		<title>The dumpster fire that is the Raspberry Pi</title>
		<link>https://wadetregaskis.com/the-dumpster-fire-that-is-the-raspberry-pi/</link>
					<comments>https://wadetregaskis.com/the-dumpster-fire-that-is-the-raspberry-pi/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Sun, 13 Oct 2019 23:19:09 +0000</pubDate>
				<category><![CDATA[Ramblings]]></category>
		<category><![CDATA[Amazon]]></category>
		<category><![CDATA[aufs-kdms]]></category>
		<category><![CDATA[balenaEtcher]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[Docker]]></category>
		<category><![CDATA[HDMI]]></category>
		<category><![CDATA[Homebridge]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Mac Mini]]></category>
		<category><![CDATA[overlayfs]]></category>
		<category><![CDATA[Raspberry Pi 4]]></category>
		<category><![CDATA[Raspbian]]></category>
		<category><![CDATA[raspi-config]]></category>
		<category><![CDATA[Snafu]]></category>
		<category><![CDATA[VNC]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=4431</guid>

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


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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



<p>Ugh.</p>



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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

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



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



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



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



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



<p>And I still haven&#8217;t even <em>started</em> installing Swift, let-alone actually running my Swift app on the Raspberry Pi, which &#8211; contrary to where all my time has gone on this project &#8211; is the <em>actual</em> purpose of this whole sad enterprise.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/the-dumpster-fire-that-is-the-raspberry-pi/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4431</post-id>	</item>
		<item>
		<title>Truly deleting &#8216;removed&#8217; files from Lightroom</title>
		<link>https://wadetregaskis.com/truly-deleting-removed-files-from-lightroom/</link>
					<comments>https://wadetregaskis.com/truly-deleting-removed-files-from-lightroom/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Sun, 10 Mar 2019 17:29:06 +0000</pubDate>
				<category><![CDATA[Howto]]></category>
		<category><![CDATA[Photography]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[Lightroom]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=4351</guid>

					<description><![CDATA[When you tell Lightroom to deleted rejected photos, it pops up a dangerous dialog box: Though it does explain itself well &#8211; i.e. if you want to actually delete the photos, you need to click &#8220;Delete from Disk&#8221; &#8211; the default option is that misleading &#8220;Remove&#8221; button, which doesn&#8217;t really remove the files at all&#8230; <a class="read-more-link" href="https://wadetregaskis.com/truly-deleting-removed-files-from-lightroom/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>When you tell Lightroom to deleted rejected photos, it pops up a dangerous dialog box:</p>


<div class="wp-block-image is-resized">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1208" height="566" src="https://wadetregaskis.com/wp-content/uploads/2019/03/Dialog-screenshot.webp" alt="Screen shot of Lightroom dialog asking if you want to actually delete rejected photos, or merely lose track of them" 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" /></figure>
</div>


<p>Though it does explain itself well &#8211; i.e. if you want to <em>actually</em> delete the photos, you need to click &#8220;Delete from Disk&#8221; &#8211; the default option is that misleading &#8220;Remove&#8221; button, which doesn&#8217;t really remove the files at all &#8211; it merely makes Lightroom lose track of them.  They&#8217;ll still be there on disk, wasting space forever.</p>



<p><em>And</em>, you can&#8217;t directly undo this operation, so if you hit return a little too quickly, or misread the dialog at any point, you&#8217;re seemingly pretty screwed (if you have a Lightroom catalog of any significant size).</p>



<p>Luckily, there is a way to find these undead files &#8211; that <em>doesn&#8217;t</em> require you walking through every single file on disk one by one &amp; comparing against Lightroom&#8217;s view of the world.</p>



<p class="has-drop-cap">1In the left-side panel, under the &#8220;Folders&#8221; section, select all the folders and right-click on them (if you have multiple volumes listed under &#8220;Folders&#8221;, you&#8217;ll have to do this one volume at a time as Lightroom won&#8217;t let you select folders across multiple volumes simultaneously).  You&#8217;ll get a contextual menu:</p>


<div class="wp-block-image is-resized">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="696" height="922" src="https://wadetregaskis.com/wp-content/uploads/2019/03/Screen-Shot-2019-03-10-at-9.53.29-am.webp" alt="Screen shot of the contextual menu from right-clicking on an entry in the 'Folders' section of the Lightroom left-side panel" class="wp-image-4354" style="width:348px" srcset="https://wadetregaskis.com/wp-content/uploads/2019/03/Screen-Shot-2019-03-10-at-9.53.29-am.webp 696w, https://wadetregaskis.com/wp-content/uploads/2019/03/Screen-Shot-2019-03-10-at-9.53.29-am-193x256.webp 193w, https://wadetregaskis.com/wp-content/uploads/2019/03/Screen-Shot-2019-03-10-at-9.53.29-am-386x512.webp 386w" sizes="auto, (max-width: 696px) 100vw, 696px" /></figure>
</div>


<p class="has-drop-cap">2Click &#8220;Synchronize Folder…&#8221;.  A dialog will appear:</p>


<div class="wp-block-image is-resized">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1168" height="778" src="https://wadetregaskis.com/wp-content/uploads/2019/03/Screen-Shot-2019-03-10-at-9.54.21-am.webp" alt="Screenshot of the &quot;Synchronize Folder&quot; dialog" class="wp-image-4355" style="width:584px" srcset="https://wadetregaskis.com/wp-content/uploads/2019/03/Screen-Shot-2019-03-10-at-9.54.21-am.webp 1168w, https://wadetregaskis.com/wp-content/uploads/2019/03/Screen-Shot-2019-03-10-at-9.54.21-am-512x341@2x.webp 1024w, https://wadetregaskis.com/wp-content/uploads/2019/03/Screen-Shot-2019-03-10-at-9.54.21-am-256x171.webp 256w, https://wadetregaskis.com/wp-content/uploads/2019/03/Screen-Shot-2019-03-10-at-9.54.21-am-512x341.webp 512w" sizes="auto, (max-width: 1168px) 100vw, 1168px" /></figure>
</div>


<p>You probably want to uncheck &#8220;Remove missing photos from catalog&#8221; (if it&#8217;s not already disabled) and &#8220;Scan for metadata updates&#8221;, as those are unrelated to the purpose here and have their own ramifications.  Instead, just select &#8220;Import new photos&#8221; and &#8220;Show import dialog before importing&#8221;.  Then, click &#8220;Synchronize&#8221;.</p>



<p class="has-drop-cap">3Lightroom&#8217;s standard import dialog will now appear, and will slowly sort through all the files in the folder(s) you selected, filtering them down to just those that exist on disk yet are not tracked in Lightroom &#8211; e.g. all those rejects you accidentally &#8220;Removed&#8221; but didn&#8217;t <em>really</em> remove previously.  You can now review those and see what you&#8217;ve got &#8211; it&#8217;s possible you&#8217;ll find in there media you <em>didn&#8217;t</em> intend to delete, but rather were somehow misplaced by Lightroom at some point.</p>



<p>You might want to, in the import dialog, change your preview generation setting to &#8216;Minimal&#8217; in order to minimise import time &amp; wasted preview generation.  You could also choose to add some keywords to the imports, e.g. &#8220;to be deleted&#8221; or &#8220;recovered&#8221; or &#8220;undead&#8221;, if you&#8217;re not going to just immediately delete them anyway.</p>



<p>In any case, you can now import some or all the undead files.  <em>Importing</em> them might seem counter-productive, since the goal here is to <em>delete</em> them &#8211; but it&#8217;s necessary for the final step…</p>



<p class="has-drop-cap">4Once they&#8217;re imported, you can now immediately mark them as rejects and delete all rejects again &#8211; <em>this</em> time correctly choosing &#8220;Removing from Disk&#8221;.<br></p>



<p>So while it&#8217;s a bit roundabout, it does get the job done pretty quickly and easily.  Now if only Lightroom would fix that stupid dialog to make the default option the one that actually does what you told Lightroom to do to begin with. 🙄<br></p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/truly-deleting-removed-files-from-lightroom/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2019/03/Dialog-screenshot.webp" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">4351</post-id>	</item>
		<item>
		<title>Full Disk Access is required to access Time Machine backups in Mojave</title>
		<link>https://wadetregaskis.com/full-disk-access-is-required-to-access-time-machine-backups-in-mojave/</link>
					<comments>https://wadetregaskis.com/full-disk-access-is-required-to-access-time-machine-backups-in-mojave/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Thu, 27 Dec 2018 17:51:51 +0000</pubDate>
				<category><![CDATA[Howto]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[Finder]]></category>
		<category><![CDATA[Full Disk Access]]></category>
		<category><![CDATA[Mojave]]></category>
		<category><![CDATA[Security]]></category>
		<category><![CDATA[Snafu]]></category>
		<category><![CDATA[System Integrity Protection]]></category>
		<category><![CDATA[Terminal]]></category>
		<category><![CDATA[Time Machine]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=4314</guid>

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



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



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



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



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



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



<p>I&#8217;m guessing whatever mechanism enforces all this so-called security is based in LaunchServices or somesuch &#8211; while the Finder and most things in general will launch apps via LaunchServices, as detached &amp; independent process sessions, Terminal doesn&#8217;t &#8211; everything it runs, from the shells down, run under it in the process hierarchy, and seemingly share its security &amp; privacy settings.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/full-disk-access-is-required-to-access-time-machine-backups-in-mojave/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4314</post-id>	</item>
		<item>
		<title>Blink XT review</title>
		<link>https://wadetregaskis.com/blink-xt-review/</link>
					<comments>https://wadetregaskis.com/blink-xt-review/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Sun, 28 Oct 2018 00:50:14 +0000</pubDate>
				<category><![CDATA[Reviews]]></category>
		<category><![CDATA[Amazon]]></category>
		<category><![CDATA[Blink XT]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[Tested]]></category>
		<category><![CDATA[video]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=4295</guid>

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



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



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



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



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



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



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



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



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



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



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

					<description><![CDATA[The FTZ adaptor has a surprising and very frustrating design flaw &#8211; it&#8217;s impossible to mount it to the camera body when you have almost any kind of mounting plate, strap, or harness (e.g. Cotton Carrier) mount point attached to the camera body. &#160;This is because the FTZ has a big fat foot, as can&#8230; <a class="read-more-link" href="https://wadetregaskis.com/ftz-adaptor-hates-tripods-straps-and-harnesses/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>The FTZ adaptor has a surprising and very frustrating design flaw &#8211; it&#8217;s impossible to mount it to the camera body when you have almost any kind of mounting plate, strap, or harness (e.g. Cotton Carrier) mount point attached to the camera body. &nbsp;This is because the FTZ has a big fat foot, as can be seen in the above photo, which sticks down well below the bottom of the camera body. &nbsp;Furthermore, the camera body&#8217;s tripod socket is&nbsp;<em>very</em> close to the front edge of the body &#8211; and thus the FTZ&#8217;s foot. &nbsp;Anything you attach to the camera body&#8217;s tripod socket tends to stick out from the front of the camera&#8217;s body &#8211; a lot. &nbsp;The FTZ&#8217;s fat foot collides with that, and makes it impossible to use both at the same time.</p>



<p>I suppose nominally you&#8217;re never supposed to use the camera body&#8217;s tripod mount when you have the FTZ attached, but that&#8217;s naive &#8211; if you&#8217;re going back and forth between native to adapted lenses, you&#8217;re not going to be constantly removing &amp; reattaching things to tripod sockets. &nbsp;At most you&#8217;d want to have the same widget in&nbsp;<em>both</em> the camera body&#8217;s and the FTZ&#8217;s tripod sockets, so that you always have one available irrespective of what lens you have attached.</p>



<p>I miss companies that gave some thought to having all their products work well together (this is just the latest example I&#8217;ve noticed in an increasing trend).</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/ftz-adaptor-hates-tripods-straps-and-harnesses/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2018/10/Z7FTZ.webp" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">4279</post-id>	</item>
		<item>
		<title>iOS 7 first impressions</title>
		<link>https://wadetregaskis.com/ios-7-first-impressions/</link>
					<comments>https://wadetregaskis.com/ios-7-first-impressions/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Wed, 10 Oct 2018 03:23:58 +0000</pubDate>
				<category><![CDATA[Ramblings]]></category>
		<category><![CDATA[Reviews]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[iOS]]></category>
		<category><![CDATA[iOS 6]]></category>
		<category><![CDATA[iOS 7]]></category>
		<category><![CDATA[iPad]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[Timeless]]></category>
		<category><![CDATA[Ugly]]></category>
		<guid isPermaLink="false">http://blog.wadetregaskis.com/?p=2750</guid>

					<description><![CDATA[I found this post in the &#8216;Drafts&#8217; folder from 2013 &#8211; evidently I started writing, got distracted, and forgot about it. It&#8217;s interesting to me even now because the aesthetics of iOS have been stuck in iOS 7 ever since. &#160;I still don&#8217;t like the look, the design language, how many things operate &#8211; the&#8230; <a class="read-more-link" href="https://wadetregaskis.com/ios-7-first-impressions/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>I found this post in the &#8216;Drafts&#8217; folder from 2013 &#8211; evidently I started writing, got distracted, and forgot about it.</p>



<p>It&#8217;s interesting to me even now because the aesthetics of iOS have been stuck in iOS 7 ever since. &nbsp;I still don&#8217;t like the look, the design language, how many things operate &#8211; the interface is ugly, unintuitive, lacks personality, and &#8211; as the hosts of <a href="https://atp.fm" data-wpel-link="external" target="_blank" rel="external noopener">ATP</a> might say &#8211; is absent the whimsy that defined Apple for decades.</p>



<p>It felt like a betrayal, too &#8211; now iOS, as of version 7, looked like a cheap Android rip-off. &nbsp;Apple had wilfully and pointlessly thrown away their most important positive differentiators. &nbsp;Insult was further added to injury by the mere existence of Windows Phone Metro, which &#8211; while still ugly to me too &#8211; at least demonstrated originality and a kind of bravery &#8211; it at least had&nbsp;<em>a</em> style, even if it wasn&#8217;t the one for me.</p>



<p>And it was dog slow. &nbsp;It basically killed my love of the iPad, because it made my iPad 3 frustrating to use. &nbsp;Even when I later got an iPad Air 2 (as a hand-me-down), my iPad love never really rekindled.</p>



<p>Nonetheless, I had been wondering for a few years: were I to go back <em>now</em> to iOS 6, would I be revolted &amp; repulsed by it, and suddenly realise that iOS 7 and its ilk are in fact the current pinnacle of user interface &amp; visual design?</p>



<p>A few months ago I got out my original iPad and turned it on. &nbsp;It was running iOS 5, the last version of iOS support on it. &nbsp;I hadn&#8217;t intended to go back in time &#8211; I&#8217;d forgotten entirely that it was pre-iOS 7. &nbsp;I didn&#8217;t realise straight away, either. &nbsp;My first thought, upon booting to the home screen, was &#8220;wow, this looks amazing&#8221;. &nbsp;It genuinely took me a while to figure out why this non-Retina, decade-old, square &amp; heavy iPad felt fantastic.</p>



<p>Then I realised &#8211; because&nbsp;<em>it looks good and is easy to use</em>.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><a href="https://www.flickr.com/photos/danewirtzfeld/6843098308" data-wpel-link="external" target="_blank" rel="external noopener"><img loading="lazy" decoding="async" width="1536" height="2048" src="https://wadetregaskis.com/wp-content/uploads/2013/06/6843098308_bc3b37fa2e_o.webp" alt="Screenshot of iPad 3 home screen running iOS 6" class="wp-image-4261" style="width:768px" srcset="https://wadetregaskis.com/wp-content/uploads/2013/06/6843098308_bc3b37fa2e_o-768x1024@2x.webp 1536w, https://wadetregaskis.com/wp-content/uploads/2013/06/6843098308_bc3b37fa2e_o-384x512@2x.webp 768w, https://wadetregaskis.com/wp-content/uploads/2013/06/6843098308_bc3b37fa2e_o-192x256.webp 192w, https://wadetregaskis.com/wp-content/uploads/2013/06/6843098308_bc3b37fa2e_o-384x512.webp 384w" sizes="auto, (max-width: 1536px) 100vw, 1536px" /></a><figcaption class="wp-element-caption">The default iPad 3 home screen under iOS 6.  Admittedly prettier than on the original iPad, thanks to the Retina display, but you get the point nonetheless.<br>Screenshot courtesy of <a href="https://www.flickr.com/photos/danewirtzfeld/6843098308" data-wpel-link="external" target="_blank" rel="external noopener">Dane Wirtzfeld via Flickr</a>.</figcaption></figure>
</div>


<p>Without further ado, my until-now unpublished iOS 7 first impressions:</p>



<ul class="wp-block-list">
<li>It&#8217;s buggy. The task switcher has a terrible time dealing with landscape orientation.</li>



<li>It&#8217;s slow. Both in general &#8211; perhaps just lacking some optimisations &#8211; and by apparent design flaws. e.g. many new animations are unnecessary to begin with, and unnecessarily slow to boot, and you can&#8217;t interact with things until the animation is done. It&#8217;s quickly frustrating.</li>



<li>The new slide-up gesture (for the little control sheet) steals scrolls periodically, which is exceedingly annoying. Perhaps in time I&#8217;ll recalibrate where I need to touch things in order to avoid that, but it&#8217;s annoying in the meantime.</li>



<li>Actually installing it was a pain and took multiple attempts, as per usual for any system restore. Le sigh.</li>



<li>Spelling correction is more aggressive now, and will even re-incorrect things after you explicitly fix them. Fucker.</li>



<li>To delete emails you now have to swipe the opposite direction &#8211; from right to left. No obvious reason, and certainly no indication on how to do that.</li>



<li>The new icons and dock design look like UI mocks. By someone who&#8217;s either not very good at them or just needs a really basic placeholder. They&#8217;re probably the most disappointing thing about iOS 7 so far.</li>



<li>The new lock screen is obtuse, as others have noted. The whole slide to unlock debacle is ridiculous and Apple has no excuse for it. But furthermore, it displays your chosen lock screen image arbitrarily cropped, and jitters it about randomly in what must be intended to be this infamous parallax effect, but in reality has no apparent relationship to the orientation of the iPad, and so just looks broken and stupid. Big cock-up all round there.</li>
</ul>



<p>I love (meaning am tremendously sad) how certain aspects of those first impressions have lasted &#8211; some becoming huge memes of their own (e.g.&nbsp;<a href="https://web.archive.org/web/20181009222124/http://www.damnyouautocorrect.com/" data-wpel-link="external" target="_blank" rel="external noopener">damnyouautocorrect.com</a>). &nbsp;And how some parts of the iOS upgrade experience &#8211; like having to do the install repeatedly to get it to work &#8211; persist to this day.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/ios-7-first-impressions/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2023/12/iOS-7-iPad-screenshot.avif" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">2750</post-id>	</item>
		<item>
		<title>Nikon Z7 second first impressions</title>
		<link>https://wadetregaskis.com/nikon-z7-second-first-impressions/</link>
					<comments>https://wadetregaskis.com/nikon-z7-second-first-impressions/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Sun, 07 Oct 2018 20:05:12 +0000</pubDate>
				<category><![CDATA[Photography]]></category>
		<category><![CDATA[Reviews]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[D500]]></category>
		<category><![CDATA[Nikon]]></category>
		<category><![CDATA[SnapBridge]]></category>
		<category><![CDATA[Wireless Transmitter Utility]]></category>
		<category><![CDATA[Z7]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=4208</guid>

					<description><![CDATA[Having spent a week or so using the Z7 &#8211; though still not as much as I&#8217;d like, given the continued need to work for a living &#8211; I have some further thoughts, beyond / expanding upon my&#160;very first impressions. Autofocus Photo mode Autofocus is a problem. It is very clear that the Z7&#8217;s AF&#8230; <a class="read-more-link" href="https://wadetregaskis.com/nikon-z7-second-first-impressions/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>Having spent a week or so using the Z7 &#8211; though still not as much as I&#8217;d like, given the continued need to work for a living &#8211; I have some further thoughts, beyond / expanding upon my&nbsp;<a href="https://wadetregaskis.com/nikon-z7-very-first-impressions/" data-wpel-link="internal">very first impressions</a>.</p>



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



<h3 class="wp-block-heading">Photo mode</h3>



<p>Autofocus is a problem.</p>



<p>It is very clear that the Z7&#8217;s AF system is not in the same league as the Advanced Multi-CAM 20K system in the D500, D5, &amp; D850. &nbsp;I&#8217;m increasingly concerned that it doesn&#8217;t even match up to the much older 51-AF-point systems used in much older DSLRs going way back to the 11-year-old D300.</p>



<p>As I immediately noticed from the moment I turned the camera on, it has big problems in anything approaching low light, especially with the slow (f/4) kit lens. &nbsp;Not just night photography low light, but indoor lighting low light. &nbsp;e.g. under 250W-equivalent LED ceiling lights, in a small room, shooting at ISO ~800, it struggles to focus accurately even on high-contrast, stationary subjects.</p>



<p>In fact for a while during my testing it back-focused to infinity, vs my subject 2 metres in front of me, and&nbsp;<em>consistently</em> kept focus there for a dozen photos, despite having AF-C engaged continuously, in single-point AF mode, with that point on my subject.</p>



<p>[Edit: &nbsp;<a href="https://www.dpreview.com/reviews/nikon-z7/5" data-wpel-link="external" target="_blank" rel="external noopener">DPReview also saw the exact same behaviour, in all respects</a>.]</p>



<p>In bright light &#8211; e.g. direct sun &#8211; it seems to do fine, but then so does any camera from the last fifty years.</p>



<p>Another very concerning and frankly infuriating behaviour is that it simply won&#8217;t even&nbsp;<em>try</em> to focus if the subject is significantly out of focus to begin with. &nbsp;Every other camera I&#8217;ve ever used in my life would at least resort to racking the focus plane back and forth, but the Z7 simply will not do anything. &nbsp;You have to use manual focus override to bring the subject closer to being in focus, before the Z7&#8217;s autofocus system will even bother engaging. &nbsp;This is mind-bogglingly stupid &#8211; and a real problem if you remapped the &#8216;function ring&#8217; on your lens to a function other than focus (e.g. aperture control).</p>



<p>Thus far in my initial experiments using the FTZ mount adapter and the Sigma 50/1.4 Art &#8211; where you&#8217;d think the huge increase in maximum aperture might alleviate some of the AF sensitivity problems &#8211; I&#8217;ve been disappointed. &nbsp;The much wider aperture seems to help a little bit, but not enough to make the AF system feel up to the Nikon name &#8211; nor the price tag for the Z7. &nbsp;(and yes, this is photographing wide-open &#8211; I&#8217;m well aware that the Z7 will stop the lens down to the shooting aperture during autofocus (down to a limit of f/5.6), unlike Nikon&#8217;s DSLR)</p>



<p>Next to consider are the AF modes, and AF tracking. &nbsp;For background, frankly I never found 3D Tracking in Nikon&#8217;s DSLRs to be very good &#8211; it&#8217;s very easily confused and will usually fail to track even the most clearly distinguished subjects. &nbsp;I have &amp; do use it occasionally, but about the only scenario where I&#8217;ve found it <em>consistently</em> usable is birds in flight against a flat sky &#8211; at which point it doesn&#8217;t actually perform any better than Auto mode, really, since it&#8217;s merely focusing on the only thing in the frame that it can.</p>



<p>Put simply, the Z7 has some dumb &#8211; baffling &#8211; user interface flaws around its AF modes, the most egregious being that:</p>



<ul class="wp-block-list">
<li>You cannot configure different physical buttons to engage different AF modes. &nbsp;My D500, for example, has the AF joystick configured so that pressing it engages single-point AF, while the dedicated AF-ON button engages a different mode (e.g. 3D Tracking, one of the dXX modes, or Group mode). &nbsp;You cannot do anything like this on the Z7, which is a bizarre regression and a serious problem not just for its own sake, but also because it compounds many of the Z7&#8217;s other flaws, below.</li>



<li>Face detection only works in Auto mode, and Auto mode continues (as with all prior Nikon cameras, and digital cameras in general) to be useless in most situations because it is utterly incompetent about determining your intended subject. &nbsp;It&#8217;s also incredibly sticky once it&#8217;s focused on something &#8211; face or otherwise. &nbsp;You actually have to move whatever it&#8217;s stuck on out of the frame entirely, re-engage AF, and hope it picks something better. &nbsp;I really wanted to use face detection, but repeatedly I find myself rushing to switch to single-point AF mode in order to get the shot that Auto mode is blocking. So while face detection itself is useful, and I&#8217;d like to use it more, the problem is that it&#8217;s rarely the only AF mode I need in any given situation, and Auto is basically&nbsp;<em>never</em> a useful AF mode. &nbsp;Given the inexplicable inability to configure different buttons to engage different AF modes, you&#8217;re stuck with this awkward choice of being able to conveniently focus on faces &#8211; but only faces, and only when it works, which is only sometimes &#8211; or do it all &#8216;manually&#8217; with single-point AF mode. &nbsp;Or try to frantically switch back and forth between the two modes constantly, which I found to be impractically slow (and dangerously reminiscent of entry-level consumer DSLRs where basic functionality is buried in menus).</li>



<li>Face detection struggles in the presence of multiple faces. &nbsp;It makes strange choices about which face to default to, and switching between faces is basically a losing game of whack-a-mole &#8211; first you have to wait for it to recognise the face you want at all, then select it before it loses it again, all the while doing your best to guess which &#8216;direction&#8217; the face you want is from the current one &#8211; you can only use the left &amp; right buttons of the d-pad, even if the faces are arranged vertically,&nbsp;<em>and</em> the movement direction isn&#8217;t even consistent. &nbsp;e.g. several times I hit left and it jumped to a face to the&nbsp;<em>right</em> of the previously selected one.</li>



<li>The &#8216;tracking&#8217; AF mode is a sub-mode of Auto mode, and frankly I find it a bit confusing to use as a result since you have to remember which of three states you&#8217;re in (normal Auto, tracking point placement, or tracking active) and use a variety of buttons to move between these states. &nbsp;It&#8217;s not quite as slow to engage as I feared from reading early reviews, and thus far it seems markedly superior to 3D Tracking in terms of actually tracking the subject, but the bad user interface really discourages its use.</li>
</ul>



<p>The baffling thing in all this is why Nikon just didn&#8217;t do the incredibly obvious thing that they&#8217;ve basically already established with their pro DSLRs, i.e. have a dedicated AF mode &#8211; ideally the default &#8211; where you place the AF point wherever you like, position it over your subject, and hit AF-ON to start tracking, and continue tracking until you release AF-ON. &nbsp;Nothing could be simpler, and Nikon&#8217;s DSLRs have done this for over a decade. &nbsp;The lack of a sensible AF interface is an inscrutable, unforced error, which makes me genuinely question who designed the Z7, and whether they&#8217;d ever used a camera before.</p>



<h3 class="wp-block-heading">Video mode</h3>



<p>One of the main attractions to me of the Z7 over all Nikon&#8217;s DSLRs is the expected improvement in video capability. &nbsp;By all rights the Z7&nbsp;<em>should</em> be dramatically superior to any DSLR, for video, even if only because it can finally do phase-detection autofocus in video mode.</p>



<p>Instead it&#8217;s a mixed bag.</p>



<p>The ability to do full-sensor-width UHD, rather than the severely cropped UHD of the D500, is very nice, and while I haven&#8217;t yet had occasion to do very wide angle video, I know when I do I&#8217;ll be very happy to actually be able to do it (even a 10mm lens on the D500 doesn&#8217;t give you an ultra-wide UHD video frame, because of the severe cropping).</p>



<p>Being able to use the viewfinder while recording video is a big improvement for general usability, and also stability &#8211; having that third point of contact, and your arms in closer to your body&#8217;s centre, make for a much more stable camera hold. &nbsp;It&#8217;s also correspondingly easier to record for long periods, since it&#8217;s an overall much more comfortable position.</p>



<p>Unfortunately, all of that is really undermined by the AF problems. &nbsp;Just as with photo mode, of course, AF in video mode struggles in anything even vaguely reminiscent of low light. &nbsp;And in video recording you just can&#8217;t have certain behaviours, like racking focus back &amp; forth searching for correct focus. &nbsp;Alas, the Z7 does that constantly. &nbsp;Its video AF performance seems very similar to the purely contrast-detection based implementations in Nikon&#8217;s DSLRs. &nbsp;It&#8217;s basically unusable, in my experience so far… maybe in bright daylight it&#8217;ll prove more reliable &#8211; I have not yet had the opportunity to test it in such circumstances.</p>



<p>So for now video mode remains predominately manual focus, which is a huge disappointment.</p>



<h2 class="wp-block-heading">Manual focus</h2>



<p>Thankfully the manual focus story is&nbsp;<em>much</em> better than the auto one. &nbsp;The ability to digitally &#8216;zoom&#8217; in the viewfinder, at the press of a button (configurable, of course), is extremely helpful for manual focusing (and verifying accurate autofocus). &nbsp;It&#8217;s the single most important focus feature in the camera, by far.</p>



<p>Focus peaking&nbsp;<em>should</em> be very helpful, but in practice I&#8217;ve found it to be inexplicably difficult to engage to begin with, and even then it doesn&#8217;t work well in many situations &#8211; e.g. it doesn&#8217;t work&nbsp;<em>at all</em> at high ISOs. &nbsp;While I did ultimately discover that if you switch the lens into manual focus mode, focus peaking enables persistently, it&#8217;s frustrating to basically be coerced out of AF entirely &#8211; given that when you&#8217;re&nbsp;<em>not</em> in complete manual-focus mode, getting focus peaking to show up requires holding down AF-ON (or similar),&nbsp;<em>and</em> moving the focus ring far enough to trigger peaking. &nbsp;It doesn&#8217;t sound like much, and maybe it&#8217;ll become more natural with practice, but right now it&#8217;s an awkward combination of actions. &nbsp;It&#8217;s baffling to me that focus peaking, when enabled, isn&#8217;t simply enabled &#8211; it shouldn&#8217;t require holding down extra buttons and jumping through hoops.</p>



<p>The 24-70/4 &#8216;function ring&#8217; is definitely different for manual focus. &nbsp;It&#8217;s noticeably sloppy compared with the auto-clutched AF rings typical of Nikon&#8217;s DSLR lenses &#8211; meaning, primarily, that you have to turn it a noticeable amount before it engages at all (though this pick-up &#8216;slop&#8217; has always varied between lenses, and the 24-70/4 isn&#8217;t necessarily worse than <em>all</em> prior ones). &nbsp;I&#8217;m also finding it difficult, so far, to get it to move the focus plane consistent amounts &#8211; presumably attributable to the &#8216;acceleration&#8217; behaviour it has, whereby the&nbsp;<em>speed</em> at which you move the ring apparently affects the magnitude of focus plane movement. &nbsp;I do expect that I&#8217;ll get used to that in time, just as I did when acceleration was introduced to computer mice many years ago. &nbsp;For now though it makes manual focus adjustment a tad more difficult than I&#8217;m used to. &nbsp;It also remains to be seen how consistent the implementation is &#8211; if you&#8217;ve ever used a cheap computer mouse vs a high quality one, you&#8217;ll know the subtle difference in accuracy &amp; precision.</p>



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



<h3 class="wp-block-heading">Size, weight, &amp; balance</h3>



<p>With a small lens (e.g. the 24-70/4 kit lens) it&#8217;s overall not too bad, though the small size &#8211; particularly of the grip &#8211; makes it noticeably less comfortable to use than a D500, D850, or D5. &nbsp;With a larger lens &#8211; e.g. a 70-200/2.8, it&#8217;s actually&nbsp;<em>less</em> of a problem, since the whole setup is much more front-heavy, putting the majority of the weight on your lens hand, so the smaller, dainty grip is less of a concern. &nbsp;Nonetheless the controls &#8211; shutter, ISO button, exposure compensation, etc &#8211; do feel very cramped, though this is odd as they don&#8217;t appear, visually, to be packed any more densely than on the D500.</p>



<h3 class="wp-block-heading">Control placement</h3>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1000" height="619" src="https://wadetregaskis.com/wp-content/uploads/2018/10/Z7_top.high_.webp" alt="" class="wp-image-4220" srcset="https://wadetregaskis.com/wp-content/uploads/2018/10/Z7_top.high_.webp 1000w, https://wadetregaskis.com/wp-content/uploads/2018/10/Z7_top.high_-256x158.webp 256w, https://wadetregaskis.com/wp-content/uploads/2018/10/Z7_top.high_-512x317.webp 512w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /></figure>
</div>


<p>The placement of the exposure compensation button is different to Nikon&#8217;s DSLRs, and is in a pretty awkward spot &#8211; it&#8217;s now much too close to the right edge of the camera. &nbsp;I frequently hit the ISO button by mistake as my pointer finger searches in vain for the exposure compensation button, starting with where it&nbsp;<em>used</em> to be on all prior Nikon DSLRs. &nbsp;Presumably I&#8217;ll get used to this in time, but it&#8217;s a strange and seemingly unnecessary change that simply makes the exposure compensation button harder to reach.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1000" height="731" src="https://wadetregaskis.com/wp-content/uploads/2018/10/Z7_back.high_.webp" alt="" class="wp-image-4221" srcset="https://wadetregaskis.com/wp-content/uploads/2018/10/Z7_back.high_.webp 1000w, https://wadetregaskis.com/wp-content/uploads/2018/10/Z7_back.high_-256x187.webp 256w, https://wadetregaskis.com/wp-content/uploads/2018/10/Z7_back.high_-512x374.webp 512w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /></figure>
</div>


<p>Similarly the placement of the d-pad on the back of the camera is very awkward &#8211; it&#8217;s basically impossible to use comfortably or quickly with a normal hand-hold, requiring you to move your hand off of the grip somewhat in order to be able to reach the d-pad with your thumb, <em>and</em> move your face away (if you use your left eye to the viewfinder) to make room. &nbsp;This is a bit of a hinderance to an otherwise exciting new possibility, given the EVF, of being able to adjust lots of settings quickly without taking your eye from the viewfinder. &nbsp;In practice I find it quicker and safer (for the camera&#8217;s sake) to just use the rear LCD as before, as that gets my face out of the way and allows me to move my hand more freely.</p>



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



<p>One surprising thing I&#8217;ve noticed is that some of the camera&#8217;s controls are noticeably laggy. &nbsp;Rotating the control dial, for example, to change aperture, has a very noticeable delay before the aperture actually changes, and the display(s) update. &nbsp;Only a fraction of a second, to be clear. &nbsp;Nonetheless, on Nikon&#8217;s DSLRs going back as far as I can remember, there has always been&nbsp;<em>zero</em> perceptible delay for such basic actions as changing the aperture. &nbsp;While it&#8217;s not strictly speaking a significant problem, it is a constant reminder in use that the Z7 is sluggish.</p>



<p>In fact, one very noticeable manifestation of that &#8220;but I am le tired&#8221; feeling the camera conveys is when you put your eye to the viewfinder &#8211; if the camera has been idle for long enough (tens of seconds, I think), it takes a couple of seconds for the viewfinder to turn on. &nbsp;I&#8217;ve already had several awkward moments where I&#8217;ve had people posed in front of me, brought the camera up to my eye, and then had to pause for an uncomfortably long time while I wait for the viewfinder to turn on. &nbsp;It&#8217;s not just me that notices this &#8211; my subjects notice the delay too, and find it a bit unsettling &#8211; like I&#8217;m staring at them motionless for an uncomfortable amount of time. &nbsp;I&#8217;m presuming this is some overly-aggressive power saving feature, which I wish I could just turn off. &nbsp;(FYI I have the camera configured to viewfinder priority mode, since that&#8217;s the only one that makes any sense to me, but I haven&#8217;t explored if other modes alleviate this problem).</p>



<h3 class="wp-block-heading">Info &amp; Display buttons</h3>



<p>I basically never used these two buttons any other Nikon DSLR &#8211; maybe occasionally in video mode to toggle the display of various things, but otherwise I just had no apparent need, or had better ways to get at the same functionality.</p>



<p>The Display button doesn&#8217;t really change from Nikon&#8217;s DSLRs &#8211; as before it toggles through various display &#8216;HUD&#8217; modes. &nbsp;As always, I wish I could more precisely configure what&#8217;s shown &#8211; certain information is only shown in certain modes that otherwise contain heaps of crap I couldn&#8217;t care less about, so being able to cherry-pick the exact &#8216;widgets&#8217; I want to show would be ideal, and eliminate the need for a mode-switching button entirely.</p>



<p>The Info button and associated functionality is something I find myself naturally using on the Z7. &nbsp;It&#8217;s unfortunately awkward to use via the touchscreen, as inexplicably you must double-tap everything to get settings to actually apply, which I consistently forget because it&#8217;s so unintuitive. &nbsp;Using the d-pad &amp; ok button is much safer, and so I do that, which is fine most of the time.</p>



<p>Being able to configure the contents of the Info panel is of course what makes it much more useful than before. &nbsp;And though the number of items you can place there simultaneously is fixed, and seemingly not many &#8211; twelve &#8211; I actually find myself searching for useful things to fill the last couple of spots. &nbsp;So thus far I&#8217;m pretty happy with it &#8211; I don&#8217;t mind using it as opposed to dedicated physical buttons, for the most part, though for now I did still find myself occasionally reaching for the AF mode and bracketing physical buttons, that no longer exist.</p>



<p>I also am having a surprisingly hard time remembering that there&#8217;s still a release mode physical button, albeit in an awkward location now &#8211; I keep going through the Info panel instead, which isn&#8217;t really a problem but makes me feel a little silly sometimes.</p>



<h2 class="wp-block-heading">Image stabilisation</h2>



<p>It&#8217;s still early for me on image quality &#8211; I have a lot of photos taken with the Z7 I haven&#8217;t even gone through yet &#8211; so I&#8217;m not certain how good or bad the in-body image stabilisation is. &nbsp;My impression from chimping is that it&#8217;s&nbsp;<em>not</em> all that great, based on significant numbers of camera-motion-blurred photos, but I&#8217;m also quite self-aware that I&#8217;m coming from (primarily) a 21 MP D500, to this 46 MP Z7, so it&#8217;s an intrinsically much more demanding sensor re. motion blur. &nbsp;And the lightness of the Z7 probably isn&#8217;t doing it any favours here, either.</p>



<p>Certainly I think it&#8217;s fair to say it helps with previously unstabilised lenses, like the Sigma 50/1.4 Art. &nbsp;More testing is needed, though, especially to estimate the degree to which it helps.</p>



<p>One of my pet peeves about the D500 is that it has huge mirror shock. &nbsp;Certain shutter speed ranges &#8211; typically ~1/50 to 1/160 &#8211; with some lenses are utterly unusable on the D500. &nbsp;I&#8217;m optimistic that the Z7 will not suffer from such issues, given its ability to utilise a purely electronic (i.e. no moving parts) mode. &nbsp;I&#8217;ve not yet put it through its paces in those specific scenarios, though (e.g. macro photography with the Sigma 105/2.8 is the worst such case with the D500, that I&#8217;ve encountered). &nbsp;I know from past experience with mirrorless cameras (e.g. a7r II, GH4) that these shutter speeds don&#8217;t&nbsp;<em>have</em> to be verboten.</p>



<p>Image stabilisation in video mode does seem noticeably better than on the D500 (with a VR lens). &nbsp;I haven&#8217;t explored it much yet, though.</p>



<h2 class="wp-block-heading">Image review</h2>



<p>One thing I noticed very quickly upon picking up the Z7 is that it has an ugly flickering problem when panning photos in review mode. &nbsp;It&#8217;s at its worst when using the d-pad for panning, but also shows up a little bit when using the touch screen to pan too. &nbsp;It&#8217;s very distracting, and I don&#8217;t understand why it would be doing that, nor how this is considered acceptable by Nikon. &nbsp;I&#8217;m hoping it&#8217;s some very stupid but fixable bug that can be addressed in a firmware update. &nbsp;No other Nikon camera I&#8217;ve ever used had this issue, or anything like it.</p>



<p>Otherwise though it&#8217;s just as on any prior Nikon DSLR &#8211; scrolling between images is plenty fast, zooming is instantaneous, the touch screen works nicely including pinch-to-zoom &amp; double-tap-to-zoom, etc. &nbsp;It&#8217;s a genuine compliment to say that image review continues to work &#8211; flickering notwithstanding &#8211; as on Nikon&#8217;s prior cameras.</p>



<h2 class="wp-block-heading">Silent mode</h2>


<div class="wp-block-image">
<figure class="alignright size-thumbnail"><a href="https://commons.wikimedia.org/wiki/File:Mute_Icon.svg" data-wpel-link="external" target="_blank" rel="external noopener"><img loading="lazy" decoding="async" width="256" height="206" src="https://wadetregaskis.com/wp-content/uploads/2018/10/Mute-256x206.png" alt="" class="wp-image-4228" srcset="https://wadetregaskis.com/wp-content/uploads/2018/10/Mute-256x206.png 256w, https://wadetregaskis.com/wp-content/uploads/2018/10/Mute.png 512w" sizes="auto, (max-width: 256px) 100vw, 256px" /></a></figure>
</div>


<p>I&#8217;m a big fan of silent mode. &nbsp;It unfortunately doesn&#8217;t always work &#8211; under some artificial lights &#8211; certainly fluorescents &#8211; it&#8217;s useless as it results in pronounced, ugly banding. &nbsp;But under better lighting (e.g. LED), or natural light, it has no such issues. &nbsp;The ability to take photos silently is really handy in a lot of situations, and I use silent mode by default even when silence isn&#8217;t strictly necessary (in part also motivated by a desire to eliminate sources of motion blur).</p>



<p>I do wish that the camera&#8217;s flicker detection feature could be enhanced to provide a warning to you when you&#8217;re in silent mode and it suspects banding will occur &#8211; a few times I started taking photos only to find out some time later, when I finally checked them on the LCD, that they were ruined by banding. &nbsp;Since it&#8217;s not always obvious when it will occur &#8211; nor does it necessarily occur consistently &#8211; it&#8217;s currently something you have to be careful about, currently.</p>



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


<div class="wp-block-image">
<figure class="alignright size-thumbnail"><a href="https://www.pngall.com/wi-fi-png/download/13963" data-wpel-link="external" target="_blank" rel="external noopener"><img loading="lazy" decoding="async" width="256" height="230" src="https://wadetregaskis.com/wp-content/uploads/2018/10/Wifi-256x230.webp" alt="" class="wp-image-4227" srcset="https://wadetregaskis.com/wp-content/uploads/2018/10/Wifi-256x230.webp 256w, https://wadetregaskis.com/wp-content/uploads/2018/10/Wifi.webp 512w" sizes="auto, (max-width: 256px) 100vw, 256px" /></a></figure>
</div>


<p>The Z7 claims to have a new ability to stream photos as they&#8217;re taken to a computer. &nbsp;That would be really handy sometimes. &nbsp;Unfortunately, the Wireless Transmitter Utility software that you need on your Mac, in order to do this, doesn&#8217;t work. &nbsp;The installer doesn&#8217;t work, more specifically. &nbsp;After clicking through the first few screens, it abruptly says it&#8217;s installed, but it isn&#8217;t &#8211; nothing has been installed.</p>



<p>My guess is that it&#8217;s incompatible with the current version of macOS, Mojave. &nbsp;Officially they <em>don&#8217;t</em> claim WTU is Mojave-compatible. &nbsp;Mojave has been out in various forms, including public let-alone developer betas &#8211; for most of this year already, so there&#8217;s zero excuse for Nikon&#8217;s software being incompatible at this point &#8211; if indeed that is the issue.</p>



<h2 class="wp-block-heading">Memory card</h2>


<div class="wp-block-image">
<figure class="alignleft size-thumbnail"><img loading="lazy" decoding="async" width="175" height="256" src="https://wadetregaskis.com/wp-content/uploads/2018/10/XQD-Lexar-128-GB-175x256.webp" alt="" class="wp-image-4225" srcset="https://wadetregaskis.com/wp-content/uploads/2018/10/XQD-Lexar-128-GB-175x256.webp 175w, https://wadetregaskis.com/wp-content/uploads/2018/10/XQD-Lexar-128-GB-699x1024.webp 699w, https://wadetregaskis.com/wp-content/uploads/2018/10/XQD-Lexar-128-GB-349x512.webp 349w, https://wadetregaskis.com/wp-content/uploads/2018/10/XQD-Lexar-128-GB.webp 776w, https://wadetregaskis.com/wp-content/uploads/2018/10/XQD-Lexar-128-GB-175x256@2x.webp 350w, https://wadetregaskis.com/wp-content/uploads/2018/10/XQD-Lexar-128-GB-349x512@2x.webp 698w" sizes="auto, (max-width: 175px) 100vw, 175px" /></figure>
</div>


<p>I do like the XQD format in general &#8211; the cards are fast, robust, &amp; reliable. &nbsp;Unfortunately right now they&#8217;re also the most expensive they&#8217;ve basically ever been, despite greater market demand than ever, more manufacturers than ever, the lowest commodity NAND prices in years, and broadening adoption across multiple camera brands. &nbsp;And since Nikon didn&#8217;t see fit to include an XQD card with U.S. orders &#8211; unlike their actions everywhere else on the planet &#8211; I find myself with just one XQD card for now, purchased way back when they weren&#8217;t so insanely expensive. &nbsp;And that&#8217;s a problem for a camera that can operation at 8 FPS with ~60 MB files. &nbsp;For the first time in pretty much ever, for me, this week I found myself abruptly unable to take any photos because I had no space left on any available memory card (nor any way to get the photos off wirelessly, thanks to SnapBridge&#8217;s refusal to transfer raws, and WTU&#8217;s inoperability as commented on above).</p>



<p>So that&#8217;s unpleasant. &nbsp;It appears for the foreseeable future I&#8217;m going to have to live with this problem, and do my best to mitigate it &#8211; at least until XQD card prices come down dramatically, to something more sensible. &nbsp;While I don&#8217;t really care about the lack of a second slot, the lack of an <em>SD</em> slot is a big problem given where the XQD market is right now.</p>



<p>Also, for the Sony fans that think the a7r III is superior specifically because it has two memory card slots &#8211; no, it doesn&#8217;t. &nbsp;Only one of those slots supports UHS-II. &nbsp;The other slot is basically useless, given how slow UHS-I is. &nbsp;I have absolutely no use cases where I could reasonably make use of a UHS-I slot, in a 46 MP camera. &nbsp;The Z7&#8217;s XQD slot is capable of&nbsp;<em>much</em> higher speeds than UHS-II. &nbsp;Alas only for a king&#8217;s ransom, currently.</p>



<h2 class="wp-block-heading">24-70/4</h2>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1000" height="746" src="https://wadetregaskis.com/wp-content/uploads/2018/10/Z24-70_4_angle3.high_.webp" alt="" class="wp-image-4226" srcset="https://wadetregaskis.com/wp-content/uploads/2018/10/Z24-70_4_angle3.high_.webp 1000w, https://wadetregaskis.com/wp-content/uploads/2018/10/Z24-70_4_angle3.high_-256x191.webp 256w, https://wadetregaskis.com/wp-content/uploads/2018/10/Z24-70_4_angle3.high_-512x382.webp 512w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /></figure>
</div>


<p>Nikon (and many reviewers) made kind of a big deal about how small they believe this lens is. &nbsp;It&#8217;s a fairly small lens I suppose, though not remotely as tiny as the 18-55s you get with Nikon&#8217;s DX DSLRs, despite having a similar focal length &amp; aperture range. &nbsp;It&#8217;s not that much smaller, volume-wise, than the 16-80/2.8-4, despite the latter&#8217;s much wider focal length range <em>and</em> wider aperture (albeit without full-frame coverage, of course). &nbsp;Maybe that&#8217;s an unfair comparison &#8211; certainly I&#8217;m more familiar with DX lenses in this focal range, than FX ones. &nbsp;I don&#8217;t know how it compares with 24-105/4 or 24-120/4 kit lenses of yesteryear.</p>



<p>Regardless, I&#8217;m not impressed by its size at all. &nbsp;Not that I think it&#8217;s too big &#8211; I&#8217;d actually much prefer it be bigger and have a better focal length range (e.g. 24-120), or a bigger aperture (e.g. f/2.8). &nbsp;I&#8217;m interested to see the Z-mount 24-70/2.8 next year.</p>



<p>I can&#8217;t comment on its optical quality yet &#8211; I haven&#8217;t reviewed enough photos. &nbsp;Certainly it&#8217;s a big net win over my D500 with pretty much any lens, in terms of sharpness, though the massive sensor resolution difference is presumably the biggest factor in that.</p>



<p>Its weather sealing seems pretty poor &#8211; I seem to recall Nikon asserting that it has pretty good weather sealing, yet within seconds of its first use, cat hair was getting inside it through the telescoping barrel. &nbsp;I definitely would not use this lens in a wet, dusty, or hairy environment if I could avoid it.</p>



<p>One small but odd note &#8211; the lens hood is surprisingly difficult to attach, whether in use or in inverted stowage mode. &nbsp;The last bit of rotation &#8211; to get it to &#8216;click&#8217; on securely &#8211; requires a surprising amount of force, so much so that I&#8217;m really worried I&#8217;m going to wrench the lens in half. &nbsp;I&#8217;ve had a few lenses in the past where this operation required a bit more force than I&#8217;d like, but none nearly so bad as this one. &nbsp;It makes me wonder if I&#8217;ve got a dud copy of the lens hood, or somesuch.</p>



<p>(it also made me, upon first attempt, spin the hood around about five times look for the latch release button that it must surely have had, given the resistance &#8211; kind of like rotating a USB type A plug six times to permute it through the four-dimensional space it exists in, in order to get it to plug in successfully in our three-dimensional space)</p>



<h2 class="wp-block-heading">Overall opinion so far</h2>



<p>I&#8217;m not returning the Z7 yet. &nbsp;I actually don&#8217;t expect that I will &#8211; despite its many shortcomings, I think it&#8217;ll still work well for some of my intended uses. &nbsp;I&#8217;m definitely not selling my D500 any time soon, though.</p>



<p>I guess the simplest expression of my feelings is to say that: &nbsp;I&#8217;m not angry with you Nikon &#8211; I&#8217;m just disappointed.</p>



<p>The Z7&nbsp;<em>should</em> have been a tour de force entrance into mirrorless for Nikon, leveraging their class-leading DSLRs to launch an unbeatable mirrorless camera. &nbsp;They seemed to have all the advantages &amp; resources they needed. &nbsp;That they&#8217;ve fallen short of that, and produced merely a decent mirrorless camera, is hugely disappointing.</p>



<p>I didn&#8217;t even cover some the features that are missing entirely &#8211; e.g. sensor shift image stacking.</p>



<p>I&#8217;d like to hold onto hope that Nikon will fix a lot of these issues, and add the more glaring missing features, in a future firmware update. &nbsp;They technically could, at least in some cases. &nbsp;However, that would be a dramatic departure from their modus operandi to date. &nbsp;A hugely positive one, for sure &#8211; but just as they seem to have not quite known what they were doing in designing the Z7, I fear they also don&#8217;t really know what they&#8217;re doing with their firmware strategy.</p>



<p>FWIW, here&#8217;s my bug fix / feature enhancement list, roughly in descending order of importance:</p>



<ol class="wp-block-list">
<li>Fix the AF system so it actually works.</li>



<li>Fix the AF interface to not be so hard to use.</li>



<li>Fix video focus so that it works well, and doesn&#8217;t imitate a mediocre contrast-based system.</li>



<li>Fix the unusually long delay in the viewfinder turning on.</li>



<li>Fix focus peaking so that it&#8217;s actually enabled when it&#8217;s enabled.</li>



<li>Support clipping warnings (zebra stripes) in photo mode.</li>



<li>Fix the flickering in picture review during panning.</li>



<li>Warn about banding in silent mode shooting under flickering lights.</li>



<li>Reconsider control placement, and the general size of the grip re. its current diminutive stature.</li>



<li>Fix the control lag.</li>



<li>Fix SnapBridge to support NEFs.</li>



<li>Make the Wireless Transmitter Utility actually work.</li>



<li>Customisable Display modes.</li>
</ol>



<p>These are of course just limited to basically fixing the obvious shortcomings &amp; bugs the Z7 currently has &#8211; it&#8217;s a much longer list if we incorporate &#8216;wishlist&#8217; items like leading-edge video capabilities (8-bit H.264 video, in 2018? &nbsp;Come on…).</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/nikon-z7-second-first-impressions/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2018/10/Z7_front.high_.webp" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">4208</post-id>	</item>
		<item>
		<title>Lightroom &#8220;Classic&#8221; doesn&#8217;t play well with others</title>
		<link>https://wadetregaskis.com/lightroom-classic-doesnt-play-well-with-others/</link>
					<comments>https://wadetregaskis.com/lightroom-classic-doesnt-play-well-with-others/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Sat, 21 Oct 2017 16:16:57 +0000</pubDate>
				<category><![CDATA[Photography]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[HDR]]></category>
		<category><![CDATA[Lightroom]]></category>
		<category><![CDATA[performance]]></category>
		<category><![CDATA[Time Machine]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=3972</guid>

					<description><![CDATA[So far the new &#8220;Classic&#8221; Lightroom looks &#38; feels mostly identical to the prior version(s), which isn&#8217;t really a compliment, but could be worse. &#160;There&#8217;s no apparent performance improvements, that&#8217;s for sure, so as expected Adobe&#8217;s promises to suddenly learn how to write efficient &#38; performant software, well… at least their marketing department gave it&#8230; <a class="read-more-link" href="https://wadetregaskis.com/lightroom-classic-doesnt-play-well-with-others/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>So far the new &#8220;Classic&#8221; Lightroom looks &amp; feels mostly identical to the prior version(s), which isn&#8217;t really a compliment, but could be worse. &nbsp;There&#8217;s no apparent performance improvements, that&#8217;s for sure, so as expected Adobe&#8217;s promises to suddenly learn how to write efficient &amp; performant software, well… at least their marketing department gave it the college try.</p>



<p>One thing I have very quickly discovered, however, is that Lightroom &#8220;Classic&#8221;&nbsp;<em>deliberately</em> chooses not to perform some functions if it is le tired. &nbsp;Or it thinks your computer is le tired. &nbsp;By which I mean, if there is pretty much&nbsp;<em>anything</em> else running and consuming CPU time (and/or RAM?), it refuses to even attempt some operations. &nbsp;HDR merges is the first one I hit. &nbsp;I was a bit flummoxed by it just happily queuing up a number of HDR merge operations, and them just sitting there in its queue, with no indication of error &#8211; just never executing.</p>



<p>Only after I quit or disabled a bunch of other processes &#8211; any and all that were using any measurable CPU time &#8211; did it finally, about ten seconds later, decide that it was now willing to consider my &#8216;requests&#8217;.</p>



<p>#%@!ing fussy little turd.</p>



<p>It&#8217;s worth noting that it&#8217;s not the only popular app, on macOS, that does this same bullshit. &nbsp;Time Machine is another big one. &nbsp;At least in Time Machine&#8217;s case I can see a more plausible line of reasoning behind it, even if it is misguided &#8211; the user&#8217;s&nbsp;<em>probably</em> not explicitly waiting for a Time Machine backup to complete. &nbsp;As in, not all the time. &nbsp;Sometimes they are. And they certainly expect backups to&nbsp;<em>happen at all</em>, which on a consistently busy machine simply&nbsp;<em>doesn&#8217;t</em> happen. &nbsp;So Time Machine&#8217;s reluctance to function on a working machine is still stupid overall. &nbsp;But Lightroom refusing to complete a&nbsp;<em>user initiated, user-interactive, and user-blocking</em> operation, is just patently stupid by its very notion.</p>



<p><strong>Update</strong>:  Worse, now it doesn&#8217;t work <em>at all</em>.  And a quick web search shows <a href="https://web.archive.org/web/20200805043215/https://feedback.photoshop.com/photoshop_family/topics/lightroom-classic-cc-photo-merge-not-working-on-mac" data-wpel-link="external" target="_blank" rel="external noopener">many</a> <a href="https://web.archive.org/web/20190604155342/https://feedback.photoshop.com/photoshop_family/topics/merge-to-hdr-simply-doesnt-work" data-wpel-link="external" target="_blank" rel="external noopener">other people</a> having the same problem, and Adobe as usual doing nothing about it.</p>



<p>Incidentally, I tried to log in to Adobe&#8217;s forums in order to &#8216;Me too&#8217; those issues, only it won&#8217;t let me log in anymore, falsely claiming my password is invalid. &nbsp;Good job, Adobe, good job.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/lightroom-classic-doesnt-play-well-with-others/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">3972</post-id>	</item>
		<item>
		<title>Your system has run out of application memory HUR HUR HUR</title>
		<link>https://wadetregaskis.com/your-system-has-run-out-of-application-memory-hur-hur-hur/</link>
					<comments>https://wadetregaskis.com/your-system-has-run-out-of-application-memory-hur-hur-hur/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Tue, 16 May 2017 17:22:47 +0000</pubDate>
				<category><![CDATA[Ramblings]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[deadlock]]></category>
		<category><![CDATA[disk space]]></category>
		<category><![CDATA[hung]]></category>
		<category><![CDATA[killall]]></category>
		<category><![CDATA[lies]]></category>
		<category><![CDATA[Lightroom]]></category>
		<category><![CDATA[macOS]]></category>
		<category><![CDATA[paging]]></category>
		<category><![CDATA[paused]]></category>
		<category><![CDATA[RAM]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[SIGCONT]]></category>
		<category><![CDATA[SIGSTOP]]></category>
		<category><![CDATA[storage]]></category>
		<category><![CDATA[What do you want?]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=3913</guid>

					<description><![CDATA[I hate this dialog with the fire&#160;of a thousand suns. When this appears, it basically means one (or both) of two things: Quitting any of the listed applications is rarely the correct move. &#160;It&#8217;s often enough the case that none of them are the root cause, and you can kill all of them if you&#8230; <a class="read-more-link" href="https://wadetregaskis.com/your-system-has-run-out-of-application-memory-hur-hur-hur/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="1078" height="944" src="https://wadetregaskis.com/wp-content/uploads/2017/05/22Your-system-has-run-out-of-application-memory22-dialog.webp" alt="" class="wp-image-3914" style="width:539px" srcset="https://wadetregaskis.com/wp-content/uploads/2017/05/22Your-system-has-run-out-of-application-memory22-dialog.webp 1078w, https://wadetregaskis.com/wp-content/uploads/2017/05/22Your-system-has-run-out-of-application-memory22-dialog-512x448@2x.webp 1024w, https://wadetregaskis.com/wp-content/uploads/2017/05/22Your-system-has-run-out-of-application-memory22-dialog-256x224.webp 256w, https://wadetregaskis.com/wp-content/uploads/2017/05/22Your-system-has-run-out-of-application-memory22-dialog-512x448.webp 512w" sizes="auto, (max-width: 1078px) 100vw, 1078px" /></figure>
</div>


<p>I hate this dialog with the fire&nbsp;of a thousand suns.</p>



<p>When this appears, it basically means one (or both) of two things:</p>



<ol class="wp-block-list">
<li>Some application went nuts and chewed through all your memory and/or disk space.</li>



<li>macOS got itself into a darkly comical &amp; embarrassing deadlock.</li>
</ol>



<p>Quitting any of the listed applications is rarely the correct move. &nbsp;It&#8217;s often enough the case that none of them are the root cause, and you can kill all of them if you want, but it won&#8217;t fix the problem.</p>



<p>One important thing to clarify first, though, is that this dialog does&nbsp;<em>not</em>&nbsp;necessarily use the term &#8216;memory&#8217; in the conventional sense &#8211; i.e. RAM. &nbsp;It&nbsp;can&nbsp;<em>also</em>&nbsp;refer to disk space. &nbsp;Unfortunately it doesn&#8217;t bother to distinguish between the two, which is particularly stupid of it since any possible resolution of the issue is&nbsp;<em>highly</em> dependent on which of the two cases it in fact is.</p>



<p>Thank goodness for iStatMenus, though, which in the most recent incident showed that I had ~20 GiB of RAM completely free (not even inactive, actually outright free). &nbsp;So immediately that rules out what the daft bloody dialog&#8217;s actually saying.</p>



<p>The worst thing about all this is when it&#8217;s #2 the occurs. &nbsp;For example, I had Lightroom do a 63-image panorama merge. &nbsp;As Lightroom is a gross memory pig when doing panorama merging, it consumed something like 40 GiB of memory. &nbsp;Which caused a bunch of stuff to page&nbsp;to disk. &nbsp;Which consumed all the disk space. &nbsp;Which led to that obnoxious dialog. &nbsp;Which&nbsp;<em>further</em> led to macOS in its infinite fucking wisdom &#8216;pausing&#8217; (SIGSTOPing) almost all running programs,&nbsp;<em>including</em> evidently whatever daemon actually handles paging. &nbsp;Thus when Lightroom actually completed the panorama merge&nbsp;and released all that memory, I now had 20 GiB of free memory and the system refused to use any of it to page back in all that memory it&#8217;d paged out. &nbsp;Because it was out of disk space.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="499" height="332" src="https://wadetregaskis.com/wp-content/uploads/2024/01/Tense.avif" alt="" class="wp-image-7320"/></figure>
</div>


<p>The only solution &#8211; short of hard rebooting and hoping it resolves itself &#8211; was to delete a bunch of files I actually do still&nbsp;want, but which will now have to be&nbsp;recovered from a backup. &nbsp;Great job macOS, thanks for all your help.</p>



<p>Of course, even once you do that and recover the system from the derpeche mode it put itself into, it won&#8217;t actually&nbsp;<em>unpause</em> any of the shit it broke. &nbsp;You have to do that manually. &nbsp;It pretends you can do that via that dialog that started the whole thing &#8211; assuming you left it open the entire time, blocking your view as you <em>actually</em> help the situation &#8211;&nbsp;but that only shows user-visible applications, not all the other system &amp; background processes that it&nbsp;<em>also</em> rudely halted.</p>



<p>So, simple tip for resuming everything:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><code>sudo killall -CONT -m '.'</code></p>
</blockquote>



<p>Elegant, after a fashion. &nbsp;Though every time, it reminds me that whomever named it &#8216;killall&#8217; was either not very friendly or not very wise.</p>



<p>Note that the system will probably still be a bit broken in places, as despite what macOS thinks, you can&#8217;t just blindly pause random system tasks and not have things get really, really confused. &nbsp;A reboot is always wise after seeing this dialog, to properly undo its fuckery.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/your-system-has-run-out-of-application-memory-hur-hur-hur/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2017/05/22Your-system-has-run-out-of-application-memory22-dialog.webp" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">3913</post-id>	</item>
		<item>
		<title>iOS Family Sharing users cannot mix authentication schemes</title>
		<link>https://wadetregaskis.com/ios-family-sharing-users-cannot-mix-authentication-schemes/</link>
					<comments>https://wadetregaskis.com/ios-family-sharing-users-cannot-mix-authentication-schemes/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Sat, 15 Apr 2017 19:51:04 +0000</pubDate>
				<category><![CDATA[Ramblings]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[AppleID]]></category>
		<category><![CDATA[authentication]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[Family Sharing]]></category>
		<category><![CDATA[iOS]]></category>
		<category><![CDATA[iPad]]></category>
		<category><![CDATA[Snafu]]></category>
		<category><![CDATA[Two-factor]]></category>
		<category><![CDATA[Two-step]]></category>
		<category><![CDATA[Undocumented]]></category>
		<category><![CDATA[What do you want?]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=3898</guid>

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



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



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



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


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


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


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


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



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



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



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



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



<p>Which would be broadly fine, if Apple hadn&#8217;t made it so needlessly&nbsp;complicated, and the two systems so&nbsp;incompatible that their own software can&#8217;t figure out what&#8217;s going on.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/ios-family-sharing-users-cannot-mix-authentication-schemes/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">3898</post-id>	</item>
		<item>
		<title>#if DEBUG in Swift</title>
		<link>https://wadetregaskis.com/if-debug-in-swift/</link>
					<comments>https://wadetregaskis.com/if-debug-in-swift/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Sat, 14 Jan 2017 21:15:33 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Broken by design]]></category>
		<category><![CDATA[clang]]></category>
		<category><![CDATA[Sad]]></category>
		<category><![CDATA[Swift]]></category>
		<category><![CDATA[Undocumented]]></category>
		<guid isPermaLink="false">https://blog.wadetregaskis.com/?p=3853</guid>

					<description><![CDATA[Sigh. The Swift team give an impeccable impression of a group of people who&#8217;ve never actually tried to use Swift. An incredibly basic compiler task is to provide code a way to distinguish between debug &#38; release builds, in order that it can&#160;behave accordingly (e.g. change the default logging verbosity, change asserts from fatal to&#8230; <a class="read-more-link" href="https://wadetregaskis.com/if-debug-in-swift/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>Sigh.</p>



<p>The Swift team give an impeccable impression of a group of people who&#8217;ve never actually tried to use Swift.</p>



<p>An incredibly basic compiler task is to provide code a way to distinguish between debug &amp; release builds, in order that it can&nbsp;behave accordingly (e.g. change the default logging verbosity, change asserts from fatal to non-fatal, etc).</p>



<p>Long story short there is no way to do this that works correctly with <code>swift build</code>.</p>



<p>You can make it work with Xcode <em>only</em>&nbsp;by way of a simple workaround &#8211; you manually define a custom Swift flag in your target&#8217;s settings (<a href="https://stackoverflow.com/questions/24003291/ifdef-replacement-in-the-swift-language/36502874#36502874" data-wpel-link="external" target="_blank" rel="external noopener">here&#8217;s one</a> of a bajillion explanations of how do this). &nbsp;You end up with basically identical code to other C-family languages, 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">#</span><span style="color: #AF00DB">if</span><span style="color: #0000FF"> DEBUG</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">let</span><span style="color: #000000"> logVerbosity = </span><span style="color: #098658">1</span></span>
<span class="line"><span style="color: #0000FF">#</span><span style="color: #AF00DB">else</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">let</span><span style="color: #000000"> logVerbosity = </span><span style="color: #098658">0</span></span>
<span class="line"><span style="color: #0000FF">#</span><span style="color: #AF00DB">endif</span></span></code></pre></div>



<p>But there is no way, when using <code>swift build</code>, to specify custom Swift flags in your package config.</p>



<p>You can specify them manually with every single <code>swift build</code> invocation, e.g.:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><code>swift build -c debug -Xswiftc '-DDEBUG'</code></p>
</blockquote>



<p>But now you have extra work and the possibility of screwing it up (e.g. omitting the flag, or mismatching it to your actual build style).</p>



<p>The closest you can get is to use some undocumented, hidden Swift internal library functions:</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">func</span><span style="color: #000000"> </span><span style="color: #795E26">_isDebugAssertConfiguration</span><span style="color: #000000">() -&gt; </span><span style="color: #267F99">Bool</span></span>
<span class="line"><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">_isFastAssertConfiguration</span><span style="color: #000000">() -&gt; </span><span style="color: #267F99">Bool</span></span></code></pre></div>



<p>These are defined in <a href="https://github.com/apple/swift/blob/adc54c8a4d13fbebfeb68244bac401ef2528d6d0/stdlib/public/core/AssertCommon.swift" data-wpel-link="external" target="_blank" rel="external noopener">swift/stdlib/public/core/AssertCommon.swift</a>.</p>



<p>Only the first is likely to be useful. &nbsp;The second applies in the case where you&#8217;re building not just for release but&nbsp;<em>unchecked</em> (<code>-Ounchecked</code>). &nbsp;If you just want to conditionalise on release builds generally, you have to do <code>!_isDebugAssertConfiguration()</code>.</p>



<p>The additional problem with this approach is that these are then, in your code,&nbsp;<em>runtime</em> checks. &nbsp;And the compiler then thinks it&#8217;s being helpful by pointing out that some of your code that uses them is unreachable.</p>



<p>And of course Swift has absolutely no way to silence compiler warnings, or otherwise tell the compiler not to trust its reachability checks.</p>



<p>Sigh.</p>



<h2 class="wp-block-heading">Update (February 2018)</h2>



<p>While the above method &#8211; using the <code>_isDebugAssertConfiguration()</code> method &#8211; does still work at time of writing, in Swift 4.1, there are some marginally better ways now available. &nbsp;There&#8217;s still not a&nbsp;<em>proper</em> solution, infuriatingly, but with some creativity, as you&#8217;ll see momentarily, you can get pretty close to expected functionality.</p>



<h3 class="wp-block-heading">First alternative</h3>



<p>This will only suit some cases, but its relative cleanliness &amp; simplicity makes it appealing when it is an option.</p>



<p>You can use&nbsp;<code>#if targetEnvironment(simulator)</code>. &nbsp;This of course only distinguishes between the simulator and a real iDevice environment, though that can often be useful too, perhaps orthogonally to DEBUG vs RELEASE concepts.</p>



<h3 class="wp-block-heading">Second alternative</h3>



<p>Wrap all uses of <code>_isDebugAssertConfiguration()</code> inside a minimal set of functions. &nbsp;All this gets you is a reduced (and fixed) number of unreachable code warnings, though.</p>



<p>For example:</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">func</span><span style="color: #000000"> </span><span style="color: #795E26">inDebugBuilds</span><span style="color: #000000">(</span><span style="color: #795E26">_</span><span style="color: #000000"> </span><span style="color: #001080">code</span><span style="color: #000000">: () -&gt; </span><span style="color: #267F99">Void</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: #795E26">_isDebugAssertConfiguration</span><span style="color: #000000">() {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #795E26">code</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: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">inReleaseBuilds</span><span style="color: #000000">(</span><span style="color: #795E26">_</span><span style="color: #000000"> </span><span style="color: #001080">code</span><span style="color: #000000">: () -&gt; </span><span style="color: #267F99">Void</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: #795E26">_isDebugAssertConfiguration</span><span style="color: #000000">() {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #795E26">code</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>



<p>You can then use these in a fairly streamlined way:</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">inDebugBuilds {</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, I only greet in debug builds!&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">inReleaseBuilds {</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;While I only greet in release builds - aloha!&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<h3 class="wp-block-heading">Third alternative</h3>



<p>It&#8217;s possible to do one better than the above, and eliminate those unreachable code warnings entirely. &nbsp;Plus, doing so actually removes any use of unofficial APIs… but it requires abusing the official API a bit.</p>



<p>The built-in <code>assert()</code> method is implemented atop&nbsp;<code>_isDebugAssertConfiguration()</code> just the same as <code>inDebugBuilds()</code> is, above. &nbsp;However, because it&#8217;s part of the Swift standard library, <em>you</em> don&#8217;t have to be concerned about its implementation, its use of private / undocumented language, compiler, or library features &#8211; that&#8217;s up to the Swift compiler team. &nbsp;Most importantly, use of it in your code doesn&#8217;t emit an unreachable code warning.</p>



<p>So we can do something like:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">inDebugBuilds</span><span style="color: #000000">(</span><span style="color: #795E26">_</span><span style="color: #000000"> </span><span style="color: #001080">code</span><span style="color: #000000">: () -&gt; </span><span style="color: #267F99">Void</span><span style="color: #000000">) {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">assert</span><span style="color: #000000">({ </span><span style="color: #795E26">code</span><span style="color: #000000">(); </span><span style="color: #AF00DB">return</span><span style="color: #000000"> </span><span style="color: #0000FF">true</span><span style="color: #000000"> }())</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>Ugly and obtuse, but functional and reasonably expected to work in all future versions of Swift.</p>



<p>You can similarly create a variant for release-build-only code, though it&#8217;s even hackier:</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">func</span><span style="color: #000000"> </span><span style="color: #795E26">inReleaseBuilds</span><span style="color: #000000">(</span><span style="color: #795E26">_</span><span style="color: #000000"> </span><span style="color: #001080">code</span><span style="color: #000000">: () -&gt; </span><span style="color: #267F99">Void</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"> skip: </span><span style="color: #267F99">Bool</span><span style="color: #000000"> = </span><span style="color: #0000FF">false</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">assert</span><span style="color: #000000">({ skip = </span><span style="color: #0000FF">true</span><span style="color: #000000">; </span><span style="color: #AF00DB">return</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: #AF00DB">if</span><span style="color: #000000"> !skip {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #795E26">code</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>



<p>Icky, but it works. &nbsp;On the upside, you only have to define this ugliness in one place in your module, and then you can try to forget about its implementation.&nbsp;😝</p>



<p>One caveat is that I don&#8217;t know if the above approach will properly strip out the unreachable code from your built binaries.</p>



<p>Another caveat with these is that since you&#8217;re invoking a function, you can&#8217;t do a natural <code>if … else …</code> pattern &#8211; instead you need explicit, distinct <code>inDebugBuilds</code> &amp; <code>inReleaseBuilds</code> blocks. &nbsp;So it can&#8217;t be completely like the vanilla <code>#if DEBUG … #else …</code> that C-family languages have had since before the dinosaurs. &nbsp;You could create versions that take two closures as parameters &#8211; one for the affirmative case, one for the other… the trade-off is that your invocations are then a little more verbose and obviously function-cally, 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">func</span><span style="color: #000000"> </span><span style="color: #795E26">forBuildStyle</span><span style="color: #000000">(</span><span style="color: #795E26">debug</span><span style="color: #000000"> </span><span style="color: #001080">debugCode</span><span style="color: #000000">: () -&gt; </span><span style="color: #267F99">Void</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                    </span><span style="color: #795E26">release</span><span style="color: #000000"> </span><span style="color: #001080">releaseCode</span><span style="color: #000000">: () -&gt; </span><span style="color: #267F99">Void</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"> debugBuild: </span><span style="color: #267F99">Bool</span><span style="color: #000000"> = </span><span style="color: #0000FF">false</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">assert</span><span style="color: #000000">({ debugBuild = </span><span style="color: #0000FF">true</span><span style="color: #000000">; </span><span style="color: #AF00DB">return</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: #AF00DB">if</span><span style="color: #000000"> debugBuild {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #795E26">debugCode</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">releaseCode</span><span style="color: #000000">()</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #795E26">forBuildStyle</span><span style="color: #000000">(</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">debug</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;I&#39;m built for debuggin&#39;!&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 style="color: #795E26">release</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;I&#39;m built for the wild!&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></code></pre></div>



<p>Up to your personal preferences as to which API you adopt, and which implementation you choose (streamlined but private-Swift-bits-dependent with bonus unnecessary compiler warnings, or official-APIs-only but hacky).</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/if-debug-in-swift/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">3853</post-id>	</item>
	</channel>
</rss>
