<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: TrozWare</title>
    <description>The latest articles on DEV Community by TrozWare (@trozware).</description>
    <link>https://dev.to/trozware</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F6585%2Fa08114ca-ea07-4f37-ba42-79c00b70b6a8.png</url>
      <title>DEV Community: TrozWare</title>
      <link>https://dev.to/trozware</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/trozware"/>
    <language>en</language>
    <item>
      <title>Moving from Process to Subprocess</title>
      <dc:creator>TrozWare</dc:creator>
      <pubDate>Thu, 27 Nov 2025 23:07:22 +0000</pubDate>
      <link>https://dev.to/trozware/moving-from-process-to-subprocess-4408</link>
      <guid>https://dev.to/trozware/moving-from-process-to-subprocess-4408</guid>
      <description>&lt;p&gt;For many years, I've used &lt;code&gt;Process&lt;/code&gt; to call Terminal commands from my macOS apps. &lt;code&gt;Process&lt;/code&gt; is an old technology, formerly known as &lt;code&gt;NSTask&lt;/code&gt;. It works, but it's complicated to set up and it can have issues. The Swift language team have now published a modern alternative called &lt;a href="https://github.com/swiftlang/swift-subprocess" rel="noopener noreferrer"&gt;&lt;code&gt;Subprocess&lt;/code&gt;&lt;/a&gt;. Since I'm currently using &lt;code&gt;Process&lt;/code&gt; in my &lt;a href="https://troz.net/manreader/" rel="noopener noreferrer"&gt;Man Reader&lt;/a&gt; app and in my &lt;a href="https://troz.net/books/macos_apps_step_by_step/" rel="noopener noreferrer"&gt;macOS Apps Step by Step&lt;/a&gt; book, I thought it was time to assess the new option and see if I should swap to it.&lt;/p&gt;

&lt;p&gt;I started by creating a sample project using the macOS App template. Then I added the package dependency by searching for &lt;code&gt;https://github.com/swiftlang/swift-subprocess&lt;/code&gt;. This also adds &lt;code&gt;swift-system&lt;/code&gt; which the ReadMe says &lt;em&gt;provides idiomatic interfaces to system calls and low-level currency types&lt;/em&gt;. Next, I removed the &lt;strong&gt;App Sandbox&lt;/strong&gt; in &lt;strong&gt;Target -&amp;gt; Signing &amp;amp; Capabilities&lt;/strong&gt;. Man Reader gets round the sandbox restrictions using temporary entitlements and security-scoped bookmarks, but I didn't want to complicate my tests with those. I set up my sample app with two tabs so I could directly compare &lt;code&gt;Subprocess&lt;/code&gt; and &lt;code&gt;Process&lt;/code&gt;. Each tab has the same buttons that run the same Terminal commands, but using the two different methods. I also switched the build settings to use &lt;strong&gt;Swift 6&lt;/strong&gt; as I am gradually adopting this in all my projects.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F06q5dysqbuuf4oqy4x01.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F06q5dysqbuuf4oqy4x01.png" alt="Subprocess Test App" width="800" height="485"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first step was to try a simple command with no parameters. In &lt;strong&gt;SubprocessView.swift&lt;/strong&gt; , I imported the &lt;code&gt;Subprocess&lt;/code&gt; library, and set the &lt;strong&gt;whoami&lt;/strong&gt; button to call this method using a &lt;code&gt;Task&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;runWhoami&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"whoami"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nv"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"no result from whoami"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is based on the first example in the Subprocess ReadMe. It uses &lt;code&gt;ls&lt;/code&gt; but I used &lt;code&gt;whoami&lt;/code&gt; in my book, so I changed to that. I resumed the Live Preview, clicked the button, and the Canvas console showed this, which I've split on to two lines here for readability:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CollectedResult, DiscardedOutput&amp;gt;(processIdentifier: 14711,
terminationStatus: exited(0), standardOutput: Optional("sarah\n"), standardError: ())
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are immediate benefits to using Subprocess. I didn't have to specify the full path to the &lt;code&gt;whoami&lt;/code&gt; command, and I didn't have to set up a pipe and a file handle to see the result. The type of &lt;code&gt;result&lt;/code&gt; is a &lt;code&gt;CollectedResult&lt;/code&gt; which contains the &lt;code&gt;processIdentifier&lt;/code&gt;, &lt;code&gt;terminationStatus&lt;/code&gt;, &lt;code&gt;standardOutput&lt;/code&gt; and &lt;code&gt;standardError&lt;/code&gt;. A &lt;code&gt;terminationStatus&lt;/code&gt; of 0 means the command completed successfully.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt; : If I had wanted to use a particular version of &lt;code&gt;whoami&lt;/code&gt;, I could have specified the full path like this:&lt;/p&gt;


&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/usr/bin/whoami"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="nv"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;This option requires &lt;code&gt;import System&lt;/code&gt; in order to have access to the &lt;code&gt;FilePath&lt;/code&gt; type.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To get the output I wanted into a &lt;code&gt;String&lt;/code&gt;, I added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;standardOutput&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"unknown"&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"whoami: &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next I wanted to test sending arguments. I added a new &lt;strong&gt;ping&lt;/strong&gt; button and set it to call this method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;runPing&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;commandName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ping"&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;arguments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"-c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"apple.com"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;commandName&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nv"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;standardOutput&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"no result from ping"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's when I found out that &lt;code&gt;arguments&lt;/code&gt; must be an array of &lt;code&gt;Arguments&lt;/code&gt;, not &lt;code&gt;Strings&lt;/code&gt;. This was solved by changing the second setup line to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;arguments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Arguments&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s"&gt;"-c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"apple.com"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This worked, but there was no output for about 5 seconds, then it all appeared. Reading further down the Subprocess ReadMe I saw a way to stream the output as an &lt;code&gt;AsyncSequence&lt;/code&gt;, so after a bit of experimentation and some help from ChatGPT, I arrived at this &lt;code&gt;runPing&lt;/code&gt; method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;runPing&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;commandName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ping"&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;arguments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Arguments&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s"&gt;"-c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"apple.com"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;pingResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;commandName&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nv"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;arguments&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;execution&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;standardOutput&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;standardOutput&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trimmingCharacters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;whitespacesAndNewlines&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;pingResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Ping result = &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"No result from ping"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's essential to &lt;code&gt;await pingResult&lt;/code&gt; or the method ends before any data arrives.&lt;/p&gt;

&lt;p&gt;This worked, but I was still getting the complete output at the end, and not seeing each line as it arrived. To help in my research, I selected &lt;strong&gt;Build Documentation&lt;/strong&gt; from the &lt;strong&gt;Product&lt;/strong&gt; menu which gave me the docs for &lt;code&gt;Subprocess&lt;/code&gt; in Xcode's Developer Documentation. There are a lot of custom types, but only one function - &lt;code&gt;run&lt;/code&gt; - which has 14 different versions. Seven of these variants expect an &lt;code&gt;Executable&lt;/code&gt;, which is what I've been using when I specify a &lt;code&gt;.name&lt;/code&gt;. The others expect a &lt;code&gt;Configuration&lt;/code&gt; which is a way of combining an executable and its arguments plus other configuration details, into a single object.&lt;/p&gt;

&lt;p&gt;I also discovered that the versions that streamed data had a &lt;code&gt;preferredBufferSize&lt;/code&gt; setting. The docs say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Larger buffer sizes may improve performance for subprocesses that produce large amounts of output, while smaller buffer sizes may reduce memory usage and improve responsiveness for interactive applications.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I set the &lt;code&gt;preferredBufferSize&lt;/code&gt; to 32 and watched the lines come in one by one. It would be great if there was an option to buffer until a line feed, but this is workable.&lt;/p&gt;

&lt;p&gt;This basically covers how I use &lt;code&gt;Process&lt;/code&gt; in &lt;a href="https://troz.net/books/macos_apps_step_by_step/" rel="noopener noreferrer"&gt;macOS Apps Step by Step&lt;/a&gt;, so now on to what &lt;a href="https://troz.net/manreader/" rel="noopener noreferrer"&gt;Man Reader&lt;/a&gt; needs. It uses &lt;code&gt;find&lt;/code&gt; to search for man pages, &lt;code&gt;man&lt;/code&gt; with a &lt;code&gt;-w&lt;/code&gt; argument to find the path to a man page, and &lt;code&gt;mandoc&lt;/code&gt; with &lt;code&gt;col&lt;/code&gt; to get the formatted page data. This adds two new challenges: &lt;code&gt;find&lt;/code&gt; will return a &lt;em&gt;lot&lt;/em&gt; of data and &lt;code&gt;mandoc&lt;/code&gt; needs to pipe its output to &lt;code&gt;col&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Starting with &lt;code&gt;find&lt;/code&gt;, Man Reader searches the &lt;code&gt;/opt&lt;/code&gt; and &lt;code&gt;/usr&lt;/code&gt; directories by default. On my system, &lt;code&gt;/opt&lt;/code&gt; contains 32504 man pages and &lt;code&gt;/usr&lt;/code&gt; has 2979. For both of these, I was able to use the basic form of &lt;code&gt;run&lt;/code&gt;, but I set the output string limit to &lt;code&gt;Int.max&lt;/code&gt;. This is &lt;em&gt;way&lt;/em&gt; more than necessary, but better to be safe.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;findManPages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;commandName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"find"&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;arguments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Arguments&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-path"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"*/man/*.*"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;commandName&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;standardOutput&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;components&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;separatedBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;newlines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"found &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt; man pages in &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"string length = &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"error finding man pages in &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time, I used a &lt;code&gt;Configuration&lt;/code&gt; to assemble the command and its arguments, then passed that to the appropriate version of &lt;code&gt;run&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I tested streaming the results, but that was more CPU intensive, even when using the default &lt;code&gt;preferredBufferSize&lt;/code&gt;. I added some timing routines so I could compare it to the &lt;code&gt;Process&lt;/code&gt; version. On my Mac, searching &lt;code&gt;/usr&lt;/code&gt; takes between 400 and 500 milliseconds, and searching &lt;code&gt;/opt&lt;/code&gt; takes between 1.2 and 2.5 seconds. Since this is all async code, the app remains totally responsive during this time.&lt;/p&gt;

&lt;p&gt;The last problem I needed to solve was how to pipe the output from one command to another. When using &lt;code&gt;Process&lt;/code&gt;, this is surprisingly easy - you create a pipe and set it as the &lt;code&gt;standardOutput&lt;/code&gt; for the first command. Then you set the same pipe as the &lt;code&gt;standardInput&lt;/code&gt; for the second command.&lt;/p&gt;

&lt;p&gt;Checking the &lt;code&gt;run&lt;/code&gt; options, I can see that some variants have an &lt;code&gt;input&lt;/code&gt; parameter. So if I get the output from the first command, I can set it as the input for the second. The command I want to run is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="n"&gt;mandoc&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="kt"&gt;T&lt;/span&gt; &lt;span class="n"&gt;ascii&lt;/span&gt; &lt;span class="sr"&gt;/usr/share/man/man1/&lt;/span&gt;&lt;span class="n"&gt;ls&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what I came up with. This has the page to the &lt;code&gt;ls&lt;/code&gt; man page hard-coded for testing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;runPipedCommands&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;manPagePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/usr/share/man/man1/ls.1"&lt;/span&gt;

  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;commandName1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"mandoc"&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;arguments1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Arguments&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s"&gt;"-T"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"ascii"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;manPagePath&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;commandName2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"col"&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;arguments2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Arguments&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s"&gt;"-b"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;output1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;commandName1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nv"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;arguments1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;outputString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;output1&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;standardOutput&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"no output from &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;commandName1&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Uncomment the next two lines to see what happens without `col`&lt;/span&gt;
  &lt;span class="c1"&gt;// print(outputString)&lt;/span&gt;
  &lt;span class="c1"&gt;// return&lt;/span&gt;

  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;output2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;commandName2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nv"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;arguments2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outputString&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nv"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;output2&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;standardOutput&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"no output from &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;commandName2&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to see why the &lt;code&gt;col -b&lt;/code&gt; part is essential, uncomment the two marked lines and you'll see a result that starts like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0urquay3h8eyslejczcr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0urquay3h8eyslejczcr.png" alt="mandoc wihout col" width="800" height="297"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The second tab has the same buttons with the same commands, but they run using &lt;code&gt;Process&lt;/code&gt;. For the &lt;code&gt;find&lt;/code&gt; command, the times are much the same, but I have to stream the incoming data as I did with &lt;code&gt;pipe&lt;/code&gt;. Without this, the app freezes and the process never completes.&lt;/p&gt;

&lt;p&gt;After these experiments, I &lt;strong&gt;will&lt;/strong&gt; be moving to &lt;code&gt;Subprocess&lt;/code&gt; in both the book and the app. Over the years I have added many tweaks, fail-safes, and workarounds to handle all the things that can go wrong with &lt;code&gt;Process&lt;/code&gt;, but it will be great to use a more modern API without these issues. Sadly, I can't use it everywhere. I have a client app that uses &lt;code&gt;Process&lt;/code&gt; but it has to support systems back to macOS 11 and it looks like&lt;code&gt;Subprocess&lt;/code&gt; only goes back to macOS 13.&lt;/p&gt;

&lt;p&gt;My sample app is available on GitHub: &lt;a href="https://github.com/trozware/subprocess-tests" rel="noopener noreferrer"&gt;https://github.com/trozware/subprocess-tests&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you have any feedback about this article, please contact me using one of the links below or through the &lt;a href="https://troz.net/contact/" rel="noopener noreferrer"&gt;Contact&lt;/a&gt; page. And if you found this useful, please &lt;a href="https://ko-fi.com/trozware" rel="noopener noreferrer"&gt;buy me a coffee&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>macos</category>
      <category>terminal</category>
      <category>process</category>
      <category>subprocess</category>
    </item>
    <item>
      <title>macOS Apps Step by Step 4.0</title>
      <dc:creator>TrozWare</dc:creator>
      <pubDate>Wed, 05 Nov 2025 23:44:23 +0000</pubDate>
      <link>https://dev.to/trozware/macos-apps-step-by-step-40-522l</link>
      <guid>https://dev.to/trozware/macos-apps-step-by-step-40-522l</guid>
      <description>&lt;p&gt;macOS by Tutorials has a new name and a new edition!&lt;/p&gt;

&lt;p&gt;The book is available for purchase or update at &lt;a href="https://sarahreichelt.gumroad.com/l/oximx" rel="noopener noreferrer"&gt;Gumroad&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;macOS Apps Step by Step&lt;/strong&gt; was previously titled &lt;strong&gt;macOS by Tutorials&lt;/strong&gt; and was released in 2022 by &lt;a href="https://www.kodeco.com/" rel="noopener noreferrer"&gt;Kodeco&lt;/a&gt;. Since they transferred the rights to me, I have released two updates under the same name, but now it feels like time to move on from the Kodeco naming convention and give it a name that is more part of my brand. After considering several options, I put a &lt;a href="https://mastodon.social/@troz/115365045162246609" rel="noopener noreferrer"&gt;poll on Mastodon&lt;/a&gt; and &lt;strong&gt;macOS Apps Step by Step&lt;/strong&gt; was the winner.&lt;/p&gt;

&lt;p&gt;If you bought the first edition of macOS by Tutorials from either Kodeco or Amazon, please &lt;a href="mailto:books@troz.net?subject=macOS%20Apps%20Step%20by%20Step%20Discount"&gt;email me&lt;/a&gt; for a 50% discount code.&lt;/p&gt;

&lt;p&gt;If you bought a previous edition of macOS by Tutorials from me via Gumroad, version 4 is a free update that you can download from your &lt;a href="https://gumroad.com/library" rel="noopener noreferrer"&gt;Gumroad library&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://troz.net/books/macos_apps_step_by_step/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwnhpwqek9ier5ikbr1ek.png" alt="Book cover" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click the image for more details, and if you’d like to check out the start of the book including the table of contents and the first chapter, you can read it online at &lt;a href="https://troz.net/books/mac_apps_sample.html" rel="noopener noreferrer"&gt;macOS Apps Step by Step Sample&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The major changes in this fourth edition include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A new title!&lt;/li&gt;
&lt;li&gt;All projects updated to include new features introduced in macOS 26 Tahoe and Xcode 26.&lt;/li&gt;
&lt;li&gt;All projects use &lt;em&gt;Swift 5 or 6&lt;/em&gt; with &lt;em&gt;Approachable Concurrency&lt;/em&gt;. Swift 5 is still the default in Xcode but I switched all the projects to Swift 6 and made sure that all the code works for both versions.&lt;/li&gt;
&lt;li&gt;The &lt;em&gt;On This Day&lt;/em&gt; app has been re-designed to use a more stable &lt;code&gt;HSplitView&lt;/code&gt; instead of the problematic &lt;code&gt;NavigationSplitView&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Icons are created using Apple's Icon Composer app.&lt;/li&gt;
&lt;li&gt;The &lt;em&gt;MarkWriter&lt;/em&gt; app uses SwiftUI's &lt;code&gt;WebView&lt;/code&gt; to display HTML content and adds the ability to work with and edit selected text.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you already own the book and have enjoyed it or found it useful, I'd really appreciate it if you could leave a rating or review at the book's page in your &lt;a href="https://gumroad.com/library" rel="noopener noreferrer"&gt;Gumroad library&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>macos</category>
      <category>swift</category>
      <category>swiftui</category>
      <category>appkit</category>
    </item>
    <item>
      <title>SwiftUI WebView</title>
      <dc:creator>TrozWare</dc:creator>
      <pubDate>Fri, 15 Aug 2025 11:11:18 +0000</pubDate>
      <link>https://dev.to/trozware/swiftui-webview-4c67</link>
      <guid>https://dev.to/trozware/swiftui-webview-4c67</guid>
      <description>&lt;p&gt;At WWDC 2025, Apple announced that SwiftUI would now have its own &lt;code&gt;WebView&lt;/code&gt;. I touched on this briefly in my &lt;a href="https://troz.net/post/2025/swiftui-mac-2025/#web-view" rel="noopener noreferrer"&gt;SwiftUI for Mac 2025&lt;/a&gt; article, but this view has a lot of features that I wanted to explore and document.&lt;/p&gt;

&lt;p&gt;My primary source was the WWDC video: &lt;a href="https://developer.apple.com/videos/play/wwdc2025/231" rel="noopener noreferrer"&gt;Meet WebKit for SwiftUI&lt;/a&gt; but as usual, there is a lot of detail hidden in the video and some of the sample code doesn't work in the later betas. I'm currently using macOS Tahoe 26 beta 7 and Xcode 26 beta 6.&lt;/p&gt;

&lt;p&gt;I've written a sample app demonstrating various aspects of the new &lt;code&gt;WebView&lt;/code&gt;, which you can download from &lt;a href="https://github.com/trozware/swiftui-webview" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. This will let you follow along with my descriptions below. The numbered sections in this article correspond to the files in the &lt;strong&gt;WebView Samples&lt;/strong&gt; folder of the project.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. WebURLView
&lt;/h3&gt;

&lt;p&gt;The simplest way to use a &lt;code&gt;WebView&lt;/code&gt; is to provide it with a URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kt"&gt;WebView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;string&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"https://www.swift.org"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The URL is optional, so there's no need to force-unwrap it.&lt;/p&gt;

&lt;p&gt;This is easy to use but doesn't allow any progress tracking or customization so I think I would rarely choose this option.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. WebPageLoad
&lt;/h3&gt;

&lt;p&gt;In the next example, I created a &lt;code&gt;WebPage&lt;/code&gt; and used it to populate the &lt;code&gt;WebView&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Toolbar buttons switch between loading an online page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;URLRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;string&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"https://troz.net"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attribution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;
&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or loading a local HTML string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bundle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resourceURL&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Apple's examples, they always use &lt;code&gt;URL(string: "about:blank")!&lt;/code&gt; for the &lt;code&gt;baseURL&lt;/code&gt;, but using &lt;code&gt;Bundle.main.resourceURL!&lt;/code&gt; lets me include a link to a stylesheet that's inside the app bundle:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnbv0qyfjlahemluro1qc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnbv0qyfjlahemluro1qc.png" alt="Web page showing local HTML" width="800" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. TrackLoad
&lt;/h3&gt;

&lt;p&gt;In the WWDC video, they demonstrated how to track the navigation events. The code in the video does not work - it doesn't even compile. But after some trial and error, I worked out how to track these events.&lt;/p&gt;

&lt;p&gt;When the view first appears, a &lt;code&gt;task&lt;/code&gt; starts monitoring the events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;startObservingEvents&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My event tracker method uses the new &lt;code&gt;Observations&lt;/code&gt; sequence to read an async stream of page navigation events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;startObservingEvents&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;eventStream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Observations&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;navigations&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;observation&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;eventStream&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;observation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;startedProvisionalNavigation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="n"&gt;statusText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Started provisional navigation for &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;absoluteString&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"unknown URL"&lt;/span&gt;&lt;span class="se"&gt;)\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;receivedServerRedirect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="n"&gt;statusText&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s"&gt;"Received server redirect&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;committed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="n"&gt;statusText&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s"&gt;"Committed&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;finished&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="n"&gt;statusText&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s"&gt;"Finished&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
        &lt;span class="kd"&gt;@unknown&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="n"&gt;statusText&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s"&gt;"Unknown navigation event&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="kt"&gt;WebPage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;NavigationError&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;failedProvisionalNavigation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;statusText&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s"&gt;"Error: Failed provisional navigation: &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;localizedDescription&lt;/span&gt;&lt;span class="se"&gt;)\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="kt"&gt;WebPage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;NavigationError&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pageClosed&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;statusText&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s"&gt;"Error: Page closed&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="kt"&gt;WebPage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;NavigationError&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;webContentProcessTerminated&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;statusText&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s"&gt;"Error: Web content process terminated&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;statusText&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s"&gt;"Unknown error: &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;localizedDescription&lt;/span&gt;&lt;span class="se"&gt;)\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This detects the &lt;code&gt;WebPage.NavigationEvent&lt;/code&gt; types: &lt;code&gt;startedProvisionalNavigation&lt;/code&gt;, &lt;code&gt;receivedServerRedirect&lt;/code&gt;, &lt;code&gt;committed&lt;/code&gt; and &lt;code&gt;finished&lt;/code&gt;. Each of the possible thrown &lt;code&gt;WebPage.NavigationError&lt;/code&gt; events is also monitored.&lt;/p&gt;

&lt;p&gt;This seems like overkill for most use cases, but this code shows how to set it up. The key is that &lt;code&gt;page.navigations&lt;/code&gt; is an &lt;code&gt;AsyncSequence&amp;lt;WebPage.NavigationEvent, any Error&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feiz5qwiagsfleu2n5a7d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feiz5qwiagsfleu2n5a7d.png" alt="Tracking events" width="800" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As an added extra, this example uses the value of &lt;code&gt;page.isLoading&lt;/code&gt; to show or hide a &lt;code&gt;ProgressView&lt;/code&gt;. This is what I'll mostly use for tracking loads and providing user feedback during a load. If you want to get fancy, you can provide the &lt;code&gt;ProgressView&lt;/code&gt; with a &lt;code&gt;value&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isLoading&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;ProgressView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;estimatedProgress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This changes it from an indeterminate spinner to a progress bar.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. CustomScheme
&lt;/h3&gt;

&lt;p&gt;If you want your &lt;code&gt;WebView&lt;/code&gt; to load custom pages, create a custom scheme. In my &lt;a href="https://troz.net/manreader/" rel="noopener noreferrer"&gt;Man Reader&lt;/a&gt; app, I use a custom scheme to load HTML versions of man pages, so I decided to try something similar here.&lt;/p&gt;

&lt;p&gt;First, you create your scheme and then a scheme handler that actually provides the data for the web view to display. The web page's configuration ties these two together.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;scheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;URLScheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"manpage"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;ManPageSchemeHandler&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;configuration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;WebPage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;urlSchemeHandlers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;scheme&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;

&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;WebPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nv"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My scheme handler struct conforms to &lt;code&gt;URLSchemeHandler&lt;/code&gt; and has the required &lt;code&gt;reply(for:)&lt;/code&gt; method to process the request. It tries to read the relevant file from the app bundle and if the file exists, uses its data to create a &lt;code&gt;URLResponse&lt;/code&gt; and then to emit the response and the file data or any error in an &lt;code&gt;AsyncSequence&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;ManPageSchemeHandler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;URLSchemeHandler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nv"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;URLRequest&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;some&lt;/span&gt; &lt;span class="kt"&gt;AsyncSequence&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;URLSchemeTaskResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;any&lt;/span&gt; &lt;span class="kt"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;AsyncThrowingStream&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;continuation&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
      &lt;span class="k"&gt;guard&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;bundleURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Bundle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;forResource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;withExtension&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;pageData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kt"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;contentsOf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bundleURL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;continuation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;throwing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;URLError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;badURL&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;URLResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"text/html"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;expectedContentLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pageData&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;textEncodingName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"utf-8"&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;continuation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;yield&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="n"&gt;continuation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;yield&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pageData&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="n"&gt;continuation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, when I try to open &lt;code&gt;manpage://cal.html&lt;/code&gt;, this reads the &lt;strong&gt;cal.html&lt;/strong&gt; file from the bundle, uses it's URL and length to create a &lt;code&gt;URLResponse&lt;/code&gt; and then yields the response and the file data before finishing the stream.&lt;/p&gt;

&lt;p&gt;If there's a problem with the file, the stream finishes by throwing an error.&lt;/p&gt;

&lt;p&gt;This example also uses &lt;code&gt;WebPage.NavigationDeciding&lt;/code&gt; to work out what to do with other schemes. Back in the initial setup phase, I created a decider and provided it to the &lt;code&gt;WebPage&lt;/code&gt;, along with the configuration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;navigationDecider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;NavigationDecider&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;WebPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nv"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;navigationDecider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;navigationDecider&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My decider class conforms to &lt;code&gt;WebPage.NavigationDeciding&lt;/code&gt; and has a &lt;code&gt;decidePolicy&lt;/code&gt; method that checks the supplied URL and works out what to do with it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;NavigationDecider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;WebPage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;NavigationDeciding&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;decidePolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nv"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;WebPage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;NavigationAction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="nv"&gt;preferences&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;inout&lt;/span&gt; &lt;span class="kt"&gt;WebPage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;NavigationPreferences&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;WKNavigationActionPolicy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"No URL supplied for decision"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cancel&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scheme&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"manpage"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Opening man page for &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allow&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Opening &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt; in default browser"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kt"&gt;NSWorkspace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cancel&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, I first check that the &lt;code&gt;WebPage.NavigationAction&lt;/code&gt; has a request with a URL. I can't imagine when this would ever be nil, but it's optional, so I check and cancel the navigation if it's missing. Then I test for my custom scheme and allow those pages to load. In this example, all other URLs open in the default browser so I return &lt;code&gt;cancel&lt;/code&gt; to stop them opening in the &lt;code&gt;WebView&lt;/code&gt;. To test this, open one of the man pages using a toolbar button and scroll to the end of the page where I added an external link.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt; : In macOS Tahoe 26 beta 7 and Xcode 26 beta 6, this external navigation prints what appears to be a crash log in the console, but the app does not crash.&lt;/p&gt;

&lt;p&gt;This example demonstrates two other &lt;code&gt;WebView&lt;/code&gt; features:&lt;/p&gt;

&lt;p&gt;By default, a &lt;code&gt;WebView&lt;/code&gt; allows bouncing so the page appears to scroll sideways even though it all fits. This is really obvious is you use a track pad and swipe sideways. To turn off this behavior, add this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scrollBounceBehavior&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;basedOnSize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;axes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;horizontal&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vertical scrolling still works and bounces, but horizontal does not, unless the content doesn't fit.&lt;/p&gt;

&lt;p&gt;The other feature is searching in the page. Presenting the find interface works much like presenting a sheet - add a Boolean and connect it up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;@State&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;findNavigatorIsPresented&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="kt"&gt;WebView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceDisabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// doesn't work yet&lt;/span&gt;
  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findNavigator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;isPresented&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;$findNavigatorIsPresented&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I added a toolbar button to toggle &lt;code&gt;findNavigatorIsPresented&lt;/code&gt; for showing and hiding the interface. I have included the &lt;code&gt;replaceDisabled(true)&lt;/code&gt; modifier but it doesn't work in a &lt;code&gt;WebView&lt;/code&gt;. The modifier doesn't stop the replace interface from appearing, but replacing doesn't actually work:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6p41dns34fl8pw68qkos.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6p41dns34fl8pw68qkos.png" alt="Find navigator" width="800" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Interestingly, in a &lt;code&gt;TextEditor&lt;/code&gt;, it is possible to hide the replace button by adding the &lt;code&gt;replaceDisabled(true)&lt;/code&gt; modifier, but the number of matches doesn't appear.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. JavaScripting
&lt;/h3&gt;

&lt;p&gt;SwiftUI's WebView provides an asynchronous method for calling Javascript on the page. In this example, I load a page from my web site and once it's loaded, I send a JavaScript command to gather all the &lt;strong&gt;H3&lt;/strong&gt; headers so I can make them into a navigation menu in the toolbar.&lt;/p&gt;

&lt;p&gt;While I could use navigation event tracking to detect when the page has finished loading, instead I use a loop containing &lt;code&gt;Task.sleep&lt;/code&gt; to wait until &lt;code&gt;page.isLoading&lt;/code&gt; becomes false:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;loadPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;Task&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;sections&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;string&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pageAddress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;URLRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isLoading&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="kt"&gt;Task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;milliseconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;listHeaders&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once I have the list of headers, I add them to a toolbar menu for jumping around the page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft1scheu3dioeav5difch.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft1scheu3dioeav5difch.png" alt="JavaScript populated sections menu" width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the WWDC video, the presenter demonstrated a similar technique, although without sharing the actual JavaScript. When testing, I found it easier to write and debug my JavaScript functions directly in the browser, so the project repo includes my &lt;strong&gt;jsTests.js&lt;/strong&gt; file.&lt;/p&gt;

&lt;p&gt;The methods that use JavaScript are gathered into an extension on the &lt;code&gt;JavaScripting&lt;/code&gt; view. I start by using multi-line strings to create the scripts, interpolating data from the view as needed. Then a &lt;code&gt;Task&lt;/code&gt; uses &lt;code&gt;try await page.callJavaScript(js)&lt;/code&gt; to call the JavaScript and get the result.&lt;/p&gt;

&lt;p&gt;The result is of type &lt;code&gt;Any?&lt;/code&gt; so then I try to cast it into the type I want. Here's the full method for listing the section headers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;listHeaders&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;js&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"""
    const headers = document.querySelectorAll("&lt;/span&gt;&lt;span class="p"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;headerElement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s"&gt;")
    return [...headers].map(header =&amp;gt; ({
      id: header.textContent.replaceAll("&lt;/span&gt; &lt;span class="s"&gt;", "&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s"&gt;").toLowerCase(),
      title: header.textContent
    }))
    """&lt;/span&gt;

  &lt;span class="kt"&gt;Task&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;jsResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callJavaScript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;js&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jsResult&lt;/span&gt; &lt;span class="k"&gt;as?&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;pageSections&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="kt"&gt;SectionLink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;jsEntry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="kt"&gt;MainActor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;sections&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pageSections&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SectionLink&lt;/code&gt; is a struct that contains the section title and ID and has a custom &lt;code&gt;init&lt;/code&gt; that takes a &lt;code&gt;[String: Any]&lt;/code&gt; dictionary.&lt;/p&gt;

&lt;p&gt;In this case, I interpolated &lt;code&gt;headerElement&lt;/code&gt; directly into the JavaScript string, but you can also pass it to &lt;code&gt;callJavaScript&lt;/code&gt; using the &lt;code&gt;arguments&lt;/code&gt; parameter. Here's how I pass in a title for working out how to scroll to its section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callJavaScript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;js&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"sectionTitle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JavaScript now has access to the &lt;code&gt;sectionTitle&lt;/code&gt; variable, which contains the title of the section to scroll to. This returns the vertical offset of the section, which I then use to scroll the web view to that position.&lt;/p&gt;

&lt;p&gt;Managing the web view's scrolling has several components. First, an &lt;code&gt;@State&lt;/code&gt; property holds a &lt;code&gt;ScrollPosition&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;@State&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;currentScroll&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;ScrollPosition&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, bind this property to the &lt;code&gt;WebView&lt;/code&gt; using &lt;code&gt;webViewScrollPosition&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kt"&gt;WebView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;webViewScrollPosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;$currentScroll&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, when the JavaScript returns a value for the section offset, use this property to adjust the scroll:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="nf"&gt;withAnimation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;easeInOut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;currentScroll&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scrollTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The animation makes the scroll look better, instead of jumping suddenly to the new location.&lt;/p&gt;

&lt;p&gt;Another interesting part of the WWDC demo was detecting the scroll and setting the selected section to match. I had started by putting the sections into a &lt;code&gt;Picker&lt;/code&gt; with a selection parameter. When the selected section changed, I scrolled the web view to that section.&lt;/p&gt;

&lt;p&gt;That worked fine until I started detecting the scroll, which also set the selected section, which reset the scroll to the top of the current section every time. To get around this, I made the toolbar item into a menu with a set of buttons with scroll actions. They did not set the selected section, but their titles showed a leading checkmark for the selected section. The scroll detector worked out the visible section and set the selected section without altering the scroll.&lt;/p&gt;

&lt;p&gt;To monitor the scroll position, I added this modifier to the &lt;code&gt;WebView&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;webViewOnScrollGeometryChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;CGFloat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;\&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;contentOffset&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newValue&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="nf"&gt;adjustSelectionTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;scrollPosition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;newValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;adjustSelectionTo(scrollPosition:)&lt;/code&gt; method uses another chunk of JavaScript to work out which section is now visible and the result of this is used to update the selected section property.&lt;/p&gt;

&lt;p&gt;I had an issue with the longer section names which were truncated when another section was checked. This was fixed by using string interpolation instead of the string itself when showing the title without the checkmark. I'm not sure why this works, but I assume string interpolation forces the view to re-calculate its width each time.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Browser
&lt;/h3&gt;

&lt;p&gt;The final example attempts to emulate a browser. There is a URL entry &lt;code&gt;TextField&lt;/code&gt; at the top and a &lt;code&gt;WebView&lt;/code&gt; below it.&lt;/p&gt;

&lt;p&gt;The new part here is tracking the browser history and adding back and forward buttons. These use the &lt;code&gt;page.backForwardList&lt;/code&gt; to create their menus with a primary action that goes back one or forward one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdpetz158ypnvmu1gn0nz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdpetz158ypnvmu1gn0nz.png" alt="Browser history" width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At first, I assumed that &lt;code&gt;page.load()&lt;/code&gt; required a &lt;code&gt;URL&lt;/code&gt; or a &lt;code&gt;URLRequest&lt;/code&gt;, but then I realized that I could also ask it to load a &lt;code&gt;WebPage.BackForwardList.Item&lt;/code&gt; directly.&lt;/p&gt;

&lt;p&gt;I had a problem with the back and forward menus which made me think that the history was not updating correctly. After temporarily re-purposing the refresh button to list the history items, I worked out that history list was correct but the menus were not being updated when the list changed.&lt;/p&gt;

&lt;p&gt;To solve this, I added an &lt;code&gt;id&lt;/code&gt; to each menu that would trigger an update when its value changed. I needed something hashable that changed on every load, so I chose &lt;code&gt;page.isLoading&lt;/code&gt;. I realize that this is updating the menus twice as often as needed, but it works and I don't think it is too inefficient.&lt;/p&gt;

&lt;p&gt;The other feature here is the refresh button. If you've done any web dev, you'll be familiar with the technique of reloading from origin so that you get all the latest files instead of any cached versions. Hold down the Option key to switch the refresh button to reload from origin mode.&lt;/p&gt;

&lt;p&gt;The only issue that I haven't tackled is opening links in new tabs or windows. On this site, all external links are set to open in a new tab and they just fail to load. I wonder is this something that a navigation decider could handle?&lt;/p&gt;

&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;After waiting several years for this view to arrive, I am not disappointed. The SwiftUI team have done a great job of integrating SwiftUI and WebKit with some modernizations that WebKit is still missing.&lt;/p&gt;

&lt;p&gt;For my personal use, I'll be updating Man Reader to use this web view. The find-in-page, custom scheme handling and scroll detection will be great. I also have to re-write the sections in my books that use &lt;code&gt;WKWebView&lt;/code&gt; but that will be part of the general macOS/Xcode 26 update cycle.&lt;/p&gt;

&lt;p&gt;All the sample code from this article is on &lt;a href="https://github.com/trozware/swiftui-webview" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; so I encourage you to download it and see how it works. Make changes, and please let me know if you find any errors or can suggest improvements using my &lt;a href="https://troz.net/contact/" rel="noopener noreferrer"&gt;Contact&lt;/a&gt; page. And if you found this article useful, please &lt;a href="https://ko-fi.com/trozware" rel="noopener noreferrer"&gt;buy me a coffee&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>swift</category>
      <category>swiftui</category>
      <category>webview</category>
    </item>
    <item>
      <title>SwiftUI for Mac 2025</title>
      <dc:creator>TrozWare</dc:creator>
      <pubDate>Thu, 31 Jul 2025 16:20:00 +0000</pubDate>
      <link>https://dev.to/trozware/swiftui-for-mac-2025-5caa</link>
      <guid>https://dev.to/trozware/swiftui-for-mac-2025-5caa</guid>
      <description>&lt;p&gt;Almost every year, I write an article and a sample app, exploring the new features of SwiftUI, with particular emphasis on Mac app development. This year, it feels like the operating systems are becoming more uniform, so there is not a lot that's Mac-specific, but there are several new features that I am keen to explore.&lt;/p&gt;

&lt;p&gt;Usually, I write an app that downloads images from &lt;a href="https://http.cat" rel="noopener noreferrer"&gt;HTTP Cats&lt;/a&gt; as well as demonstrating other features. This year, navigation hasn't really changed, so the HTTP Cats app didn't seem relevant. Instead, I've created a sample app that features some of the new aspects of SwiftUI for macOS 26.&lt;/p&gt;

&lt;p&gt;Right now, I'm using Beta 4: macOS 26.0 Beta (25A5316i) and Xcode 26.0 beta 4 (17A5285i). And by the way, I love the new year numbering system so that all the operating systems are easily identifiable and even Xcode is in line with everything else.&lt;/p&gt;

&lt;p&gt;Here is a list of the topics I'm going to cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;App icons&lt;/li&gt;
&lt;li&gt;New look controls&lt;/li&gt;
&lt;li&gt;Web view&lt;/li&gt;
&lt;li&gt;Rich text&lt;/li&gt;
&lt;li&gt;Long lists&lt;/li&gt;
&lt;li&gt;Menu item icons&lt;/li&gt;
&lt;li&gt;Concurrency&lt;/li&gt;
&lt;li&gt;Toolbars&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  App icons
&lt;/h3&gt;

&lt;p&gt;When you create a new project in Xcode 26, the old &lt;strong&gt;Assets.xcassets&lt;/strong&gt; folder is still there and it still contains an &lt;strong&gt;AppIcon&lt;/strong&gt; entry, but this is no longer the only way to create an app icon. The new icon format applies to all platforms, but this is an area where macOS has lagged behind iOS, so it's great to see all the systems using the same tool and format now.&lt;/p&gt;

&lt;p&gt;Right-click on the Xcode icon in your Dock and choose &lt;strong&gt;Open Developer Tool → Icon Composer&lt;/strong&gt; from the popup menu. This opens a new app for creating icon files. Create a new document and start by using the Document tab in the inspector to turn off watchOS and select macOS only:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk04zltr3rh1dz2y9tgrs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk04zltr3rh1dz2y9tgrs.png" alt="Icon document settings" width="300" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then switch back to the paint brush inspector to set the background. The app supplies a few fill options, so play around with them to see what you like. After that, the recommendation is to add one or two layers using SVGs.&lt;/p&gt;

&lt;p&gt;Being graphically challenged, I searched for a free cat icon in SVG format and found this: &lt;a href="https://iconscout.com/icon/cat-9608559" rel="noopener noreferrer"&gt;https://iconscout.com/icon/cat-9608559&lt;/a&gt;. I dragged the file on to my icon and started playing around with it. The most useful controls in the main portion of the app are the one at the top for toggling on the grid - you can use the dropdown button on the right to switch between light and dark modes - and the three preview options at the bottom for toggling between default, dark and mono modes.&lt;/p&gt;

&lt;p&gt;In the sidebar, select &lt;strong&gt;Icon&lt;/strong&gt; to edit the background and select your image to change its properties. When working with the image, you can select whether options apply to all the versions or just the currently selected variant. This is useful as I found that the Liquid Glass effect looked great in dark mode but not in default mode with my gradient. I wasn't pleased with the mono version, but I'll see how it looks in real life. I can't imagine myself ever using mono icons on my Mac, but maybe that's just me.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnvqnb3dc8wapx1srq5cp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnvqnb3dc8wapx1srq5cp.png" alt="Icon composer" width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Anyway, with my icon composed, I saved it as &lt;strong&gt;Cat.icon&lt;/strong&gt; and then dragged it in to the project folder, copying it to my app target. Then, in General settings for the app target, I set &lt;strong&gt;App Icon&lt;/strong&gt; to &lt;strong&gt;Cat&lt;/strong&gt; without the extension. After running the app, I right-clicked on its icon in the Dock and selected &lt;strong&gt;Options → Show in Finder&lt;/strong&gt; so I could see the icon fully. It looked pretty good in default mode, but when I used &lt;strong&gt;System Settings → Appearance&lt;/strong&gt; to alter the icon style, it was clear that some of the options would require more work. But still, this was a good start.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvipdlf7we0ionju7gvcj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvipdlf7we0ionju7gvcj.png" alt="Icon appearance" width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want to support older systems and have an AppIcon set in &lt;strong&gt;Assets.xcassets&lt;/strong&gt; , set the name of your Icon Composer file to AppIcon and your app will use the new one and fall back to the old one if required.&lt;/p&gt;

&lt;p&gt;One nice touch is that if you select the icon file in the Xcode Project navigator, you get a button under the preview for opening the file in Icon Composer directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  New look controls
&lt;/h3&gt;

&lt;p&gt;With the introduction of Liquid Glass, many controls have a new look. In the sample app, I added a &lt;code&gt;TabView&lt;/code&gt; to group the controls I wanted to test. Tabs have a title and an icon, as they did last year. The default style groups them in the toolbar, although since Beta 3, there is not a lot of transparency:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmrymftknmckhwsao7win.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmrymftknmckhwsao7win.png" alt="Default tabs" width="800" height="236"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice how this has hidden the window title too.&lt;/p&gt;

&lt;p&gt;Setting the &lt;code&gt;tabViewStyle&lt;/code&gt; to &lt;code&gt;sidebarAdaptable&lt;/code&gt; moved the tabs to a sidebar overlay and displays the window title. This looks much more like a standard sidebar and shows the tab icons too. I didn't like the way this looked last year, but I think it fits well with the new UI design. It's like a navigation sidebar but for a static list.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6bnv9xkugxceywf6sqfj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6bnv9xkugxceywf6sqfj.png" alt="Sidebar tabs" width="800" height="253"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Not only does this style display the window title, but it also allows for a window subtitle using the &lt;code&gt;navigationSubtitle&lt;/code&gt; modifier. And it displays a sidebar toggle button. There is a way to remove the sidebar toggle button if it's part of a navigation view, but that technique didn't work here. Despite the style name indicating that this style is adaptable, it only adapts to suit the window size in an iPad app.&lt;/p&gt;

&lt;h4&gt;
  
  
  Buttons
&lt;/h4&gt;

&lt;p&gt;The main change for buttons is the two new options for &lt;code&gt;buttonStyle&lt;/code&gt;: &lt;code&gt;glass&lt;/code&gt; and &lt;code&gt;glassProminent&lt;/code&gt;. Both styles can have a &lt;code&gt;tint&lt;/code&gt; applied. I found that with certain tints, the &lt;code&gt;glassProminent&lt;/code&gt; style button doesn't show the mouse down effect very distinctly. By default, the &lt;code&gt;glassProminent&lt;/code&gt; style is tinted using the &lt;code&gt;accentColor&lt;/code&gt; or the user's preferred theme color if chosen.&lt;/p&gt;

&lt;p&gt;Toggles have a slightly new look. The button style gives a much more prominent indicator of the selected state, and the switch style uses the new Liquid Glass style which you can really see if you drag the switch instead of clicking on it.&lt;/p&gt;

&lt;p&gt;In this screenshot, I've added a background to the view and tinted the &lt;code&gt;glassProminent&lt;/code&gt; button. The background looks great with the tab sidebar but the title bar looks a bit jarring:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff5snj6p5iblg88yjdm6n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff5snj6p5iblg88yjdm6n.png" alt="Buttons" width="600" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I wrapped the &lt;code&gt;VStack&lt;/code&gt; containing the sample buttons in a &lt;code&gt;ScrollView&lt;/code&gt; and that made the title bar transparent and allowed the content to scroll up behind the title bar. Toggle the background on and shrink the window to see this clearly:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fojyjyuo2201wzae80lur.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fojyjyuo2201wzae80lur.png" alt="Hidden title bar" width="600" height="214"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sometimes, this caused a graphical glitch in the &lt;code&gt;glassProminent&lt;/code&gt; button, but I expect this is a beta issue.&lt;/p&gt;

&lt;h4&gt;
  
  
  Numbers
&lt;/h4&gt;

&lt;p&gt;For entering numbers, I tested the &lt;code&gt;Slider&lt;/code&gt; and &lt;code&gt;Stepper&lt;/code&gt; controls. The &lt;code&gt;Slider&lt;/code&gt; has the new Liquid Glass style thumb and a cute little bounce of the icons when you reach either end. The &lt;code&gt;step&lt;/code&gt; parameter displays tick marks along the slider.&lt;/p&gt;

&lt;p&gt;For the stepper, the up and down arrows are larger, which I think is a good idea - they were very small before. There's also a format parameter which has probably been there for years but I've never used it. What I like about it is that it displays the selected value in the format you specify and it's editable! I'm using &lt;code&gt;percent&lt;/code&gt; in this example:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz6ariq0gkfv0ggovzxuf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz6ariq0gkfv0ggovzxuf.png" alt="Numbers" width="600" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One slight oddity of the stepper showing an editable value is that it lets you enter a value that doesn't match the step size or is inside the range. In this example, I set the step size to 0.05 (5%) and then entered 13%. The arrow buttons still go up and down in 5% increments, so I got 18% or 8% depending on which way I clicked. Also, you can enter a non-numeric value, but it is ignored: tabbing out of the entry resets it, and the arrow buttons operate on your previous entry.&lt;/p&gt;

&lt;p&gt;I added a view to test the various &lt;code&gt;Picker&lt;/code&gt; styles, but they seem to be the same as last year, so for fun, I added two &lt;code&gt;Text&lt;/code&gt; views with glass effects applied:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kt"&gt;HStack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Glass Effect - regular"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glassEffect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;regular&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;colorForTint&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

  &lt;span class="kt"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Glass Effect - clear"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glassEffect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clear&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;colorForTint&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They take their tint color from the picker selection. The &lt;code&gt;clear&lt;/code&gt; style is slightly more translucent than the default &lt;code&gt;regular&lt;/code&gt; style but doesn't have as glassy a border. Neither are as glassy as I expected, so I added a &lt;code&gt;ZStack&lt;/code&gt; to apply a background of the same color only much less opaque and with a &lt;code&gt;backgroundExtensionEffect&lt;/code&gt; to bring the color under the side tab bar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kt"&gt;ZStack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;colorForTint&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;backgroundExtensionEffect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="kt"&gt;VStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I expect it would look even more obvious with an image behind it. Actually, leaving it un-tinted on a color background is a probably the best way to see the effect:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjumzazx2vvpn50tjh7ax.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjumzazx2vvpn50tjh7ax.png" alt="Glass effect" width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And if you really want to see something unusual, put the app into the background. The three glass effects are all very different.&lt;/p&gt;

&lt;h3&gt;
  
  
  Web view
&lt;/h3&gt;

&lt;p&gt;One of the most exciting additions for me was the new &lt;code&gt;WebView&lt;/code&gt;. So many apps need to display web content, and until now, the only option has been to use &lt;code&gt;NSViewRepresentable&lt;/code&gt; or &lt;code&gt;UIViewRepresentable&lt;/code&gt; to bring in a &lt;code&gt;WKWebView&lt;/code&gt;. The SwiftUI team has exceed my expectations with the features in the new &lt;code&gt;WebView&lt;/code&gt; and it's going to need its own article, but I'll cover the two main ways to use it here.&lt;/p&gt;

&lt;p&gt;Before working with any web view in a Mac app, you first need to turn on &lt;strong&gt;Outgoing Connections (Client)&lt;/strong&gt; in the &lt;strong&gt;Signing &amp;amp; Capabilities&lt;/strong&gt; for the target. Also, any SwiftUI view that needs to use &lt;code&gt;WebView&lt;/code&gt; must import &lt;code&gt;WebKit&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With those in place, here's my code for displaying my web site in a SwiftUI WebView:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;SwiftUI&lt;/span&gt;
&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;WebKit&lt;/span&gt;

&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;WebDemo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;myPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;string&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"https://troz.net"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;

  &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;some&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;WebView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;myPage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I have found that the only way to create an invalid URL from a string is to provide an empty string, so I am now happy to force-unwrap URLs. After that, I added a &lt;code&gt;WebView&lt;/code&gt; and set its &lt;code&gt;url&lt;/code&gt; to &lt;code&gt;myPage&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm4tm9x9a112wsq3jbkmd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm4tm9x9a112wsq3jbkmd.png" alt="Web view" width="600" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, a contextual menu offers basic navigation options. So to display any web page off the internet, all you need is the URL.&lt;/p&gt;

&lt;p&gt;To gain more control, initialize a &lt;code&gt;WebView&lt;/code&gt; with a &lt;code&gt;WebPage&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;WebDemo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;@State&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;WebPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;blank&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;string&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"about:blank"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;

  &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;some&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;WebView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;onAppear&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;blank&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"""
      HTML goes here (truncated for reading)
    """&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I added a &lt;code&gt;Picker&lt;/code&gt; to the toolbar for switching between the two web view sources. Here's the one showing a local HTML string:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyz3kkp569prh73ax40xr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyz3kkp569prh73ax40xr.png" alt="Local web" width="600" height="339"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Using a &lt;code&gt;WebPage&lt;/code&gt; gives a lot of different options, but this shows the basic idea. I will tend to use a &lt;code&gt;WebPage&lt;/code&gt; as that lets me track the loading state so I can show a progress indicator.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rich text
&lt;/h3&gt;

&lt;p&gt;When I'm asked if I recommend starting a Mac app with SwiftUI or with AppKit, I used to say SwiftUI unless your app uses lists with thousands of entries or long form text editing. SwiftUI lists have improved a lot in macOS this year, but I'm still not sure about that, however it looks like text editing can come off my AppKit list.&lt;/p&gt;

&lt;p&gt;The key is to use a &lt;code&gt;TextEditor&lt;/code&gt; but to link it to an &lt;code&gt;AttributedString&lt;/code&gt; instead of a plain &lt;code&gt;String&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;@State&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;AttributedString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;some&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;TextEditor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;$text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With an &lt;code&gt;AttributedString&lt;/code&gt;, you get a lot of formatting options, although not as many as I expected. Setting bold, italic or underline is easy and so is changing the font size. I have not yet worked out how to change the font, except by pasting in text in a different font. At that point, the newly pasted font becomes one of the document's styles and can be applied to other portions of the text, but this can't be the only way. There is a &lt;strong&gt;Show Fonts&lt;/strong&gt; menu item in &lt;strong&gt;Format → Font&lt;/strong&gt; , but it is always disabled. Spell checking is also a bit erratic - it works really well if you open the spell checker dialog, but it doesn't work as you type.&lt;/p&gt;

&lt;p&gt;I added &lt;code&gt;TextEditingCommands&lt;/code&gt; and &lt;code&gt;TextFormattingCommands&lt;/code&gt; to the menus, but they all seemed to be available through the contextual menu anyway. The Apple Intelligence Writing Tools are also available either through the &lt;strong&gt;Edit&lt;/strong&gt; menu or the contextual menu.&lt;/p&gt;

&lt;p&gt;For saving and loading the formatted text, I realized that &lt;code&gt;AttributedString&lt;/code&gt; is a &lt;code&gt;Codable&lt;/code&gt; type, so I used &lt;code&gt;JSONEncoder&lt;/code&gt; and &lt;code&gt;JSONDecoder&lt;/code&gt; to save and load as the view appears and disappears. This worked really well, whether I closed the editor window manually or just quit the app. This is just a quick hack for a single editor, but it could be the basis for a more complex app.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F85h8h68iqvwumpkxsq5u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F85h8h68iqvwumpkxsq5u.png" alt="Text editing" width="600" height="543"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I think there will be more to discover with this, but it's already a lot better than before. And I haven't even started to get into tracking the selection or adding custom formatting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Long lists
&lt;/h3&gt;

&lt;p&gt;SwiftUI lists on iOS have always performed better than on macOS but this year, macOS is catching up. I added a &lt;strong&gt;List Demo&lt;/strong&gt; window to test out various lengths of list. Before, I found that more than about 3,000 items in a list would cause performance issues, but now I can have 10,000 items and it still feels snappy, both for drawing, scrolling and selecting. With 50,000 items, it's still usable but selection is a bit slow. Interestingly, clearing the list is a big performance hit, but I'm not sure why. 500,000 items takes a very long time to draw. Scrolling is not too bad but selecting a new item takes several seconds. Clearing a list this big takes more than a minute.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Forenqihdd1b5gntyifbt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Forenqihdd1b5gntyifbt.png" alt="Long list" width="600" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm still not convinced that SwiftUI lists are the best way to display long lists, but I'm glad to see some performance improvements. I think my limit would be around 20,000 items, but I'd need to test on various machines.&lt;/p&gt;

&lt;h3&gt;
  
  
  Menu item icons
&lt;/h3&gt;

&lt;p&gt;A new thing in macOS 26 and iPadOS 26 is icons attached to menu items. Most of the default menu items have icons, but what about my own?&lt;/p&gt;

&lt;p&gt;Starting with the &lt;strong&gt;Window&lt;/strong&gt; menu, I see the entries for my three demo windows and the main window:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F60uxigzhm54pghc678o2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F60uxigzhm54pghc678o2.png" alt="Window menu" width="300" height="404"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Interestingly, the main window shows the title and its subtitle. I can't see how to add an icon to any of my windows in this menu, but the standard system items all have icons.&lt;/p&gt;

&lt;p&gt;What about a menu with items to mimic the tab and toolbar buttons? I added a new &lt;strong&gt;Show&lt;/strong&gt; menu and used a &lt;code&gt;Picker&lt;/code&gt; to switch between the main window tab and buttons to open the demo windows. For each one, I used a &lt;code&gt;Label&lt;/code&gt; instead of a &lt;code&gt;Text&lt;/code&gt; view or a plain title. This let me assign an system image to each. I realize that this means the demo windows each have two menu items, but I wanted to experiment. And anyway, Xcode has two menu items to open the docs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flbamnppg57rvqhoqfdnb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flbamnppg57rvqhoqfdnb.png" alt="New menu" width="300" height="213"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the &lt;code&gt;Picker&lt;/code&gt; items, I set the &lt;code&gt;pickerStyle&lt;/code&gt; to &lt;code&gt;inline&lt;/code&gt; and set &lt;code&gt;labelIsHidden&lt;/code&gt; to true. I like this style as it gives me a picker at the top level of the menu, instead of in a submenu. For passing the selection to the window, I used &lt;code&gt;@AppStorage&lt;/code&gt; which is not a great choice as it makes the choice apply to every open instance of the main window, but I didn't want to get bogged down here. If you're interested in a more complete solution, check out my article on &lt;a href="https://troz.net/post/2025/mac_menu_data/" rel="noopener noreferrer"&gt;The Mac Menubar and SwiftUI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;As you can see, I was able to show icons, check marks and keyboard shortcuts. I like this, but it can be over done. The WWDC video on the topic said that if you have related menu items like multiple &lt;strong&gt;New&lt;/strong&gt; or &lt;strong&gt;Open&lt;/strong&gt; options, you should only add an icon to the first.&lt;/p&gt;

&lt;p&gt;On a side note, the &lt;strong&gt;+&lt;/strong&gt; button for opening the Library has disappeared from Xcode's toolbar. I'm not sure if this is a bug or a feature but you have to use the &lt;strong&gt;View&lt;/strong&gt; menu now, or the &lt;strong&gt;Shift-Command-L&lt;/strong&gt; shortcut.&lt;/p&gt;

&lt;h3&gt;
  
  
  Concurrency
&lt;/h3&gt;

&lt;p&gt;This is another thing that is not Mac-specific, but I feel it is important.&lt;/p&gt;

&lt;p&gt;Last year, I sat out the change to Swift 6 and its strict concurrency checking. While I could get it to work by following Xcode's suggestions, I was left feeling that I was adding code to make the compiler happy, instead of code that actually made my apps better. This year, things are different. For starters, a new Xcode project has &lt;strong&gt;Approachable Concurrency&lt;/strong&gt; turned on and has &lt;strong&gt;Default Actor Isolation&lt;/strong&gt; set to &lt;strong&gt;MainActor&lt;/strong&gt;. This means that everything is isolated to the main thread by default, but you can step out into a background thread if required.&lt;/p&gt;

&lt;p&gt;The default Swift version is still set to 5, but I changed it to 6 for this sample app and had no compile-time warnings or errors. I did use &lt;code&gt;@concurrent&lt;/code&gt; when generating the items for the list demo, but it turned out that it was the UI updates on the main thread that were slowing things down, not the data generation. Even so, I prefer the new approach and I'll probably be using Swift 6 from now on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Toolbars
&lt;/h3&gt;

&lt;p&gt;As you will have seen in the screenshots, I added a toolbar to &lt;code&gt;ContentView&lt;/code&gt; with buttons for navigating to various demo windows. I wanted to try out the new floating toolbar features with spacers, so I copied the format from Apple's Landmarks app. This didn't work - all the toolbar items were still grouped together. This turned out to be a &lt;em&gt;feature&lt;/em&gt; of using the &lt;code&gt;sidebarAdaptable&lt;/code&gt; tab style. When I reverted to a standard tab style, the toolbar appeared with multiple groups as I expected:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0dukx2v0630pgxojem10.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0dukx2v0630pgxojem10.png" alt="Toolbar with spacers" width="600" height="136"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There was no logical reason for separating these buttons, but I wanted to test using &lt;code&gt;ToolbarSpacer(.fixed)&lt;/code&gt; and &lt;code&gt;ToolbarSpacer(.flexible)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There are a lot of new toolbar positioning options that are not available in macOS. It used to be possible to identify a Catalyst app from its missing Settings menu item. I think that in the future, we'll identify them from toolbars floating in unexpected places!&lt;/p&gt;

&lt;p&gt;Looking back at the screenshots from the web view section, the window without a toolbar already looks old-fashioned. Adding the toolbar made it look more modern.&lt;/p&gt;

&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;The big thing this year is Liquid Glass. I have always advocated using default controls wherever possible and for apps that did this, most of the new look will be applied by re-compiling in Xcode 26. I'm not convinced that having content blurring behind a toolbar or title bar is a good idea - it ends up looking like a layout error much of the time. Of the new features, I'm most excited about the &lt;code&gt;WebView&lt;/code&gt; so expect a separate article on that soon.&lt;/p&gt;

&lt;p&gt;The sample app is available on GitHub: &lt;a href="https://github.com/trozware/swiftui-mac-2025" rel="noopener noreferrer"&gt;https://github.com/trozware/swiftui-mac-2025&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you have any feedback about this article, please contact me using my &lt;a href="https://troz.net/contact/" rel="noopener noreferrer"&gt;Contact&lt;/a&gt; page. And if you found this useful, please &lt;a href="https://ko-fi.com/trozware" rel="noopener noreferrer"&gt;buy me a coffee&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>macos</category>
      <category>swift</category>
      <category>swiftui</category>
    </item>
    <item>
      <title>Apple Developer Relations</title>
      <dc:creator>TrozWare</dc:creator>
      <pubDate>Tue, 20 May 2025 23:53:52 +0000</pubDate>
      <link>https://dev.to/trozware/apple-developer-relations-3c6b</link>
      <guid>https://dev.to/trozware/apple-developer-relations-3c6b</guid>
      <description>&lt;p&gt;Apple's Worldwide Developer Conference is just weeks away, but I'm sensing a lot of apathy in the community. The company's relationship with third-party developers is at a low point.&lt;/p&gt;

&lt;p&gt;We all know that Tim Cook and his senior people will stand up at WWDC and say how much they value their developers and boast about how much money they've paid out to them. Being so enthusiastic about the money is very strange - it's like a rent collector bragging about how much money he has given to the landlord when all he's doing is collecting the rent and taking his cut. And it's difficult to take Apple's apparent enthusiasm for their developers seriously given their behavior over the rest of the year.&lt;/p&gt;

&lt;p&gt;Trust is a hard thing to gain. Apple used to have the developers' trust but now they've lost it. It's much more difficult to regain lost trust than it is to gain it in the first place. I have read many reports of talented developers leaving the Apple ecosystem because they can't take it any more. This is bad for all of us, but particularly bad for Apple.&lt;/p&gt;

&lt;p&gt;I don't imagine that anyone at Apple reads my blog, but I have thought of some things I think they could do to improve their relationship with their developers.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Open up iOS and all other systems to be like macOS
&lt;/h3&gt;

&lt;p&gt;macOS has the Gatekeeper system which allows you to run apps from the App Store and from known developers. If you want to distribute a macOS app outside the App Store, you submit your app to Apple for notarization. This is not app review by a person, but an automated check for malware.&lt;/p&gt;

&lt;p&gt;Apple states that iOS needs to be kept locked down to protect us, but macOS is not full of viruses and malware despite it's greater openness. Apple also states that the App Store and its review process protects users, but the most cursory glance through the iOS App Store shows that it's full of scam apps, copycat apps and apps with fake reviews.&lt;/p&gt;

&lt;p&gt;If Apple were to open up iOS in the same way that macOS is open, it would immediately end a vast array of court cases against them, as well as avoiding the constant stream of fines from the EU which does not take kindly to Apple's habit of malicious compliance with their regulations. As a side-note, isn't it odd how Apple fights the regulations enacted by the democratically elected EU parliament, but falls over itself to comply instantly with the orders of the Russian and Chinese governments?&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Give registered developers access to feedback reports
&lt;/h3&gt;

&lt;p&gt;Every year, Apple releases betas of their operating systems to registered developers. They ask for all bugs to be reported using the Feedback Assistant app. This is a time-consuming process for developers and we are becoming increasingly reluctant to provide Apple with unpaid quality control services.&lt;/p&gt;

&lt;p&gt;When you encounter a bug, first you have to make sure it's not in your code. You analyze it and then try to create a stripped down project that demonstrates it. Next, you file a report with as much detail as you can. This usually includes a system log, which used to be fine but now shows a dialog saying that although this may contain confidential information, it will be used to train AI! So much for confidential.&lt;/p&gt;

&lt;p&gt;Once you submit the bug, the usual response is nothing. Even if the bug is fixed, you get no acknowledgement. It seems to be your responsibility to keep testing and close the bug once you can prove it's fixed. Very occasionally, you get a response. Sometimes, this is asking for more information. Other times, it's saying that the bug is closed as a duplicate of some other bug. The problem is that you have no access to the other bug, so now you're cut out of the loop entirely. Only once, in all my years of filing bugs, have I received a real human response. Filing bug reports feels like a giant waste of my time.&lt;/p&gt;

&lt;p&gt;Now imagine that the bug database was open to all registered developers. When you encounter a bug, you could immediately search to see if anyone else had reported it. If so, you could mark it as "I see this too" and maybe add more information. This would be much faster and more efficient.&lt;/p&gt;

&lt;p&gt;From Apple's point of view, it would also be vastly more efficient. Instead of having to correlate many reports about the same issue but worded and tagged differently, they could have a much smaller and more manageable database of reported bugs, with a better indication of how widespread each bug was.&lt;/p&gt;

&lt;p&gt;To me, this seems like a win-win situation. I have trouble coming up with a reason for Apple to keep the bug reports secret. Do they think the sheer number of them is embarrassing? Are they afraid of people exploiting the bugs? Either way, having a more efficient system would be better.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Improve the app review process
&lt;/h3&gt;

&lt;p&gt;This has got to be the most frustrating part of an Apple developer's life. Apple's app review process is a capricious mess and every time you click &lt;strong&gt;Submit for Review&lt;/strong&gt; , it feels like spinning a roulette wheel. Even apps that have been on the App Store for years are not immune to this. Some apps get blocked for the most bizarre reasons, but the same app will be approved without issue for a different platform or on a different day.&lt;/p&gt;

&lt;p&gt;I think the app review team is probably under-staffed and without clear guidelines. It also appears that the larger development companies are allowed more latitude than the independent developers.&lt;/p&gt;

&lt;p&gt;I would suggest that app review be mainly a check for malware and to confirm that the app doesn't crash on various hardware and software combinations. After that, there should be checks to see if the app is a scam or an obvious copycat.&lt;/p&gt;

&lt;p&gt;Apple's reviewers are not familiar with everything that can be done on an iPhone, iPad or Mac and I don't expect them to be. The problem is when they reject apps purely because they don't understand them.&lt;/p&gt;

&lt;p&gt;And while I'm on the topic of app review, how about some validation of App Store reviews before they go online? It wouldn't take a very sophisticated system to detect when an app suddenly receives hundreds of reviews, all virtually identical.&lt;/p&gt;




&lt;p&gt;I could go on and on, but these are my top three suggestions for Apple to improve developer relations. What are yours? If you could speak to the Apple board for one meeting, what would you say?&lt;/p&gt;

&lt;p&gt;Let me know using my &lt;a href="https://troz.net/contact/" rel="noopener noreferrer"&gt;Contact&lt;/a&gt; page. And if you found this article interesting, please &lt;a href="https://ko-fi.com/trozware" rel="noopener noreferrer"&gt;buy me a coffee&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>apple</category>
      <category>wwdc</category>
    </item>
    <item>
      <title>Swift Measurements</title>
      <dc:creator>TrozWare</dc:creator>
      <pubDate>Wed, 16 Apr 2025 09:47:17 +0000</pubDate>
      <link>https://dev.to/trozware/swift-measurements-5d37</link>
      <guid>https://dev.to/trozware/swift-measurements-5d37</guid>
      <description>&lt;p&gt;Recently, I was working with units and unit conversions in Swift. After a while, I then remembered that Swift has a built-in structure for doing this: &lt;code&gt;Measurement&lt;/code&gt;. This article is an introduction to the power and usage of &lt;code&gt;Measurement&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To use a &lt;code&gt;Measurement&lt;/code&gt;, you initialize it with a value and a unit type. The value must be a &lt;code&gt;Double&lt;/code&gt; and the unit type specifies the type of measurement and the scale of the units. Here's an example of specifying a length in centimeters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1234.56&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;lengthMeasurement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Measurement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UnitLength&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;centimeters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are many supported unit types. They all begin with &lt;code&gt;Unit&lt;/code&gt;, so start typing and see what you get. Once you have the unit type, enter a period and code completion will suggest all the possible units for that type. There's a huge range, including some very obscure ones:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqrgzpkif3gpybwy0btjh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqrgzpkif3gpybwy0btjh.png" alt="Measurement init" width="800" height="343"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm running this in a playground and when I print &lt;code&gt;lengthMeasurement&lt;/code&gt; I get &lt;code&gt;1234.56 cm&lt;/code&gt; as you'd expect.&lt;/p&gt;

&lt;p&gt;The magic happens when you start formatting the &lt;code&gt;Measurement&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;formattedLength&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lengthMeasurement&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formatted&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;formattedLength&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running this code gave me &lt;strong&gt;12 m&lt;/strong&gt; which is a much more readable version.&lt;/p&gt;

&lt;p&gt;Playing around with larger and smaller values, I got it to show &lt;strong&gt;cm&lt;/strong&gt; , &lt;strong&gt;m&lt;/strong&gt; or &lt;strong&gt;km&lt;/strong&gt;. It even added in the thousands separator when I went to a really big number. Strangely, I couldn't get it to use a smaller unit than the one I started with. Even with 0.00001 cm, it still didn't use &lt;strong&gt;mm&lt;/strong&gt; , so I guess the lesson there is to start with the smallest units you want to show.&lt;/p&gt;

&lt;p&gt;Now for the super cool bit. I went into &lt;strong&gt;System Settings -&amp;gt; General -&amp;gt; Language &amp;amp; Region&lt;/strong&gt; and changed &lt;strong&gt;Measurement system&lt;/strong&gt; from &lt;strong&gt;Metric&lt;/strong&gt; to &lt;strong&gt;US&lt;/strong&gt;. Running the playground now, my original value of 1234.56 cm gave a formatted value of &lt;strong&gt;41 ft&lt;/strong&gt;! A larger value gave me &lt;strong&gt;mi&lt;/strong&gt; (miles) and a smaller one showed &lt;strong&gt;in&lt;/strong&gt; (inches). So the formatting uses the system setting to choose what units to show. There's no need to convert measurements to suit your user's locale - a formatted &lt;code&gt;Measurement&lt;/code&gt; does that for you!&lt;/p&gt;

&lt;h3&gt;
  
  
  Converting
&lt;/h3&gt;

&lt;p&gt;But what if you want to show the conversion? I like to cook and most recipes online assume the everyone uses Fahrenheit for their ovens. My thanks to all the recipe writers who show Celsius as well, it's greatly appreciated.&lt;/p&gt;

&lt;p&gt;As you can imagine, I spend some time in the kitchen talking to my wrist so that my Apple Watch and Siri can do the conversion for me:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjfem7jej6524cm8bkdy0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjfem7jej6524cm8bkdy0.png" alt="Temperature conversion" width="410" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this screenshot, the result of my query shows both Fahrenheit and Celsius. Let's try that in the playground.&lt;/p&gt;

&lt;p&gt;Firstly, &lt;code&gt;Measurement&lt;/code&gt; can do conversions, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;fahrenheit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;400.0&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;fahrenheitMeasurement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Measurement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fahrenheit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UnitTemperature&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fahrenheit&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fahrenheitMeasurement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;celsiusMeasurement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fahrenheitMeasurement&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;converted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;celsius&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;celsiusMeasurement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prints &lt;strong&gt;400.0 °F&lt;/strong&gt; and &lt;strong&gt;204.44444444444832 °C&lt;/strong&gt;. If you need to access the numeric result of the conversion, you can get the &lt;code&gt;Double&lt;/code&gt; using &lt;code&gt;celsiusMeasurement.value&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But as you'd expect, if you use &lt;code&gt;formatted&lt;/code&gt;, both versions are shown in Celsius because that's my system setting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;fahrenheitMeasurement&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formatted&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt; = &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;celsiusMeasurement&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formatted&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prints &lt;strong&gt;204.444444°C = 204.444444°C&lt;/strong&gt; which while factually indisputable, is not at all helpful. And it shows far too many decimal places - my oven is not that accurate!.&lt;/p&gt;

&lt;p&gt;Here's where we can use a &lt;code&gt;MeasurementFormatter&lt;/code&gt; which allows us to specify several parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;formatter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;MeasurementFormatter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;formatter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unitOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;providedUnit&lt;/span&gt;
&lt;span class="n"&gt;formatter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;numberFormatter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maximumFractionDigits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;formatter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unitStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;medium&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this in place, I tried:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;formattedFahrenheit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;formatter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fahrenheitMeasurement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;formattedCelsius&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;formatter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;celsiusMeasurement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;formattedFahrenheit&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt; = &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;formattedCelsius&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And got &lt;strong&gt;400°F = 204°C&lt;/strong&gt; which is even more useful for cooking than the display my watch showed.&lt;/p&gt;

&lt;p&gt;The key for showing both temperatures was to set the &lt;code&gt;unitOptions&lt;/code&gt; to &lt;code&gt;providedUnit&lt;/code&gt; which made it use the unit specified in the &lt;code&gt;Measurement&lt;/code&gt;. After that, &lt;code&gt;numberFormatter&lt;/code&gt; is a standard &lt;code&gt;NumberFormatter&lt;/code&gt; with all its options. &lt;code&gt;unitStyle&lt;/code&gt; can be &lt;code&gt;short&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt; or &lt;code&gt;long&lt;/code&gt;. The default is &lt;code&gt;medium&lt;/code&gt; but I specified it so I could experiment with the different styles.&lt;/p&gt;

&lt;p&gt;So any time you find yourself working with units and conversions, give &lt;code&gt;Measurement&lt;/code&gt; a try. It handles localization, conversion and formatting for you.&lt;/p&gt;

&lt;p&gt;If you have any feedback about this article, please contact me via my &lt;a href="https://troz.net/contact/" rel="noopener noreferrer"&gt;Contact&lt;/a&gt; page. And if you found this useful, please &lt;a href="https://ko-fi.com/trozware" rel="noopener noreferrer"&gt;buy me a coffee&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>swift</category>
    </item>
    <item>
      <title>Moving to Eleventy</title>
      <dc:creator>TrozWare</dc:creator>
      <pubDate>Wed, 19 Mar 2025 04:15:33 +0000</pubDate>
      <link>https://dev.to/trozware/moving-to-eleventy-1k82</link>
      <guid>https://dev.to/trozware/moving-to-eleventy-1k82</guid>
      <description>&lt;p&gt;I have been running my blog at &lt;a href="https://troz.net" rel="noopener noreferrer"&gt;troz.net&lt;/a&gt; since 2014 and it's seen several major changes along the way. I started with WordPress but it felt slow and clumsy, so in 2015 I entered the world of static site generators and &lt;a href="https://troz.net/post/2015/new-site-for-trozware/" rel="noopener noreferrer"&gt;transferred to Jekyll&lt;/a&gt;. That worked for a couple of years until Jekyll was updated from version 2 to version 3 which broke my setup. At that point I &lt;a href="https://troz.net/post/2017/moving-to-hugo/" rel="noopener noreferrer"&gt;converted to Hugo&lt;/a&gt; which has worked fine for more than six years.&lt;/p&gt;

&lt;p&gt;But as with &lt;a href="https://jekyllrb.com" rel="noopener noreferrer"&gt;Jekyll&lt;/a&gt;, updates broke my site and I didn't know enough about how it all worked to fix it. As a temporary measure, I reverted to an old version of &lt;a href="https://gohugo.io" rel="noopener noreferrer"&gt;Hugo&lt;/a&gt; which got everything working again, but this was not a long term solution.&lt;/p&gt;

&lt;p&gt;After considering a number of options, I decided to try &lt;a href="https://www.11ty.dev" rel="noopener noreferrer"&gt;Eleventy&lt;/a&gt; and that's what you're looking at right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Were My Criteria?
&lt;/h2&gt;

&lt;p&gt;My requirements for the site haven't changed much over the years. I still want a simple, lightweight site that is easy to maintain and easy to update:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Responsive&lt;/li&gt;
&lt;li&gt;Fast with minimal client-side Javascript&lt;/li&gt;
&lt;li&gt;Blog style:

&lt;ul&gt;
&lt;li&gt;front page with recent posts&lt;/li&gt;
&lt;li&gt;tags&lt;/li&gt;
&lt;li&gt;archives&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Searching within the site&lt;/li&gt;

&lt;li&gt;Written using Markdown&lt;/li&gt;

&lt;li&gt;Static pages for books and apps&lt;/li&gt;

&lt;li&gt;Contact form&lt;/li&gt;

&lt;li&gt;Syntax highlighting&lt;/li&gt;

&lt;li&gt;Light and dark themes&lt;/li&gt;

&lt;li&gt;Social links&lt;/li&gt;

&lt;li&gt;JSON Feeds&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Did I Choose Eleventy?
&lt;/h2&gt;

&lt;p&gt;At first, I was looking for an option like Jekyll or Hugo that provided themes that I could plug my content into. After a while, I realized that this was what had caused my problems with the previous generators: they were black boxes, written and configured in languages that I didn't know, so I didn't know how to fix them when things went wrong.&lt;/p&gt;

&lt;p&gt;While I was daunted by the prospect of having to create my own theme, I was very attracted by the idea of using a generator that I understood, with a site structure that I could control completely.&lt;/p&gt;

&lt;p&gt;Given my language preferences, I was looking for a generator that used Swift, Python or Javascript. I asked for advice on Mastodon and got a number of recommendations, but in the end I decided to try Eleventy for several reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's written in JavaScript, which is a language that I'm familiar with.&lt;/li&gt;
&lt;li&gt;It has a large and active community, with lots of articles, tutorials and videos available.&lt;/li&gt;
&lt;li&gt;There are a lot of plugins available both from Eleventy and from third parties.&lt;/li&gt;
&lt;li&gt;I can deploy it automatically from Github using CloudFlare Pages, just like I did with Hugo.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How Did I Learn Eleventy?
&lt;/h2&gt;

&lt;p&gt;I started on the &lt;a href="https://www.11ty.dev" rel="noopener noreferrer"&gt;Eleventy website&lt;/a&gt; and then watched the &lt;a href="https://www.youtube.com/watch?v=BKdQEXqfFA0" rel="noopener noreferrer"&gt;Build an 11ty Site in 3 Minutes&lt;/a&gt; video for a quick start guide. This was a bit like the tutorials on how to draw an owl: draw a circle, add another circle, now draw the rest of the owl.&lt;/p&gt;

&lt;p&gt;After that, I found a great guide at &lt;a href="https://learn-eleventy.pages.dev" rel="noopener noreferrer"&gt;Learn Eleventy&lt;/a&gt;. I got stuck at lesson 18 when I couldn't get &lt;code&gt;gulp&lt;/code&gt; to work, but by that stage I had learned enough to be able to set up my site, configure the folders and files and use the templates to create the pages that I needed.&lt;/p&gt;

&lt;p&gt;The key for me was to accept that my first draft was going to be HTML only, with no styling. This was completely different to Jekyll or Hugo where you start by choosing a theme and then adding your content, so it felt a bit strange at first, but following the example at &lt;a href="https://learn-eleventy.pages.dev" rel="noopener noreferrer"&gt;Learn Eleventy&lt;/a&gt;, my first goal was to get my content appearing with my preferred navigation structure.&lt;/p&gt;

&lt;p&gt;Here's how my files and folders are organized:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl7xccny4wg7jahc2r3u5.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl7xccny4wg7jahc2r3u5.jpeg" alt="Folders and files" width="800" height="620"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At the top level, I have these folders and files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;node_modules: where &lt;code&gt;npm&lt;/code&gt; stores all the required modules, including &lt;code&gt;eleventy&lt;/code&gt; and all of its dependencies.&lt;/li&gt;
&lt;li&gt;public: where the generated files are stored.&lt;/li&gt;
&lt;li&gt;scripts: where I keep my custom Python scripts for bulk edits and configuration.&lt;/li&gt;
&lt;li&gt;src: where the source files are stored - posts, scripts, images, etc.&lt;/li&gt;
&lt;li&gt;notes.md: a file to help me remember what I still need to do.&lt;/li&gt;
&lt;li&gt;package-lock.json: a file that records the exact versions of installed modules.&lt;/li&gt;
&lt;li&gt;package.json: the configuration file for &lt;code&gt;npm&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;README.md: for display on the GitHub page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The main action is in the &lt;code&gt;src&lt;/code&gt; folder, where I have these folders:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;_data: JSON files with data to fill in the navigation menu, footer links, books and apps pages.&lt;/li&gt;
&lt;li&gt;_includes:

&lt;ul&gt;
&lt;li&gt;layouts: Nunjucks templates that are used to create the pages.&lt;/li&gt;
&lt;li&gt;partials: Nunjucks templates that are used for sections of the pages.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;apps: Markdown files for each app page.&lt;/li&gt;

&lt;li&gt;books: Markdown files for book pages and samples.&lt;/li&gt;

&lt;li&gt;feeds: Nunjucks templates for the RSS feeds: XML, Atom and JSON.&lt;/li&gt;

&lt;li&gt;filters: Javascript helper functions.&lt;/li&gt;

&lt;li&gt;images: all the images except the favicon.&lt;/li&gt;

&lt;li&gt;js: client-side Javascript files.&lt;/li&gt;

&lt;li&gt;post: Markdown files for each post, with a subfolder for each year.&lt;/li&gt;

&lt;li&gt;scss: the SCSS files, which are compiled to CSS.&lt;/li&gt;

&lt;li&gt;styles: the CSS files, generated from the SCSS files.&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;src&lt;/code&gt; directory contains a Markdown file or Nunjucks template for each page, the favicon images and the invisible &lt;code&gt;.eleventy.js&lt;/code&gt; file that ties everything together. The Markdown files contain nothing but front matter that points to the relevant layout and adds pagination information if required. Eleventy can use many different templating engines, but the tutorial I followed used &lt;a href="https://mozilla.github.io/nunjucks/" rel="noopener noreferrer"&gt;Nunjucks&lt;/a&gt; and having used &lt;a href="https://handlebarsjs.com/" rel="noopener noreferrer"&gt;Handlebars&lt;/a&gt; for another project, this was reasonably familiar territory.&lt;/p&gt;

&lt;p&gt;One key concept for me was to realize that a &lt;code&gt;filter&lt;/code&gt; in eleventy is not the same as the &lt;code&gt;filter&lt;/code&gt; function in Javascript or Swift. It's easier to think of it as a formatter that takes data and returns all or part of it in a different format. For example, the info at the top of each post shows the date, the number of words and an estimated reading time. That's all produced by a filter written in JavaScript.&lt;/p&gt;

&lt;p&gt;Collections are an incredibly powerful feature of Eleventy, especially when combined with pagination. I'm still learning how to use them effectively, but they allow me to assemble the posts for the main page and for the archives, as well as the list of unique tags for the tags page.&lt;/p&gt;

&lt;p&gt;I'm particularly pleased with the &lt;a href="https://troz.net/archives/" rel="noopener noreferrer"&gt;Archives&lt;/a&gt; page where I wanted to add a year as the header for each year's posts. In the template, I set a variable to hold the year, then use a filter to get the year of each post. If the year is different from the previous post, I add a new header to the list.&lt;/p&gt;

&lt;p&gt;I'm not so thrilled with the &lt;a href="https://troz.net/tags/" rel="noopener noreferrer"&gt;Tags&lt;/a&gt; page, but hopefully I can improve it. The problem is that Eleventy is excellent at collections, but not at collections of collections. I wanted a collection of tags, then a collection of posts for each tag. I haven't worked out how to do this yet, but I have an interim solution that works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plugins
&lt;/h2&gt;

&lt;p&gt;I've used a few plugins although I tried to keep the number down and use my own JavaScript where possible.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@11ty/eleventy-plugin-syntaxhighlight&lt;/code&gt; to highlight the code using Prism.js&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@aloskutov/eleventy-plugin-external-links&lt;/code&gt; to make external links open in a new tab.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@11ty/eleventy-plugin-rss&lt;/code&gt; to generate the RSS feeds.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pagefind&lt;/code&gt; to add search to the site.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If anyone uses an RSS reader for this site, I'd love to know if the formatting works for you. The various links are in the footer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Do I Miss From Hugo?
&lt;/h2&gt;

&lt;p&gt;In Hugo, I could run the command to add a new post and it would fill in the template for me, with the basic front matter, a header and a &lt;code&gt;&amp;lt;!--more--&amp;gt;&lt;/code&gt; marker to separate the summary from the full post. For Eleventy, I wrote a Python script to create a new post. For displaying the first few paragraphs of a post on the home page, I wrote a filter that looks for the &lt;code&gt;&amp;lt;!--more--&amp;gt;&lt;/code&gt; marker and displays the text up to that point. If there is no marker, it displays the first few paragraphs, up to a certain word count.&lt;/p&gt;

&lt;p&gt;Tags were better handled in Hugo, but I haven't finished exploring different ways to do them in Eleventy.&lt;/p&gt;

&lt;p&gt;Hugo has a draft server mode that includes posts marked as draft. My collections exclude draft posts so I have to toggle this setting when writing, but I expect I can get around this using environment settings.&lt;/p&gt;

&lt;p&gt;It was easy to generate a table of contents for a post in Hugo as it automatically made all headers into anchor links. I had to fix this manually for the existing posts with a ToC.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Tips for Eleventy Beginners
&lt;/h2&gt;

&lt;p&gt;If things don't appear to be working, stop the eleventy development server and re-start it. I think if you make too many mistakes, it gets lost and gives up reporting them. When it is showing errors, they are usually quite helpful, pointing to the exact line of code that is causing the problem.&lt;/p&gt;

&lt;p&gt;Don't be afraid to trash the output folder and start again at any time. This is especially important if you've deleted or moved any files as Eleventy will not automatically delete the files in the output folder.&lt;/p&gt;

&lt;p&gt;If you're using SASS or SCSS, compile the CSS into a folder inside the &lt;code&gt;src&lt;/code&gt; folder and then use &lt;code&gt;addPassthroughCopy&lt;/code&gt; to copy these files to the output folder. I made the mistake of compiling the SCSS files directly into the output folder, but when I uploaded the site to CloudFlare, it ran the eleventy command to re-generate the site but didn't re-compile the SCSS files.&lt;/p&gt;

&lt;p&gt;Sometimes, the live server refresh doesn't update the styles, so refresh the page manually before panicking.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Plain HTML Site
&lt;/h2&gt;

&lt;p&gt;With the structure in place, and all my old posts imported, here's what the home page looked like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fucwmxiacdpcz08xkml9x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fucwmxiacdpcz08xkml9x.png" alt="home page" width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And here's what a post looked like, using Prism.js for syntax highlighting:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fscurid5vsvdswphycyih.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fscurid5vsvdswphycyih.png" alt="post page" width="800" height="568"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But now I was stuck. I'm not a complete beginner when it comes to CSS and I knew the sort of look that I wanted, but I knew it would take a lot of my time and effort to get there. However, I use &lt;a href="https://www.cursor.com" rel="noopener noreferrer"&gt;Cursor - The AI Code Editor&lt;/a&gt; as my editor so I decided to see how its integrated AI could help me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Styling With AI
&lt;/h2&gt;

&lt;p&gt;Here was my first prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Create a responsive css theme with light and dark modes. It should have a header with the navigation and a footer with links and copyright notices. The home page has a list of blog posts summaries and each post has its own page. The site is mainly text with some images. Use a sans serif font and a maximum width.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As context, I pointed it at the entire &lt;code&gt;src&lt;/code&gt; folder and I added web search to help it find the right information.&lt;/p&gt;

&lt;p&gt;This was amazingly successful, although it took a lot of tweaks and edits. What I liked was that the AI - claude-3.5-sonnet - told me what it was doing at each step. At the beginning, it listed some web sites that it had consulted about responsive design and then it generated a lot of SCSS. Then it told me what it had done and what I needed to do to finish the job.&lt;/p&gt;

&lt;p&gt;One of the tasks listed was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Navigation should use the nav element with proper structure&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I didn't know what the proper structure was, so I asked the AI to do it.&lt;/p&gt;

&lt;p&gt;After that, I spent 3 - 4 hours talking to the AI, tweaking the styles and getting the layout just right. It was fascinating. I felt like a designer leaning over the shoulder of a CSS expert, although a real-life expert would have become quickly exasperated by my demands!&lt;/p&gt;

&lt;p&gt;At times, I would ask it to revert the previous change and try something different. Other times I would accept the changes but ask for a modification.&lt;/p&gt;

&lt;p&gt;At one point it got a bit cheeky about my existing code:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'll modify the &lt;code&gt;apps.njk&lt;/code&gt; template to match the side-by-side layout style we created for books. I'll also fix the unclosed &lt;code&gt;&amp;lt;/li&amp;gt;&lt;/code&gt; tags in the current file.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once I had the basic styling, here are some examples of the more interesting things I asked the AI to do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a capsule shape for the tag buttons.&lt;/li&gt;
&lt;li&gt;Generate Prism.js styles for the code blocks, so that they looked similar to Xcode's default dark and light themes.&lt;/li&gt;
&lt;li&gt;Add a rotation effect to the dark/light toggle with animation.&lt;/li&gt;
&lt;li&gt;Implement a copy button for the code blocks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Importantly, I was able to understand all the SCSS the AI generated.&lt;/p&gt;

&lt;p&gt;Context is really important. In order to get the best results out of any AI, you have to tell it where it should start. As an example, I got my books page styled in a way that I liked, and then gave the AI the books page layout file and the app page layout file and asked it to apply a similar style to the apps page.&lt;/p&gt;

&lt;p&gt;Also, in many cases, it was faster and easier for me to edit the SCSS manually, than to ask the AI.&lt;/p&gt;

&lt;p&gt;I'm sure that I'll continue to refine the styles over time, but I'm very happy with what I have right now. Using AI to generate the styles was a lot of fun and really demonstrated to me how I can use AI to help me with my work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;You're probably reading this page on my site, so you can see how it looks now. If you're at &lt;a href="https://dev.to/trozware"&gt;dev.to&lt;/a&gt; or some other location, you can see my version at &lt;a href="https://troz.net" rel="noopener noreferrer"&gt;https://troz.net&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I used &lt;a href="https://lighthouse-metrics.com" rel="noopener noreferrer"&gt;Lighthouse Metrics&lt;/a&gt; to check the performance of both the old site and the new site. Here are the results:&lt;/p&gt;

&lt;p&gt;For the old site, I got the following scores:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Performance: 69%&lt;/li&gt;
&lt;li&gt;Accessibility: 95%&lt;/li&gt;
&lt;li&gt;Best Practices: 96%&lt;/li&gt;
&lt;li&gt;SEO: 100%&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the new site, I got these scores:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Performance: 100%&lt;/li&gt;
&lt;li&gt;Accessibility: 100%&lt;/li&gt;
&lt;li&gt;Best Practices: 96%&lt;/li&gt;
&lt;li&gt;SEO: 100%&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;strong&gt;Best Practices&lt;/strong&gt; score is less than 100% because apparently some errors are being logged to the console, but I'm not seeing them so I'm not sure what's causing that.&lt;/p&gt;

&lt;p&gt;But I am thrilled with the improvement in every other metric.&lt;/p&gt;

&lt;p&gt;Deploying to Cloudflare is also faster - about 35 seconds compared to 48 seconds for Hugo. I think this is probably due to having to download two repos for Hugo - mine and the theme's.&lt;/p&gt;

&lt;p&gt;One aspect that I'm really pleased with is the site search. Eleventy uses &lt;a href="https://pagefind.app" rel="noopener noreferrer"&gt;Pagefind&lt;/a&gt; so I started there and found it easy to set up. This is much better than the custom DuckDuckGo search I had embedded on the old site. I'm using the default Pagefind styling for the search results, but I may change that later.&lt;/p&gt;

&lt;p&gt;If you find any bugs or any styling errors, please let me know. And if you would like any further information about how I built the site, please ask. You can contact me using one of the links below or through the &lt;a href="https://troz.net/contact/" rel="noopener noreferrer"&gt;Contact&lt;/a&gt; page.&lt;/p&gt;

</description>
      <category>blog</category>
      <category>eleventy</category>
    </item>
    <item>
      <title>macOS Apprentice Update</title>
      <dc:creator>TrozWare</dc:creator>
      <pubDate>Thu, 06 Mar 2025 11:22:21 +0000</pubDate>
      <link>https://dev.to/trozware/macos-apprentice-update-4ffc</link>
      <guid>https://dev.to/trozware/macos-apprentice-update-4ffc</guid>
      <description>&lt;p&gt;The second edition of &lt;a href="https://www.kodeco.com/books/macos-apprentice/" rel="noopener noreferrer"&gt;macOS Apprentice&lt;/a&gt; has been released!&lt;/p&gt;

&lt;p&gt;If you're a beginner or near-beginner who wants to start learning Swift, SwiftUI and AppKit for building Mac apps, then this is the book for you.&lt;/p&gt;

&lt;p&gt;This edition has been updated for Swift 5.9, macOS 15 and Xcode 16.2.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffc80e43fzrss4q0of8qe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffc80e43fzrss4q0of8qe.png" alt="Book Cover" width="400" height="518"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;macOS Apprentice is a series of multi-chapter tutorials where you'll learn about developing native macOS apps in Swift, using both SwiftUI — Apple's newest user interface technology — and AppKit — the venerable UI framework. Along the way, you'll learn several ways to execute Swift code and you'll build two fully featured apps from scratch.&lt;/p&gt;

&lt;p&gt;If you're new to macOS and Swift, or to programming in general, learning how to write an app can seem incredibly overwhelming.&lt;/p&gt;

&lt;p&gt;That's why you need a guide that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shows you how to write an app step-by-step.&lt;/li&gt;
&lt;li&gt;Uses tons of illustrations and screenshots to make everything clear.&lt;/li&gt;
&lt;li&gt;Guides you in a fun and easy-going manner.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You'll start at the very beginning. The first section assumes you have little to no knowledge of programming in Swift. It walks you through installing Xcode and then teaches you the basics of the Swift programming language. Along the way, you'll explore several different ways to run Swift code, taking advantage of the fact that you're developing natively on macOS.&lt;/p&gt;

&lt;p&gt;macOS Apprentice doesn't cover every single feature of macOS; it focuses on the absolutely essential ones. Instead of just covering a list of features, macOS Apprentice does something much more important: It explains how all the building blocks fit together and what is involved in building real apps. You're not going to create quick example programs that demonstrate how to accomplish a single feature. Instead, you'll develop complete, fully-formed apps, while exploring many of the complexities and joys of programming macOS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Contents
&lt;/h3&gt;

&lt;p&gt;The book consists of four sections:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Section 1&lt;/strong&gt; : Install Xcode and learn the basics of programming in Swift. Experiment with several different ways to execute Swift code on your Mac.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Section 2&lt;/strong&gt; : Use SwiftUI to develop a word-guessing game called Snowman. Learn about data flow in SwiftUI, managing multiple windows, using charts and adding macOS-specific features such as toolbars and menus.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Section 3&lt;/strong&gt; : There are still a number of tasks where AppKit works better than SwiftUI. In this section, build an AppKit app to browse movie data from IMDb, the online movie database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Section 4&lt;/strong&gt; : Add AppKit to your SwiftUI app and add SwiftUI to your AppKit app in order to add some finishing touches to both of the apps from the previous sections.&lt;/p&gt;

&lt;p&gt;You can read more details of the book contents in the &lt;a href="https://www.kodeco.com/books/macos-apprentice/v2.0/chapters/v-introduction" rel="noopener noreferrer"&gt;Introduction&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you are a more experienced iOS developer who wants to branch out into macOS development, then my previous book - &lt;a href="https://troz.net/books/macos_tutorials/" rel="noopener noreferrer"&gt;macOS by Tutorials&lt;/a&gt; - might be a better fit, although I think you could still learn a lot of Mac app tips and tricks from this one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where Can You Find the Resources?
&lt;/h3&gt;

&lt;p&gt;You can read the book online at &lt;a href="https://www.kodeco.com/books/macos-apprentice/" rel="noopener noreferrer"&gt;kodeco.com&lt;/a&gt; as part of a Kodeco subscription. The introduction and the first chapter are free to read.&lt;/p&gt;

&lt;p&gt;All the code and extra materials for the book can be downloaded or cloned from &lt;a href="https://github.com/kodecocodes/maca-materials/tree/editions/2.0" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For support, to ask questions or to report any errors, please go to the &lt;a href="https://forums.kodeco.com/c/books/macos-apprentice/107" rel="noopener noreferrer"&gt;forum&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Thanks to...
&lt;/h3&gt;

&lt;p&gt;This book was made possible by an awesome team. There were a lot of people involved, but I want to give special thanks to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.kodeco.com/u/rcritz" rel="noopener noreferrer"&gt;Richard Critz&lt;/a&gt;, the wonderful editor who had the unenviable task of fixing my grammar.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.kodeco.com/u/audrey" rel="noopener noreferrer"&gt;Audrey Tam&lt;/a&gt; and &lt;a href="https://www.kodeco.com/u/ehabamer" rel="noopener noreferrer"&gt;Ehab Amer&lt;/a&gt; were the amazing tech editors who had to make sure that it all worked. Audrey also contributed the first chapter on installing and setting up Xcode.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole team at &lt;a href="https://www.kodeco.com/" rel="noopener noreferrer"&gt;Kodeco&lt;/a&gt; was great, so a huge thanks to them all. I hope they enjoyed working on the book as much as I did! It's wonderful to have such a supportive group working to make my content as good as it can be.&lt;/p&gt;

&lt;h3&gt;
  
  
  Feedback
&lt;/h3&gt;

&lt;p&gt;I would love to hear from anyone who read the book, loved it, hated it, found an error or just wanted to say hello.&lt;/p&gt;

&lt;p&gt;You can contact me directly using any of the contact links at the bottom of this page or through the &lt;a href="https://troz.net/contact/" rel="noopener noreferrer"&gt;Contact&lt;/a&gt; page.&lt;/p&gt;

</description>
      <category>swift</category>
      <category>swiftui</category>
      <category>appkit</category>
    </item>
    <item>
      <title>The Mac Menubar and SwiftUI</title>
      <dc:creator>TrozWare</dc:creator>
      <pubDate>Mon, 17 Feb 2025 07:16:34 +0000</pubDate>
      <link>https://dev.to/trozware/the-mac-menubar-and-swiftui-id1</link>
      <guid>https://dev.to/trozware/the-mac-menubar-and-swiftui-id1</guid>
      <description>&lt;p&gt;When you create a Mac app using SwiftUI, you get the standard Mac menubar by default. The &lt;code&gt;commands&lt;/code&gt; modifier lets you customize the menu bar, either by adding, replacing or removing items and menus. You can even add some presets which give a consistent way to add groups of common items.&lt;/p&gt;

&lt;p&gt;The problem comes when you want to communicate back to the SwiftUI views from the menubar. How can you direct your menubar commands to the correct destination? AppKit uses the responder chain, so it effectively broadcasts any menubar message until something handles it. This might be an edit field, a view, a window or even the app itself. SwiftUI doesn’t work like this, but I’ve explored multiple possibilities for passing messages from the menubar to the active window.&lt;/p&gt;

&lt;p&gt;My usual approach is to use &lt;code&gt;NotificationCenter&lt;/code&gt; to send messages. This takes a few steps to set up.&lt;/p&gt;

&lt;h2&gt;
  
  
  NotificationCenter
&lt;/h2&gt;

&lt;p&gt;Start by defining a name for your custom notification, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;extension&lt;/span&gt; &lt;span class="kt"&gt;Notification&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;menuSelected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Notification&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"menuSelected"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in your menu item’s action, &lt;code&gt;post&lt;/code&gt; the notification like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kt"&gt;NotificationCenter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;menuSelected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;object&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add an object if you want the notification to be more specific. This allows you to combine several menu items into a single notification name.&lt;/p&gt;

&lt;p&gt;Now, you need to add a listener as a property of your SwiftUI view, receiving it on the main run loop so that UI updates are handled correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;menuSelectedNotification&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;NotificationCenter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;
  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publisher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;menuSelected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;receive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;RunLoop&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, add an &lt;code&gt;onReceive&lt;/code&gt; modifier to your view, with a closure to process the notification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onReceive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;menuSelectedNotification&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;notification&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="c1"&gt;// process the notification, checking for the object if you added one&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;stringObject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;object&lt;/span&gt; &lt;span class="k"&gt;as?&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// do something with the string&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// do something else&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main issue with this is that it broadcasts the message to all instances of the view, so if you have multiple windows open and displaying this view, they’ll all receive and process the notification. There are cases where this will be valid - maybe changing the theme of the complete app or setting something else which effects every open window. But mostly, you only want to send the message to the active window.&lt;/p&gt;

&lt;p&gt;You can improve this by using the &lt;code&gt;appearsActive&lt;/code&gt; environment value to see if the current view is active. Truncated for ease of reading, &lt;code&gt;ContentView&lt;/code&gt; now looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;ContentView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;@Environment&lt;/span&gt;&lt;span class="p"&gt;(\&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;appearsActive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;appearsActive&lt;/span&gt;

  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;menuSelectedNotification&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;NotificationCenter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publisher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;menuSelected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;receive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;RunLoop&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;some&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// UI defined here&lt;/span&gt;

    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onReceive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;menuSelectedNotification&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;notification&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
      &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="n"&gt;appearsActive&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;stringObject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;object&lt;/span&gt; &lt;span class="k"&gt;as?&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// process the new string&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;onReceive&lt;/code&gt; closure starts with a &lt;code&gt;guard&lt;/code&gt; to check that the view is active. If it’s not, the closure returns immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt; : You may have used &lt;code&gt;controlActiveState&lt;/code&gt; for this in the past, but it has been deprecated in favor of &lt;code&gt;appearsActive&lt;/code&gt; which is easier to use as it’s a Boolean instead of an enum.&lt;/p&gt;

&lt;p&gt;This looks like exactly what we need, but there’s a problem. If you merge multiple windows into tabs, the &lt;code&gt;appearsActive&lt;/code&gt; property will be the same for all the tabs in the window.&lt;/p&gt;

&lt;p&gt;So basically, this only works if you disallow tabbing for your windows, so it’s not a complete solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failed Attempts
&lt;/h2&gt;

&lt;p&gt;My next attempt was to step into AppKit and have &lt;code&gt;NSApp&lt;/code&gt; send a selector through the responder chain. This looked like it should work, but I could never get it to, and it looked clunky&lt;/p&gt;

&lt;p&gt;Looking though an Apple tutorial, it appears that they prefer to use &lt;code&gt;@FocusedBinding&lt;/code&gt; and &lt;code&gt;focusedValue&lt;/code&gt;. This worked in their sample app, but there was something different about my data structure that meant it never worked for me, even after extracting my data into a separate data type.&lt;/p&gt;

&lt;p&gt;Then I came across &lt;code&gt;focusedSceneObject&lt;/code&gt;. This required me to create an &lt;code&gt;@ObservableObject&lt;/code&gt; data type, but then it worked perfectly, even in tabbed windows. But this is not viable going forward, as I have switched to using &lt;code&gt;Observation&lt;/code&gt;. However the docs did point me in the right direction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Success at Last
&lt;/h2&gt;

&lt;p&gt;After a bit of research and a &lt;em&gt;lot&lt;/em&gt; of testing, I finally came up with a complete solution, using &lt;code&gt;@FocusedBinding&lt;/code&gt; and &lt;code&gt;focusedSceneValue&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;First, you need a data object to handle your menu messages. This can be a &lt;code&gt;struct&lt;/code&gt; or an observable &lt;code&gt;class&lt;/code&gt;. My sample app has both in &lt;strong&gt;Symbol.swift&lt;/strong&gt; with one commented out, so you can test this. The data model has properties for an icon and a color and you can also have it choose a random icon and color. For convenience, it has two static arrays to provide the names and colors for the menus and pickers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;Symbol&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"globe"&lt;/span&gt;
  &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Color&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;blue&lt;/span&gt;

  &lt;span class="k"&gt;mutating&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;chooseRandomSymbolAndColor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// choose random values&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or as a class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;@Observable&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;Symbol&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"globe"&lt;/span&gt;
  &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Color&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;blue&lt;/span&gt;

  &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;chooseRandomSymbolAndColor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// choose random values&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, you need to extend &lt;code&gt;FocusedValues&lt;/code&gt; so it has a key to your data type. This has become a lot less verbose with the new &lt;code&gt;@Entry&lt;/code&gt; macro. The binding must be to an optional:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;extension&lt;/span&gt; &lt;span class="kt"&gt;FocusedValues&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;@Entry&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;selectedSymbol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Binding&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;Symbol&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your &lt;code&gt;ContentView&lt;/code&gt; (or wherever you’re using this property), set the data property as the &lt;code&gt;focusedSceneValue&lt;/code&gt; for this key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;ContentView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;@State&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;symbol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Symbol&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;some&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;VStack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// UI defined here&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;focusedSceneValue&lt;/span&gt;&lt;span class="p"&gt;(\&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;selectedSymbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;$symbol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last step is to use this in the menu (again, truncated for brevity):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;@main&lt;/span&gt;
&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;MenuDataApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;App&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;@FocusedBinding&lt;/span&gt;&lt;span class="p"&gt;(\&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;selectedSymbol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;selectedSymbol&lt;/span&gt;

  &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;some&lt;/span&gt; &lt;span class="kt"&gt;Scene&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;WindowGroup&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kt"&gt;ContentView&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;commands&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kt"&gt;CommandMenu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Symbol"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;Menu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Symbol"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="kt"&gt;ForEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Symbol&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;\&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="kt"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;selectedSymbol&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;"✔︎ &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="n"&gt;selectedSymbol&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;selectedSymbol&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="kt"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Random"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;selectedSymbol&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chooseRandomSymbolAndColor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keyboardShortcut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"r"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;selectedSymbol&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important features here are the &lt;code&gt;@FocusedBinding&lt;/code&gt; property at the top and the fact that the menu items use optional chaining to access this property if it exists. If you have no open windows, this will be nil, otherwise it will point to the instance of the data property in the active window.&lt;/p&gt;

&lt;p&gt;To improve the user experience, I added a &lt;code&gt;disabled&lt;/code&gt; modifier to the menus and the button so that the menus are not active unless there’s a valid &lt;code&gt;selectedSymbol&lt;/code&gt;. Unfortunately, you can’t disable an entire &lt;code&gt;CommandMenu&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The wonderful part of this is that it works with windows and tabbed windows. Hurray!&lt;/p&gt;

&lt;h3&gt;
  
  
  Showing the Current Selections in the Menu
&lt;/h3&gt;

&lt;p&gt;The ugly part is that the menu item indicating the current choice is set to show a check mark manually, instead of using the standard menu checkmark. But you can’t bind a &lt;code&gt;@FocusedBinding&lt;/code&gt; property to a &lt;code&gt;Picker&lt;/code&gt; as its selection.&lt;/p&gt;

&lt;p&gt;&lt;del&gt;My solution was to use a &lt;code&gt;Picker&lt;/code&gt; with a local state property. Then I track for changes to the &lt;code&gt;@FocusedBinding&lt;/code&gt; property and to the local property. When either changes, the other is set to match, remembering that the &lt;code&gt;@FocusedBinding&lt;/code&gt; property may be nil:&lt;br&gt;
It was important to add the &lt;code&gt;onChange&lt;/code&gt; modifiers to the &lt;code&gt;ContentView&lt;/code&gt;. I tried them on the &lt;code&gt;Picker&lt;/code&gt; first, but they only got updated as the menu opened.&lt;/del&gt;&lt;/p&gt;

&lt;p&gt;I've updated the project, thanks to &lt;a href="https://github.com/malhal" rel="noopener noreferrer"&gt;Malcolm Hall&lt;/a&gt; who added this comment:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;onChange is just for external actions. For linking states its Binding and it has an init that handles conversion from optional, that allows you to access the keypath binding to the symbol's colour and name you were missing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now, the picker code looks like this (I omitted the color picker to make for a shorter and more readable code block):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;selectedSymbol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Binding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;$selectedSymbol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;Picker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Symbol"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;selectedSymbol&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;ForEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Symbol&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;\&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
      &lt;span class="kt"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pickerStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Symbol"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also made the pickers inline for better visibility. With this version, I really like the way the menus use a &lt;code&gt;Text&lt;/code&gt; view to display the header with no possible selections, when there's no &lt;code&gt;selectedSymbol&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqfg1wfm4pfq1uzl4gkzz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqfg1wfm4pfq1uzl4gkzz.png" alt="Final app" width="800" height="599"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So this is my new technique for communications between the menubar and SwiftUI. It takes a bit of setting up, but then it works really well. For a more complex app, I might need more bindings, or I could merge the relevant data objects in a larger struct or class.&lt;/p&gt;

&lt;p&gt;Going back to Apple’s example which used &lt;code&gt;@FocusedBinding&lt;/code&gt; and &lt;code&gt;focusedValue&lt;/code&gt;, I think this worked because they had a &lt;code&gt;NavigationSplitView&lt;/code&gt; and the focused value was the selected item, so there was an object to focus on. From the Apple’s docs for &lt;code&gt;focusedSceneValue&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use this method instead of focusedValue(&lt;em&gt;:&lt;/em&gt;:) for values that must be visible regardless of where focus is located in the active scene.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is what solved it for me because there was no real focus element in my view. I was able to go back to the Apple sample app and get it working using &lt;code&gt;focusedSceneValue&lt;/code&gt; instead of &lt;code&gt;focusedValue&lt;/code&gt;, so I think this is a solid technique.&lt;/p&gt;

&lt;p&gt;I’m sure there are other ways to do this, so if you have an alternative method or can suggest any improvements to my technique, I’d love to hear about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Code
&lt;/h2&gt;

&lt;p&gt;The sample project is available &lt;a href="https://github.com/trozware/MacMenuComms" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;. Check out the &lt;code&gt;main&lt;/code&gt; branch for the final version of the code, but explore the other branches to see my experiments along the way.&lt;/p&gt;

&lt;p&gt;Here they are, in order of progress:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;basic_ui&lt;/strong&gt; : The starter version of the app with the UI set up for selecting an icon and a color.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;menu&lt;/strong&gt; : Menus added but not working.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;notifications&lt;/strong&gt; : Using &lt;code&gt;NotificationCenter&lt;/code&gt; to send menu messages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;focusedSceneObject&lt;/strong&gt; : Works but only with &lt;code&gt;@ObservableObject&lt;/code&gt; data types.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;main&lt;/strong&gt; : The technique I’ve settled on with &lt;code&gt;@FocusedBinding&lt;/code&gt; and &lt;code&gt;focusedSceneValue&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can contact me using one of the links below or through the &lt;a href="https://troz.net/contact/" rel="noopener noreferrer"&gt;Contact&lt;/a&gt; page. And if you found this article useful, please &lt;a href="https://ko-fi.com/trozware" rel="noopener noreferrer"&gt;buy me a coffee&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>macos</category>
      <category>swift</category>
      <category>swiftui</category>
    </item>
    <item>
      <title>macOS by Tutorials 3.0</title>
      <dc:creator>TrozWare</dc:creator>
      <pubDate>Mon, 23 Dec 2024 04:22:15 +0000</pubDate>
      <link>https://dev.to/trozware/macos-by-tutorials-30-4949</link>
      <guid>https://dev.to/trozware/macos-by-tutorials-30-4949</guid>
      <description>&lt;p&gt;macOS by Tutorials Edition 3.0 is now available!&lt;/p&gt;

&lt;p&gt;The book is available for purchase or update at &lt;a href="https://sarahreichelt.gumroad.com/l/oximx" rel="noopener noreferrer"&gt;Gumroad&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you previously bought the first edition of this book from either Kodeco or Amazon, please &lt;a href="//mailto:books@troz.net?subject=macOS%20by%20Tutorials%20Discount"&gt;email me&lt;/a&gt; for a 50% discount code.&lt;/p&gt;

&lt;p&gt;if you already bought the second edition from me via Gumroad, this is a free update that you can download from your Gumroad library.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://troz.net/books/macos_tutorials/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fprnr01tld8gugmf9joai.png" alt="Book cover" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click the image for more details, and if you’d like to check out the start of the book including the table of contents and the first chapter, you can read it online at &lt;a href="https://troz.net/books/mos_sample.html" rel="noopener noreferrer"&gt;macOS by Tutorials Sample&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The major changes in this edition include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Updated projects to include new features introduced in macOS 15 Sequoia and Xcode 16.&lt;/li&gt;
&lt;li&gt;More granular chapter organization. The table of contents is now more of an index so you can find the sections you need, with each chapter being more focussed on a single topic.&lt;/li&gt;
&lt;li&gt;All projects use Xcode’s folder structure.&lt;/li&gt;
&lt;li&gt;Tabs use the new syntax.&lt;/li&gt;
&lt;li&gt;Previews use the new &lt;code&gt;@Previewable&lt;/code&gt; macro.&lt;/li&gt;
&lt;li&gt;Tracking the active window uses the &lt;code&gt;@Entry&lt;/code&gt; macro.&lt;/li&gt;
&lt;li&gt;The app in section 4 demonstrates the new windowing options.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you already own the book and have enjoyed it or found it useful, I’d really appreciate it if you could leave a rating or review at the book’s page in your Gumroad library.&lt;/p&gt;

</description>
      <category>macos</category>
      <category>swift</category>
      <category>swiftui</category>
      <category>appkit</category>
    </item>
    <item>
      <title>Swift Format in Xcode</title>
      <dc:creator>TrozWare</dc:creator>
      <pubDate>Wed, 06 Nov 2024 06:29:20 +0000</pubDate>
      <link>https://dev.to/trozware/swift-format-in-xcode-285e</link>
      <guid>https://dev.to/trozware/swift-format-in-xcode-285e</guid>
      <description>&lt;p&gt;In Xcode 16, Apple quietly introduced the ability to format your Swift files using Swift Format. I’m a long-time user of SwiftLint, but having such a tool built into Xcode would be a great convenience, so I decided to give it a try. Here is my description of why I use such a tool, how well it works compared to the alternatives, and how I configured it for my own purposes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Format Your Code?
&lt;/h2&gt;

&lt;p&gt;I have always been a big fan of consistent code formatting. As I wrote years ago in &lt;a href="https://troz.net/post/2018/swiftlint/" rel="noopener noreferrer"&gt;Consistent Swift Style&lt;/a&gt;, we read code far more often than we write it, so anything that improves readability is great for productivity, even for a solo developer. If you work as part of a team, consistency is even more important as you don’t want other people to have to spend their valuable time trying to understand your code, or the other way round.&lt;/p&gt;

&lt;p&gt;I have been an editor and writer for &lt;a href="https://www.kodeco.com/" rel="noopener noreferrer"&gt;Kodeco&lt;/a&gt; (formerly raywenderlich.com) for many years and with so many different authors on the team, it was crucial for there to be a consistent style. This is why I always use SwiftLint in my Kodeco projects, using the configuration file found here: &lt;a href="https://github.com/kodecocodes/swift-style-guide/blob/main/SWIFTLINT.markdown" rel="noopener noreferrer"&gt;Kodeco Swift Style Guide&lt;/a&gt;. For my own projects, I vary this configuration slightly, but I always use SwiftLint. Similarly, when doing web development, I always use &lt;a href="https://prettier.io" rel="noopener noreferrer"&gt;Prettier&lt;/a&gt; which integrates beautifully with Visual Studio Code so that my files are automatically prettified on save.&lt;/p&gt;

&lt;h2&gt;
  
  
  Swift Format
&lt;/h2&gt;

&lt;p&gt;The name Apple chose is logical but confusing. I had experimented with Nick Lockwood’s &lt;a href="https://github.com/nicklockwood/SwiftFormat" rel="noopener noreferrer"&gt;SwiftFormat&lt;/a&gt; a few years ago, but found that I preferred SwiftLint, so I stuck with the latter. When I saw that Xcode included Swift Format, I assumed that they had acquired Nick’s formatter but it appears to be a different tool, officially called &lt;a href="https://github.com/swiftlang/swift-format" rel="noopener noreferrer"&gt;swift-format&lt;/a&gt;. It’s downloaded as part of the toolchain when you install Xcode 16, so no further installation is required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test Code
&lt;/h2&gt;

&lt;p&gt;I created a test project in Xcode 16 and wrote a chunk of Swift and SwiftUI for testing. I deliberately formatted it badly so as to see which formatter did what to it, so don’t hate me for this nonsensical code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;SwiftUI&lt;/span&gt;

&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;ContentView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;@State&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;]()&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;some&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kt"&gt;NavigationSplitView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;sidebar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;\&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
          &lt;span class="kt"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nv"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;someComputedProperty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onAppear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;createTestData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;createTestData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;..&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"Row #&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;downloadData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://zenquotes.io/api/quotes"&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;string&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;URLSession&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;

      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utf8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;someComputedProperty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"""
    This is a multiline string,
    that is a computed property which can be tricky to format.

    It has nothing to do with the list on the side, but I wanted to see how swift-format would handle it.
    """&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;longFunctionNameThatDoesSomething&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"This is a long function name that does something"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kt"&gt;FunctionwithLotsOfArguments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;arg1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;arg2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;arg3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;arg4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;arg5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;arg6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;arg7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;arg8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;arg9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;arg10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Yes, this deliberately starts with an uppercase letter&lt;/span&gt;
  &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="kt"&gt;FunctionwithLotsOfArguments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;arg1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;arg2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;arg3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;arg4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;arg5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;arg6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;arg7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;arg8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;arg9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;arg10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"This is a function with a lot of arguments"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cp"&gt;#Preview {&lt;/span&gt;
    &lt;span class="kt"&gt;ContentView&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some comments on this code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I use 2 space indentation, but Xcode creates files with 4 spaces, so indentation is a mess.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;NavigationSplitView&lt;/code&gt; is not using multiple trailing closure syntax.&lt;/li&gt;
&lt;li&gt;I’ve left unnecessary blank lines in the code.&lt;/li&gt;
&lt;li&gt;The networking code is not what I would write, but it was suggested by GitHub Copilot.&lt;/li&gt;
&lt;li&gt;Multi-line strings are often difficult to format.&lt;/li&gt;
&lt;li&gt;The two functions at the end really need to spread over more lines and the last one should start with a lowercase letter.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Using Other Formatters
&lt;/h2&gt;

&lt;p&gt;Before checking out Xcode’s &lt;code&gt;swift-format&lt;/code&gt;, I want to try &lt;strong&gt;SwiftLint&lt;/strong&gt; and &lt;strong&gt;Prettier&lt;/strong&gt;. I have SwiftLint installed already, so I can run it from the Terminal. After I used &lt;code&gt;cd&lt;/code&gt; to step into the project’s code folder, I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;swiftlint ContentView.swift
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is using the default SwiftLint configuration and gave this result (I deleted the folder path but you’ll still have to scroll sideways to read this.):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Linting Swift files at paths ContentView.swift
Linting 'ContentView.swift' (1/1)
ContentView.swift:28:25: warning: Comma Spacing Violation: There should be no space before and one after any comma (comma)
ContentView.swift:51:3: error: Function Parameter Count Violation: Function should have 5 parameters or less: it currently has 10 (function_parameter_count)
ContentView.swift:51:8: error: Identifier Name Violation: Function name 'FunctionwithLotsOfArguments(arg1:arg2:arg3:arg4:arg5:arg6:arg7:arg8:arg9:arg10:)' should start with a lowercase character (identifier_name)
ContentView.swift:47:1: warning: Line Length Violation: Line should be 120 characters or less; currently it has 123 characters (line_length)
ContentView.swift:51:1: warning: Line Length Violation: Line should be 120 characters or less; currently it has 147 characters (line_length)
ContentView.swift:28:39: warning: Non-Optional String &amp;lt;-&amp;gt; Data Conversion Violation: Prefer using UTF-8 encoded strings when converting between `String` and `Data` (non_optional_string_data_conversion)
ContentView.swift:59:1: warning: Trailing Newline Violation: Files should have a single trailing newline (trailing_newline)
ContentView.swift:40:1: warning: Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
ContentView.swift:26:62: warning: Unused Closure Parameter Violation: Unused parameter in a closure should be replaced with _ (unused_closure_parameter)
ContentView.swift:26:72: warning: Unused Closure Parameter Violation: Unused parameter in a closure should be replaced with _ (unused_closure_parameter)
ContentView.swift:20:1: warning: Vertical Whitespace Violation: Limit vertical whitespace to a single empty line; currently 2 (vertical_whitespace)
Done linting! Found 11 violations, 2 serious in 1 file.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SwiftLint has the ability to fix some issues, so I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;swiftlint &lt;span class="nt"&gt;--fix&lt;/span&gt; ContentView.swift
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Correcting Swift files at paths ContentView.swift
Correcting 'ContentView.swift' (1/1)
ContentView.swift:28:25 Corrected Comma Spacing
ContentView.swift:58:1 Corrected Trailing Newline
ContentView.swift:40:1 Corrected Trailing Whitespace
ContentView.swift:26:62 Corrected Unused Closure Parameter
ContentView.swift:26:72 Corrected Unused Closure Parameter
ContentView.swift:19:1 Corrected Vertical Whitespace
Done correcting 1 file!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reduced the number of problems to 5, with 2 serious. What it fixed was mostly whitespace issues, but it also removed the two used parameters from the networking closure. It did not fix the indentation.&lt;/p&gt;

&lt;p&gt;Next on my list was Prettier, so I reverted to the badly formatted code and opened the project folder in Visual Studio Code. I had already installed the Swift and Prettier extensions, so I opened and re-saved the file to make Prettier do its thing. This did a slightly better job than SwiftLint: it fixed the indentation and spread the long function call and definition over three lines. It also removed the two parameters from the networking closure and added some line feeds to make the code more readable.&lt;/p&gt;

&lt;h2&gt;
  
  
  swift-format
&lt;/h2&gt;

&lt;p&gt;Finally, it was time to see what &lt;code&gt;swift-format&lt;/code&gt; can do. After reverting to the original code again, I chose &lt;strong&gt;Editor -&amp;gt; Structure -&amp;gt; Format File with ‘swift-format’&lt;/strong&gt; :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxlzzpmofws1a1ut9n8o4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxlzzpmofws1a1ut9n8o4.png" alt="Swift Format" width="800" height="339"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This fixed the indentation and the whitespace issues. Interestingly, it split the long function call and definition over multiple lines, but still with more than one argument per line. It did not remove the two parameters from the networking closure. I think I may be running into the difference between a formatter and a linter.&lt;/p&gt;

&lt;p&gt;While I was impressed overall, I disliked the way it removed spaces around the range operator. I prefer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;..&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"Row #&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But after using swift-format, it was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"Row #&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configuring swift-format #
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;swift-format&lt;/code&gt; tool is installed as part of the Xcode toolchain, so the first step in configuring it was to locate it, using the instructions found &lt;a href="https://github.com/swiftlang/swift-format" rel="noopener noreferrer"&gt;here&lt;/a&gt; under &lt;strong&gt;Included in the Swift Toolchain&lt;/strong&gt;. In Terminal, I used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xcrun &lt;span class="nt"&gt;--find&lt;/span&gt; swift-format
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which gave me: &lt;strong&gt;/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-format&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Using this path, I was able to export the default configuration file (I included the full path to the tool in the command, but faked it here for ease of reading):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;path-to/swift-format dump-configuration &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; swift-format-default-config.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Opening the file, it has a bunch of settings and a long list of rules. I’m not going to go through them, but the file is in the project folder, so you can see it if you download the project from &lt;a href="https://github.com/trozware/swift-format-tests" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and switch to the &lt;code&gt;swift-format&lt;/code&gt; branch.&lt;/p&gt;

&lt;p&gt;The indentation is set to use 2 spaces. I assume that was picked up from my Xcode settings, since it’s different to the default. The setting I want to change is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"spacesAroundRangeFormationOperators"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before making any changes, I need to save the file in a location and with a name that &lt;code&gt;swift-format&lt;/code&gt; can find. In my tests, I found that saving it as &lt;strong&gt;.swift-format&lt;/strong&gt; in my home directory meant that it applied to every Xcode project. To do this, I followed this sequence in Finder:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open my &lt;strong&gt;Home&lt;/strong&gt; directory.&lt;/li&gt;
&lt;li&gt;Press &lt;strong&gt;Command-Shift-Period&lt;/strong&gt; to show hidden files.&lt;/li&gt;
&lt;li&gt;Option-drag &lt;strong&gt;swift-format-default-config.json&lt;/strong&gt; to the Home directory.&lt;/li&gt;
&lt;li&gt;Rename the file to &lt;strong&gt;.swift-format&lt;/strong&gt; agreeing to the warning about making it invisible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then I edited it, changing the setting to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"spacesAroundRangeFormationOperators"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Back in Xcode, I pressed &lt;strong&gt;Shift-Control-I&lt;/strong&gt; to reformat the file. This time, the range operator was formatted with spaces as I wanted.&lt;/p&gt;

&lt;p&gt;To make future configuration changes easier, I created an alias to &lt;strong&gt;.swift-format&lt;/strong&gt; in my Home folder and called it &lt;strong&gt;swift-format alias.json&lt;/strong&gt;. This gives me a visible link that will open in a JSON editor. With that in place, I pressed &lt;strong&gt;Shift-Command-Period&lt;/strong&gt; again to hide invisible files.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;swift-format&lt;/code&gt; has a lint option, so in Terminal, I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;swift-format lint ContentView.swift
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which reported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ContentView.swift:55:8: warning: [AlwaysUseLowerCamelCase] rename the function 'FunctionwithLotsOfArguments' using lowerCamelCase
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If I was going to do this on a regular basis, I would want to add the path to the&lt;code&gt;swift-format&lt;/code&gt; tool to my &lt;code&gt;PATH&lt;/code&gt; variable or create an alias to it in my &lt;strong&gt;.zshrc&lt;/strong&gt; file, but it was valid information.&lt;/p&gt;

&lt;p&gt;One extra tip: use &lt;strong&gt;Editor -&amp;gt; Structure -&amp;gt; Format to Multiple Lines&lt;/strong&gt; or &lt;strong&gt;Control-M&lt;/strong&gt; to split long lines into multiples. This is a much more consistent way of spreading out long function calls and definitions. You’d think that &lt;strong&gt;Editor -&amp;gt; Structure -&amp;gt; Reformat to Width&lt;/strong&gt; would do the opposite, but in my tests, it did either nothing, or the same thing as &lt;strong&gt;Format to Multiple Lines&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I am going to use &lt;code&gt;swift-format&lt;/code&gt; in my projects from now on. It’s less intrusive than SwiftLint and I like that it’s built into the Xcode toolchain. I am going to assign a different keyboard shortcut as &lt;strong&gt;Shift-Control-I&lt;/strong&gt; is an awkward combination to reach on my Ergodox Moonlander keyboard.&lt;/p&gt;

&lt;p&gt;Conveniently, the command applies to the entire active file, regardless of selection. I’m used to using &lt;strong&gt;Command-A -&amp;gt; Control-I&lt;/strong&gt; to fix indentation issues, but this method only requires a single key command.&lt;/p&gt;

&lt;p&gt;For future reference, be aware of this note from the &lt;a href="https://github.com/swiftlang/swift-format" rel="noopener noreferrer"&gt;swift-format GitHub page&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;NOTE: No default Swift code style guidelines have yet been proposed. The style that is currently applied by swift-format is just one possibility, and the code is provided so that it can be tested on real-world code and experiments can be made by modifying it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I am hoping that Apple or the Swift team does develop a style guide while still allowing for customization, but in the meantime, I will continue to tweak the settings to suit myself. I presume that if I include a &lt;code&gt;.swift-format&lt;/code&gt; file in my project, it will override any other settings. This would be great for distributing tutorial projects with a consistent style. I’d also love to be able to set the formatter to run on save, like Prettier does in Visual Studio Code.&lt;/p&gt;

&lt;p&gt;The test project is on GitHub at &lt;a href="https://github.com/trozware/swift-format-tests" rel="noopener noreferrer"&gt;trozware/swift-format-tests&lt;/a&gt; if you want to try it out for yourself. There’s a separate branch for each formatter, with &lt;code&gt;main&lt;/code&gt; holding the original, badly formatted code.&lt;/p&gt;

&lt;p&gt;If you have any other thoughts or suggestions, I’d love to hear them. You can contact me using the &lt;a href="https://troz.net/contact/" rel="noopener noreferrer"&gt;Contact&lt;/a&gt; page on my site. And if you found this, or any of my articles useful, please &lt;a href="https://ko-fi.com/trozware" rel="noopener noreferrer"&gt;buy me a coffee&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>swift</category>
      <category>xcode</category>
      <category>swiftlint</category>
      <category>swiftformat</category>
    </item>
    <item>
      <title>App Permissions on macOS Sequoia</title>
      <dc:creator>TrozWare</dc:creator>
      <pubDate>Wed, 02 Oct 2024 01:33:26 +0000</pubDate>
      <link>https://dev.to/trozware/app-permissions-on-macos-sequoia-5dgo</link>
      <guid>https://dev.to/trozware/app-permissions-on-macos-sequoia-5dgo</guid>
      <description>&lt;p&gt;In 2012, with OS X Mountain Lion, Apple added a feature called &lt;a href="https://en.wikipedia.org/wiki/Gatekeeper_%28macOS%29" rel="noopener noreferrer"&gt;Gatekeeper&lt;/a&gt;. It had been available earlier as a command line utility, but this was the first time they made it accessible through System Preferences. Gatekeeper allowed users to control which apps could be installed on their Macs by offering three options: allow apps from &lt;strong&gt;App Store&lt;/strong&gt; , &lt;strong&gt;App Store and identified developers&lt;/strong&gt; or &lt;strong&gt;Anywhere&lt;/strong&gt;. This was the start of Apple trying to lock Macs down, similarly to how iOS devices are locked down, but it allowed power users to install any apps they wanted.&lt;/p&gt;

&lt;p&gt;In macOS Sierra (2019), the &lt;strong&gt;Anywhere&lt;/strong&gt; option was removed. It was still possible to open any app by right-clicking and selecting &lt;strong&gt;Open&lt;/strong&gt;. You had to get past a couple of warning dialogs, but it worked. Now, in macOS Sequoia, even that has gone. So how can you open an app that isn’t signed by an identified developer?&lt;/p&gt;

&lt;p&gt;TL;DR: You can still run unsigned apps, but it’s a bit more difficult. After trying once, you have to go to &lt;strong&gt;System Settings -&amp;gt; Privacy &amp;amp; Security&lt;/strong&gt; , scroll to the end and click &lt;strong&gt;Open Anyway&lt;/strong&gt; for that app.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing on my Computer
&lt;/h3&gt;

&lt;p&gt;I created a test app called &lt;strong&gt;UnsignedApp&lt;/strong&gt; , and made sure to leave the developer team set to &lt;strong&gt;None&lt;/strong&gt; so the app could not be code signed. I archived the app in Xcode and clicked &lt;strong&gt;Distribute App&lt;/strong&gt; in the Organizer window. Instead of selecting one of the standard options, I chose &lt;strong&gt;Custom&lt;/strong&gt; and clicked &lt;strong&gt;Next&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7sq95k8ci9ms55xk0wfe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7sq95k8ci9ms55xk0wfe.png" alt="Custom distribution" width="800" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I selected &lt;strong&gt;Copy App&lt;/strong&gt; as it was the only option that wasn’t going to sign the app.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqvfow0ftjyrs51nk7ca1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqvfow0ftjyrs51nk7ca1.png" alt="Copy App option" width="800" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I saved the app to my desktop and opened it without any problems. Next, I moved it into my &lt;strong&gt;Applications&lt;/strong&gt; folder and tried again. Still no problems. So presumably, even though it was unsigned, it was OK because I had created it. Next, I tried running it when logged in as a different user on my computer. This still worked, rather to my surprise. So I assume that my computer is registered to my developer account and any apps I create are allowed to run on it, regardless of user.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing on Someone Else’s Computer
&lt;/h3&gt;

&lt;p&gt;The problems started when I tried running the app on someone else’s Mac. This dialog appeared (presumably the default button says Move to Trash where appropriate):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc63qlandytqfvtu6mgta.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc63qlandytqfvtu6mgta.png" alt="Unsigned app warning" width="350" height="312"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I clicked &lt;strong&gt;Done&lt;/strong&gt; to close the dialog and tried the old right-click and &lt;strong&gt;Open&lt;/strong&gt; trick. No good - this showed the identical dialog.&lt;/p&gt;

&lt;p&gt;Finally, I remembered reading something about this on Mastodon. I forget who posted it, so if it was you, thank you very much because Apple certainly wasn’t helping here. I opened &lt;strong&gt;System Settings&lt;/strong&gt; and went to &lt;strong&gt;Privacy &amp;amp; Security&lt;/strong&gt;. After scrolling all the way down to the end, past the sign marked &lt;a href="https://www.azquotes.com/quote/354892" rel="noopener noreferrer"&gt;&lt;strong&gt;Beware of the Leopard&lt;/strong&gt;&lt;/a&gt; (with apologies to the late, great Douglas Adams), I found this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj854hx7iuauf48667bvt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj854hx7iuauf48667bvt.png" alt="System Settings" width="800" height="191"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is the Gatekeeper setting with the remaining two options, but added on is a message about my app being blocked. I clicked &lt;strong&gt;Open Anyway&lt;/strong&gt; and got this dialog:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhcm3zqr9m2dfsfxwk4zd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhcm3zqr9m2dfsfxwk4zd.png" alt="Open Anyway dialog" width="350" height="442"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At least this gave me an option to proceed. I clicked &lt;strong&gt;Open Anyway&lt;/strong&gt; and got this scary (and badly written) message:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwfo1jj6zi81l4pbul1xo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwfo1jj6zi81l4pbul1xo.png" alt="Final warning" width="350" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I authenticated and finally, the app ran. But there’s one more interesting twist. The app has the ability to show its running location. When I ran the app from the &lt;strong&gt;Applications&lt;/strong&gt; folder, it was in /Applications as expected, but when I deleted it from there and tried running it from the &lt;strong&gt;Downloads&lt;/strong&gt; folder, macOS moved it into a hidden AppTranslocation folder and ran it from there:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvic2ws8zw3j7cunxbth1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvic2ws8zw3j7cunxbth1.png" alt="Running from secure location" width="600" height="319"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Using Terminal
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;spctl&lt;/code&gt; command line utility used to allow full manual control of Gatekeeper. In macOS Sequoia, it has lost most of its power, but you can still use it to re-enable the &lt;strong&gt;Anywhere&lt;/strong&gt; option in &lt;strong&gt;System Settings -&amp;gt; Privacy &amp;amp; Security -&amp;gt; Allow applications from&lt;/strong&gt; using this command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;spctl --global-disable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhkq76g85p8ror0kucvlj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhkq76g85p8ror0kucvlj.png" alt="After running spctl command" width="800" height="114"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you’re already in &lt;strong&gt;System Settings -&amp;gt; Privacy &amp;amp; Security&lt;/strong&gt; , go to a different settings page and back again to see the change.&lt;/p&gt;

&lt;p&gt;If you choose &lt;strong&gt;Anywhere&lt;/strong&gt; , you have to authenticate, but then you can run any app. I wouldn’t bet on this staying around forever, but for the moment, it’s nice to see it’s still there. The extra choice disappears after about 8 minutes if you don’t select it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;You can work around this limitation, but proceed with caution. There are bad Mac apps out there, so don’t use these workarounds unless you’re confident of the source. Be particularly wary of any app that asks you to authenticate.&lt;/p&gt;

&lt;p&gt;Apple has always been very keen to keep iOS devices locked down. They maintain that this is essential for security but given some of the scams in the App Store, that is a debatable point. The Mac has historically always been open and allowed users to do what they wanted. Over the past few years we have seen Apple gradually closing down the Mac to bring it more in line with the other devices.&lt;/p&gt;

&lt;p&gt;With my tech support hat on I can see the benefits of this, but as a power user, I want to have the tools to work around it if necessary.&lt;/p&gt;

&lt;p&gt;As a developer, I realize that it is now virtually impossible to release any Mac apps without having a developer account. Mac apps must be notarized by Apple so that they fall into the &lt;strong&gt;Known Developers&lt;/strong&gt; category, regardless of whether they are distributed through the App Store or by other means.&lt;/p&gt;

&lt;p&gt;If you have any thoughts or suggestions about this, contact me using the &lt;a href="https://troz.net/contact/" rel="noopener noreferrer"&gt;Contact&lt;/a&gt; page on my site. And if you found this article useful, please &lt;a href="https://ko-fi.com/trozware" rel="noopener noreferrer"&gt;buy me a coffee&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>macos</category>
      <category>gatekeeper</category>
    </item>
  </channel>
</rss>
