<?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: Kirk Int</title>
    <description>The latest articles on DEV Community by Kirk Int (@kirk_int64).</description>
    <link>https://dev.to/kirk_int64</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4121836%2Fbfe0ca1a-450b-4687-926d-84b70f06798b.jpg</url>
      <title>DEV Community: Kirk Int</title>
      <link>https://dev.to/kirk_int64</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kirk_int64"/>
    <language>en</language>
    <item>
      <title>Shipping a paid Mac app outside the App Store in two days</title>
      <dc:creator>Kirk Int</dc:creator>
      <pubDate>Sat, 12 Sep 2026 07:55:00 +0000</pubDate>
      <link>https://dev.to/kirk_int64/shipping-a-paid-mac-app-outside-the-app-store-in-two-days-5b4l</link>
      <guid>https://dev.to/kirk_int64/shipping-a-paid-mac-app-outside-the-app-store-in-two-days-5b4l</guid>
      <description>&lt;p&gt;On Wednesday Apple showed the iPhone Duo and its status ring: battery, Wi-Fi and volume folded into one small ring. I wanted it on my Mac. By Friday the app was notarized, on sale and updating itself.&lt;/p&gt;

&lt;p&gt;Writing the app took about a day. Everything else  signing, notarization, payments, license keys, updates, the installer took the other day, and that's the part nobody writes about. So here it is, including the mistakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The app, briefly
&lt;/h2&gt;

&lt;p&gt;A menu bar app in Swift: AppKit for the status item and the panel, SwiftUI for the panel's contents. A 3.8 MB download, no Electron, one dependency (Sparkle).&lt;/p&gt;

&lt;p&gt;Three system reads: &lt;code&gt;IOPSCopyPowerSourcesInfo&lt;/code&gt; for battery, CoreWLAN for Wi-Fi, CoreAudio for volume, each with a change listener so the icon redraws only when something actually changes.&lt;/p&gt;

&lt;p&gt;One non-obvious thing: &lt;strong&gt;Wi-Fi on/off needs no permission, but reading the SSID does.&lt;/strong&gt; &lt;code&gt;CWInterface.setPower(_:)&lt;/code&gt; just works. The moment you call &lt;code&gt;scanForNetworks&lt;/code&gt; or read the current network name, macOS wants Location access — for a menu bar utility, a Location prompt on first launch looks like spyware. So the app never reads the SSID and never asks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rounding a glass panel kills the glass
&lt;/h2&gt;

&lt;p&gt;The panel is an &lt;code&gt;NSPanel&lt;/code&gt; with an &lt;code&gt;NSVisualEffectView&lt;/code&gt;. I rounded it the obvious way:&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;effect&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layer&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cornerRadius&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;
&lt;span class="n"&gt;effect&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layer&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;masksToBounds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The corners rounded. The blur silently disappeared — the panel became a flat grey rectangle, and no API told me why. Behind-window blending can't survive being composited into a masked layer. The fix is to let the effect view do its own masking:&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;effect&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;material&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;menu&lt;/span&gt;
&lt;span class="n"&gt;effect&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;blendingMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;behindWindow&lt;/span&gt;
&lt;span class="n"&gt;effect&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt;
&lt;span class="n"&gt;effect&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maskImage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;roundedMask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// a resizable capImage-style mask&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worth knowing: offscreen renders (&lt;code&gt;bitmapImageRepForCachingDisplay&lt;/code&gt;) always come out opaque, so you cannot verify vibrancy in a headless screenshot. I wasted time comparing renders that could never show the bug. Verify it on a real screen capture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cmd-V doesn't work in an LSUIElement app
&lt;/h2&gt;

&lt;p&gt;The app is &lt;code&gt;LSUIElement&lt;/code&gt; (no Dock icon). The license key window had a text field, and paste did nothing. Neither did Cmd-A or Cmd-Z.&lt;/p&gt;

&lt;p&gt;Those shortcuts aren't implemented by the text field — they're menu items. An accessory app has no menu bar, so there are no menu items, so the key equivalents never fire. You have to build a main menu yourself, even though the user will never see 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="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;edit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;NSMenu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Edit"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;edit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;withTitle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Cut"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;#selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;NSText&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;_&lt;/span&gt;&lt;span class="p"&gt;:)),&lt;/span&gt; &lt;span class="nv"&gt;keyEquivalent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"x"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;edit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;withTitle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Copy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;#selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;NSText&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;_&lt;/span&gt;&lt;span class="p"&gt;:)),&lt;/span&gt; &lt;span class="nv"&gt;keyEquivalent&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="n"&gt;edit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;withTitle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Paste"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;#selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;NSText&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;paste&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;_&lt;/span&gt;&lt;span class="p"&gt;:)),&lt;/span&gt; &lt;span class="nv"&gt;keyEquivalent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"v"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// …plus Select All and Undo/Redo&lt;/span&gt;
&lt;span class="kt"&gt;NSApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mainMenu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mainMenu&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Keychain froze my app on launch
&lt;/h2&gt;

&lt;p&gt;I stored the license record in the Keychain, which is what you're supposed to do. Then every time I re-signed the app with a different identity, launch hung on a &lt;code&gt;SecurityAgent&lt;/code&gt; prompt asking whether this "new" app may read its own item.&lt;/p&gt;

&lt;p&gt;Keychain items are bound to the signing identity. For a $4.99 utility whose secret is a license key the customer already has in their email, that's a bad trade. It now lives in &lt;code&gt;UserDefaults&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;UserDefaults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;suiteName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"app.dynamicring.mac"&lt;/span&gt;&lt;span class="p"&gt;)?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&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;forKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"license.polar"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A determined user can edit it. A determined user can also patch the binary. The Keychain wasn't buying real protection here, only launch hangs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Payments and license keys without building an account system
&lt;/h2&gt;

&lt;p&gt;I didn't want accounts, logins or a server. &lt;a href="https://polar.sh" rel="noopener noreferrer"&gt;Polar&lt;/a&gt; sells the product, emails a license key, and exposes a customer-portal API that needs no API key in the client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /v1/customer-portal/license-keys/activate
POST /v1/customer-portal/license-keys/validate
POST /v1/customer-portal/license-keys/deactivate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app posts the key plus the organization ID, stores the returned activation ID, and revalidates on launch. One rule that matters: &lt;strong&gt;only remove a stored license on an explicit rejection.&lt;/strong&gt; If the network is down or the API 500s, the customer keeps working. Failing closed on a flaky connection is how you earn refund requests.&lt;/p&gt;

&lt;p&gt;Polar has a sandbox environment, which I compiled into debug builds only, behind a &lt;code&gt;UserDefaults&lt;/code&gt; flag:&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="cp"&gt;#if DEBUG&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="kt"&gt;UserDefaults&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;standard&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;forKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"PolarSandbox"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kt"&gt;PolarConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;useSandbox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="cp"&gt;#endif&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Release builds cannot be pointed at sandbox by any user default. That turned out to matter the first time I tested: I activated a sandbox key in a release build and spent ten minutes confused about why "the key isn't recognised."&lt;/p&gt;

&lt;h2&gt;
  
  
  Developer ID and notarization
&lt;/h2&gt;

&lt;p&gt;Two surprises here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Only the Account Holder can create a Developer ID certificate.&lt;/strong&gt; Admin isn't enough. If you're using a company account where someone else is the holder, plan for that before launch day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;notarytool&lt;/code&gt; with an Apple ID and app-specific password returned 401 for two different accounts.&lt;/strong&gt; The same credentials worked on the website. Switching to an App Store Connect API key worked immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xcrun notarytool store-credentials dynamicring-notary &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key&lt;/span&gt; ~/.appstoreconnect/private_keys/AuthKey_XXXXXXXX.p8 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key-id&lt;/span&gt; XXXXXXXX &lt;span class="nt"&gt;--issuer&lt;/span&gt; &amp;lt;issuer-uuid&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notarize the zipped app, staple it, build the DMG, then notarize and staple the DMG too. Verify the way Gatekeeper will, on a copy that carries a quarantine flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xattr &lt;span class="nt"&gt;-w&lt;/span&gt; com.apple.quarantine &lt;span class="s2"&gt;"0081;&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; %x &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;))&lt;/span&gt;&lt;span class="s2"&gt;;Safari;"&lt;/span&gt; DynamicRing.dmg
spctl &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-vv&lt;/span&gt; /Volumes/DynamicRing/DynamicRing.app   &lt;span class="c"&gt;# source=Notarized Developer ID&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you only test the build sitting in your own &lt;code&gt;dist/&lt;/code&gt; folder, you're testing the case Gatekeeper doesn't care about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sparkle: sign inside out, never &lt;code&gt;--deep&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Sparkle ships XPC services and a helper app inside its framework, and they carry entitlements the other binaries must not inherit. &lt;code&gt;codesign --deep&lt;/code&gt; flattens that and produces an updater that fails in ways you'll only see on a customer's machine. Sign the nested parts first, the framework next, the app last:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;nested &lt;span class="k"&gt;in &lt;/span&gt;Downloader.xpc Installer.xpc Autoupdate Updater.app&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;codesign &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="nt"&gt;--options&lt;/span&gt; runtime &lt;span class="nt"&gt;--timestamp&lt;/span&gt; &lt;span class="nt"&gt;--sign&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERSIONS&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$nested&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done
&lt;/span&gt;codesign &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="nt"&gt;--options&lt;/span&gt; runtime &lt;span class="nt"&gt;--timestamp&lt;/span&gt; &lt;span class="nt"&gt;--sign&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$APP&lt;/span&gt;&lt;span class="s2"&gt;/Contents/Frameworks/Sparkle.framework"&lt;/span&gt;
codesign &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="nt"&gt;--options&lt;/span&gt; runtime &lt;span class="nt"&gt;--timestamp&lt;/span&gt; &lt;span class="nt"&gt;--sign&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$APP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SwiftPM also doesn't add the rpath a bundled framework needs, so build with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;swift build &lt;span class="nt"&gt;-c&lt;/span&gt; release &lt;span class="nt"&gt;-Xlinker&lt;/span&gt; &lt;span class="nt"&gt;-rpath&lt;/span&gt; &lt;span class="nt"&gt;-Xlinker&lt;/span&gt; @executable_path/../Frameworks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Back up the EdDSA private key (&lt;code&gt;generate_keys -x&lt;/code&gt;) somewhere that isn't the Mac you're typing on. Lose it and you can never ship an update to existing customers again.&lt;/p&gt;

&lt;p&gt;One more: &lt;code&gt;generate_appcast&lt;/code&gt; writes delta updates into the appcast by default. My release script copied the DMG and the appcast to the site, but not the &lt;code&gt;.delta&lt;/code&gt; files, so the feed advertised downloads that 404. Either upload the deltas or turn them off (&lt;code&gt;--maximum-deltas 0&lt;/code&gt;). For an app this small, off is fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Immutable caching will serve your old bytes forever
&lt;/h2&gt;

&lt;p&gt;The site is on Cloudflare Pages with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/releases/*
  Cache-Control: public, max-age=31536000, immutable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I shipped 0.1.0, found a bug, rebuilt, and re-uploaded the &lt;strong&gt;same filename&lt;/strong&gt;. Cloudflare kept serving the old file for a year, as instructed. Sparkle downloaded it, compared it against the new EdDSA signature in the appcast, and refused the update — correctly.&lt;/p&gt;

&lt;p&gt;New version, new filename, always. My release script now refuses to run if the target filename already exists in the site folder, and refuses if the build number isn't higher than the one already in the published appcast. Guards like that are cheap and they only fire on the day you're rushing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The DMG that renders nothing on macOS 26
&lt;/h2&gt;

&lt;p&gt;I built a proper installer with &lt;code&gt;dmgbuild&lt;/code&gt;: app icon on the left, Applications alias on the right, a background image with an arrow.&lt;/p&gt;

&lt;p&gt;On macOS 26 the background didn't show. Same DMG, same &lt;code&gt;.DS_Store&lt;/code&gt;, older Macs fine. What fixed it was deleting the &lt;code&gt;pBBk&lt;/code&gt; record — a bookmark Finder writes into &lt;code&gt;.DS_Store&lt;/code&gt; next to the background picture. Newer Finder seems to prefer that bookmark over the embedded picture and then render nothing when it can't resolve it. Strip it after building:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;DSStore&lt;/span&gt;&lt;span class="p"&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;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pBBk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pBBk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note &lt;code&gt;b"pBBk"&lt;/code&gt; — the code is bytes. Passing the string silently deletes nothing, which is how I lost twenty minutes.&lt;/p&gt;

&lt;p&gt;Second Finder quirk: with a background picture, Finder draws icon labels in black even in dark mode. Design the installer art light, or your labels vanish.&lt;/p&gt;

&lt;h2&gt;
  
  
  Low Power Mode can only be changed by root
&lt;/h2&gt;

&lt;p&gt;The panel has a switch for Low Power Mode. There is no public API for it, and &lt;code&gt;pmset -a lowpowermode 1&lt;/code&gt; needs root.&lt;/p&gt;

&lt;p&gt;The supported route is a launchd daemon registered with &lt;code&gt;SMAppService&lt;/code&gt;, which the user approves once under Login Items › Allow in the Background. The daemon plist ships inside the bundle at &lt;code&gt;Contents/Library/LaunchDaemons/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;BundleProgram&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;string&amp;gt;&lt;/span&gt;Contents/MacOS/DynamicRingHelper&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;MachServices&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;dict&amp;gt;&amp;lt;key&amp;gt;&lt;/span&gt;app.dynamicring.mac.helper&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;true/&amp;gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;AssociatedBundleIdentifiers&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;array&amp;gt;&amp;lt;string&amp;gt;&lt;/span&gt;app.dynamicring.mac&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&amp;lt;/array&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The helper is 66 lines. It runs one command and nothing else, and it only listens to the app:&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;listener&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;NSXPCListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;machServiceName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;PowerHelper&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;listener&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setConnectionCodeSigningRequirement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s"&gt;"identifier &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;app.dynamicring.mac&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt; and anchor apple generic "&lt;/span&gt;
  &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"and certificate leaf[subject.OU] = &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;TEAMID&amp;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;It also exits after 60 seconds of inactivity, so an app update never leaves an old root binary running.&lt;/p&gt;

&lt;p&gt;Two things I'd tell myself: &lt;code&gt;SMAppService.daemon(_:).status&lt;/code&gt; reports &lt;code&gt;.notFound&lt;/code&gt; before the first registration, not &lt;code&gt;.notRegistered&lt;/code&gt;, so treat anything that isn't &lt;code&gt;.enabled&lt;/code&gt; as "try to register". And after &lt;code&gt;register()&lt;/code&gt; the status is usually &lt;code&gt;.requiresApproval&lt;/code&gt; — that's your cue to call &lt;code&gt;SMAppService.openSystemSettingsLoginItems()&lt;/code&gt; and tell the user what to do, not to show an error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was it worth it?
&lt;/h2&gt;

&lt;p&gt;The plumbing is about 300 lines of shell and one afternoon of reading documentation, and it's reusable for every app I ship after this one. Now &lt;code&gt;VERSION=0.1.2 BUILD=3 ./scripts/release.sh&lt;/code&gt; builds, signs, notarizes, staples, makes the DMG, notarizes that, checks Gatekeeper, regenerates the appcast and stages everything into the website repo. The whole release takes under fifteen minutes, most of it waiting on Apple's notary service.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx9rgqlgvg7bfyno5kp8m.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx9rgqlgvg7bfyno5kp8m.png" alt="ScreenShot" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The app is &lt;a href="https://dynamicring.app" rel="noopener noreferrer"&gt;DynamicRing&lt;/a&gt;&lt;br&gt;
$4.99, macOS 14+, Apple silicon — and it exists because a keynote gave me an idea on Wednesday and nothing in the toolchain stopped me from selling it on Friday. That's a pretty good deal, once you know where the tripwires are.&lt;/p&gt;

</description>
      <category>swift</category>
      <category>showdev</category>
      <category>indiehackers</category>
      <category>macos</category>
    </item>
  </channel>
</rss>
