<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://swiftjectivec.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://swiftjectivec.com/" rel="alternate" type="text/html" /><updated>2026-09-09T15:11:40-05:00</updated><id>https://swiftjectivec.com/feed.xml</id><title type="html">Swiftjective-C</title><subtitle>Swift, SwiftUI, UIKit, iOS programming, and indie development articles by Jordan Morgan.</subtitle><author><name>Jordan Morgan</name></author><entry><title type="html">iPhone Duo: First Developer Good-to-Knows</title><link href="https://swiftjectivec.com/iPhone-Duo-First-Developer-Good-to-Knows/" rel="alternate" type="text/html" title="iPhone Duo: First Developer Good-to-Knows" /><published>2026-09-09T00:00:00-05:00</published><updated>2026-09-09T00:00:00-05:00</updated><id>https://swiftjectivec.com/iPhone-Duo-First-Developer-Good-to-Knows</id><content type="html" xml:base="https://swiftjectivec.com/iPhone-Duo-First-Developer-Good-to-Knows/"><![CDATA[<p>It’s time for Apple to get into the… fold… with foldables. Anyways, on the day of the announcement, Cupertino and Friends™️ dropped six new videos over developing, designing, and crafting great apps for the nascent device:</p>

<ol>
  <li><a href="https://developer.apple.com/videos/play/tech-talks/111466/">Design for iPhone Duo</a></li>
  <li><a href="https://developer.apple.com/videos/play/tech-talks/111461/">Prepare your app for iPhone Duo</a></li>
  <li><a href="https://developer.apple.com/videos/play/tech-talks/111462/">Raise the bar with iPhone Duo</a></li>
  <li><a href="https://developer.apple.com/videos/play/tech-talks/111463/">Strike a pose with adaptive layouts on iPhone Duo</a></li>
  <li><a href="https://developer.apple.com/videos/play/tech-talks/111464/">Leverage multiple displays and scenes on iPhone Duo</a></li>
  <li><a href="https://developer.apple.com/videos/play/tech-talks/111465/">Build a great camera experience for iPhone Duo</a></li>
</ol>

<p>And there’s some ground to cover, because it seems like this thing can contort your app’s layout about 7,000 different ways:</p>

<p><img class="lozad mx-auto max-w-full h-auto shadow-2xl rounded-lg h-18" data-src="../assets/images/iphone-duo-layouts.jpg" role="presentation" srcset="../assets/images/iphone-duo-layouts.jpg" alt="Mail adapting to six iPhone Duo layouts, including closed, open, landscape, portrait, and partially folded poses." /></p>

<p>I’ve only watched one, and the other five are next for me to marathon, but in the interim I had Astra check out each one because I’m excited and impatient. Here’s what it thinks the critical things to know are, with the obvious caveat that this is at the discretion of an agent.</p>

<section class="bg-indigo-50 border-l-8 border-indigo-500 text-gray-800 rounded-md my-10 dark:text-gray-300 dark:bg-gray-600 dark:border-indigo-700 ">
    <div class="px-5 font-mono">
        <p class="pt-2 md:pt-4"><span class="text-purple-600 dark:text-purple-400">let</span> concatenatedThoughts = <span class="text-red-600">"""</span></p>
        <p class="text-gray-600 dark:text-gray-300 text-sm md:text-base">These takeaways come from Apple's English video captions. Some new iOS 27.1 API reference pages weren't available at writing; documentation links below point to available references, with timestamps for the new APIs.</p>
        <p class="pb-2 md:pb-4 text-gray-600 text-base"><span class="text-red-600">"""</span></p>
    </div>
</section>

<section class="agent-notes" aria-label="AI-generated summary">
  <div class="agent-notes-header">
  <span aria-hidden="true" class="agent-notes-prompt">&gt;_</span>
  <span>astra / session notes</span>
</div>
  <div class="agent-notes-disclaimer">
  <strong>AI-generated summary</strong>
  <p>The following section was generated by Astra from Apple's video captions. Follow the linked videos and documentation for the original guidance.</p>
</div>
  <div class="agent-notes-body">

    <h2 id="build-with-ios-271">Build With iOS 27.1</h2>

    <p>Existing apps run, but rebuilding with the iOS 27.1 SDK enables edge-to-edge content and vertical bars. Use Xcode 27.1’s Device Hub to test opening, closing, rotating, and partially folding; its updated App Resizability skill also covers SwiftUI and Duo.</p>

    <ul>
      <li>Video: <a href="https://developer.apple.com/videos/play/tech-talks/111461/?time=30">Prepare your app — 0:30</a></li>
      <li>Video: <a href="https://developer.apple.com/videos/play/tech-talks/111461/?time=552">App Resizability skill — 9:12</a></li>
      <li>Docs: <a href="https://developer.apple.com/documentation/xcode/device-hub">Device Hub</a></li>
    </ul>

    <h2 id="size-classes-beat-device-assumptions">Size Classes Beat Device Assumptions</h2>

    <p>Duo remains an iPhone even when its inner display has regular width and height size classes. Use size classes and local view or scene geometry; avoid layout branches based on idiom, interface orientation, or <code class="language-plaintext highlighter-rouge">UIScreen.main</code>. The inner display doesn’t honor supported interface orientations for normally resizable apps.</p>

    <ul>
      <li>Video: <a href="https://developer.apple.com/videos/play/tech-talks/111461/?time=166">Prepare your app — 2:46</a></li>
      <li>Docs: <a href="https://developer.apple.com/documentation/swiftui/environmentvalues/horizontalsizeclass">horizontalSizeClass</a></li>
    </ul>

    <h2 id="keep-the-same-app-inside-and-out">Keep the Same App Inside and Out</h2>

    <p>Use the extra space for columns or richer navigation while preserving the same hierarchy and functionality across poses. <code class="language-plaintext highlighter-rouge">NavigationSplitView</code> and <code class="language-plaintext highlighter-rouge">UISplitViewController</code> adapt automatically. A tabletop layout can put media above controls, but opening or folding the phone shouldn’t be required to access a feature.</p>

    <ul>
      <li>Video: <a href="https://developer.apple.com/videos/play/tech-talks/111466/?time=222">Design for iPhone Duo — 3:42</a></li>
      <li>Video: <a href="https://developer.apple.com/videos/play/tech-talks/111466/?time=454">Inner display layouts — 7:34</a></li>
      <li>Docs: <a href="https://developer.apple.com/documentation/swiftui/navigationsplitview">NavigationSplitView</a></li>
    </ul>

    <h2 id="safe-areas-are-asymmetric">Safe Areas Are Asymmetric</h2>

    <p>Vertical controls can occupy either edge, especially in Split View. Handle each safe-area inset and layout margin independently: keep interactive content inside, let backgrounds extend behind bars, and test your app on both sides of the inner display.</p>

    <ul>
      <li>Video: <a href="https://developer.apple.com/videos/play/tech-talks/111461/?time=366">Prepare your app — 6:06</a></li>
      <li>Docs: <a href="https://developer.apple.com/documentation/uikit/positioning-content-relative-to-the-safe-area">Positioning content relative to the safe area</a></li>
    </ul>

    <h2 id="let-containers-move-your-bars">Let Containers Move Your Bars</h2>

    <p>Use bars managed by navigation and tab containers; standalone UIKit bar instances don’t participate in the new vertical layout. Supply both titles and symbols so the system can choose a representation. Custom controls need an explicit vertical opt-in and must fit the bar’s fixed width; meaningful text can stay horizontal.</p>

    <ul>
      <li>Video: <a href="https://developer.apple.com/videos/play/tech-talks/111462/?time=120">Raise the bar — 2:00</a></li>
      <li>Video: <a href="https://developer.apple.com/videos/play/tech-talks/111462/?time=356">Toolbar content and axis behavior — 5:56</a></li>
      <li>Docs: <a href="https://developer.apple.com/documentation/swiftui/toolbaritem">ToolbarItem</a></li>
    </ul>

    <h2 id="decide-what-overflows-first">Decide What Overflows First</h2>

    <p>Toolbars compress before tabs by default; task-focused screens can change that preference. Give frequent actions and status-bearing items higher visibility priority, and consolidate custom overflow actions into <code class="language-plaintext highlighter-rouge">ToolbarOverflowMenu</code> or UIKit’s <code class="language-plaintext highlighter-rouge">additionalOverflowItems</code> so users get one system overflow menu.</p>

    <ul>
      <li>Video: <a href="https://developer.apple.com/videos/play/tech-talks/111462/?time=700">Raise the bar — 11:40</a></li>
      <li>Docs: <a href="https://developer.apple.com/documentation/swiftui/toolbaroverflowmenu">ToolbarOverflowMenu</a></li>
      <li>Docs: <a href="https://developer.apple.com/documentation/swiftui/toolbarcontent/visibilitypriority(_:)">visibilityPriority</a></li>
    </ul>

    <h2 id="move-controls-away-from-the-fold">Move Controls Away From the Fold</h2>

    <p>System sheets, alerts, and menus already avoid the fold. For custom layouts, query <code class="language-plaintext highlighter-rouge">reservedRegion</code> on <code class="language-plaintext highlighter-rouge">GeometryProxy</code> or <code class="language-plaintext highlighter-rouge">UIView</code>: division regions describe the fold, while occlusion regions describe obstructions such as the active inner camera. Reposition important controls; continuous scrolling articles, feeds, and lists don’t need displacement.</p>

    <ul>
      <li>Video: <a href="https://developer.apple.com/videos/play/tech-talks/111463/?time=146">Displacement patterns — 2:26</a></li>
      <li>Video: <a href="https://developer.apple.com/videos/play/tech-talks/111463/?time=399">Reserved-region APIs — 6:39</a></li>
    </ul>

    <h2 id="arrangements-handle-two-view-layouts">Arrangements Handle Two-View Layouts</h2>

    <p><code class="language-plaintext highlighter-rouge">ArrangementView</code> and <code class="language-plaintext highlighter-rouge">UIArrangementViewController</code> adapt two views around available space and the fold. Choose split when both views must remain unobscured, or overlay for a foreground/background relationship. Arrangements provide layout, not navigation: avoid placing navigation containers inside them or embedding arrangements inside lists and scroll views.</p>

    <ul>
      <li>Video: <a href="https://developer.apple.com/videos/play/tech-talks/111463/?time=677">Strike a pose — 11:17</a></li>
      <li>Video: <a href="https://developer.apple.com/videos/play/tech-talks/111463/?time=879">Choosing an arrangement and limitations — 14:39</a></li>
    </ul>

    <h2 id="hinge-angles-drive-effects">Hinge Angles Drive Effects</h2>

    <p><code class="language-plaintext highlighter-rouge">onHingeChange</code> and <code class="language-plaintext highlighter-rouge">UIHingeInteraction</code> expose discrete hinge states and continuous angles for interactions like a musical pitch bend. Handle a missing hinge on other devices and reset effects when appropriate. Use arrangements and reserved regions for layout decisions.</p>

    <ul>
      <li>Video: <a href="https://developer.apple.com/videos/play/tech-talks/111464/?time=49">Leverage multiple displays and scenes — 0:49</a></li>
    </ul>

    <h2 id="multitasking-can-change-both-dimensions">Multitasking Can Change Both Dimensions</h2>

    <p>Side-by-side apps reduce width; pinned Picture in Picture can reduce height. Duo also supports multiple instances of your app, but new windows can only be created on the inner display. Handle scene-request errors; <code class="language-plaintext highlighter-rouge">UIWindowScene.ActivationAction</code> automatically hides when opening another window is unavailable.</p>

    <ul>
      <li>Video: <a href="https://developer.apple.com/videos/play/tech-talks/111466/?time=201">Design: pinned video — 3:21</a></li>
      <li>Video: <a href="https://developer.apple.com/videos/play/tech-talks/111464/?time=179">Multitasking and multiple scenes — 2:59</a></li>
      <li>Docs: <a href="https://developer.apple.com/documentation/uikit/supporting-multiple-windows-on-ipad">Supporting multiple windows</a></li>
      <li>Docs: <a href="https://developer.apple.com/documentation/uikit/uiwindowscene/activationaction">ActivationAction</a></li>
    </ul>

    <h2 id="camera-apps-can-use-both-displays">Camera Apps Can Use Both Displays</h2>

    <p><code class="language-plaintext highlighter-rouge">CameraCaptureAccessory</code> can show a subject-facing preview or teleprompter on the outer display while the main camera UI occupies the inner display. It requires a full-screen app on the inner display and an active camera session. Register it with the camera view and observe availability changes.</p>

    <ul>
      <li>Video: <a href="https://developer.apple.com/videos/play/tech-talks/111464/?time=262">Scene accessories — 4:22</a></li>
      <li>Background docs: <a href="https://developer.apple.com/documentation/uikit/presenting-content-on-a-connected-display">Scene accessory registration</a></li>
    </ul>

    <h2 id="choose-automatic-camera-switching-or-full-capabilities">Choose Automatic Camera Switching or Full Capabilities</h2>

    <p>Front-camera discovery using wide or ultrawide types returns a virtual front camera that switches between displays automatically, limited to 1080p/60fps and no depth. Access the physical cameras individually for depth or the outer camera’s higher limits—up to 4K and 120fps—but manage switching yourself.</p>

    <ul>
      <li>Video: <a href="https://developer.apple.com/videos/play/tech-talks/111465/?time=58">Build a great camera experience — 0:58</a></li>
      <li>Docs: <a href="https://developer.apple.com/documentation/avfoundation/choosing-a-capture-device">Choosing a capture device</a></li>
    </ul>

    <h2 id="camera-position-isnt-camera-direction">Camera Position Isn’t Camera Direction</h2>

    <p>A camera marked <code class="language-plaintext highlighter-rouge">.front</code> can face away from the person using your app. AVKit’s <code class="language-plaintext highlighter-rouge">AVCaptureDeviceDirectionCoordinator</code> reports direction relative to a view; use one per display view. Its callback runs on the main actor: pass the sendable <code class="language-plaintext highlighter-rouge">AVCaptureDeviceDescriptor</code> to your camera actor before reconfiguring capture, then update preview mirroring and UI.</p>

    <ul>
      <li>Video: <a href="https://developer.apple.com/videos/play/tech-talks/111465/?time=166">Camera direction — 2:46</a></li>
      <li>Video: <a href="https://developer.apple.com/videos/play/tech-talks/111465/?time=338">Actor handling and switching — 5:38</a></li>
    </ul>

    <h2 id="revisit-preview-framing-and-rotation">Revisit Preview Framing and Rotation</h2>

    <p>Use preview <code class="language-plaintext highlighter-rouge">videoGravity</code> to choose fit versus fill, and dynamic aspect ratios to exploit the square front sensors. Adopt <code class="language-plaintext highlighter-rouge">AVCaptureDevice.RotationCoordinator</code> for upright previews and captures across display changes; after adopting it, disable camera-sensor-orientation compensation to improve performance.</p>

    <ul>
      <li>Video: <a href="https://developer.apple.com/videos/play/tech-talks/111465/?time=423">Camera preview and rotation — 7:03</a></li>
      <li>Docs: <a href="https://developer.apple.com/documentation/avfoundation/avcapturevideopreviewlayer/videogravity">videoGravity</a></li>
      <li>Docs: <a href="https://developer.apple.com/documentation/avfoundation/avcapturedevice/setdynamicaspectratio(_:completionhandler:)">Dynamic aspect ratio</a></li>
      <li>Docs: <a href="https://developer.apple.com/documentation/avfoundation/avcapturedevice/rotationcoordinator">RotationCoordinator</a></li>
    </ul>

  </div>
  <div class="agent-notes-footer">&gt; _ end of agent notes</div>
</section>

<p>Until next time ✌️</p>]]></content><author><name>Jordan Morgan</name></author><category term="Swift" /><summary type="html"><![CDATA[Several new videos dropped today covering iPhone Dou development best practices. Here's the summary from an agent.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://swiftjectivec.com/assets/images/logo.png" /><media:content medium="image" url="https://swiftjectivec.com/assets/images/logo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">The Spend Stack Revival</title><link href="https://swiftjectivec.com/The-Spend-Stack-Revival/" rel="alternate" type="text/html" title="The Spend Stack Revival" /><published>2026-08-31T00:00:00-05:00</published><updated>2026-08-31T00:00:00-05:00</updated><id>https://swiftjectivec.com/The-Spend-Stack-Revival</id><content type="html" xml:base="https://swiftjectivec.com/The-Spend-Stack-Revival/"><![CDATA[<p>Ah, Spend Stack. I’m pleased to announce that everyone’s favorite finance app/running total list thingy/do-it-all-calculator-with-1000-use-cases-but-not-one-clearly-defined is coming back from the dead…<em>again</em>! The previous owners, who had let me <a href="https://swiftjectivec.com/Spend-Stack-is-Open-Source/">open source</a> it, have given me back full IP rights. She’s back home:</p>

<p><img class="lozad mx-auto max-w-full h-auto shadow-2xl rounded-lg h-18" data-src="../assets/images/ss_back_1.jpg" role="presentation" srcset="../assets/images/ss_back_1.jpg" alt="Spend Stack restored to Jordan's App Store Connect account." /></p>

<p>If you need a refresher, Spend Stack was my first indie darling. It was well received among press and had a few fans over at Apple, but I never could quite figure out what exactly it was (spoiler, I still haven’t) or how to monetize it. It was originally sold around the covid-era, with a blog post from me covering the details <a href="https://www.swiftjectivec.com/a-new-home-for-spend-stack/">here</a>:</p>

<div class="jekyll-twitter-plugin"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">***Deep breath***<br /><br />👋 Hi friends<br /><br />It&#39;s with a wide range of emotions, but the biggest one being excitement, that I&#39;ve got some personal news to share:<br /><br />Spend Stack has been acquired.<a href="https://t.co/6HY9wn9vvd">https://t.co/6HY9wn9vvd</a></p>&mdash; Jordan Morgan (@JordanMorgan10) <a href="https://x.com/JordanMorgan10/status/1334602617792700416?ref_src=twsrc%5Etfw">December 3, 2020</a></blockquote>
<script async="" src="https://platform.x.com/widgets.js" charset="utf-8"></script>

</div>

<p>And, so it is, nearly six years later, I guess Spend Stack has a new home, again. Which is back home! For now, I’m not going to change much about it, here’s my plan:</p>

<ol>
  <li>This thing is <em>old</em>. Most of it is in Objective-C. I have Codex on a loop modernizing it now. I’ll get it buiding in a Swift-first rewrite.</li>
  <li>Then, I’ll integrate modern APIs such as GRDB and <code class="language-plaintext highlighter-rouge">CKSyncEngine</code>.</li>
  <li>I’ll make the app free (it launched as a paid up front app), with an optional $9.99/yr sub for cloud sync.</li>
  <li>And once it’s stable, I’ll open it up for TestFlight.</li>
</ol>

<p>It warms my heart that, years and years later, I’ll still get a random, out-of-the-blue Spend Stack message from someone. I don’t know where it goes from here, but I can bring it back as it once was without too much hassle in the age of agents. And, if we’ve become internet friends post-Spend Stack era, here’s most of what I wrote about it if you’d like to catch up:</p>

<ul>
  <li><a href="https://swiftjectivec.com/Introducing-Spend-Stack/">Introducing Spend Stack</a></li>
  <li><a href="https://swiftjectivec.com/Spend-Stack-Best-In-Class/">A Best In Class App: Spend Stack Checkup</a></li>
  <li><a href="https://swiftjectivec.com/Creating-A-Retail-Demo/">Creating a Retail Demo for Apple</a></li>
  <li><a href="https://swiftjectivec.com/The-Big-Update/">The Big Update</a></li>
  <li><a href="https://swiftjectivec.com/Spend-Stack-One-Year/">Spend Stack: Year One</a></li>
  <li><a href="https://swiftjectivec.com/A-New-Home-For-Spend-Stack/">A New Home for Spend Stack</a></li>
  <li><a href="https://swiftjectivec.com/Remembering-My-First-App/">Remembering My First App</a></li>
  <li><a href="https://swiftjectivec.com/So-Long-Old-Friend-Spend-Stack-Retired-From-App-Store/">So Long, Old Friend</a></li>
  <li><a href="https://swiftjectivec.com/Spend-Stack-is-Open-Source/">Spend Stack is Now Open Source</a></li>
</ul>

<p>Here’s to new, old beginnings!</p>

<p>Until next time ✌️</p>]]></content><author><name>Jordan Morgan</name></author><category term="The Indie Dev Diaries" /><summary type="html"><![CDATA[The little finance app that just won't die. The little guy deserves a third life.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://swiftjectivec.com/assets/images/logo.png" /><media:content medium="image" url="https://swiftjectivec.com/assets/images/logo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Introducing Indie Walks</title><link href="https://swiftjectivec.com/Introducing-Indie-Walks/" rel="alternate" type="text/html" title="Introducing Indie Walks" /><published>2026-08-24T00:00:00-05:00</published><updated>2026-08-24T00:00:00-05:00</updated><id>https://swiftjectivec.com/Introducing-Indie-Walks</id><content type="html" xml:base="https://swiftjectivec.com/Introducing-Indie-Walks/"><![CDATA[<p>For a long time, the homies and I have joked about starting a podcast.</p>

<p>Spoiler: we will never <em>actually</em> start a podcast. But, it would be fun though.</p>

<p>The idea of a podcast is tantilizing. I love talking shop, writing about indie life, or chatting with anyone about the ongoings of our insane vocation. And while I’ve written on this blog for years now, I do love talkin’.</p>

<p>So, introducing…</p>

<div class="not-prose mt-8 -mb-4">
  <h3 class="bg-gradient-to-l from-blue-500 to-fuchsia-500 bg-clip-text text-transparent text-2xl font-bold leading-relaxed">
    Indie Walks
  </h3>
</div>

<p>Short form video essays/journals/episode/talk show??? from me over indie stuff. So, MRR, web to app, how to manage time as a parent with side projects, that kind of stuff. You can watch the first episode wherever is best for you:</p>

<ol>
  <li><a href="https://x.com/JordanMorgan10/status/2091934796926627863?s=20">X</a></li>
  <li><a href="https://www.threads.com/@lookitsjordanmorgan/post/Dcbj0P0goUN">Threads</a></li>
  <li><a href="https://www.instagram.com/theindiewalks?utm_source=qr">Instagram</a></li>
  <li><a href="/indie-walks.html">Episode Directory</a></li>
</ol>

<p>And, as linked above, I’ll keep that evergreen page up to date here with each new  episode. I hope you’ll follow along, and feel free to shoot over any questions or suggestions for topics.</p>

<p>Until next time ✌️</p>]]></content><author><name>Jordan Morgan</name></author><category term="The Indie Dev Diaries" /><summary type="html"><![CDATA[Introducing Indie Walks. Bite sized, quick video journals from me about indie life.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://swiftjectivec.com/assets/images/logo.png" /><media:content medium="image" url="https://swiftjectivec.com/assets/images/logo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">The Skills Conundrum</title><link href="https://swiftjectivec.com/The-Skills-Conundrum/" rel="alternate" type="text/html" title="The Skills Conundrum" /><published>2026-08-10T00:00:00-05:00</published><updated>2026-08-10T00:00:00-05:00</updated><id>https://swiftjectivec.com/The-Skills-Conundrum</id><content type="html" xml:base="https://swiftjectivec.com/The-Skills-Conundrum/"><![CDATA[<p>Skills are a paramount puzzle piece to my agent usage. The rub? They are too easily fragmented:</p>

<ol>
  <li>You could be using different agents. Claude, unlike other agents, looks at its own spot for skills. Everything else uses an open standard, <code class="language-plaintext highlighter-rouge">~/.agents/skills</code>.</li>
  <li>Then, there’s different machines you could be running. Symlinking is great, but run <code class="language-plaintext highlighter-rouge">npx skills update</code> on your MacBook at home and then your MacBook on the go is now stale.</li>
  <li>Each harness seems to want to funnel you into different paths for installing skills.</li>
</ol>

<p>Is there an open standard to solve? Surely, if there is, it would have to be cloud based. For example, at Superwall, we simply have GitHub repo for proprietary, shared skills. So, I’ve adopted that same pattern for two things:</p>

<ol>
  <li>The skills I’ve personally made, and just as important…</li>
  <li>Skills I’ve installed via Vercel’s <a href="https://www.skills.sh">skills.sh</a>.</li>
</ol>

<p>The whole process hinges on three skills: <code class="language-plaintext highlighter-rouge">push-my-skills</code>, <code class="language-plaintext highlighter-rouge">pull-my-skills</code>, and <code class="language-plaintext highlighter-rouge">sync-npx-skills</code>. So, uhhh, naturally - I made a Windows XP recreation to show you how the process works. You got two computers, each needing to be current on 3rd party skills from <code class="language-plaintext highlighter-rouge">skills.sh</code>, and my own personal ones. Click on any of the skill buttons below to see how they operate:</p>

<section class="skills-sync not-prose" data-skills-sync="" aria-labelledby="skills-sync-title">
  <div class="skills-sync__window">
    <div class="skills-sync__titlebar">
      <span class="skills-sync__app-icon" aria-hidden="true">
        <span></span><span></span><span></span>
      </span>
      <h3 id="skills-sync-title">Skills Sync Center</h3>
      <span class="skills-sync__window-controls" aria-hidden="true">
        <span>_</span><span>□</span><span>×</span>
      </span>
    </div>

    <div class="skills-sync__menubar" aria-hidden="true">
      <span><u>F</u>ile</span>
      <span><u>V</u>iew</span>
      <span><u>S</u>kills</span>
      <span><u>H</u>elp</span>
    </div>

    <div class="skills-sync__addressbar" aria-hidden="true">
      <span class="skills-sync__address-label">Address</span>
      <span class="skills-sync__address-field">
        <span class="skills-sync__tiny-folder"></span>
        C:\Jordan\Skills\Keep In Sync
      </span>
      <span class="skills-sync__go"><span>➜</span> Go</span>
    </div>

    <div class="skills-sync__desktop" data-sync-stage="">
      <div class="skills-sync__desktop-heading">
        <span>My Network Places</span>
        <strong data-sync-stage-label="">Choose a job below to watch it travel.</strong>
      </div>

      <div class="skills-sync__network" aria-label="MacBook A and MacBook B exchange skill data through GitHub">
        <article class="skills-sync__node skills-sync__node--mac" data-node="mac-a">
          <span class="skills-sync__node-light" aria-hidden="true"></span>
          <div class="skills-sync__computer" aria-hidden="true">
            <span class="skills-sync__computer-screen">
              <span class="skills-sync__screen-cloud"></span>
              <span class="skills-sync__screen-hill"></span>
            </span>
            <span class="skills-sync__computer-base"></span>
          </div>
          <div class="skills-sync__node-copy">
            <span class="skills-sync__node-kicker">At home</span>
            <h4>MacBook A</h4>
          </div>
          <div class="skills-sync__shelves">
            <div class="skills-sync__shelf" data-anchor="a-source">
              <span class="skills-sync__folder" aria-hidden="true"></span>
              <span><strong>My source files</strong><small>personal-skills/</small></span>
            </div>
            <div class="skills-sync__shelf" data-anchor="a-agents">
              <span class="skills-sync__stack" aria-hidden="true"><i></i><i></i><i></i></span>
              <span><strong>Agent copies</strong><small>Claude · Codex · more</small></span>
            </div>
            <div class="skills-sync__shelf" data-anchor="a-lock">
              <span class="skills-sync__ledger" aria-hidden="true"></span>
              <span><strong>npx lock</strong><small>What is installed here</small></span>
            </div>
          </div>
        </article>

        <div class="skills-sync__wire skills-sync__wire--left" aria-hidden="true"><span></span></div>

        <article class="skills-sync__node skills-sync__node--cloud" data-node="cloud">
          <span class="skills-sync__node-light" aria-hidden="true"></span>
          <div class="skills-sync__globe" aria-hidden="true">
            <span class="skills-sync__globe-grid"></span>
            <span class="skills-sync__globe-cursor">↗</span>
          </div>
          <div class="skills-sync__node-copy">
            <span class="skills-sync__node-kicker">The handoff</span>
            <h4>GitHub</h4>
          </div>
          <div class="skills-sync__shelves">
            <div class="skills-sync__shelf" data-anchor="cloud-source">
              <span class="skills-sync__folder skills-sync__folder--open" aria-hidden="true"></span>
              <span><strong>Skills I made</strong><small>DreamingInBinary/skills</small></span>
            </div>
            <div class="skills-sync__shelf" data-anchor="cloud-manifest">
              <span class="skills-sync__manifest" aria-hidden="true"><i></i><i></i><i></i></span>
              <span><strong>Third-party manifest</strong><small>Desired state + baselines</small></span>
            </div>
          </div>
        </article>

        <div class="skills-sync__wire skills-sync__wire--right" aria-hidden="true"><span></span></div>

        <article class="skills-sync__node skills-sync__node--mac" data-node="mac-b">
          <span class="skills-sync__node-light" aria-hidden="true"></span>
          <div class="skills-sync__computer" aria-hidden="true">
            <span class="skills-sync__computer-screen">
              <span class="skills-sync__screen-cloud"></span>
              <span class="skills-sync__screen-hill"></span>
            </span>
            <span class="skills-sync__computer-base"></span>
          </div>
          <div class="skills-sync__node-copy">
            <span class="skills-sync__node-kicker">On the go</span>
            <h4>MacBook B</h4>
          </div>
          <div class="skills-sync__shelves">
            <div class="skills-sync__shelf" data-anchor="b-source">
              <span class="skills-sync__folder" aria-hidden="true"></span>
              <span><strong>My source files</strong><small>personal-skills/</small></span>
            </div>
            <div class="skills-sync__shelf" data-anchor="b-agents">
              <span class="skills-sync__stack" aria-hidden="true"><i></i><i></i><i></i></span>
              <span><strong>Agent copies</strong><small>Claude · Codex · more</small></span>
            </div>
            <div class="skills-sync__shelf" data-anchor="b-lock">
              <span class="skills-sync__ledger" aria-hidden="true"></span>
              <span><strong>npx lock</strong><small>What is installed here</small></span>
            </div>
          </div>
        </article>
      </div>

      <div class="skills-sync__packet" data-sync-packet="" aria-hidden="true">
        <span class="skills-sync__packet-folder"></span>
        <span class="skills-sync__packet-label" data-sync-packet-label="">SKILL.md</span>
      </div>
    </div>

    <div class="skills-sync__control-panel">
      <div class="skills-sync__jobs" role="group" aria-label="Choose a skills sync job">
        <button type="button" data-sync-job="push" aria-pressed="false">
          <span class="skills-sync__button-icon skills-sync__button-icon--up" aria-hidden="true">↑</span>
          <span><strong>push-my-skills</strong><small>I made or edited a skill</small></span>
        </button>
        <button type="button" data-sync-job="pull" aria-pressed="false">
          <span class="skills-sync__button-icon skills-sync__button-icon--down" aria-hidden="true">↓</span>
          <span><strong>pull-my-skills</strong><small>My other Mac needs it</small></span>
        </button>
        <button type="button" data-sync-job="npx" aria-pressed="false">
          <span class="skills-sync__button-icon skills-sync__button-icon--sync" aria-hidden="true">↻</span>
          <span><strong>sync-npx-skills</strong><small>I added a third-party skill</small></span>
        </button>
      </div>

      <div class="skills-sync__explanation">
        <div class="skills-sync__explanation-copy">
          <span class="skills-sync__eyebrow" data-sync-eyebrow="">THE WHOLE IDEA</span>
          <h4 data-sync-title="">Two kinds of skills. One cloud handoff.</h4>
          <p data-sync-description="">Your skills no longer depend on whichever Mac or agent you happened to use last.</p>
        </div>
        <ol class="skills-sync__steps" data-sync-steps="">
          <li><span>1</span><p><strong>Skills you author</strong><small>Push the real files up; pull them down elsewhere.</small></p></li>
          <li><span>2</span><p><strong>Skills you install</strong><small>Publish the list; reconcile each Mac against it.</small></p></li>
          <li><span>✓</span><p><strong>Installed folders are copies</strong><small>GitHub is the durable source of truth.</small></p></li>
        </ol>
        <p class="skills-sync__status" role="status" aria-live="polite">
          <span class="skills-sync__status-light" aria-hidden="true"></span>
          <span data-sync-status="">Ready. Pick a job to run.</span>
        </p>
      </div>
    </div>

    <div class="skills-sync__taskbar" aria-hidden="true">
      <span class="skills-sync__start"><i></i> start</span>
      <span class="skills-sync__task">📁 Skills Sync Center</span>
      <span class="skills-sync__tray">☁︎ &nbsp; ALL CURRENT</span>
    </div>
  </div>

  <noscript>
    <p class="skills-sync__noscript"><strong>The short version:</strong> <code>push-my-skills</code> publishes skills you wrote, <code>pull-my-skills</code> installs them on another Mac, and <code>sync-npx-skills</code> reconciles third-party installs through a shared manifest.</p>
  </noscript>
</section>

<style>
  .skills-sync {
    --xp-blue: #245edb;
    --xp-blue-deep: #1747a6;
    --xp-blue-bright: #4a9bff;
    --xp-silver: #ece9d8;
    --xp-silver-deep: #c8c5b8;
    --xp-sky: #8fcfff;
    --xp-hill: #63b13f;
    --xp-grass: #2f8e31;
    --xp-green: #35a32f;
    --xp-ink: #202020;
    width: 100%;
    margin: 2.4rem 0 2.8rem;
    color: var(--xp-ink);
    font-family: Tahoma, Verdana, "Segoe UI", sans-serif;
    font-size: 14px;
    -webkit-tap-highlight-color: transparent;
  }

  .skills-sync *,
  .skills-sync *::before,
  .skills-sync *::after {
    box-sizing: border-box;
  }

  .skills-sync__window {
    overflow: hidden;
    border: 3px solid var(--xp-blue);
    border-radius: 9px 9px 2px 2px;
    background: var(--xp-silver);
    box-shadow:
      0 24px 55px rgba(21, 47, 91, 0.26),
      0 0 0 1px #0c3d9c;
  }

  .skills-sync__titlebar {
    display: flex;
    min-height: 36px;
    align-items: center;
    gap: 8px;
    padding: 4px 5px 4px 7px;
    color: white;
    background:
      linear-gradient(180deg, rgba(255, 255, 255, 0.26), transparent 17%, transparent 78%, rgba(0, 38, 125, 0.42)),
      linear-gradient(90deg, #1755ce, #2e87ee 60%, #1d63d5);
    text-shadow: 1px 1px 1px #123a8e;
  }

  .skills-sync__titlebar h3 {
    flex: 1;
    margin: 0;
    color: white;
    font: 700 13px/1 Tahoma, Verdana, sans-serif;
    letter-spacing: 0;
  }

  .skills-sync__app-icon {
    display: grid;
    width: 22px;
    height: 22px;
    grid-template-columns: repeat(2, 1fr);
    gap: 2px;
    padding: 3px;
    border: 1px solid rgba(255, 255, 255, 0.8);
    background: #f7f7ee;
    box-shadow: 1px 1px #15469a;
  }

  .skills-sync__app-icon span {
    background: #f1b430;
  }

  .skills-sync__app-icon span:nth-child(2) { background: #46a738; }
  .skills-sync__app-icon span:nth-child(3) { grid-column: 1 / -1; background: #347edc; }

  .skills-sync__window-controls {
    display: flex;
    gap: 2px;
  }

  .skills-sync__window-controls span {
    display: grid;
    width: 23px;
    height: 23px;
    place-items: center;
    border: 1px solid white;
    border-radius: 3px;
    background: linear-gradient(135deg, #57a5fa, #1b5fc9);
    box-shadow: inset 1px 1px rgba(255, 255, 255, 0.35), 1px 1px #113c91;
    font: 700 15px/1 Tahoma, sans-serif;
  }

  .skills-sync__window-controls span:last-child {
    background: linear-gradient(135deg, #ef967e, #c73718);
  }

  .skills-sync__menubar {
    display: flex;
    min-height: 28px;
    align-items: center;
    gap: 20px;
    padding: 2px 10px;
    border-bottom: 1px solid #aaa89d;
    background: var(--xp-silver);
    color: #222;
    font-size: 12px;
  }

  .skills-sync__addressbar {
    display: flex;
    min-height: 33px;
    align-items: center;
    gap: 6px;
    padding: 4px 6px;
    border-top: 1px solid white;
    border-bottom: 1px solid #8b897e;
    background: var(--xp-silver);
    color: #565656;
    font-size: 11px;
  }

  .skills-sync__address-label { padding: 0 2px; }

  .skills-sync__address-field {
    display: flex;
    min-width: 0;
    flex: 1;
    align-items: center;
    gap: 7px;
    height: 23px;
    padding: 2px 6px;
    overflow: hidden;
    border: 1px solid #7f9db9;
    background: white;
    color: #202020;
    white-space: nowrap;
  }

  .skills-sync__tiny-folder {
    position: relative;
    width: 16px;
    height: 11px;
    flex: none;
    border: 1px solid #9a6815;
    background: #f4c64a;
    box-shadow: inset 1px 1px #ffe58b;
  }

  .skills-sync__tiny-folder::before {
    position: absolute;
    top: -4px;
    left: 1px;
    width: 8px;
    height: 4px;
    border: 1px solid #9a6815;
    border-bottom: 0;
    background: #e4ae32;
    content: "";
  }

  .skills-sync__go {
    display: flex;
    align-items: center;
    gap: 4px;
    color: #202020;
  }

  .skills-sync__go span {
    display: grid;
    width: 20px;
    height: 20px;
    place-items: center;
    border-radius: 50%;
    color: white;
    background: #54ad2c;
    font-weight: 900;
  }

  .skills-sync__desktop {
    position: relative;
    min-height: 450px;
    padding: 19px clamp(12px, 2.2vw, 25px) 27px;
    overflow: hidden;
    isolation: isolate;
    background:
      radial-gradient(ellipse at 15% 33%, rgba(255, 255, 255, 0.98) 0 7%, transparent 7.5%),
      radial-gradient(ellipse at 23% 31%, rgba(255, 255, 255, 0.92) 0 10%, transparent 10.5%),
      radial-gradient(ellipse at 83% 25%, rgba(255, 255, 255, 0.9) 0 8%, transparent 8.5%),
      radial-gradient(ellipse at 72% 103%, #7acc4d 0 45%, transparent 45.5%),
      linear-gradient(180deg, var(--xp-sky) 0 64%, #66bd43 64% 72%, var(--xp-grass) 100%);
  }

  .skills-sync__desktop::before {
    position: absolute;
    inset: 0;
    z-index: -1;
    background:
      linear-gradient(104deg, transparent 0 17%, rgba(255, 255, 255, 0.17) 17.3% 17.7%, transparent 18%),
      repeating-linear-gradient(102deg, transparent 0 18px, rgba(7, 79, 26, 0.07) 19px, transparent 20px);
    content: "";
    pointer-events: none;
  }

  .skills-sync__desktop-heading {
    position: relative;
    z-index: 2;
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 16px;
    margin-bottom: 17px;
    color: #143b70;
    text-shadow: 1px 1px rgba(255, 255, 255, 0.72);
  }

  .skills-sync__desktop-heading span {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
  }

  .skills-sync__desktop-heading strong {
    font-size: 12px;
    font-weight: 700;
    text-align: right;
  }

  .skills-sync__network {
    position: relative;
    z-index: 1;
    display: grid;
    grid-template-columns: minmax(0, 1fr) 54px minmax(0, 1fr) 54px minmax(0, 1fr);
    align-items: center;
    gap: 0;
  }

  .skills-sync__node {
    position: relative;
    min-width: 0;
    min-height: 334px;
    padding: 15px 12px 12px;
    border: 1px solid #7598bb;
    border-radius: 4px;
    background: rgba(250, 252, 255, 0.92);
    box-shadow:
      inset 1px 1px white,
      inset -1px -1px rgba(108, 130, 153, 0.26),
      4px 6px 0 rgba(37, 81, 124, 0.12);
    transition: filter 180ms ease, transform 180ms ease, box-shadow 180ms ease;
  }

  .skills-sync__node.is-active {
    z-index: 3;
    border-color: #2867b4;
    box-shadow:
      inset 1px 1px white,
      inset -1px -1px rgba(108, 130, 153, 0.26),
      0 0 0 3px rgba(255, 221, 69, 0.88),
      5px 8px 0 rgba(37, 81, 124, 0.18);
    transform: translateY(-3px);
  }

  .skills-sync__node-light {
    position: absolute;
    top: 9px;
    right: 9px;
    width: 9px;
    height: 9px;
    border: 1px solid #747474;
    border-radius: 50%;
    background: #b8b8ae;
    box-shadow: inset 1px 1px rgba(255, 255, 255, 0.65);
  }

  .skills-sync__node.is-active .skills-sync__node-light {
    border-color: #1f741c;
    background: #48ca39;
    box-shadow: 0 0 7px #64e85b;
  }

  .skills-sync__computer {
    position: relative;
    width: 84px;
    height: 61px;
    margin: 0 auto 8px;
  }

  .skills-sync__computer-screen {
    position: absolute;
    inset: 0 5px 12px;
    overflow: hidden;
    border: 5px solid #3e4654;
    border-radius: 4px 4px 2px 2px;
    background: #8fd2ff;
    box-shadow: inset 0 0 0 1px #d4efff, 2px 2px #a8a8a2;
  }

  .skills-sync__screen-cloud {
    position: absolute;
    top: 6px;
    left: 9px;
    width: 26px;
    height: 9px;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.92);
  }

  .skills-sync__screen-hill {
    position: absolute;
    right: -10px;
    bottom: -17px;
    width: 75px;
    height: 42px;
    border-radius: 50%;
    background: #5eba3c;
    transform: rotate(-8deg);
  }

  .skills-sync__computer-base {
    position: absolute;
    right: 0;
    bottom: 4px;
    left: 0;
    height: 11px;
    border: 1px solid #555b62;
    border-radius: 2px 2px 8px 8px;
    background: linear-gradient(#d6d9dc, #737980);
    box-shadow: 0 2px 0 rgba(31, 42, 51, 0.25);
  }

  .skills-sync__globe {
    position: relative;
    width: 67px;
    height: 67px;
    margin: -3px auto 5px;
    overflow: hidden;
    border: 3px solid white;
    border-radius: 50%;
    background:
      radial-gradient(circle at 35% 34%, #c9f1ff 0 5%, transparent 6%),
      linear-gradient(135deg, #43a2ed, #1263bd);
    box-shadow: 0 0 0 1px #215889, 3px 4px 0 rgba(32, 64, 96, 0.2);
  }

  .skills-sync__globe::before,
  .skills-sync__globe::after {
    position: absolute;
    border: 2px solid rgba(231, 249, 255, 0.72);
    border-radius: 50%;
    content: "";
  }

  .skills-sync__globe::before { inset: 7px 19px; }
  .skills-sync__globe::after { inset: 27px -4px; }

  .skills-sync__globe-grid {
    position: absolute;
    top: -7px;
    bottom: -7px;
    left: 31px;
    width: 2px;
    background: rgba(231, 249, 255, 0.72);
  }

  .skills-sync__globe-cursor {
    position: absolute;
    right: 4px;
    bottom: 1px;
    z-index: 2;
    color: white;
    font: 900 23px/1 Tahoma, sans-serif;
    text-shadow: 1px 1px #164b7e;
  }

  .skills-sync__node-copy {
    margin-bottom: 11px;
    text-align: center;
  }

  .skills-sync__node-kicker {
    display: block;
    margin-bottom: 2px;
    color: #5b7189;
    font-size: 9px;
    font-weight: 700;
    letter-spacing: 0.09em;
    text-transform: uppercase;
  }

  .skills-sync__node-copy h4 {
    margin: 0;
    color: #163d73;
    font: 700 16px/1.1 "Trebuchet MS", Tahoma, sans-serif;
    letter-spacing: -0.01em;
  }

  .skills-sync__shelves {
    display: grid;
    gap: 7px;
  }

  .skills-sync__shelf {
    display: flex;
    min-width: 0;
    min-height: 50px;
    align-items: center;
    gap: 8px;
    padding: 7px;
    border: 1px solid #b6c4d4;
    background: rgba(255, 255, 255, 0.82);
    box-shadow: inset 1px 1px white;
    transition: background 160ms ease, border-color 160ms ease;
  }

  .skills-sync__shelf.is-active {
    border-color: #3c73b2;
    background: #fffbc9;
  }

  .skills-sync__shelf > span:last-child {
    min-width: 0;
  }

  .skills-sync__shelf strong,
  .skills-sync__shelf small {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .skills-sync__shelf strong {
    color: #17385d;
    font-size: 10px;
    line-height: 1.25;
  }

  .skills-sync__shelf small {
    margin-top: 2px;
    color: #5f6973;
    font-size: 8px;
    line-height: 1.2;
  }

  .skills-sync__folder,
  .skills-sync__packet-folder {
    position: relative;
    display: block;
    width: 28px;
    height: 21px;
    flex: none;
    border: 1px solid #986211;
    border-radius: 1px;
    background: linear-gradient(135deg, #ffe078, #e6a928);
    box-shadow: inset 2px 2px #fff0a4, 1px 1px #976b22;
  }

  .skills-sync__folder::before,
  .skills-sync__packet-folder::before {
    position: absolute;
    top: -6px;
    left: 1px;
    width: 15px;
    height: 7px;
    border: 1px solid #986211;
    border-bottom: 0;
    border-radius: 2px 2px 0 0;
    background: #e8af31;
    content: "";
  }

  .skills-sync__folder--open {
    transform: skewY(-5deg);
  }

  .skills-sync__stack {
    position: relative;
    display: block;
    width: 29px;
    height: 27px;
    flex: none;
  }

  .skills-sync__stack i {
    position: absolute;
    width: 21px;
    height: 17px;
    border: 1px solid #3b6398;
    background: linear-gradient(135deg, #f8fbff, #9ac7fa);
    box-shadow: inset 1px 1px white;
  }

  .skills-sync__stack i:nth-child(1) { top: 0; left: 0; }
  .skills-sync__stack i:nth-child(2) { top: 4px; left: 4px; }
  .skills-sync__stack i:nth-child(3) { top: 8px; left: 8px; }

  .skills-sync__ledger,
  .skills-sync__manifest {
    position: relative;
    display: block;
    width: 24px;
    height: 29px;
    flex: none;
    border: 1px solid #627385;
    background: #fbfbf6;
    box-shadow: inset 2px 0 #80aee1, 1px 1px #8a98a5;
  }

  .skills-sync__ledger::before,
  .skills-sync__manifest::before {
    position: absolute;
    top: 5px;
    right: 3px;
    bottom: 4px;
    left: 6px;
    background: repeating-linear-gradient(to bottom, #6c8aab 0 1px, transparent 1px 5px);
    content: "";
  }

  .skills-sync__manifest i {
    position: absolute;
    left: -5px;
    width: 4px;
    height: 4px;
    border: 1px solid #647584;
    background: white;
  }

  .skills-sync__manifest i:nth-child(1) { top: 4px; }
  .skills-sync__manifest i:nth-child(2) { top: 12px; }
  .skills-sync__manifest i:nth-child(3) { top: 20px; }

  .skills-sync__wire {
    position: relative;
    height: 20px;
    overflow: hidden;
  }

  .skills-sync__wire::before,
  .skills-sync__wire::after {
    position: absolute;
    top: 7px;
    width: 7px;
    height: 7px;
    border: 1px solid #e8f5ff;
    border-radius: 50%;
    background: #2f81d6;
    box-shadow: 0 0 0 1px #255f9a;
    content: "";
  }

  .skills-sync__wire::before { left: 2px; }
  .skills-sync__wire::after { right: 2px; }

  .skills-sync__wire span {
    position: absolute;
    top: 10px;
    right: 5px;
    left: 5px;
    height: 2px;
    background: repeating-linear-gradient(90deg, #f8fdff 0 5px, #2d6ca9 5px 10px);
    box-shadow: 0 1px rgba(12, 73, 128, 0.35);
  }

  .skills-sync__packet {
    position: absolute;
    z-index: 10;
    display: none;
    width: 66px;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    pointer-events: none;
    will-change: transform;
  }

  .skills-sync__packet.is-visible { display: flex; }

  .skills-sync__packet-folder {
    width: 37px;
    height: 28px;
    filter: drop-shadow(3px 4px 0 rgba(24, 53, 81, 0.24));
  }

  .skills-sync__packet-folder::before {
    width: 19px;
    height: 8px;
  }

  .skills-sync__packet.is-manifest .skills-sync__packet-folder::after {
    position: absolute;
    top: 5px;
    right: 6px;
    width: 11px;
    height: 14px;
    border: 1px solid #65768b;
    background: repeating-linear-gradient(to bottom, white 0 3px, #76a1cd 3px 4px);
    content: "";
  }

  .skills-sync__packet-label {
    max-width: 90px;
    padding: 3px 5px;
    border: 1px solid rgba(30, 69, 112, 0.7);
    background: rgba(255, 255, 255, 0.94);
    box-shadow: 1px 1px rgba(33, 75, 120, 0.25);
    color: #17385d;
    font-size: 8px;
    font-weight: 700;
    line-height: 1;
    text-align: center;
    white-space: nowrap;
  }

  .skills-sync__control-panel {
    border-top: 1px solid white;
    background: var(--xp-silver);
  }

  .skills-sync__jobs {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 7px;
    padding: 9px;
    border-bottom: 1px solid #ada99c;
    background: #d8e8fa;
  }

  .skills-sync__jobs button {
    display: flex;
    min-width: 0;
    min-height: 54px;
    align-items: center;
    gap: 8px;
    padding: 6px 8px;
    border: 1px solid #7f9db9;
    border-radius: 3px;
    color: #19324d;
    background: linear-gradient(#fff, #e1edf9);
    box-shadow: inset 1px 1px white, inset -1px -1px #b0bdc8;
    font-family: inherit;
    text-align: left;
    cursor: pointer;
  }

  .skills-sync__jobs button:hover {
    border-color: #245edb;
    background: linear-gradient(#fffde9, #f7e9a1);
  }

  .skills-sync__jobs button[aria-pressed="true"] {
    border-color: #245edb;
    background: linear-gradient(#fff9ca, #f0cf54);
    box-shadow: inset 2px 2px #d3aa2f, inset -1px -1px white;
  }

  .skills-sync__jobs button:focus-visible {
    outline: 2px dotted #153e7b;
    outline-offset: 2px;
  }

  .skills-sync__jobs button > span:last-child {
    min-width: 0;
  }

  .skills-sync__jobs strong,
  .skills-sync__jobs small {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .skills-sync__jobs strong {
    font: 700 10px/1.2 "Lucida Console", Monaco, monospace;
    white-space: nowrap;
  }

  .skills-sync__jobs small {
    margin-top: 3px;
    color: #5c6875;
    font-size: 8px;
    line-height: 1.25;
    white-space: nowrap;
  }

  .skills-sync__button-icon {
    display: grid;
    width: 29px;
    height: 29px;
    flex: none;
    place-items: center;
    border: 1px solid #315a84;
    border-radius: 3px;
    color: white;
    background: linear-gradient(135deg, #6db8ff, #1e67bd);
    box-shadow: inset 1px 1px rgba(255, 255, 255, 0.7), 1px 1px rgba(31, 65, 99, 0.35);
    font-size: 20px;
    font-weight: 900;
  }

  .skills-sync__button-icon--up { background: linear-gradient(135deg, #70bd62, #278625); }
  .skills-sync__button-icon--sync { background: linear-gradient(135deg, #f0a966, #ce581e); }

  .skills-sync__explanation {
    display: grid;
    grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
    gap: 18px;
    padding: 16px 17px 13px;
  }

  .skills-sync__eyebrow {
    display: block;
    margin-bottom: 4px;
    color: #27558d;
    font-size: 9px;
    font-weight: 700;
    letter-spacing: 0.08em;
  }

  .skills-sync__explanation h4 {
    margin: 0 0 6px;
    color: #193b68;
    font: 700 clamp(16px, 2.7vw, 20px)/1.15 "Trebuchet MS", Tahoma, sans-serif;
    letter-spacing: -0.02em;
  }

  .skills-sync__explanation-copy p {
    max-width: 37rem;
    margin: 0;
    color: #465668;
    font-size: 11px;
    line-height: 1.5;
  }

  .skills-sync__steps {
    display: grid;
    gap: 5px;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .skills-sync__steps li {
    display: flex;
    align-items: center;
    gap: 8px;
    min-height: 36px;
    padding: 4px 6px;
    border: 1px solid transparent;
    opacity: 0.58;
    transition: opacity 160ms ease, background 160ms ease, border-color 160ms ease;
  }

  .skills-sync__steps li > span {
    display: grid;
    width: 23px;
    height: 23px;
    flex: none;
    place-items: center;
    border: 1px solid #78899a;
    border-radius: 50%;
    background: #f8f7ee;
    color: #526476;
    font-size: 10px;
    font-weight: 700;
  }

  .skills-sync__steps li.is-active {
    border-color: #e0bb39;
    background: #fff9c5;
    opacity: 1;
  }

  .skills-sync__steps li.is-complete { opacity: 1; }

  .skills-sync__steps li.is-active > span {
    border-color: #245edb;
    color: white;
    background: var(--xp-blue);
  }

  .skills-sync__steps li.is-complete > span {
    border-color: #267a21;
    color: white;
    background: #35a32f;
  }

  .skills-sync__steps p,
  .skills-sync__steps strong,
  .skills-sync__steps small {
    display: block;
    margin: 0;
  }

  .skills-sync__steps strong {
    color: #223b55;
    font-size: 9px;
    line-height: 1.2;
  }

  .skills-sync__steps small {
    margin-top: 2px;
    color: #687583;
    font-size: 8px;
    line-height: 1.25;
  }

  .skills-sync__status {
    grid-column: 1 / -1;
    display: flex;
    min-height: 25px;
    align-items: center;
    gap: 7px;
    margin: 0;
    padding: 5px 8px;
    border: 1px inset #bcb9ad;
    background: #f7f6ef;
    color: #35475a;
    font-size: 9px;
    line-height: 1.35;
  }

  .skills-sync__status-light {
    width: 8px;
    height: 8px;
    flex: none;
    border: 1px solid #66727d;
    border-radius: 50%;
    background: #aab1b6;
  }

  .skills-sync.is-running .skills-sync__status-light {
    border-color: #164d87;
    background: #3a9df6;
    box-shadow: 0 0 5px #57aeff;
  }

  .skills-sync.is-complete .skills-sync__status-light {
    border-color: #267a21;
    background: #43c238;
    box-shadow: 0 0 5px #5cd550;
  }

  .skills-sync__taskbar {
    display: flex;
    height: 33px;
    align-items: stretch;
    color: white;
    background:
      linear-gradient(180deg, rgba(255,255,255,0.25), transparent 18%, transparent 80%, rgba(7,46,133,0.48)),
      #2867db;
    font-size: 10px;
    text-shadow: 1px 1px #17468e;
  }

  .skills-sync__start {
    display: flex;
    width: 82px;
    flex: none;
    align-items: center;
    gap: 6px;
    padding: 0 10px 0 8px;
    border-radius: 0 10px 10px 0;
    background: linear-gradient(180deg, #67c663, #289024 90%);
    box-shadow: inset 0 2px rgba(255,255,255,0.36), 2px 0 rgba(12,80,24,0.3);
    font-size: 13px;
    font-style: italic;
    font-weight: 700;
  }

  .skills-sync__start i {
    width: 17px;
    height: 17px;
    background:
      linear-gradient(90deg, #ef5a31 0 46%, transparent 46% 54%, #4fbc41 54%),
      linear-gradient(#ef5a31 0 46%, transparent 46% 54%, #4599ef 54%);
    filter: drop-shadow(1px 1px rgba(0,0,0,0.3));
    transform: skew(-4deg);
  }

  .skills-sync__task {
    display: flex;
    min-width: 0;
    flex: 1;
    align-items: center;
    margin: 3px 4px;
    padding: 0 10px;
    overflow: hidden;
    border: 1px solid #174ca8;
    background: #3981e5;
    box-shadow: inset 1px 1px rgba(255,255,255,0.18);
    white-space: nowrap;
  }

  .skills-sync__tray {
    display: flex;
    width: 116px;
    flex: none;
    align-items: center;
    justify-content: center;
    border-left: 1px solid #1b55b5;
    background: #1590df;
    font-size: 8px;
  }

  .skills-sync__noscript {
    margin: 10px 0 0;
    padding: 12px;
    border: 1px solid #b2b2aa;
    background: #f7f6ef;
    font-size: 12px;
    line-height: 1.5;
  }

  @media (max-width: 720px) {
    .skills-sync__address-label,
    .skills-sync__go,
    .skills-sync__menubar { display: none; }

    .skills-sync__addressbar { padding: 5px; }

    .skills-sync__desktop {
      min-height: 0;
      padding: 15px 12px 20px;
    }

    .skills-sync__desktop-heading {
      align-items: flex-start;
      flex-direction: column;
      gap: 3px;
      margin-bottom: 12px;
    }

    .skills-sync__desktop-heading strong { text-align: left; }

    .skills-sync__network {
      grid-template-columns: minmax(0, 1fr);
      gap: 0;
    }

    .skills-sync__node {
      min-height: 0;
      padding: 10px;
    }

    .skills-sync__node-copy {
      position: absolute;
      top: 13px;
      left: 82px;
      text-align: left;
    }

    .skills-sync__computer,
    .skills-sync__globe {
      margin-right: auto;
      margin-left: 0;
      transform: scale(0.72);
      transform-origin: left center;
    }

    .skills-sync__computer { margin-bottom: -2px; }
    .skills-sync__globe { margin-bottom: 1px; }

    .skills-sync__shelves {
      grid-template-columns: repeat(3, minmax(0, 1fr));
      gap: 5px;
    }

    .skills-sync__node--cloud .skills-sync__shelves {
      grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    .skills-sync__shelf {
      min-height: 43px;
      padding: 5px;
    }

    .skills-sync__shelf > span:first-child { transform: scale(0.78); }
    .skills-sync__shelf strong { font-size: 8px; }
    .skills-sync__shelf small { display: none; }

    .skills-sync__wire {
      width: 20px;
      height: 31px;
      margin: 0 auto;
    }

    .skills-sync__wire::before,
    .skills-sync__wire::after {
      left: 6px;
      right: auto;
    }

    .skills-sync__wire::before { top: 2px; }
    .skills-sync__wire::after { top: auto; bottom: 2px; }

    .skills-sync__wire span {
      top: 5px;
      bottom: 5px;
      left: 9px;
      width: 2px;
      height: auto;
      background: repeating-linear-gradient(180deg, #f8fdff 0 5px, #2d6ca9 5px 10px);
    }

    .skills-sync__jobs {
      grid-template-columns: minmax(0, 1fr);
    }

    .skills-sync__jobs button { min-height: 48px; }
    .skills-sync__jobs strong { font-size: 9px; }
    .skills-sync__jobs small { font-size: 8px; }

    .skills-sync__explanation {
      grid-template-columns: minmax(0, 1fr);
      gap: 11px;
      padding: 14px 12px 11px;
    }

    .skills-sync__status { grid-column: auto; }
    .skills-sync__task { display: none; }
    .skills-sync__tray { margin-left: auto; }
  }

  @media (max-width: 420px) {
    .skills-sync__window-controls span:nth-child(-n + 2) { display: none; }
    .skills-sync__titlebar h3 { font-size: 12px; }
    .skills-sync__shelf { justify-content: center; }
    .skills-sync__shelf > span:last-child { display: none; }
    .skills-sync__steps small { font-size: 7px; }
  }

  @media (prefers-reduced-motion: reduce) {
    .skills-sync *,
    .skills-sync *::before,
    .skills-sync *::after {
      scroll-behavior: auto !important;
      transition-duration: 0.01ms !important;
      animation-duration: 0.01ms !important;
      animation-iteration-count: 1 !important;
    }
  }
</style>

<script>
  (() => {
    const root = document.currentScript.previousElementSibling.previousElementSibling;
    if (!root || root.dataset.initialized === "true") return;
    root.dataset.initialized = "true";

    const stage = root.querySelector("[data-sync-stage]");
    const stageLabel = root.querySelector("[data-sync-stage-label]");
    const packet = root.querySelector("[data-sync-packet]");
    const packetLabel = root.querySelector("[data-sync-packet-label]");
    const eyebrow = root.querySelector("[data-sync-eyebrow]");
    const title = root.querySelector("[data-sync-title]");
    const description = root.querySelector("[data-sync-description]");
    const status = root.querySelector("[data-sync-status]");
    const steps = root.querySelector("[data-sync-steps]");
    const buttons = root.querySelectorAll("[data-sync-job]");
    const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    let runID = 0;

    const workflows = {
      push: {
        eyebrow: "YOUR SKILLS · MACBOOK A",
        title: "Publish the source. Refresh the copies.",
        description: "The personal repo is canonical. The folders each agent reads are reinstallable snapshots, so they never become a second source of truth.",
        stageLabel: "MacBook A → installed agents + GitHub",
        done: "Published. MacBook B can pull the same source whenever you are ready.",
        steps: [
          {
            title: "Edit the real skill",
            detail: "Work in Documents/Projects/personal-skills.",
            status: "Found a new or edited skill in the personal repo.",
            pulse: ["a-source"]
          },
          {
            title: "Refresh every agent",
            detail: "Re-add the repo globally on MacBook A.",
            status: "Refreshing the installed snapshots for Claude, Codex, and friends…",
            pulse: ["a-source", "a-agents"],
            leg: ["a-source", "a-agents", "fresh copies", "folder"]
          },
          {
            title: "Commit + push",
            detail: "GitHub receives the source files, not the snapshots.",
            status: "Committing a descriptive change and pushing DreamingInBinary/skills…",
            pulse: ["a-source", "cloud-source"],
            leg: ["a-source", "cloud-source", "SKILL.md", "folder"]
          }
        ]
      },
      pull: {
        eyebrow: "YOUR SKILLS · MACBOOK B",
        title: "Pull the source. Rebuild the copies.",
        description: "MacBook B fast-forwards the same personal repo, then the installer fans those skills back out to every agent on that machine.",
        stageLabel: "GitHub → MacBook B → installed agents",
        done: "Pulled and refreshed. Both Macs now install from the same authored source.",
        steps: [
          {
            title: "Fast-forward from GitHub",
            detail: "Local edits are protected; nothing is discarded.",
            status: "Pulling the latest personal-skills commit onto MacBook B…",
            pulse: ["cloud-source", "b-source"],
            leg: ["cloud-source", "b-source", "SKILL.md", "folder"]
          },
          {
            title: "Refresh every agent",
            detail: "Re-add the local repo globally on MacBook B.",
            status: "Installing fresh snapshots for every agent on MacBook B…",
            pulse: ["b-source", "b-agents"],
            leg: ["b-source", "b-agents", "fresh copies", "folder"]
          },
          {
            title: "Same skills, new machine",
            detail: "Agent-specific install paths no longer matter.",
            status: "Claude, Codex, and friends are reading the refreshed copies.",
            pulse: ["b-agents"]
          }
        ]
      },
      npx: {
        eyebrow: "THIRD-PARTY SKILLS · BOTH MACS",
        title: "Sync the list, not a pile of folders.",
        description: "A three-way comparison separates changes you made here from changes made elsewhere: the shared manifest, this Mac's last-sync baseline, and its live npx lock.",
        stageLabel: "Local lock ↔ manifest → the other Mac",
        done: "Manifest published. Run sync-npx-skills on the other Mac when convenient to converge it.",
        steps: [
          {
            title: "Compare three states",
            detail: "Manifest + machine baseline + live npx lock.",
            status: "Planning additions and removals without touching personal or vendor-managed skills…",
            pulse: ["a-lock", "cloud-manifest"]
          },
          {
            title: "Apply here + publish",
            detail: "Install remote changes; publish local changes.",
            status: "Reconciling MacBook A and recording its new baseline…",
            pulse: ["a-lock", "cloud-manifest"],
            leg: ["a-lock", "cloud-manifest", "manifest Δ", "manifest"]
          },
          {
            title: "Converge MacBook B",
            detail: "Its next sync applies the same desired state.",
            status: "MacBook B picks up the manifest the next time sync-npx-skills runs there…",
            pulse: ["cloud-manifest", "b-lock"],
            leg: ["cloud-manifest", "b-lock", "npx skills", "manifest"]
          }
        ]
      }
    };

    const wait = (milliseconds) => new Promise((resolve) => window.setTimeout(resolve, milliseconds));

    const anchor = (name) => root.querySelector(`[data-anchor="${name}"]`);

    const clearHighlights = () => {
      root.querySelectorAll(".skills-sync__node.is-active, .skills-sync__shelf.is-active").forEach((element) => {
        element.classList.remove("is-active");
      });
    };

    const highlight = (names) => {
      clearHighlights();
      names.forEach((name) => {
        const shelf = anchor(name);
        if (!shelf) return;
        shelf.classList.add("is-active");
        const node = shelf.closest("[data-node]");
        if (node) node.classList.add("is-active");
      });
    };

    const renderSteps = (items) => {
      steps.replaceChildren();
      items.forEach((item, index) => {
        const row = document.createElement("li");
        const number = document.createElement("span");
        const copy = document.createElement("p");
        const heading = document.createElement("strong");
        const detail = document.createElement("small");
        number.textContent = String(index + 1);
        heading.textContent = item.title;
        detail.textContent = item.detail;
        copy.append(heading, detail);
        row.append(number, copy);
        steps.appendChild(row);
      });
    };

    const setStep = (activeIndex) => {
      Array.from(steps.children).forEach((step, index) => {
        step.classList.toggle("is-active", index === activeIndex);
        step.classList.toggle("is-complete", index < activeIndex);
        step.firstElementChild.textContent = index < activeIndex ? "✓" : String(index + 1);
      });
    };

    const finishSteps = () => {
      Array.from(steps.children).forEach((step) => {
        step.classList.remove("is-active");
        step.classList.add("is-complete");
        step.firstElementChild.textContent = "✓";
      });
    };

    const movePacket = async (leg, id) => {
      const [fromName, toName, label, kind] = leg;
      const from = anchor(fromName);
      const to = anchor(toName);
      if (!from || !to || id !== runID) return;

      const stageRect = stage.getBoundingClientRect();
      const fromRect = from.getBoundingClientRect();
      const toRect = to.getBoundingClientRect();
      const startX = fromRect.left - stageRect.left + (fromRect.width / 2) - 33;
      const startY = fromRect.top - stageRect.top + (fromRect.height / 2) - 27;
      const deltaX = (toRect.left + toRect.width / 2) - (fromRect.left + fromRect.width / 2);
      const deltaY = (toRect.top + toRect.height / 2) - (fromRect.top + fromRect.height / 2);
      const arc = Math.min(45, Math.max(20, Math.abs(deltaX) * 0.09 + Math.abs(deltaY) * 0.05));

      packet.style.left = `${startX}px`;
      packet.style.top = `${startY}px`;
      packetLabel.textContent = label;
      packet.classList.toggle("is-manifest", kind === "manifest");
      packet.classList.add("is-visible");

      if (reduceMotion || typeof packet.animate !== "function") {
        packet.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
        await wait(220);
      } else {
        const animation = packet.animate([
          { transform: "translate(0, 0) scale(0.88)", opacity: 0 },
          { transform: `translate(${deltaX * 0.12}px, ${deltaY * 0.12 - arc * 0.4}px) scale(1)`, opacity: 1, offset: 0.14 },
          { transform: `translate(${deltaX * 0.5}px, ${deltaY * 0.5 - arc}px) scale(1.06)`, opacity: 1, offset: 0.5 },
          { transform: `translate(${deltaX}px, ${deltaY}px) scale(0.94)`, opacity: 1 }
        ], {
          duration: 1250,
          easing: "cubic-bezier(.2,.75,.2,1)",
          fill: "forwards"
        });
        try { await animation.finished; } catch (_) { return; }
      }

      await wait(reduceMotion ? 80 : 180);
      packet.classList.remove("is-visible");
      packet.style.transform = "";
    };

    const run = async (key) => {
      const workflow = workflows[key];
      if (!workflow) return;

      runID += 1;
      const id = runID;
      packet.getAnimations().forEach((animation) => animation.cancel());
      packet.classList.remove("is-visible");
      root.classList.remove("is-complete");
      root.classList.add("is-running");

      buttons.forEach((button) => {
        button.setAttribute("aria-pressed", String(button.dataset.syncJob === key));
      });

      eyebrow.textContent = workflow.eyebrow;
      title.textContent = workflow.title;
      description.textContent = workflow.description;
      stageLabel.textContent = workflow.stageLabel;
      renderSteps(workflow.steps);

      for (let index = 0; index < workflow.steps.length; index += 1) {
        if (id !== runID) return;
        const step = workflow.steps[index];
        setStep(index);
        status.textContent = step.status;
        highlight(step.pulse);
        if (step.leg) {
          await movePacket(step.leg, id);
        } else {
          await wait(reduceMotion ? 250 : 760);
        }
        if (id !== runID) return;
      }

      finishSteps();
      clearHighlights();
      status.textContent = workflow.done;
      root.classList.remove("is-running");
      root.classList.add("is-complete");
    };

    buttons.forEach((button) => {
      button.addEventListener("click", () => run(button.dataset.syncJob));
    });
  })();
</script>

<p>Here’s the technical breakdown.</p>

<p>For the skills that <em>I’ve made</em>…
…one GitHub repo is the source of truth; so everything inside <code class="language-plaintext highlighter-rouge">~/.agents/skills</code> is merely an installed copy. On one Mac, <code class="language-plaintext highlighter-rouge">push-my-skills</code> refreshes those copies, commits my changes and pushes them to GitHub in one shot. On another, <code class="language-plaintext highlighter-rouge">pull-my-skills</code> pulls that repo and reinstalls the same skills for every agent. Now, my personal skills are source controlled and current across machines.</p>

<p>But, that’s just half of it. Third-party skills actually use a shared manifest instead…</p>

<p>…so <code class="language-plaintext highlighter-rouge">sync-npx-skills</code> compares that manifest against the Mac’s <em>last</em> sync and its current npx lock file, determining whether a difference was made here, or on another machine. Local additions and removals are published; which means remote changes are applied so each Mac eventually converges on the same set of skills without manually copying folders around.</p>

<p>At this point, I have to ask: Did I solve the problem? Absolutely. Did I engineer myself into a small pyrrhic victory along the way? Also absolutely maybe yes! My hope is that an open standard arrives tomorrow, and makes all of this gloriously obsolete. Until then, at least both Macs agree on what skills they have.</p>

<p>Until next time ✌️</p>]]></content><author><name>Jordan Morgan</name></author><category term="AI" /><summary type="html"><![CDATA[Skills are an essential part of working with agents. Storing them across devices currently kinda sucks.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://swiftjectivec.com/assets/images/logo.png" /><media:content medium="image" url="https://swiftjectivec.com/assets/images/logo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Copywriting with Agents and…Apple Designers?</title><link href="https://swiftjectivec.com/Copywriting-with-Agents-and-Apple-Designers/" rel="alternate" type="text/html" title="Copywriting with Agents and…Apple Designers?" /><published>2026-07-17T00:00:00-05:00</published><updated>2026-07-17T00:00:00-05:00</updated><id>https://swiftjectivec.com/Copywriting-with-Agents-and-Apple-Designers</id><content type="html" xml:base="https://swiftjectivec.com/Copywriting-with-Agents-and-Apple-Designers/"><![CDATA[<p>It’s hard <em>not</em> to have tragic copywriting as a programmer. As we stepped into this indie scene, it was (and is) all too common to ship something like this:</p>

<section class="copy-lab not-prose" aria-labelledby="copy-lab-title">
  <div class="copy-lab__toolbar">
    <div>
      <span class="copy-lab__eyebrow">The Button Label Test</span>
      <h3 id="copy-lab-title">What will happen when you tap it?</h3>
    </div>

    <label class="copy-lab__toggle">
      <span>Humanize copy</span>
      <input class="copy-lab__toggle-input" type="checkbox" role="switch" aria-describedby="copy-lab-hint" />
      <span class="copy-lab__toggle-track" aria-hidden="true">
        <span class="copy-lab__toggle-thumb"></span>
      </span>
    </label>
  </div>

  <div class="copy-lab__stage">
    <span class="copy-lab__annotation copy-lab__annotation--before" aria-hidden="true">The button text reads like a launch code sequence 😅.</span>
    <span class="copy-lab__annotation copy-lab__annotation--after" aria-hidden="true">This names the result ✨!</span>

    <div class="copy-lab__phone" aria-label="A fictional calendar app">
      <div class="copy-lab__screen">
        <div class="copy-lab__statusbar" aria-hidden="true">
          <span>9:41</span>
          <span class="copy-lab__island"></span>
          <span class="copy-lab__status-icons">
            <svg class="copy-lab__signal" viewBox="0 0 14 10"><path d="M0 10h2.2V7.2H0V10Zm4 0h2.2V4.8H4V10Zm4 0h2.2V2.4H8V10Zm4 0h2.2V0H12v10Z" fill="currentColor" /></svg>
            <svg class="copy-lab__battery" viewBox="0 0 13 10"><rect x="0.75" y="0.75" width="9.5" height="8.5" rx="1.8" fill="none" stroke="currentColor" stroke-width="1.5" /><rect x="2.4" y="2.35" width="6.2" height="5.3" rx="0.8" fill="currentColor" /><path d="M11 3.1h1.2c.44 0 .8.36.8.8v2.2c0 .44-.36.8-.8.8H11V3.1Z" fill="currentColor" /></svg>
          </span>
        </div>

        <div class="copy-lab__nav">
          <button type="button" class="copy-lab__cancel" tabindex="-1" aria-hidden="true">Cancel</button>
          <strong>New Event</strong>
          <span aria-hidden="true"></span>
        </div>

        <div class="copy-lab__event-card">
          <div class="copy-lab__date" aria-hidden="true">
            <span>JUL</span>
            <strong>17</strong>
          </div>
          <div class="copy-lab__event-copy">
            <strong>Design review</strong>
            <span>Today, 2:00–2:30 PM</span>
          </div>
        </div>

        <div class="copy-lab__details" aria-hidden="true">
          <div><span>Calendar</span><strong><i></i>Work</strong></div>
          <div><span>Alert</span><strong>10 minutes before</strong></div>
        </div>

        <div class="copy-lab__spacer"></div>

        <div class="copy-lab__action-area">
          <p id="copy-lab-hint" class="copy-lab__hint">The action stays the same. Only the words get clearer.</p>
          <button type="button" class="copy-lab__action">
            <span class="copy-lab__action-label">Initiate Add Sequence</span>
          </button>
          <p class="copy-lab__confirmation" role="status" aria-live="polite">
            <span class="copy-lab__toast-check" aria-hidden="true"></span>
            <span class="copy-lab__toast-message"></span>
          </p>
        </div>

        <span class="copy-lab__home-indicator" aria-hidden="true"></span>
      </div>
    </div>
  </div>
</section>

<style>
  .copy-lab {
    --lab-ink: #17202d;
    --lab-muted: #637084;
    --lab-blue: #2477f4;
    --lab-red: #ff453a;
    --lab-mist: #dbe6f3;
    margin: 2.25rem 0 2.75rem;
    overflow: hidden;
    color: var(--lab-ink);
    background: #f7f9fc;
    border: 1px solid #d9e1eb;
    border-radius: 1.25rem;
    box-shadow: 0 24px 60px rgba(37, 57, 80, 0.12);
  }

  .copy-lab * {
    box-sizing: border-box;
  }

  .copy-lab__toolbar {
    position: relative;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1.25rem;
    padding: 1.1rem 1.25rem;
    background: rgba(255, 255, 255, 0.9);
    border-bottom: 1px solid #d9e1eb;
  }

  .copy-lab__eyebrow {
    display: block;
    margin-bottom: 0.18rem;
    color: #778396;
    font: 700 0.64rem/1.2 ui-monospace, SFMono-Regular, Menlo, monospace;
    letter-spacing: 0.1em;
    text-transform: uppercase;
  }

  .copy-lab__toolbar h3 {
    margin: 0;
    color: var(--lab-ink);
    font: 750 1rem/1.25 -apple-system, BlinkMacSystemFont, "SF Pro Display", sans-serif;
    letter-spacing: -0.02em;
  }

  .copy-lab__toggle {
    display: flex;
    flex: none;
    align-items: center;
    gap: 0.65rem;
    color: #364154;
    font: 650 0.76rem/1 -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
    cursor: pointer;
    user-select: none;
  }

  .copy-lab__toggle-input {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
  }

  .copy-lab__toggle-track {
    display: flex;
    width: 2.55rem;
    height: 1.48rem;
    padding: 0.14rem;
    background: #b9c1cd;
    border-radius: 999px;
    transition: background 220ms ease;
  }

  .copy-lab__toggle-thumb {
    width: 1.2rem;
    height: 1.2rem;
    background: white;
    border-radius: 50%;
    box-shadow: 0 1px 4px rgba(23, 32, 45, 0.3);
    transition: transform 260ms cubic-bezier(0.2, 0.9, 0.25, 1.15);
  }

  .copy-lab__toggle-input:checked + .copy-lab__toggle-track {
    background: #30c759;
  }

  .copy-lab__toggle-input:checked + .copy-lab__toggle-track .copy-lab__toggle-thumb {
    transform: translateX(1.06rem);
  }

  .copy-lab__toggle-input:focus-visible + .copy-lab__toggle-track {
    outline: 3px solid rgba(36, 119, 244, 0.36);
    outline-offset: 3px;
  }

  .copy-lab__stage {
    position: relative;
    display: grid;
    min-height: 31rem;
    place-items: center;
    padding: 1.8rem;
    overflow: clip;
    isolation: isolate;
    background:
      radial-gradient(circle at 50% 42%, rgba(255, 255, 255, 0.95) 0 17%, transparent 46%),
      linear-gradient(135deg, #e9eff7 0%, var(--lab-mist) 100%);
    transition: background 420ms ease;
  }

  .copy-lab.is-humanized .copy-lab__stage {
    background:
      radial-gradient(circle at 50% 42%, rgba(255, 255, 255, 0.98) 0 17%, transparent 46%),
      linear-gradient(135deg, #e5f0ff 0%, #d9f0e4 100%);
  }

  .copy-lab__stage::before,
  .copy-lab__stage::after {
    position: absolute;
    z-index: -1;
    width: 16rem;
    height: 16rem;
    content: "";
    border: 1px solid rgba(72, 94, 120, 0.12);
    border-radius: 50%;
  }

  .copy-lab__stage::before {
    top: -7rem;
    left: -3rem;
  }

  .copy-lab__stage::after {
    right: -5rem;
    bottom: -8rem;
  }

  .copy-lab__annotation {
    position: absolute;
    top: 48%;
    max-width: 8.5rem;
    color: #536176;
    font: 700 0.67rem/1.35 ui-monospace, SFMono-Regular, Menlo, monospace;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    transition: opacity 180ms ease, transform 260ms ease;
  }

  .copy-lab__annotation::after {
    display: block;
    width: 4rem;
    margin-top: 0.55rem;
    content: "";
    border-top: 1px solid currentColor;
    opacity: 0.55;
  }

  .copy-lab__annotation--before {
    left: 1.6rem;
  }

  .copy-lab__annotation--after {
    right: 1.6rem;
    text-align: right;
    opacity: 0;
    transform: translateX(0.5rem);
  }

  .copy-lab__annotation--after::after {
    margin-left: auto;
  }

  .copy-lab.is-humanized .copy-lab__annotation--before {
    opacity: 0;
    transform: translateX(-0.5rem);
  }

  .copy-lab.is-humanized .copy-lab__annotation--after {
    opacity: 1;
    transform: translateX(0);
  }

  .copy-lab__phone {
    position: relative;
    width: 14rem;
    height: 27.8rem;
    padding: 0.42rem;
    background: #151719;
    border: 1px solid #34373a;
    border-radius: 2.6rem;
    box-shadow:
      0 2rem 3rem rgba(31, 50, 72, 0.26),
      0 0 0 1px rgba(255, 255, 255, 0.3) inset;
    transform: rotate(-1deg);
    transition: transform 420ms cubic-bezier(0.2, 0.8, 0.2, 1);
  }

  .copy-lab.is-humanized .copy-lab__phone {
    transform: rotate(0deg) translateY(-0.15rem);
  }

  .copy-lab__screen {
    position: relative;
    display: flex;
    height: 100%;
    flex-direction: column;
    overflow: hidden;
    background: #f4f4f8;
    border-radius: 2.2rem;
    font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
  }

  .copy-lab__statusbar {
    display: grid;
    height: 2.15rem;
    flex: none;
    grid-template-columns: 1fr 4rem 1fr;
    align-items: center;
    padding: 0.3rem 0.8rem 0;
    color: #101317;
    background: rgba(255, 255, 255, 0.9);
    font-size: 0.58rem;
    font-weight: 700;
  }

  .copy-lab__island {
    width: 3.75rem;
    height: 1.15rem;
    margin: auto;
    background: #050505;
    border-radius: 999px;
  }

  .copy-lab__status-icons {
    display: flex;
    align-items: center;
    gap: 0.28rem;
    justify-self: end;
  }

  .copy-lab__status-icons svg {
    display: block;
    height: auto;
  }

  .copy-lab__signal {
    width: 0.9rem;
  }

  .copy-lab__battery {
    width: 0.82rem;
  }

  .copy-lab__nav {
    display: grid;
    grid-template-columns: 1fr 1.3fr 1fr;
    align-items: center;
    padding: 0.55rem 0.78rem 0.7rem;
    background: rgba(255, 255, 255, 0.9);
    border-bottom: 1px solid #e3e3e8;
    font-size: 0.7rem;
  }

  .copy-lab__nav strong {
    color: #17191e;
    font-size: 0.73rem;
    text-align: center;
  }

  .copy-lab__cancel {
    justify-self: start;
    padding: 0;
    color: var(--lab-red);
    background: none;
    border: 0;
    font: inherit;
  }

  .copy-lab__event-card {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin: 0.8rem 0.7rem 0.65rem;
    padding: 0.75rem;
    background: white;
    border-radius: 0.85rem;
    box-shadow: 0 1px 2px rgba(20, 25, 31, 0.06);
  }

  .copy-lab__date {
    display: grid;
    width: 2.45rem;
    height: 2.45rem;
    flex: none;
    place-content: center;
    text-align: center;
    background: #fff1f0;
    border-radius: 0.65rem;
  }

  .copy-lab__date span {
    color: var(--lab-red);
    font-size: 0.43rem;
    font-weight: 800;
    letter-spacing: 0.06em;
  }

  .copy-lab__date strong {
    color: #1e2025;
    font-size: 1rem;
    line-height: 1;
  }

  .copy-lab__event-copy {
    display: flex;
    min-width: 0;
    flex-direction: column;
    gap: 0.16rem;
  }

  .copy-lab__event-copy strong {
    color: #1d2025;
    font-size: 0.76rem;
  }

  .copy-lab__event-copy span {
    color: #777d87;
    font-size: 0.56rem;
  }

  .copy-lab__details {
    margin: 0 0.7rem;
    padding: 0 0.72rem;
    background: white;
    border-radius: 0.85rem;
    box-shadow: 0 1px 2px rgba(20, 25, 31, 0.05);
  }

  .copy-lab__details > div {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0.62rem 0;
    color: #6e7580;
    font-size: 0.57rem;
  }

  .copy-lab__details > div + div {
    border-top: 1px solid #ededf1;
  }

  .copy-lab__details strong {
    display: flex;
    align-items: center;
    gap: 0.28rem;
    color: #252930;
    font-weight: 550;
  }

  .copy-lab__details i {
    width: 0.42rem;
    height: 0.42rem;
    background: #ff9f0a;
    border-radius: 50%;
  }

  .copy-lab__spacer {
    flex: 1;
  }

  .copy-lab__action-area {
    padding: 0.75rem 0.72rem 1.45rem;
  }

  .copy-lab__hint {
    min-height: 1.7rem;
    margin: 0 0 0.52rem;
    color: #777d87;
    font-size: 0.5rem;
    line-height: 1.35;
    text-align: center;
  }

  .copy-lab__action {
    width: 100%;
    padding: 0.72rem 0.6rem;
    color: white;
    background: var(--lab-blue);
    border: 0;
    border-radius: 0.78rem;
    box-shadow: 0 0.3rem 0.8rem rgba(36, 119, 244, 0.24);
    font: 700 0.65rem/1 -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
    cursor: pointer;
    transition: background 180ms ease, transform 120ms ease, box-shadow 180ms ease;
  }

  .copy-lab__action:hover {
    background: #1269e6;
  }

  .copy-lab__action:active {
    transform: scale(0.98);
  }

  .copy-lab__action:focus-visible {
    outline: 3px solid rgba(36, 119, 244, 0.38);
    outline-offset: 3px;
  }

  .copy-lab__confirmation {
    position: absolute;
    z-index: 4;
    top: 4.75rem;
    left: 50%;
    display: flex;
    width: max-content;
    max-width: calc(100% - 2rem);
    align-items: center;
    justify-content: center;
    margin: 0;
    padding: 0.48rem 0.7rem 0.48rem 0.52rem;
    color: white;
    background: rgba(24, 27, 32, 0.94);
    border: 1px solid rgba(255, 255, 255, 0.13);
    border-radius: 999px;
    box-shadow: 0 0.65rem 1.3rem rgba(14, 21, 30, 0.24);
    font-size: 0.53rem;
    font-weight: 700;
    line-height: 1;
    text-align: center;
    opacity: 0;
    pointer-events: none;
    transform: translate(-50%, -0.45rem) scale(0.96);
    transition: opacity 180ms ease, transform 240ms cubic-bezier(0.2, 0.85, 0.25, 1.1);
  }

  .copy-lab__toast-check {
    position: relative;
    display: inline-grid;
    width: 0.9rem;
    height: 0.9rem;
    margin-right: 0.32rem;
    color: #132117;
    content: "✓";
    place-items: center;
    background: #63da7b;
    border-radius: 50%;
    font-size: 0.58rem;
  }

  .copy-lab__toast-check::after {
    position: absolute;
    top: 0.18rem;
    left: 0.32rem;
    width: 0.23rem;
    height: 0.42rem;
    content: "";
    border: solid #132117;
    border-width: 0 0.1rem 0.1rem 0;
    transform: rotate(45deg);
  }

  .copy-lab__confirmation.is-visible {
    opacity: 1;
    transform: translate(-50%, 0) scale(1);
  }

  .copy-lab__home-indicator {
    position: absolute;
    bottom: 0.38rem;
    left: 50%;
    width: 4.5rem;
    height: 0.23rem;
    background: #1e2023;
    border-radius: 999px;
    transform: translateX(-50%);
  }

  @media (max-width: 680px) {
    .copy-lab__toolbar {
      align-items: flex-start;
      padding: 1rem;
    }

    .copy-lab__toolbar h3 {
      max-width: 10rem;
      font-size: 0.88rem;
    }

    .copy-lab__toggle {
      gap: 0.45rem;
      font-size: 0.68rem;
    }

    .copy-lab__stage {
      min-height: 29.5rem;
      padding: 1.1rem 0.75rem;
    }

    .copy-lab__phone {
      width: 13.2rem;
      height: 26.2rem;
    }

    .copy-lab__annotation {
      display: none;
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .copy-lab *,
    .copy-lab *::before,
    .copy-lab *::after {
      scroll-behavior: auto !important;
      transition-duration: 0.01ms !important;
    }
  }
</style>

<script>
  (() => {
    const lab = document.currentScript.previousElementSibling.previousElementSibling;
    if (!lab || lab.dataset.initialized === "true") return;
    lab.dataset.initialized = "true";

    const toggle = lab.querySelector(".copy-lab__toggle-input");
    const action = lab.querySelector(".copy-lab__action");
    const label = lab.querySelector(".copy-lab__action-label");
    const confirmation = lab.querySelector(".copy-lab__confirmation");
    const confirmationMessage = lab.querySelector(".copy-lab__toast-message");

    toggle.addEventListener("change", () => {
      const humanized = toggle.checked;
      lab.classList.toggle("is-humanized", humanized);
      label.textContent = humanized ? "Add Event" : "Initiate Sequence";
      confirmation.classList.remove("is-visible");
      confirmationMessage.textContent = "";
    });

    let toastTimer;

    action.addEventListener("click", () => {
      window.clearTimeout(toastTimer);
      confirmationMessage.textContent = "Event added";
      confirmation.classList.add("is-visible");
      toastTimer = window.setTimeout(() => {
        confirmation.classList.remove("is-visible");
      }, 2600);
    });
  })();
</script>

<p>Even nearly 15 years (!!!) into this business, I can still fall into this trap. Left to my own devices, I’ll happily ship some gormless button label like “Initiate Sequence”, its copy that reads like it was written for someone who should be code reviewing a recursive function. But instead, most people who use my apps are the everyman, who isn’t serially online chatting about AI agents. Writing for them doesn’t come as naturally. Other than experience and being aware of it, is there a way to audit your interface controls for terrible copywriting?</p>

<p>Turns out, Apple has the answer. And, if you power it up with an agent, it can audit your entire app for you. It all starts with this <a href="https://developer.apple.com/videos/play/wwdc2026/290">banger session</a>:</p>

<p><img class="lozad mx-auto max-w-full h-auto shadow-2xl rounded-lg h-18" data-src="../assets/images/agent-copywriting/banger-session.jpg" role="presentation" srcset="../assets/images/agent-copywriting/banger-session.jpg" alt="Apple Developer app showing the WWDC26 session Craft clear names for features and labels in your app." /></p>

<p>In it, Heej walks through how to craft copy that feels human. That really helps someone understand the impact of a button press, or simply set an expectation of what will happen. This kind of thing is absolutely gold. It is the number one thing I think people like me struggle with. Here’s how I hooked up an agent to learn about that session, and then apply it to one of my apps:</p>

<ol>
  <li>
    <p>Install <a href="https://sosumi.ai">Sosumi MCP</a> or install the <a href="https://wwdc.ai">WWDC.ai</a> skill.</p>

    <p>For Sosumi MCP:</p>

    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>claude mcp add <span class="nt">--transport</span> http sosumi https://sosumi.ai/mcp
</code></pre></div>    </div>

    <p>Or, for the WWDC.ai skill:</p>

    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npx skills add https://github.com/superwall/skills <span class="nt">--skill</span> wwdc <span class="nt">--global</span> <span class="nt">--yes</span> <span class="nt">--agent</span> claude-code universal
</code></pre></div>    </div>
  </li>
  <li>Have an agent watch that session via whatever you installed on step 1.</li>
  <li>Then, point it your app and give it context on who your target market is, what your app does, and using its new copywriting knowledge - propose updates to strings throughout.</li>
</ol>

<p>When I did this, I had fantastic results:</p>

<p><img class="lozad mx-auto max-w-full h-auto shadow-2xl rounded-lg h-18" data-src="../assets/images/agent-copywriting/obvious-cleanup.jpg" role="presentation" srcset="../assets/images/agent-copywriting/obvious-cleanup.jpg" alt="A code diff showing clearer share-link copy proposed across an app." /></p>

<p>Some of them were so obvious, it hurt. But, this is just a blindspot I have. I’m not great at interface label naming, or naming in general. There’s a lot of talk about where the line should be with creativity and AI, and it’s a necessary discourse. In this case? This is one area where I’m happy to have its help.</p>

<p>Until next time ✌️</p>]]></content><author><name>Jordan Morgan</name></author><category term="AI" /><summary type="html"><![CDATA[On using agents to sharpen copywriting while keeping the Apple design bar in view.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://swiftjectivec.com/assets/images/logo.png" /><media:content medium="image" url="https://swiftjectivec.com/assets/images/logo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">noUI()</title><link href="https://swiftjectivec.com/noUI/" rel="alternate" type="text/html" title="noUI()" /><published>2026-07-01T00:00:00-05:00</published><updated>2026-07-01T00:00:00-05:00</updated><id>https://swiftjectivec.com/noUI</id><content type="html" xml:base="https://swiftjectivec.com/noUI/"><![CDATA[<p>Lately, I’ve been thinking a lot about the concept of a user interface. Like, weirdo, existential stuff. Why did we even make them in the first place?</p>

<p>Remember the good old days of Microsoft DOS? Just a welcoming, plain and greenish terminal. Alawys complete with a blinking green cursor inviting you to press enter, and a list of options:</p>

<style>
  .dos-prompt-shell {
    margin: 2.5rem auto;
    max-width: 840px;
    -webkit-tap-highlight-color: transparent;
  }

  .dos-prompt-hardware {
    position: relative;
    padding: 1.35rem 1.35rem 1rem;
    border: 1px solid rgba(117, 98, 59, 0.55);
    border-radius: 18px;
    background:
      linear-gradient(135deg, rgba(255, 255, 255, 0.52), transparent 28%),
      linear-gradient(180deg, #d6c38f 0%, #bca86f 58%, #9d8755 100%);
    box-shadow:
      0 26px 54px rgba(0, 0, 0, 0.28),
      inset 0 1px 0 rgba(255, 255, 255, 0.6),
      inset 0 -2px 4px rgba(70, 55, 27, 0.22);
  }

  .dos-prompt-hardware::before {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background:
      repeating-linear-gradient(
        36deg,
        rgba(74, 57, 30, 0.08) 0,
        rgba(74, 57, 30, 0.08) 1px,
        transparent 1px,
        transparent 8px
      );
    opacity: 0.42;
    pointer-events: none;
  }

  .dos-prompt-bezel {
    position: relative;
    z-index: 1;
    padding: 1rem;
    border: 1px solid rgba(62, 52, 30, 0.55);
    border-radius: 13px;
    background:
      linear-gradient(180deg, #857650 0%, #594b2f 100%);
    box-shadow:
      inset 0 2px 5px rgba(255, 255, 255, 0.18),
      inset 0 -3px 8px rgba(0, 0, 0, 0.35);
  }

  .dos-prompt-screen {
    position: relative;
    overflow: hidden;
    height: clamp(360px, 48vw, 510px);
    cursor: text;
    border: 2px solid #1f8f42;
    border-radius: 10px;
    background:
      radial-gradient(circle at 50% 45%, rgba(76, 255, 116, 0.12), transparent 62%),
      #020602;
    box-shadow:
      0 22px 44px rgba(0, 0, 0, 0.32),
      inset 0 0 46px rgba(63, 255, 104, 0.16),
      inset 0 0 0 2px rgba(120, 255, 151, 0.12);
    animation: dos-prompt-idle-flicker 7s steps(1, end) infinite;
  }

  .dos-prompt-shell:focus {
    outline: none;
  }

  .dos-prompt-shell:focus-visible .dos-prompt-screen {
    box-shadow:
      0 22px 44px rgba(0, 0, 0, 0.32),
      0 0 0 3px rgba(85, 255, 119, 0.22),
      inset 0 0 46px rgba(63, 255, 104, 0.16),
      inset 0 0 0 2px rgba(120, 255, 151, 0.12);
  }

  .dos-prompt-shell.is-off .dos-prompt-screen {
    cursor: default;
    border-color: #1d2b1b;
    background:
      radial-gradient(circle at 50% 45%, rgba(39, 62, 39, 0.22), transparent 62%),
      #020402;
    box-shadow:
      0 18px 32px rgba(0, 0, 0, 0.28),
      inset 0 0 58px rgba(0, 0, 0, 0.7),
      inset 0 0 0 2px rgba(95, 90, 67, 0.2);
  }

  .dos-prompt-shell.is-off .dos-prompt-screen::before {
    background: repeating-linear-gradient(
      to bottom,
      rgba(90, 90, 70, 0.035) 0,
      rgba(90, 90, 70, 0.035) 1px,
      transparent 1px,
      transparent 4px
    );
  }

  .dos-prompt-shell.is-off .dos-prompt-text {
    opacity: 0;
    pointer-events: none;
    text-shadow: none;
  }

  .dos-prompt-shell.is-flickering .dos-prompt-screen {
    animation: dos-prompt-power-flicker 620ms steps(1, end);
  }

  .dos-prompt-shell.is-flickering .dos-prompt-text {
    animation: dos-prompt-text-flicker 620ms steps(1, end);
  }

  .dos-prompt-screen::before {
    content: "";
    position: absolute;
    inset: 0;
    background: repeating-linear-gradient(
      to bottom,
      rgba(108, 255, 137, 0.06) 0,
      rgba(108, 255, 137, 0.06) 1px,
      transparent 1px,
      transparent 4px
    );
    pointer-events: none;
  }

  .dos-prompt-screen::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(90deg, rgba(255, 255, 255, 0.05), transparent 24%, transparent 72%, rgba(0, 0, 0, 0.2));
    mix-blend-mode: screen;
    pointer-events: none;
  }

  .dos-prompt-text {
    position: relative;
    z-index: 1;
    box-sizing: border-box;
    height: 100%;
    margin: 0;
    padding: 1.25rem;
    color: #55ff77;
    font-family: Consolas, "Lucida Console", "Courier New", monospace;
    font-size: clamp(0.74rem, 1.65vw, 0.95rem);
    font-weight: 700;
    line-height: 1.52;
    letter-spacing: 0;
    overflow: auto;
    scrollbar-color: #55ff77 #020602;
    text-shadow: 0 0 8px rgba(85, 255, 119, 0.75);
    white-space: normal;
  }

  .dos-prompt-text.is-output {
    white-space: pre-wrap;
  }

  .dos-prompt-dim {
    color: #32d757;
  }

  .dos-prompt-strong {
    color: #c7ffd2;
  }

  .dos-prompt-line {
    display: block;
    min-height: 1.52em;
  }

  .dos-prompt-option {
    display: block;
    width: fit-content;
    max-width: 100%;
    margin: 0;
    padding: 0;
    border: 0;
    border-radius: 0;
    background: transparent;
    color: #55ff77;
    cursor: pointer;
    font: inherit;
    letter-spacing: inherit;
    text-align: left;
    text-shadow: inherit;
    touch-action: manipulation;
  }

  .dos-prompt-option:hover,
  .dos-prompt-option:focus-visible {
    color: #c7ffd2;
    outline: none;
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-underline-offset: 0.2em;
  }

  .dos-prompt-option[disabled] {
    cursor: wait;
    opacity: 0.7;
    text-decoration: none;
  }

  .dos-prompt-log {
    display: block;
    white-space: pre-wrap;
  }

  .dos-prompt-link-list {
    display: block;
    margin-top: 0.25rem;
    white-space: normal;
  }

  .dos-prompt-link {
    display: block;
    width: fit-content;
    max-width: 100%;
    color: #c7ffd2;
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-underline-offset: 0.2em;
  }

  .dos-prompt-link:hover,
  .dos-prompt-link:focus-visible {
    color: #ffffff;
    outline: none;
    text-shadow: 0 0 12px rgba(199, 255, 210, 0.95);
  }

  .dos-prompt-chin {
    position: relative;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.7rem 0.3rem 0;
    color: rgba(48, 40, 24, 0.74);
    font-family: Georgia, "Times New Roman", serif;
    font-size: 0.66rem;
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
  }

  .dos-prompt-brand {
    white-space: nowrap;
  }

  .dos-prompt-controls {
    display: flex;
    align-items: center;
    gap: 0.85rem;
  }

  .dos-prompt-leds {
    display: flex;
    gap: 0.35rem;
  }

  .dos-prompt-led {
    width: 0.42rem;
    height: 0.42rem;
    border-radius: 999px;
    background: #6cff84;
    box-shadow: 0 0 9px rgba(58, 255, 95, 0.85);
  }

  .dos-prompt-led:last-child {
    background: #34281a;
    box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.16);
  }

  .dos-prompt-shell.is-off .dos-prompt-led:first-child {
    background: #34281a;
    box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.16);
  }

  .dos-prompt-power {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    margin: 0;
    padding: 0;
    border: 0;
    background: transparent;
    color: rgba(48, 40, 24, 0.74);
    cursor: pointer;
    font: inherit;
    letter-spacing: inherit;
    touch-action: manipulation;
    text-transform: inherit;
  }

  .dos-prompt-power:focus-visible {
    outline: 2px solid rgba(42, 31, 18, 0.45);
    outline-offset: 4px;
    border-radius: 999px;
  }

  .dos-prompt-power-track {
    position: relative;
    width: 2.7rem;
    height: 1.22rem;
    border: 1px solid rgba(50, 39, 20, 0.58);
    border-radius: 999px;
    background:
      linear-gradient(180deg, #4e3f24 0%, #241b10 100%);
    box-shadow:
      inset 0 2px 4px rgba(0, 0, 0, 0.55),
      0 1px 0 rgba(255, 255, 255, 0.25);
  }

  .dos-prompt-power-knob {
    position: absolute;
    top: 0.12rem;
    left: 1.52rem;
    width: 0.9rem;
    height: 0.9rem;
    border-radius: 999px;
    background:
      radial-gradient(circle at 35% 30%, rgba(255, 255, 255, 0.86), transparent 28%),
      linear-gradient(180deg, #efe1b5 0%, #9f8956 100%);
    box-shadow:
      0 1px 3px rgba(0, 0, 0, 0.42),
      inset 0 -1px 1px rgba(75, 54, 25, 0.35);
    transition: left 160ms ease, background 160ms ease;
  }

  .dos-prompt-shell.is-off .dos-prompt-power-knob {
    left: 0.16rem;
    background:
      radial-gradient(circle at 35% 30%, rgba(255, 255, 255, 0.65), transparent 28%),
      linear-gradient(180deg, #9b875b 0%, #5c4a2f 100%);
  }

  .dos-prompt-power-label {
    white-space: nowrap;
  }

  .dos-prompt-cursor {
    display: inline-block;
    width: 0.62em;
    height: 1em;
    background: #55ff77;
    box-shadow: 0 0 12px rgba(85, 255, 119, 0.9);
    transform: translateY(0.15em);
    animation: dos-prompt-blink 1s steps(1, end) infinite;
  }

  @keyframes dos-prompt-blink {
    50% {
      opacity: 0;
    }
  }

  @keyframes dos-prompt-idle-flicker {
    0%, 94%, 100% {
      filter: brightness(1);
    }
    95% {
      filter: brightness(0.84);
    }
    96% {
      filter: brightness(1.12);
    }
    97% {
      filter: brightness(0.92);
    }
  }

  @keyframes dos-prompt-power-flicker {
    0%, 14%, 34%, 52%, 72%, 100% {
      filter: brightness(1.12);
    }
    8%, 24%, 44%, 62%, 84% {
      filter: brightness(0.16);
    }
  }

  @keyframes dos-prompt-text-flicker {
    0%, 14%, 34%, 52%, 72%, 100% {
      opacity: 1;
    }
    8%, 24%, 44%, 62%, 84% {
      opacity: 0.05;
    }
  }

  @media (max-width: 560px) {
    .dos-prompt-shell {
      margin: 2rem auto;
    }

    .dos-prompt-hardware {
      padding: 0.85rem 0.85rem 0.65rem;
      border-radius: 14px;
    }

    .dos-prompt-bezel {
      padding: 0.65rem;
      border-radius: 10px;
    }

    .dos-prompt-screen {
      height: 330px;
      border-radius: 7px;
    }

    .dos-prompt-text {
      padding: 0.9rem;
      font-size: 0.68rem;
      line-height: 1.45;
    }

    .dos-prompt-chin {
      align-items: flex-start;
      flex-direction: column;
      gap: 0.65rem;
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .dos-prompt-screen,
    .dos-prompt-shell.is-flickering .dos-prompt-screen,
    .dos-prompt-shell.is-flickering .dos-prompt-text {
      animation: none;
    }

    .dos-prompt-cursor {
      animation: none;
    }
  }
</style>

<figure class="dos-prompt-shell" data-dos-prompt="" tabindex="0" aria-label="A fake MS-DOS style prompt showing a menu of options.">
  <div class="dos-prompt-hardware">
    <div class="dos-prompt-bezel">
      <div class="dos-prompt-screen">
        <div class="dos-prompt-text" data-dos-output="" aria-live="polite" aria-atomic="false">
          <span class="dos-prompt-line dos-prompt-dim">Microsoft(R) MS-DOS Version 6.22</span>
          <span class="dos-prompt-line dos-prompt-dim">(C) Copyright Microsoft Corp 1981-1994.</span>
          <span class="dos-prompt-line">&nbsp;</span>
          <span class="dos-prompt-line">C:\&gt;<span class="dos-prompt-strong">NOUI</span></span>
          <span class="dos-prompt-line">&nbsp;</span>
          <span class="dos-prompt-line">SWIFTJECTIVE-C AGENT HARNESS</span>
          <span class="dos-prompt-line">----------------------------------------</span>
          <button class="dos-prompt-option" type="button" data-dos-option="1">1. Customize server-driven Event</button>
          <button class="dos-prompt-option" type="button" data-dos-option="2">2. Review Elite Hoops feature requests</button>
          <button class="dos-prompt-option" type="button" data-dos-option="3">3. Manage Superwall company posts</button>
          <button class="dos-prompt-option" type="button" data-dos-option="4">4. Exit to prompt</button>
          <span class="dos-prompt-line">&nbsp;</span>
          <span class="dos-prompt-line">Select option: <span data-dos-input=""></span><span class="dos-prompt-cursor" data-dos-cursor="" aria-hidden="true"></span></span><span class="dos-prompt-log" data-dos-log=""></span>
        </div>
      </div>
    </div>
    <div class="dos-prompt-chin">
      <span class="dos-prompt-brand" aria-hidden="true">SJC Lima 1</span>
      <span class="dos-prompt-controls">
        <span class="dos-prompt-leds" aria-hidden="true">
          <span class="dos-prompt-led"></span>
          <span class="dos-prompt-led"></span>
        </span>
        <button class="dos-prompt-power" type="button" data-dos-power="" aria-pressed="true" aria-label="Turn CRT screen off">
          <span class="dos-prompt-power-track" aria-hidden="true">
            <span class="dos-prompt-power-knob"></span>
          </span>
          <span class="dos-prompt-power-label" aria-hidden="true">Power</span>
        </button>
      </span>
    </div>
  </div>
</figure>

<script>
  (() => {
    const prompts = document.querySelectorAll("[data-dos-prompt]");

    prompts.forEach((prompt) => {
      const output = prompt.querySelector("[data-dos-output]");
      const input = prompt.querySelector("[data-dos-input]");
      const options = prompt.querySelectorAll("[data-dos-option]");
      const powerButton = prompt.querySelector("[data-dos-power]");
      const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
      const canFocusForKeyboard = window.matchMedia("(hover: hover) and (pointer: fine)").matches;
      let isTyping = false;
      let isPoweredOn = true;
      let typingRunId = 0;
      let flickerTimeout;

      const responses = {
        "1": {
          command: "EVENT.EXE",
          text: [
            "Loading server-driven Event editor...",
            "No dashboard found.",
            "Agent received: customize the upcoming Event.",
            "Schema patched. Copy adjusted. Preview ready.",
            "UI rendered: 0 screens."
          ].join("\n")
        },
        "2": {
          command: "FEATURES.EXE",
          text: [
            "Opening Elite Hoops request queue...",
            "Grouping duplicates by team, coach, and workflow.",
            "Top request: faster practice plan setup.",
            "Recommendation written to product notes.",
            "UI rendered: still just this prompt."
          ].join("\n")
        },
        "3": {
          command: "BLOG.EXE",
          text: [
            "Connecting to Superwall company blog...",
            "Scanning draft posts and metadata.",
            "Agent can edit, review, and prep the post directly.",
            "Admin CMS skipped.",
            "Output complete."
          ].join("\n")
        },
        "4": {
          command: "EXIT",
          text: [
            "Closing agent harness...",
            "Returning to the blinking cursor.",
            "Maybe DOS had it right all along."
          ].join("\n")
        }
      };
      const finalLine = `You know what? Just go download Jordan's caffeine tracking app. Search "Alyx: Caffeine Tracker" in the App Store.\nOr just hit one of these links:`;
      const appLinks = [
        {
          label: "Alyx: Caffeine Tracker",
          href: "https://itunes.apple.com/us/app/id6473840226"
        },
        {
          label: "Elite Hoops",
          href: "https://apps.apple.com/us/app/elite-hoops-basketball/id6443711183"
        }
      ];

      const wait = (ms) => new Promise((resolve) => window.setTimeout(resolve, ms));

      const setBusy = (busy) => {
        isTyping = busy;
        prompt.setAttribute("aria-busy", String(busy));
        options.forEach((option) => {
          option.disabled = busy || !isPoweredOn;
        });
      };

      const setPower = (poweredOn) => {
        isPoweredOn = poweredOn;
        typingRunId += 1;
        window.clearTimeout(flickerTimeout);
        prompt.classList.remove("is-flickering");
        void prompt.offsetWidth;
        prompt.classList.add("is-flickering");
        flickerTimeout = window.setTimeout(() => {
          prompt.classList.remove("is-flickering");
        }, 660);
        prompt.classList.toggle("is-off", !poweredOn);
        prompt.setAttribute("aria-disabled", String(!poweredOn));
        output.setAttribute("aria-hidden", String(!poweredOn));
        powerButton.setAttribute("aria-pressed", String(poweredOn));
        powerButton.setAttribute("aria-label", poweredOn ? "Turn CRT screen off" : "Turn CRT screen on");
        setBusy(false);
      };

      const appendAppLinks = (cursor) => {
        const linkList = document.createElement("span");
        linkList.className = "dos-prompt-link-list";

        appLinks.forEach((link, index) => {
          const anchor = document.createElement("a");
          anchor.className = "dos-prompt-link";
          anchor.href = link.href;
          anchor.target = "_blank";
          anchor.rel = "noopener noreferrer";
          anchor.textContent = `[${index + 1}] ${link.label}`;
          linkList.appendChild(anchor);
        });

        output.appendChild(linkList);
        output.appendChild(cursor);
        output.scrollTop = output.scrollHeight;
      };

      const typeText = async (text) => {
        const runId = typingRunId + 1;
        typingRunId = runId;
        output.classList.add("is-output");
        output.replaceChildren();
        output.scrollTop = 0;
        const typedText = document.createTextNode("");
        const cursor = document.createElement("span");
        cursor.className = "dos-prompt-cursor";
        cursor.setAttribute("aria-hidden", "true");

        output.appendChild(typedText);
        output.appendChild(cursor);

        if (reduceMotion) {
          typedText.textContent = text;
          appendAppLinks(cursor);
          return true;
        }

        for (const character of text) {
          if (runId !== typingRunId || !isPoweredOn) {
            return false;
          }

          typedText.textContent += character;
          output.scrollTop = output.scrollHeight;
          await wait(character === "\n" ? 120 : 18);
        }

        if (runId !== typingRunId || !isPoweredOn) {
          return false;
        }

        appendAppLinks(cursor);
        return true;
      };

      const runOption = async (optionKey) => {
        const response = responses[optionKey];

        if (!response || isTyping || !isPoweredOn) {
          return;
        }

        setBusy(true);
        input.textContent = optionKey;

        const commandPrefix = `Select option: ${optionKey}\n\nC:\\>${response.command}\n`;
        const terminalOutput = `${response.text}\n${finalLine}`;

        await typeText(commandPrefix + terminalOutput);
        setBusy(false);
      };

      const focusForKeyboard = () => {
        if (canFocusForKeyboard) {
          prompt.focus({ preventScroll: true });
        }
      };

      options.forEach((option) => {
        option.addEventListener("click", () => {
          focusForKeyboard();
          runOption(option.dataset.dosOption);
        });
      });

      powerButton.addEventListener("click", (event) => {
        event.stopPropagation();
        setPower(!isPoweredOn);
      });

      prompt.addEventListener("click", () => {
        focusForKeyboard();
      });

      prompt.addEventListener("keydown", (event) => {
        if (!responses[event.key]) {
          return;
        }

        event.preventDefault();
        runOption(event.key);
      });
    });
  })();
</script>

<p>I guess the idea of a user interface is to help people get something done, perform a certain task, accomplish whatever it is they have in mind. And the easiest way to do that, is…well, visually.</p>

<p>Though recently, I’ve started to have a weird realization that maybe the best UI in this world of agents is sometimes… not having one at all. In fact, I dictated this post all in Codex’s mac app (yes, I know — which is a UI! Hang with me though):</p>

<p><img class="lozad mx-auto max-w-full h-auto shadow-2xl rounded-lg h-18" data-src="../assets/images/noUI_1.jpg" role="presentation" srcset="../assets/images/noUI_1.jpg" alt="A screenshot of writing a blog post in Codex for Mac." /></p>

<p>Here are few things I would’ve made an admin dashboard, custom interface, or any interface at all for about a year ago:</p>

<ol>
  <li>Customizing a server driven “Event” in a bespoke upcoming app.</li>
  <li>Reviewing feature requests for Elite Hoops.</li>
  <li>Managing blog posts for the company blog at Superwall.</li>
</ol>

<p>Of course, in all three instances, I realized each of these things are agent driven processes. So why bother? I just tell the agent what I need. The only UI, at any stage, is the harness running the agent.</p>

<p>So I’m curious — is this a prominent direction the world is headed? I don’t think it’ll go so far to where we get to a place of “An ad-hoc agent and your prompt <em>is</em> the app for the given task”, as some people tend to think. I am, naturally, using an agent, without an extra UI in the target “thing” I’m working on, to produce some sort of output for an existing app.</p>

<p>Buuuuuut, it is <em>also</em> a little bit more in that direction than I would’ve thought. For me, a prompt can have the verisimilitude of an app: it’s a place to express intent, then I make revisions, and finally end up with the thing I wanted. Or maybe DOS had it right all along?</p>

<p>Until next time ✌️</p>]]></content><author><name>Jordan Morgan</name></author><category term="AI" /><summary type="html"><![CDATA[If you can ask an agent to do something, it does it. So do you really need that UI?]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://swiftjectivec.com/assets/images/logo.png" /><media:content medium="image" url="https://swiftjectivec.com/assets/images/logo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">iOS 27, Your App, and Siri</title><link href="https://swiftjectivec.com/Siri-AI-for-iOS027/" rel="alternate" type="text/html" title="iOS 27, Your App, and Siri" /><published>2026-06-15T00:00:00-05:00</published><updated>2026-06-15T00:00:00-05:00</updated><id>https://swiftjectivec.com/Siri-AI-for-iOS027</id><content type="html" xml:base="https://swiftjectivec.com/Siri-AI-for-iOS027/"><![CDATA[<p>The dawn of a new Siri (AI) looms in the air. When it ships, the promise of just saying what you want done, and having Siri just…do it, should be realistic. Apple, true to their roots of being masterful storytellers in terms of product, had many examples on tap. “Hey Siri, what was the water bottle I was thinking of getting a few months ago?”, and BAM — Siri went through a mountain of personal context, and found the answer.</p>

<p>That stuff is obvious. But, what about <em>your</em> app?</p>

<p>“Hey Siri, what team did I last make a play for in Elite Hoops?”</p>

<p>How do we do that, what’s involved, and how should you think about Siri and your app over the summer? I <em>think</em> I have a fairly good grasp on it, and I’ll summarize it here. I’m skipping deep dives, and just keeping it to a <a href="https://www.reddit.com/r/DunderMifflin/comments/1d8xkz4/what_is_a_rundown/">rundown</a>.</p>

<section class="bg-indigo-50 border-l-8 border-indigo-500 text-gray-800 rounded-md my-10 dark:text-gray-300 dark:bg-gray-600 dark:border-indigo-700 ">
    <div class="px-5 font-mono">
        <p class="pt-2 md:pt-4"><span class="text-purple-600 dark:text-purple-400">let</span> concatenatedThoughts = <span class="text-red-600">"""</span></p>
        <p class="text-gray-600 dark:text-gray-300 text-sm md:text-base">This is based off of having on seen this <a href="https://developer.apple.com/videos/play/wwdc2026/240?">WWDC session</a>, so I might be missing some context. Though, from what I've seen, this is where the goods are at.</p>
        <p class="pb-2 md:pb-4 text-gray-600 text-base"><span class="text-red-600">"""</span></p>
    </div>
</section>

<p>First off, the parts involved. The whole process is the same — it’s still app entities for your app’s models, schemas for a particular definition of those if applicable, and a few APIs to make those known to Siri when they are on screen.</p>

<h3 id="app-entity">App Entity</h3>
<p>First, each of your app’s data models that should participate in Shortcuts, an App Intent, or general system-wide intelligence should hav a lightweight <code class="language-plaintext highlighter-rouge">AppEntity</code> version. This isn’t the same as the model <em>itself</em>, but a lightweight version of it:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">TeamEntity</span><span class="p">:</span> <span class="kt">AppEntity</span> <span class="p">{</span>
    <span class="kd">static</span> <span class="k">var</span> <span class="nv">defaultQuery</span> <span class="o">=</span> <span class="kt">TeamEntityQuery</span><span class="p">()</span>
    <span class="kd">static</span> <span class="k">var</span> <span class="nv">typeDisplayRepresentation</span><span class="p">:</span> <span class="kt">TypeDisplayRepresentation</span> <span class="o">=</span> <span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="nv">stringLiteral</span><span class="p">:</span> <span class="s">"Team"</span><span class="p">)</span>
    
    <span class="k">var</span> <span class="nv">displayRepresentation</span><span class="p">:</span> <span class="kt">DisplayRepresentation</span> <span class="p">{</span>
        <span class="kt">DisplayRepresentation</span><span class="p">(</span><span class="nv">title</span><span class="p">:</span> <span class="s">"</span><span class="se">\(</span><span class="n">name</span><span class="se">)</span><span class="s">"</span><span class="p">,</span>
                              <span class="nv">subtitle</span><span class="p">:</span> <span class="s">"</span><span class="se">\(</span><span class="n">rosterSize</span><span class="se">)</span><span class="s"> </span><span class="se">\(</span><span class="n">rosterSize</span> <span class="o">==</span> <span class="mi">1</span> <span class="p">?</span> <span class="s">"player"</span> <span class="p">:</span> <span class="s">"players"</span><span class="se">)</span><span class="s">"</span><span class="p">,</span>
                              <span class="nv">image</span><span class="p">:</span> <span class="kt">DisplayRepresentation</span><span class="o">.</span><span class="kt">Image</span><span class="p">(</span><span class="nv">data</span><span class="p">:</span> <span class="n">logo</span><span class="p">))</span>
    <span class="p">}</span>
    
    <span class="k">let</span> <span class="nv">id</span><span class="p">:</span> <span class="kt">String</span>
    
    <span class="kd">@Property</span><span class="p">(</span><span class="nv">title</span><span class="p">:</span> <span class="s">"Team Name"</span><span class="p">)</span>
    <span class="k">var</span> <span class="nv">name</span><span class="p">:</span> <span class="kt">String</span>
    
    <span class="kd">@Property</span><span class="p">(</span><span class="nv">title</span><span class="p">:</span> <span class="s">"Roster Size"</span><span class="p">)</span>
    <span class="k">var</span> <span class="nv">rosterSize</span><span class="p">:</span> <span class="kt">Int</span>
    
    <span class="kd">@Property</span><span class="p">(</span><span class="nv">title</span><span class="p">:</span> <span class="s">"Roster"</span><span class="p">)</span>
    <span class="k">var</span> <span class="nv">roster</span><span class="p">:</span> <span class="p">[</span><span class="kt">String</span><span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="surfacing-app-entities">Surfacing App Entities</h3>
<p>From there, Siri needs to be able to find them. There are two routes. First, if a network trip is required or otherwise surfacing content takes more work, then you should use <code class="language-plaintext highlighter-rouge">EntityStringQuery</code>:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">extension</span> <span class="kt">TeamEntityQuery</span><span class="p">:</span> <span class="kt">EntityStringQuery</span> <span class="p">{</span>
    <span class="kd">func</span> <span class="nf">entities</span><span class="p">(</span><span class="n">matching</span> <span class="nv">string</span><span class="p">:</span> <span class="kt">String</span><span class="p">)</span> <span class="k">async</span> <span class="k">throws</span> <span class="o">-&gt;</span> <span class="p">[</span><span class="kt">TeamEntity</span><span class="p">]</span> <span class="p">{</span>
        <span class="kt">Logs</span><span class="o">.</span><span class="n">appIntents</span><span class="o">.</span><span class="nf">info</span><span class="p">(</span><span class="s">"TeamEntityQuery: String query for term </span><span class="se">\(</span><span class="n">string</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
        
        <span class="k">guard</span> <span class="k">let</span> <span class="nv">dao</span> <span class="o">=</span> <span class="kt">DAO</span><span class="o">.</span><span class="n">readOnly</span> <span class="k">else</span> <span class="p">{</span>
            <span class="kt">Logs</span><span class="o">.</span><span class="n">appIntents</span><span class="o">.</span><span class="nf">info</span><span class="p">(</span><span class="s">"There is no database available for the intent to read from."</span><span class="p">)</span>
            <span class="k">return</span> <span class="p">[]</span>
        <span class="p">}</span>
        
        <span class="k">let</span> <span class="nv">courts</span> <span class="o">=</span> <span class="k">try</span> <span class="k">await</span> <span class="n">dao</span><span class="o">.</span><span class="nf">fetchCourtsBy</span><span class="p">(</span><span class="nv">name</span><span class="p">:</span> <span class="n">string</span><span class="p">)</span>
        <span class="k">let</span> <span class="nv">entities</span><span class="p">:</span> <span class="p">[</span><span class="kt">TeamEntity</span><span class="p">]</span> <span class="o">=</span> <span class="k">try</span> <span class="k">await</span> <span class="kt">TeamEntity</span><span class="o">.</span><span class="nf">fetchData</span><span class="p">(</span><span class="nv">for</span><span class="p">:</span> <span class="n">courts</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">entities</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>More commonly, <code class="language-plaintext highlighter-rouge">IndexedEntity</code> is the best path forward. The system automatically indexes this content for you. In fact, most cases just need you to declare conformance:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">extension</span> <span class="kt">TeamEntity</span><span class="p">:</span> <span class="kt">IndexedEntity</span> <span class="p">{}</span>
</code></pre></div></div>

<p>In fact, you can simply conform to <code class="language-plaintext highlighter-rouge">IndexedEntity</code> directly as opposed to <code class="language-plaintext highlighter-rouge">AppEntity</code>. Once you do that, you can identify which parts should be index for search, too, with <code class="language-plaintext highlighter-rouge">@Property(indexingKey:)</code>.</p>

<p>This is, however, where I’m not entirely sure of the relationships between Siri and your app. If you fill type <code class="language-plaintext highlighter-rouge">@Property(indexingKey: \.)</code> — you’ll get a list of autocompleted things you can use here. I believe all of these tie into a predefined App Schema, and the properties within them. So, what if your app doesn’t fit into those? I’m not sure, but I don’t think you can use indexing keys in that case.</p>

<h3 id="app-schemas">App Schemas</h3>
<p>Speaking of App Schemas, and their associated domains, are basically <em>juiced up</em> App Intents. Apple has already trained Siri to the moon and back over them, it’ll understand context better, and participate in follow up questions and everything else in-between. They’ve done the work for you. So, if you app fits into one of these schemas, absolutely use it.</p>

<p>You do so by using a property wrapper above your entity declaration:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@AppEntity</span><span class="p">(</span><span class="nv">schema</span><span class="p">:</span> <span class="o">.</span><span class="n">messages</span><span class="o">.</span><span class="n">message</span><span class="p">)</span>
<span class="kd">struct</span> <span class="kt">MessageEntity</span><span class="p">:</span> <span class="kt">IndexedEntity</span> <span class="p">{</span>
  <span class="c1">// The text content of the message</span>
  <span class="kd">@Property</span><span class="p">(</span><span class="nv">indexingKey</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="n">textContent</span><span class="p">)</span>
  <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kt">AttributedString</span><span class="p">?</span>
<span class="p">}</span>
</code></pre></div></div>

<p>…where <code class="language-plaintext highlighter-rouge">messages</code> is the overall schema, and the <code class="language-plaintext highlighter-rouge">message</code> is the domain within it. Xcode will also autocomplete the fields you need to implement a domain. Again, if your app’s data fits into a schema, it’s 100% what you should use. What’s less clear to me is what you can do, or should do, and how far your app can participate with Siri AI if you don’t fit into one of these predefined boxes.</p>

<h3 id="on-screen-awareness">On-screen Awareness</h3>
<p>Once you’ve indexed data and exposed it to the system, next you wanna take part in that whole “Yo Siri, tell me about X or Y”, where X or Y is something currently on screen. Two different routes here:</p>

<ol>
  <li>
    <p><code class="language-plaintext highlighter-rouge">NSUserActivity</code>: The tried and true API around, since what — iOS 8?, can help with singular content. For example, a photo.</p>
  </li>
  <li>
    <p>View annotations API: Use this when you’ve got more than one singular thing that’s presented, like a list of stuff.</p>
  </li>
</ol>

<p>Together, these tie back to an <code class="language-plaintext highlighter-rouge">AppEntity</code>, which means everything is structured in a way Siri can understand. Here’s how they look:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">MyPractice</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">practice</span><span class="p">:</span> <span class="kt">Practice</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">VStack</span> <span class="p">{</span>
            <span class="kt">StuffAndThings</span><span class="p">()</span>
            <span class="kt">PracticeContainer</span><span class="p">()</span>
            <span class="o">.</span><span class="nf">userActivity</span><span class="p">(</span><span class="s">"com.example.elitehoops.practice"</span><span class="p">,</span> 
                          <span class="nv">element</span><span class="p">:</span> <span class="n">practice</span><span class="o">.</span><span class="nf">asEntity</span><span class="p">())</span> <span class="p">{</span> <span class="n">entity</span><span class="p">,</span> <span class="n">activity</span> <span class="k">in</span>
              <span class="n">activity</span><span class="o">.</span><span class="n">title</span> <span class="o">=</span>  <span class="n">practice</span><span class="o">.</span><span class="n">title</span>
              <span class="n">activity</span><span class="o">.</span><span class="n">appEntityIdentifier</span> <span class="o">=</span> <span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="nv">for</span><span class="p">:</span> <span class="n">entity</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And for view annotations:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">MyPractices</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">practices</span><span class="p">:</span> <span class="p">[</span><span class="kt">Practice</span><span class="p">]</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">ForEach</span><span class="p">(</span><span class="n">practices</span><span class="p">)</span> <span class="p">{</span> <span class="n">p</span> <span class="k">in</span> 
            <span class="kt">PracticeView</span><span class="p">(</span><span class="nv">practice</span><span class="p">:</span> <span class="n">p</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">appEntityIdentifier</span><span class="p">(</span>
                      <span class="kt">EntityIdentifier</span><span class="p">(</span>
                          <span class="nv">for</span><span class="p">:</span> <span class="kt">PracticeEntity</span><span class="o">.</span><span class="k">self</span><span class="p">,</span>
                          <span class="nv">identifier</span><span class="p">:</span> <span class="n">p</span><span class="o">.</span><span class="n">id</span>
                      <span class="p">)</span>
                <span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="handle-data-coming-in-and-out">Handle Data Coming In and Out</h3>
<p>Finally, getting data out and in. When Siri wants to chain together stuff, like “Hey Siri, email this practice to Jansyn.” — we’ve now got a few moving parts. There’s the “practice”, I need to export my own data, and then there’s the recipient, Mail. To export or import, look no further than <code class="language-plaintext highlighter-rouge">Transferable</code>.</p>

<p>There is some nuance depending on the context. If you’re trying to handle an incoming request against some existing content, then you’ll need <code class="language-plaintext highlighter-rouge">IntentValueQuery</code>. But, if that request means your app should creating a new model or data, then implement the <code class="language-plaintext highlighter-rouge">importing</code> bit when wiring up your <code class="language-plaintext highlighter-rouge">transferRepresentation</code> within <code class="language-plaintext highlighter-rouge">Transferable</code>.</p>

<p>In <a href="https://developer.apple.com/videos/play/wwdc2026/240?time=1159">Apple’s session</a>, they use this example:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">ContactEntityQuery</span><span class="p">:</span> <span class="kt">IntentValueQuery</span> <span class="p">{</span>
    <span class="kd">func</span> <span class="nf">values</span><span class="p">(</span><span class="k">for</span> <span class="nv">input</span><span class="p">:</span> <span class="p">[</span><span class="kt">IntentPerson</span><span class="p">])</span> <span class="k">async</span> <span class="k">throws</span> <span class="o">-&gt;</span> <span class="p">[</span><span class="kt">ContactEntity</span><span class="p">]</span> <span class="p">{</span>
      <span class="k">let</span> <span class="nv">names</span> <span class="o">=</span> <span class="n">input</span><span class="o">.</span><span class="nf">map</span><span class="p">(\</span><span class="o">.</span><span class="n">displayName</span><span class="p">)</span>
      <span class="k">let</span> <span class="nv">descriptor</span> <span class="o">=</span> <span class="kt">FetchDescriptor</span><span class="o">&lt;</span><span class="kt">Contact</span><span class="o">&gt;</span><span class="p">()</span>
      <span class="k">let</span> <span class="nv">contacts</span> <span class="o">=</span> <span class="k">try</span> <span class="n">model</span><span class="o">.</span><span class="n">mainContext</span><span class="o">.</span><span class="nf">fetch</span><span class="p">(</span><span class="n">descriptor</span><span class="p">)</span>
      <span class="k">let</span> <span class="nv">matches</span> <span class="o">=</span> <span class="n">contacts</span><span class="o">.</span><span class="n">filter</span> <span class="p">{</span> <span class="n">contact</span> <span class="k">in</span>
          <span class="n">names</span><span class="o">.</span><span class="nf">contains</span><span class="p">(</span><span class="nv">where</span><span class="p">:</span> <span class="p">{</span> <span class="n">name</span> <span class="k">in</span>
              <span class="n">contact</span><span class="o">.</span><span class="n">name</span><span class="o">.</span><span class="nf">localizedStandardContains</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>
          <span class="p">})</span>
      <span class="p">}</span>
      <span class="k">return</span> <span class="n">matches</span><span class="o">.</span><span class="nf">map</span><span class="p">(\</span><span class="o">.</span><span class="n">entity</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>However, this is the part that feels slightly limiting. It’s typed to handle only an <code class="language-plaintext highlighter-rouge">IntentPerson</code> — something the system already defines. As far as I can tell, your app would <em>have</em> to use some value-known intent already, like <code class="language-plaintext highlighter-rouge">IntentPerson</code> or something from the App Schemas. Otherwise, you also wouldn’t be able to use <code class="language-plaintext highlighter-rouge">IntentValueRepresentation</code> — which is exactly what Siri uses for this type of stuff.</p>

<p>Here’s another example I found <a href="https://developer.apple.com/documentation/appintents/intentvaluerepresentation">in the docs</a>, which handles things bidirectionally:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">extension</span> <span class="kt">TeamEntity</span><span class="p">:</span> <span class="kt">Transferable</span> <span class="p">{</span>
    <span class="kd">static</span> <span class="k">var</span> <span class="nv">transferRepresentation</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">TransferRepresentation</span> <span class="p">{</span>
        <span class="kt">IntentValueRepresentation</span><span class="p">(</span>
            <span class="nv">exporting</span><span class="p">:</span> <span class="p">{</span> <span class="n">entity</span> <span class="k">in</span>
                <span class="kt">IntentPerson</span><span class="p">(</span><span class="nv">name</span><span class="p">:</span> <span class="o">.</span><span class="nf">displayName</span><span class="p">(</span><span class="n">entity</span><span class="o">.</span><span class="n">name</span><span class="p">))</span>
            <span class="p">},</span>
            <span class="nv">importing</span><span class="p">:</span> <span class="p">{</span> <span class="n">person</span> <span class="k">in</span>
                <span class="kt">ContactEntity</span><span class="p">(</span><span class="nv">name</span><span class="p">:</span> <span class="n">person</span><span class="o">.</span><span class="n">name</span><span class="o">.</span><span class="n">displayString</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">)</span>
        <span class="kt">DataRepresentation</span><span class="p">(</span><span class="nv">exportedContentType</span><span class="p">:</span> <span class="o">.</span><span class="n">utf8PlainText</span><span class="p">)</span> <span class="p">{</span> <span class="n">entity</span> <span class="k">in</span>
            <span class="n">entity</span><span class="o">.</span><span class="nf">teamSummaryText</span><span class="p">()</span><span class="o">.</span><span class="nf">data</span><span class="p">(</span><span class="nv">using</span><span class="p">:</span> <span class="o">.</span><span class="n">utf8</span><span class="p">)</span> <span class="p">??</span> <span class="o">.</span><span class="nf">init</span><span class="p">()</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>So, I’m unsure of how to handle this beyond that. If our apps don’t fit into schema, where does that leave us? I haven’t found the answer to that.</p>

<h3 id="final-thoughts">Final Thoughts</h3>
<p>New Siri looks fantastic. No doubt, integrating it will finally bring us to that “This is what I wanted” phase of the tech. It should be an epochal event for our little digital assistant. For us? The APIs are easy to use, and there isn’t much guesswork. My only question mark is how to best integrate when our data models don’t fit into an App Schema, and if I find concrete answers — I’ll update this post.</p>

<p>Until next time ✌️</p>]]></content><author><name>Jordan Morgan</name></author><category term="Siri" /><summary type="html"><![CDATA[How do we think about Siri AI with iOS 27? Here's how to make the most of new APIs.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://swiftjectivec.com/assets/images/logo.png" /><media:content medium="image" url="https://swiftjectivec.com/assets/images/logo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">iOS 27: Notable UIKit Additions</title><link href="https://swiftjectivec.com/iOS-27-Notable-UIKit-Additions/" rel="alternate" type="text/html" title="iOS 27: Notable UIKit Additions" /><published>2026-06-08T00:00:00-05:00</published><updated>2026-06-08T00:00:00-05:00</updated><id>https://swiftjectivec.com/iOS-27-Notable-UIKit-Additions</id><content type="html" xml:base="https://swiftjectivec.com/iOS-27-Notable-UIKit-Additions/"><![CDATA[<p>Times…they <code class="language-plaintext highlighter-rouge">are.concact(.haveBeen)</code> changing. In years past, I’d wait for the “What’s New in UIKit” video to hit towards the end of dub dub week, pour through the new docs to see what was added, and then fumble my way around Xcode to get the first examples of the changes up and running.</p>

<p>And now? Well, I just…kinda, did this:</p>

<p><img class="lozad mx-auto max-w-full h-auto shadow-2xl rounded-lg h-18" data-src="../assets/images/UIKit2026_1.jpg" role="presentation" srcset="../assets/images/UIKit2026_1.jpg" alt="Xcode Agentic Prompting for UIKit iOS 27 Changes." /></p>

<p>Within minutes, I now had workable code samples of all the changes, explanations of what they were, and docs. I prayed for days like this, man.</p>

<p><img class="lozad mx-auto max-w-full h-auto shadow-2xl rounded-lg h-18" data-src="../assets/images/UIKit2026_3.jpg" role="presentation" srcset="../assets/images/UIKit2026_3.jpg" alt="Code catalog of UIKit changes in iOS27." /></p>

<p><em>It used to take so much more work!</em></p>

<p>There’s even a skill for this sort of thing, I have to imagine Cupertino &amp; Friends™️ made it just to throw me a bone for my annual UIKit post:</p>

<p><img class="lozad mx-auto max-w-full h-auto shadow-2xl rounded-lg h-18" data-src="../assets/images/UIKit2026_2.jpg" role="presentation" srcset="../assets/images/UIKit2026_2.jpg" alt="Xcode Skill for UIKit." /></p>

<p>As of today, the pathway to utilizing UIKit is virtually nothing. I’d argue now is a better time than ever (yes — really!) to give our trusty ol’ UI framework a go in your app. Apple has given access to <a href="https://x.com/luka_bernardi/status/2064125324485390383">skills</a> to do it correctly, efficiently, and quickly. So, let’s see what’s new.</p>

<p>If you want to catch up on this series first, you can view the <a href="https://swiftjectivec.com/iOS-11-notable-uikit-additions">iOS 11</a>, <a href="https://swiftjectivec.com/iOS-12-notable-uikit-additions">iOS 12</a>, <a href="https://swiftjectivec.com/iOS-13-notable-uikit-additions">iOS 13</a>, <a href="https://swiftjectivec.com/iOS-14-notable-uikit-additions">iOS 14</a>, <a href="https://swiftjectivec.com/iOS-15-notable-uikit-additions">iOS 15</a>, <a href="https://swiftjectivec.com/iOS-16-notable-uikit-additions">iOS 16</a>, <a href="https://swiftjectivec.com/iOS-17-notable-uikit-additions">iOS 17</a>, <a href="https://swiftjectivec.com/iOS-18-notable-uikit-additions">iOS 18</a>, and <a href="https://swiftjectivec.com/iOS-26-notable-uikit-additions">iOS 26</a> versions of this article.</p>

<section class="bg-indigo-50 border-l-8 border-indigo-500 text-gray-800 rounded-md my-10 dark:text-gray-300 dark:bg-gray-600 dark:border-indigo-700 ">
    <div class="px-5 font-mono">
        <p class="pt-2 md:pt-4"><span class="text-purple-600 dark:text-purple-400">let</span> concatenatedThoughts = <span class="text-red-600">"""</span></p>
        <p class="text-gray-600 dark:text-gray-300 text-sm md:text-base">Looking for the header diffs? Kyle Howells posted them <a href="https://gist.github.com/kylehowells/32c7f7475698b5f74eeb183f8db9d186">here</a>.</p>
        <p class="pb-2 md:pb-4 text-gray-600 text-base"><span class="text-red-600">"""</span></p>
    </div>
</section>

<h2 id="navigation-bar--bar-buttons">Navigation Bar &amp; Bar Buttons</h2>

<p>The nav bar and its friends got quite a bit of attention, and to be honest that’s where we will find most of the changes. They adapt to all sorts of sizes and situations…hmmm, <a href="https://x.com/samhenrigold/status/2064069948486320528/photo/1">I WONDER WHY</a>!?</p>

<h3 id="navigation-bar-minimization">Navigation Bar Minimization</h3>

<p><a href="https://developer.apple.com/documentation/uikit/uinavigationitem/barminimizebehavior">Docs</a></p>

<p>Each view controller now gets a say in whether its navigation bar shrinks down as you scroll. It’s a great fit for content-heavy screens where every vertical point counts, while your settings or tool-laden screens can keep the bar pinned right where it is. There’s a companion safe-area API too, which decides whether your content slides up into the space that minimized bar leaves behind.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">navigationItem</span><span class="o">.</span><span class="n">barMinimizeBehavior</span> <span class="o">=</span> <span class="o">.</span><span class="n">onScrollDown</span>
<span class="n">navigationItem</span><span class="o">.</span><span class="n">barMinimizationSafeAreaAdjustment</span> <span class="o">=</span> <span class="o">.</span><span class="n">enabled</span>
</code></pre></div></div>

<h3 id="bar-button-visibility-priority">Bar Button Visibility Priority</h3>

<p><a href="https://developer.apple.com/documentation/uikit/uibarbuttonitem/visibilitypriority">Docs</a></p>

<p>You can now rank your bar button items, so UIKit knows which actions to hang onto longest when space gets tight. Think iPad, Stage Manager, toolbars, resizable windows, FOLDABLES, — anywhere the available bar width is a moving target. The important stuff you tagged sticks around, and the rest dips out.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">saveItem</span><span class="o">.</span><span class="n">visibilityPriority</span> <span class="o">=</span> <span class="o">.</span><span class="n">high</span>
<span class="n">shareItem</span><span class="o">.</span><span class="n">visibilityPriority</span> <span class="o">=</span> <span class="o">.</span><span class="n">standard</span>
<span class="n">printItem</span><span class="o">.</span><span class="n">visibilityPriority</span> <span class="o">=</span> <span class="kt">UIBarButtonItemVisibilityPriority</span><span class="p">(</span><span class="nv">lowerThan</span><span class="p">:</span> <span class="o">.</span><span class="n">standard</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="removed-bar-button-padding">Removed Bar Button Padding</h3>

<p><a href="https://developer.apple.com/documentation/uikit/uibarbuttonitem/ispaddingremoved">Docs</a></p>

<p>Does exactly what it says on the tin — strips the default outer padding off a <code class="language-plaintext highlighter-rouge">UIBarButtonItem</code>. I guess this one would be handy when your custom bar button view already brings its own inherent spacing, or you just need things to line up a bit tighter. Just keep an eye on your touch targets, the golden 44 point rule applies:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">colorItem</span> <span class="o">=</span> <span class="kt">UIBarButtonItem</span><span class="p">(</span><span class="nv">customView</span><span class="p">:</span> <span class="n">colorSwatch</span><span class="p">)</span>
<span class="n">colorItem</span><span class="o">.</span><span class="n">isPaddingRemoved</span> <span class="o">=</span> <span class="kc">true</span>
</code></pre></div></div>

<h2 id="scenes-windows--orientation">Scenes, Windows &amp; Orientation</h2>

<p>Apple keeps marching toward a multi-window, resizable, scene-everywhere world, and…yeah, forget it. Because of the foldable that’s coming, that’s why. To wit, <a href="https://developer.apple.com/documentation/xcode/device-hub">device hub</a> makes all of this quick to test out.</p>

<h3 id="scene-closure-confirmation">Scene Closure Confirmation</h3>

<p><a href="https://developer.apple.com/documentation/UIKit/UISceneClosureConfirmation">Docs</a></p>

<p>Ever close a window and immediately wish you hadn’t? This lets your app throw up a system confirmation before a scene actually closes, mimicking how several web apps work today. It’s made for multi-window document, editing, or workflow-ish type apps — basically anywhere slamming a window shut could toss out unsaved work or cut off something mid-flight.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">windowScene</span><span class="o">.</span><span class="n">closureConfirmation</span> <span class="o">=</span> <span class="kt">UISceneClosureConfirmation</span><span class="p">(</span>
    <span class="nv">title</span><span class="p">:</span> <span class="s">"Unsaved Changes"</span><span class="p">,</span>
    <span class="nv">message</span><span class="p">:</span> <span class="s">"Save before closing this window?"</span><span class="p">,</span>
    <span class="nv">actions</span><span class="p">:</span> <span class="p">[</span>
        <span class="kt">UIAlertAction</span><span class="p">(</span><span class="nv">title</span><span class="p">:</span> <span class="s">"Save and Close"</span><span class="p">,</span> <span class="nv">style</span><span class="p">:</span> <span class="o">.</span><span class="k">default</span><span class="p">)</span> <span class="p">{</span> <span class="n">_</span> <span class="k">in</span> <span class="nf">saveDocument</span><span class="p">()</span> <span class="p">},</span>
        <span class="kt">UIAlertAction</span><span class="p">(</span><span class="nv">title</span><span class="p">:</span> <span class="s">"Discard Changes"</span><span class="p">,</span> <span class="nv">style</span><span class="p">:</span> <span class="o">.</span><span class="n">destructive</span><span class="p">)</span> <span class="p">{</span> <span class="n">_</span> <span class="k">in</span> <span class="nf">discardChanges</span><span class="p">()</span> <span class="p">}</span>
    <span class="p">]</span>
<span class="p">)</span>
</code></pre></div></div>

<h3 id="external-display-scene-accessories">External Display Scene Accessories</h3>

<p><a href="https://developer.apple.com/documentation/uikit/uiviewcontroller/registersceneaccessory(_:)">Docs</a></p>

<p>A view controller can now register a secondary, noninteractive scene built for an external display. Looking at the docs, I think this would be cool for auxiallary presentation screens, dashboards, reference views, or any companion output you’d want to toss up on a connected display where touch input isn’t always required:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">configuration</span> <span class="o">=</span> <span class="kt">UISceneConfiguration</span><span class="p">(</span><span class="nv">name</span><span class="p">:</span> <span class="s">"Presenter Display"</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">accessory</span> <span class="o">=</span> <span class="kt">UISceneAccessory</span><span class="o">.</span><span class="nf">externalNonInteractive</span><span class="p">(</span><span class="nv">sceneConfiguration</span><span class="p">:</span> <span class="n">configuration</span><span class="p">)</span>

<span class="k">let</span> <span class="nv">registration</span> <span class="o">=</span> <span class="nf">registerSceneAccessory</span><span class="p">(</span><span class="n">accessory</span><span class="p">)</span>
<span class="n">registration</span><span class="o">.</span><span class="n">isEnabled</span> <span class="o">=</span> <span class="n">registration</span><span class="o">.</span><span class="n">isAvailable</span>
</code></pre></div></div>

<h3 id="per-scene-supported-orientations">Per-Scene Supported Orientations</h3>

<p><a href="https://developer.apple.com/documentation/UIKit/UIWindowSceneDelegate/supportedInterfaceOrientations(for:)">Docs</a></p>

<p>Each window scene can now declare its <em>own</em> supported orientations. So in a multi-window app, your video scene can stay locked to landscape while the browsing or editing scene happily rotates whichever way it likes. This flexibility is great, as I’ve had several times where one-size-fits-all orientation rules didn’t quite apply for the whole app:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">supportedInterfaceOrientations</span><span class="p">(</span>
    <span class="k">for</span> <span class="nv">windowScene</span><span class="p">:</span> <span class="kt">UIWindowScene</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">UIInterfaceOrientationMask</span> <span class="p">{</span>
    <span class="o">.</span><span class="n">landscape</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="tab-bars--sidebars">Tab Bars &amp; Sidebars</h2>

<p>The tab bar and sidebar duo (the latter of which ditched the design from iOS 26 i loved 😢) picked up some welcome controls for steering which one shows up, and what gets the spotlight.</p>

<h3 id="prominent-tab-selection">Prominent Tab Selection</h3>

<p><a href="https://developer.apple.com/documentation/uikit/uitabbarcontroller/prominenttabidentifier">Docs</a></p>

<p>Got one tab that matters more than the others? A <code class="language-plaintext highlighter-rouge">UITabBarController</code> can now crown a single tab as visually prominent — perfect for a headline action like compose, search, cart, create, or record:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">tabBarController</span><span class="o">.</span><span class="n">prominentTabIdentifier</span> <span class="o">=</span> <span class="s">"compose"</span>
<span class="n">tabBarController</span><span class="o">.</span><span class="nf">setProminentTabIdentifier</span><span class="p">(</span><span class="s">"inbox"</span><span class="p">,</span> <span class="nv">animated</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="sidebar-preferred-placement">Sidebar Preferred Placement</h3>

<p><a href="https://developer.apple.com/documentation/UIKit/UITabBarController/Sidebar-swift.class/preferredPlacement">Docs</a></p>

<p>When only one can win, this lets a tab bar controller say whether it’d rather show up as a sidebar or a tab bar. It’s great for apps that are sidebar-first at heart, but still need to fold down gracefully to a tab bar on more compact devices or…whatever, you know at this point. I’d assume foldables:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">tabBarController</span><span class="o">.</span><span class="n">mode</span> <span class="o">=</span> <span class="o">.</span><span class="n">tabSidebar</span>
<span class="n">tabBarController</span><span class="o">.</span><span class="n">sidebar</span><span class="o">.</span><span class="n">preferredPlacement</span> <span class="o">=</span> <span class="o">.</span><span class="n">sidebar</span>
</code></pre></div></div>

<h3 id="batching-tab-bar-updates">Batching Tab Bar Updates</h3>

<p><a href="https://developer.apple.com/documentation/uikit/uitabbarcontroller/performbatchupdates(_:)">Docs</a></p>

<p>Changing a whole bunch of tab stuff at once — badges, images, ordering, prominent state — and want to dodge the flicker? Batch it. UIKit folds all of those changes into a single update pass instead of grinding through a separate layout pass for each one.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">tabBarController</span><span class="o">.</span><span class="n">performBatchUpdates</span> <span class="p">{</span>
    <span class="n">tabBarController</span><span class="o">.</span><span class="n">tabs</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">badgeValue</span> <span class="o">=</span> <span class="s">"3"</span>
    <span class="n">tabBarController</span><span class="o">.</span><span class="n">tabs</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="o">.</span><span class="n">image</span> <span class="o">=</span> <span class="kt">UIImage</span><span class="p">(</span><span class="nv">systemName</span><span class="p">:</span> <span class="s">"star.fill"</span><span class="p">)</span>
    <span class="n">tabBarController</span><span class="o">.</span><span class="nf">setProminentTabIdentifier</span><span class="p">(</span><span class="s">"favorites"</span><span class="p">,</span> <span class="nv">animated</span><span class="p">:</span> <span class="kc">false</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="sidebar-availability-tracking">Sidebar Availability Tracking</h3>

<p><a href="https://developer.apple.com/documentation/uikit/uitabbarcontroller/sidebar-swift.class/isavailable">Docs</a></p>

<p>A simple read on whether your tab/sidebar interface actually has a sidebar available at the moment. It’s handy for adjusting your supporting UI when that sidebar slides in or out thanks to a size class change, a rotation, or a window resize.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">tabBarController</span><span class="p">(</span>
    <span class="n">_</span> <span class="nv">tabBarController</span><span class="p">:</span> <span class="kt">UITabBarController</span><span class="p">,</span>
    <span class="n">sidebarAvailabilityDidChange</span> <span class="nv">sidebar</span><span class="p">:</span> <span class="kt">UITabBarController</span><span class="o">.</span><span class="kt">Sidebar</span>
<span class="p">)</span> <span class="p">{</span>
    <span class="n">tabBarController</span><span class="o">.</span><span class="n">navigationItem</span><span class="o">.</span><span class="n">leftBarButtonItem</span> <span class="o">=</span> <span class="n">sidebar</span><span class="o">.</span><span class="n">isAvailable</span> <span class="p">?</span> <span class="nv">nil</span> <span class="p">:</span> <span class="n">menuButton</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="bonus-points">Bonus Points</h2>

<p>And, as always, then there’s the usual grab bag. Here are a few odds and ends that didn’t fit tidly into the stuff above, but are worth a look all the same.</p>

<h3 id="menu-image-visibility">Menu Image Visibility</h3>

<p><a href="https://developer.apple.com/documentation/uikit/uimenuelement/preferredimagevisibility">Docs</a></p>

<p>Menu elements can now explicitly ask for their images to be shown, hidden, or simply left up to the system. Lean on it when the icon <em>is</em> the information — color swatches, say — or hide them away when icons would only add noise to an otherwise text-heavy menu.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">red</span> <span class="o">=</span> <span class="kt">UIAction</span><span class="p">(</span>
    <span class="nv">title</span><span class="p">:</span> <span class="s">"Red"</span><span class="p">,</span>
    <span class="nv">image</span><span class="p">:</span> <span class="kt">UIImage</span><span class="p">(</span><span class="nv">systemName</span><span class="p">:</span> <span class="s">"circle.fill"</span><span class="p">),</span>
    <span class="nv">preferredImageVisibility</span><span class="p">:</span> <span class="o">.</span><span class="n">visible</span>
<span class="p">)</span> <span class="p">{</span> <span class="n">_</span> <span class="k">in</span> <span class="nf">applyColor</span><span class="p">(</span><span class="o">.</span><span class="n">systemRed</span><span class="p">)</span> <span class="p">}</span>

<span class="k">let</span> <span class="nv">reset</span> <span class="o">=</span> <span class="kt">UIAction</span><span class="p">(</span>
    <span class="nv">title</span><span class="p">:</span> <span class="s">"Reset Formatting"</span><span class="p">,</span>
    <span class="nv">image</span><span class="p">:</span> <span class="kt">UIImage</span><span class="p">(</span><span class="nv">systemName</span><span class="p">:</span> <span class="s">"textformat"</span><span class="p">),</span>
    <span class="nv">preferredImageVisibility</span><span class="p">:</span> <span class="o">.</span><span class="n">hidden</span>
<span class="p">)</span> <span class="p">{</span> <span class="n">_</span> <span class="k">in</span> <span class="nf">resetFormatting</span><span class="p">()</span> <span class="p">}</span>
</code></pre></div></div>

<h3 id="document-launch-screen-subtitle">Document Launch Screen Subtitle</h3>

<p><a href="https://developer.apple.com/documentation/uikit/uidocumentviewcontroller/launchoptions-swift.class/subtitle">Docs</a></p>

<p>I mean…why not? Subtitles!</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">launchOptions</span> <span class="o">=</span> <span class="kt">UIDocumentViewController</span><span class="o">.</span><span class="kt">LaunchOptions</span><span class="p">()</span>
<span class="n">launchOptions</span><span class="o">.</span><span class="n">subtitle</span> <span class="o">=</span> <span class="s">"Create and edit presentations"</span>

<span class="k">let</span> <span class="nv">documentViewController</span> <span class="o">=</span> <span class="kt">UIDocumentViewController</span><span class="p">(</span><span class="nv">launchOptions</span><span class="p">:</span> <span class="n">launchOptions</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="final-thoughts">Final Thoughts</h2>

<p>A few years ago, I wondered if UIKit would be deprecated altogether. That would’ve been drastic, sure, but I <em>did</em> wonder. These days, I don’t share the same concern anymore. SwiftUI gets better each year, and yeah — it’ll have top billing. But UIKit is solid, and it appears to be a core part of Apple’s strategy into the future.</p>

<p>Not much changed this year, and not much was added that’s flashy. But, that’s true of iOS 27 in several ways. This year, it’s about getting Siri right, and I think that’s the right move.</p>

<p>Until next time ✌️</p>]]></content><author><name>Jordan Morgan</name></author><category term="UIKit" /><summary type="html"><![CDATA[iOS 27 is here. So now we check in with our invincible UI framework, UIKit.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://swiftjectivec.com/assets/images/logo.png" /><media:content medium="image" url="https://swiftjectivec.com/assets/images/logo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">W.W.D.C. 2026: The Pregame Quiz</title><link href="https://swiftjectivec.com/WWDC-2026-The-Pregame-Quiz/" rel="alternate" type="text/html" title="W.W.D.C. 2026: The Pregame Quiz" /><published>2026-05-26T00:00:00-05:00</published><updated>2026-05-26T00:00:00-05:00</updated><id>https://swiftjectivec.com/WWDC-2026-The-Pregame-Quiz</id><content type="html" xml:base="https://swiftjectivec.com/WWDC-2026-The-Pregame-Quiz/"><![CDATA[<p>Tern up for what! They’ll never Ternus against each other! Okay. <em>Sorry</em>. I’m done now. But it is that time…</p>

<p><span class="relative inline-block not-prose pb-2 md:pb-[0.25px]"><span class="bg-clip-text text-transparent bg-gradient-to-r from-blue-500 to-purple-500 font-semibold">The 12th annual Swiftjective-C pregame quiz is here!</span><span class="absolute left-0 bottom-0 w-full h-1.5" style="background-image: url(&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 6' preserveAspectRatio='none'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0%25' y1='0%25' x2='100%25' y2='0%25'%3E%3Cstop offset='0%25' stop-color='%233b82f6'/%3E%3Cstop offset='100%25' stop-color='%238b5cf6'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath d='M0,6 Q50,0 100,6' fill='none' stroke='url(%23g)' stroke-width='1.75'/%3E%3C/svg%3E&quot;); background-size: 100% 100%; background-repeat: no-repeat;"></span></span></p>

<p>This year, I’m rolling with (mostly) 2026 news, updates, and shenanigans. No “Objective-C without the C” questions, or Steve Notes, or other Swiftjective-C staples of year’s past. This edition is all about stuff Cupertino &amp; Friends™️ have shipped, announced, clarified, regulated, re-org’d or otherwise made us all read release notes about this year.</p>

<p>If you want to warm up first, you’ve got eleven years of quiz backlog to spelunk through:</p>

<div class="not-prose grid grid-cols-2 md:grid-cols-3 gap-x-6 gap-y-6 my-12">
  <!-- WWDC 2015 -->
  <a href="https://swiftjectivec.com/WWDC-2015-The-Pregame-Quiz" class="flex flex-col items-center w-full max-w-xs md:max-w-none mx-auto">
    <div class="w-full aspect-[2.2/1] md:aspect-[2.86/1] overflow-hidden rounded-md shadow transition-transform hover:scale-105 flex items-center justify-center">
      <img src="../assets/images/dd2015.png" alt="WWDC 2015" class="object-cover h-full w-full" loading="lazy" width="160" height="56" />
    </div>
    <div class="text-center mt-2 text-xs font-semibold text-gray-600">WWDC 2015</div>
  </a>
  <!-- WWDC 2016 -->
  <a href="https://swiftjectivec.com/WWDC-2016-The-Pregame-Quiz" class="flex flex-col items-center w-full max-w-xs md:max-w-none mx-auto">
    <div class="w-full aspect-[2.2/1] md:aspect-[2.86/1] overflow-hidden rounded-md shadow transition-transform hover:scale-105 flex items-center justify-center">
      <img src="../assets/images/dd2016.png" alt="WWDC 2016" class="object-cover h-full w-full" loading="lazy" width="160" height="56" />
    </div>
    <div class="text-center mt-2 text-xs font-semibold text-gray-600">WWDC 2016</div>
  </a>
  <!-- WWDC 2017 -->
  <a href="https://swiftjectivec.com/WWDC-2017-The-Pregame-Quiz" class="flex flex-col items-center w-full max-w-xs md:max-w-none mx-auto">
    <div class="w-full aspect-[2.2/1] md:aspect-[2.86/1] overflow-hidden rounded-md shadow transition-transform hover:scale-105 flex items-center justify-center">
      <img src="../assets/images/dd2017.png" alt="WWDC 2017" class="object-cover h-full w-full" loading="lazy" width="160" height="56" />
    </div>
    <div class="text-center mt-2 text-xs font-semibold text-gray-600">WWDC 2017</div>
  </a>
  <!-- WWDC 2018 -->
  <a href="https://swiftjectivec.com/WWDC-2018-The-Pregame-Quiz" class="flex flex-col items-center w-full max-w-xs md:max-w-none mx-auto">
    <div class="w-full aspect-[2.2/1] md:aspect-[2.86/1] overflow-hidden rounded-md shadow transition-transform hover:scale-105 flex items-center justify-center">
      <img src="../assets/images/dd2018.png" alt="WWDC 2018" class="object-cover h-full w-full" loading="lazy" width="160" height="56" />
    </div>
    <div class="text-center mt-2 text-xs font-semibold text-gray-600">WWDC 2018</div>
  </a>
  <!-- WWDC 2019 -->
  <a href="https://swiftjectivec.com/WWDC-2019-The-Pregame-Quiz" class="flex flex-col items-center w-full max-w-xs md:max-w-none mx-auto">
    <div class="w-full aspect-[2.2/1] md:aspect-[2.86/1] overflow-hidden rounded-md shadow transition-transform hover:scale-105 flex items-center justify-center">
      <img src="../assets/images/dd2019.png" alt="WWDC 2019" class="object-cover h-full w-full" loading="lazy" width="160" height="56" />
    </div>
    <div class="text-center mt-2 text-xs font-semibold text-gray-600">WWDC 2019</div>
  </a>
  <!-- WWDC 2020 -->
  <a href="https://swiftjectivec.com/WWDC-2020-The-Pregame-Quiz" class="flex flex-col items-center w-full max-w-xs md:max-w-none mx-auto">
    <div class="w-full aspect-[2.2/1] md:aspect-[2.86/1] overflow-hidden rounded-md shadow transition-transform hover:scale-105 flex items-center justify-center">
      <img src="../assets/images/dd2020.png" alt="WWDC 2020" class="object-cover h-full w-full" loading="lazy" width="160" height="56" />
    </div>
    <div class="text-center mt-2 text-xs font-semibold text-gray-600">WWDC 2020</div>
  </a>
  <!-- WWDC 2021 -->
  <a href="https://swiftjectivec.com/WWDC-2021-The-Pregame-Quiz" class="flex flex-col items-center w-full max-w-xs md:max-w-none mx-auto">
    <div class="w-full aspect-[2.2/1] md:aspect-[2.86/1] overflow-hidden rounded-md shadow transition-transform hover:scale-105 flex items-center justify-center">
      <img src="../assets/images/dd2021.png" alt="WWDC 2021" class="object-cover h-full w-full" loading="lazy" width="160" height="56" />
    </div>
    <div class="text-center mt-2 text-xs font-semibold text-gray-600">WWDC 2021</div>
  </a>
  <!-- WWDC 2022 -->
  <a href="https://swiftjectivec.com/WWDC-2022-The-Pregame-Quiz" class="flex flex-col items-center w-full max-w-xs md:max-w-none mx-auto">
    <div class="w-full aspect-[2.2/1] md:aspect-[2.86/1] overflow-hidden rounded-md shadow transition-transform hover:scale-105 flex items-center justify-center">
      <img src="../assets/images/dd2022.png" alt="WWDC 2022" class="object-cover h-full w-full" loading="lazy" width="160" height="56" />
    </div>
    <div class="text-center mt-2 text-xs font-semibold text-gray-600">WWDC 2022</div>
  </a>
  <!-- WWDC 2023 -->
  <a href="https://swiftjectivec.com/WWDC-2023-The-Pregame-Quiz" class="flex flex-col items-center w-full max-w-xs md:max-w-none mx-auto">
    <div class="w-full aspect-[2.2/1] md:aspect-[2.86/1] overflow-hidden rounded-md shadow transition-transform hover:scale-105 flex items-center justify-center">
      <img src="../assets/images/dd2023.png" alt="WWDC 2023" class="object-cover h-full w-full" loading="lazy" width="160" height="56" />
    </div>
    <div class="text-center mt-2 text-xs font-semibold text-gray-600">WWDC 2023</div>
  </a>
  <!-- WWDC 2024 -->
  <a href="https://swiftjectivec.com/WWDC-2024-The-Pregame-Quiz-copy" class="flex flex-col items-center w-full max-w-xs md:max-w-none mx-auto">
    <div class="w-full aspect-[2.2/1] md:aspect-[2.86/1] overflow-hidden rounded-md shadow transition-transform hover:scale-105 flex items-center justify-center">
      <img src="../assets/images/dd2024.png" alt="WWDC 2024" class="object-cover h-full w-full" loading="lazy" width="160" height="56" />
    </div>
    <div class="text-center mt-2 text-xs font-semibold text-gray-600">WWDC 2024</div>
  </a>
  
  <!-- WWDC 2025 -->
  <a href="https://swiftjectivec.com/WWDC-2025-The-Pregame-Quiz" class="flex flex-col items-center w-full max-w-xs md:max-w-none mx-auto">
    <div class="w-full aspect-[2.2/1] md:aspect-[2.86/1] overflow-hidden rounded-md shadow transition-transform hover:scale-105 flex items-center justify-center">
      <img src="../assets/images/wwdc2025.jpg" alt="WWDC 2025" class="object-cover h-full w-full" loading="lazy" width="160" height="56" />
    </div>
    <div class="text-center mt-2 text-xs font-semibold text-gray-600">WWDC 2025</div>
  </a>
  
  
  <!-- WWDC 2026 -->
  <a href="https://swiftjectivec.com/WWDC-2026-The-Pregame-Quiz" class="flex flex-col items-center w-full max-w-xs md:max-w-none mx-auto">
    <div class="w-full aspect-[2.2/1] md:aspect-[2.86/1] overflow-hidden rounded-md shadow transition-transform hover:scale-105 flex items-center justify-center">
      <img src="../assets/images/wwdc2026.jpg" alt="WWDC 2026" class="object-cover h-full w-full" loading="lazy" width="160" height="56" />
    </div>
    <div class="text-center mt-2 text-xs font-semibold text-gray-600">WWDC 2026</div>
  </a>
  
</div>

<h3 id="ground-rules">Ground Rules</h3>

<p>There are three rounds, and the point break down is as follows:</p>

<ul>
  <li><strong>Round 1</strong> - 1 point each answer</li>
  <li><strong>Round 2</strong> - 2 points each answer</li>
  <li><strong>Round 3</strong> - 3 points each answer</li>
</ul>

<p>The last question of each round is an optional wildcard question. Get it right, and your team gets <strong>4</strong> <strong>points</strong>, <em>but</em> miss it and the team will be <strong>deducted 2 points</strong>.</p>

<div class="not-prose mt-8 -mb-4">
  <h3 class="bg-gradient-to-l from-blue-500 to-fuchsia-500 bg-clip-text text-transparent text-2xl font-bold leading-relaxed">
    Round 1 - App Store Paperwork Speedrun
  </h3>
</div>
<p>Nothing says pre-W.W.D.C. vibes quite like reading support docs, release notes and regulatory compliance copy. With that, let’s emotionally process App Store Connect together.</p>

<fieldset class="not-prose quiz-question">
  <legend class="quiz-kicker">Question 1</legend>
  <p class="quiz-prompt">In 2026, Apple detailed changes to iOS in Japan that created new options for developers, including alternative marketplaces and payment options. Which law are those changes designed to comply with?</p>
  <div class="quiz-options">
    <label class="quiz-option"><input type="radio" name="q1" value="A" /><span><strong>A:</strong> Digital Markets Act</span></label>
    <label class="quiz-option"><input type="radio" name="q1" value="B" /><span><strong>B:</strong> Mobile Software Competition Act</span></label>
    <label class="quiz-option"><input type="radio" name="q1" value="C" /><span><strong>C:</strong> App Store Modernization Act</span></label>
    <label class="quiz-option"><input type="radio" name="q1" value="D" /><span><strong>D:</strong> Platform Fairness Act</span></label>
  </div>
</fieldset>

<fieldset class="not-prose quiz-question">
  <legend class="quiz-kicker">Question 2</legend>
  <p class="quiz-prompt">The App Review Guidelines got a February 2026 clarification that apps with random or anonymous chat are subject to which guideline?</p>
  <div class="quiz-options">
    <label class="quiz-option"><input type="radio" name="q2" value="A" /><span><strong>A:</strong> 2.5 Software Requirements</span></label>
    <label class="quiz-option"><input type="radio" name="q2" value="B" /><span><strong>B:</strong> 1.2 User-Generated Content</span></label>
    <label class="quiz-option"><input type="radio" name="q2" value="C" /><span><strong>C:</strong> 3.1.1 In-App Purchase</span></label>
    <label class="quiz-option"><input type="radio" name="q2" value="D" /><span><strong>D:</strong> 5.1.1 Data Collection and Storage</span></label>
  </div>
</fieldset>

<fieldset class="not-prose quiz-question">
  <legend class="quiz-kicker">Question 3</legend>
  <p class="quiz-prompt">Before February 17, 2026, an App Store bundle had a very hall-monitor rule: every included app had to support the same platform as the primary app. For new bundles, what did Apple stop requiring?</p>
  <div class="quiz-options">
    <label class="quiz-option"><input type="radio" name="q3" value="A" /><span><strong>A:</strong> A bundle can include TestFlight-only beta builds</span></label>
    <label class="quiz-option"><input type="radio" name="q3" value="B" /><span><strong>B:</strong> A bundle can mix apps from multiple developer accounts</span></label>
    <label class="quiz-option"><input type="radio" name="q3" value="C" /><span><strong>C:</strong> A bundle can go live before every included app passes review</span></label>
    <label class="quiz-option"><input type="radio" name="q3" value="D" /><span><strong>D:</strong> Apps in a bundle no longer need to support the same platform as the primary app</span></label>
  </div>
</fieldset>

<fieldset class="not-prose quiz-question">
  <legend class="quiz-kicker">Question 4</legend>
  <p class="quiz-prompt">Apple's new monthly subscription with a 12-month commitment is basically an annual plan masquerading as a monthly sub. But, to me, that's not the oddest bit, it's the availability. Developers can offer it almost everywhere, but not in two storefronts. Which two are left out?</p>
  <div class="quiz-options">
    <label class="quiz-option"><input type="radio" name="q4" value="A" /><span><strong>A:</strong> The United States and Singapore</span></label>
    <label class="quiz-option"><input type="radio" name="q4" value="B" /><span><strong>B:</strong> Japan and South Korea</span></label>
    <label class="quiz-option"><input type="radio" name="q4" value="C" /><span><strong>C:</strong> The European Union and United Kingdom</span></label>
    <label class="quiz-option"><input type="radio" name="q4" value="D" /><span><strong>D:</strong> Canada and Australia</span></label>
  </div>
</fieldset>

<fieldset class="not-prose quiz-question">
  <legend class="quiz-kicker">Wildcard</legend>
  <p class="quiz-prompt">In March 2026, Apple quietly retired a tiny App Store Connect lever that developers used when they wanted to hand out a free in-app thing without creating a whole campaign around it. What went away?</p>
  <div class="quiz-options">
    <label class="quiz-option"><input type="radio" name="q5" value="A" /><span><strong>A:</strong> Introductory offers</span></label>
    <label class="quiz-option"><input type="radio" name="q5" value="B" /><span><strong>B:</strong> Win-back offers</span></label>
    <label class="quiz-option"><input type="radio" name="q5" value="C" /><span><strong>C:</strong> Promo codes for In-App Purchases</span></label>
    <label class="quiz-option"><input type="radio" name="q5" value="D" /><span><strong>D:</strong> Offer codes for subscriptions</span></label>
    <label class="quiz-option quiz-option-skip"><input type="radio" name="q5" value="skip" /><span><strong>Skip:</strong> <span class="quiz-skip-copy">I am a coward.</span></span></label>
  </div>
</fieldset>

<div class="not-prose mt-8 -mb-4">
  <h3 class="bg-gradient-to-l from-blue-500 to-fuchsia-500 bg-clip-text text-transparent text-2xl font-bold leading-relaxed">
    Round 2 - Hardware and Tooling Side Quests
  </h3>
</div>
<p>This year’s spring product and tooling cycle was inspired by…The Matrix? Let’s see who read the footnotes!</p>

<fieldset class="not-prose quiz-question">
  <legend class="quiz-kicker">Question 1</legend>
  <p class="quiz-prompt">iPhone 17e starts at the same $599 price as its predecessor, but Apple doubled the entry storage. What storage capacity does it start with?</p>
  <div class="quiz-options">
    <label class="quiz-option"><input type="radio" name="q6" value="A" /><span><strong>A:</strong> 64GB</span></label>
    <label class="quiz-option"><input type="radio" name="q6" value="B" /><span><strong>B:</strong> 128GB</span></label>
    <label class="quiz-option"><input type="radio" name="q6" value="C" /><span><strong>C:</strong> 256GB</span></label>
    <label class="quiz-option"><input type="radio" name="q6" value="D" /><span><strong>D:</strong> 512GB</span></label>
  </div>
</fieldset>

<fieldset class="not-prose quiz-question">
  <legend class="quiz-kicker">Question 2</legend>
  <p class="quiz-prompt">Apple's March 2026 retail update says the new fanless MacBook Neo starts at $599 and, somehow, runs on an iPhone chip. Which chip powers it?</p>
  <div class="quiz-options">
    <label class="quiz-option"><input type="radio" name="q7" value="A" /><span><strong>A:</strong> M4</span></label>
    <label class="quiz-option"><input type="radio" name="q7" value="B" /><span><strong>B:</strong> M5</span></label>
    <label class="quiz-option"><input type="radio" name="q7" value="C" /><span><strong>C:</strong> A18 Pro</span></label>
    <label class="quiz-option"><input type="radio" name="q7" value="D" /><span><strong>D:</strong> A19</span></label>
  </div>
</fieldset>

<fieldset class="not-prose quiz-question">
  <legend class="quiz-kicker">Question 3</legend>
  <p class="quiz-prompt">Hardware nerd alert question. The 2026 iPad Air with M4 brought two Apple-designed connectivity chips to the Air line. Which pair was it?</p>
  <div class="quiz-options">
    <label class="quiz-option"><input type="radio" name="q8" value="A" /><span><strong>A:</strong> H2 and U2</span></label>
    <label class="quiz-option"><input type="radio" name="q8" value="B" /><span><strong>B:</strong> S10 and W4</span></label>
    <label class="quiz-option"><input type="radio" name="q8" value="C" /><span><strong>C:</strong> R1 and C1</span></label>
    <label class="quiz-option"><input type="radio" name="q8" value="D" /><span><strong>D:</strong> N1 and C1X</span></label>
  </div>
</fieldset>

<fieldset class="not-prose quiz-question">
  <legend class="quiz-kicker">Question 4</legend>
  <p class="quiz-prompt">MacBook Neo's color lineup looks like Apple put an iMac G3, some Starburst and a $599 price point in the same room. iPhone 5c vibes! Which of these is <i>not</i> one of the official colors?</p>
  <div class="quiz-options">
    <label class="quiz-option"><input type="radio" name="q9" value="A" /><span><strong>A:</strong> Silver</span></label>
    <label class="quiz-option"><input type="radio" name="q9" value="B" /><span><strong>B:</strong> Blush</span></label>
    <label class="quiz-option"><input type="radio" name="q9" value="C" /><span><strong>C:</strong> Sage</span></label>
    <label class="quiz-option"><input type="radio" name="q9" value="D" /><span><strong>D:</strong> Indigo</span></label>
  </div>
</fieldset>

<fieldset class="not-prose quiz-question">
  <legend class="quiz-kicker">Wildcard</legend>
  <p class="quiz-prompt">Still on the Neo, Apple's marketing went fully TikTok fever dream for it: fruit, FaceTime and a laptop color called Citrus doing a lot of emotional work. And, I kinda dig it? In the clip that made r/apple take notice, which two fruits FaceTimed each other?</p>
  <div class="quiz-options">
    <label class="quiz-option"><input type="radio" name="q10" value="A" /><span><strong>A:</strong> A lime and a lemon</span></label>
    <label class="quiz-option"><input type="radio" name="q10" value="B" /><span><strong>B:</strong> A banana and a blueberry</span></label>
    <label class="quiz-option"><input type="radio" name="q10" value="C" /><span><strong>C:</strong> An orange and a grapefruit</span></label>
    <label class="quiz-option"><input type="radio" name="q10" value="D" /><span><strong>D:</strong> A kiwi and a strawberry</span></label>
    <label class="quiz-option quiz-option-skip"><input type="radio" name="q10" value="skip" /><span><strong>Skip:</strong> <span class="quiz-skip-copy">I am a coward.</span></span></label>
  </div>
</fieldset>

<div class="not-prose mt-8 -mb-4">
  <h3 class="bg-gradient-to-l from-blue-500 to-fuchsia-500 bg-clip-text text-transparent text-2xl font-bold leading-relaxed">
    Round 3 - Coming Bright Up
  </h3>
</div>
<p>The conference page is glowing, Apple Park attendees are afoot, and Tim Cook has a new title on deck.</p>

<fieldset class="not-prose quiz-question">
  <legend class="quiz-kicker">Question 1</legend>
  <p class="quiz-prompt">As we all converge for dub dub, there is a central rally point in San Jose where the Apple developer diaspora seems to materialize before the week kicks off. Where are the hallowed grounds where everyone first meets on Saturday, and sometimes Sunday, before W.W.D.C.?</p>
  <div class="quiz-options">
    <label class="quiz-option"><input type="radio" name="q11" value="A" /><span><strong>A:</strong> San Pedro Square Market</span></label>
    <label class="quiz-option"><input type="radio" name="q11" value="B" /><span><strong>B:</strong> Plaza de Cesar Chavez</span></label>
    <label class="quiz-option"><input type="radio" name="q11" value="C" /><span><strong>C:</strong> San Jose McEnery Convention Center</span></label>
    <label class="quiz-option"><input type="radio" name="q11" value="D" /><span><strong>D:</strong> SoFA Market</span></label>
  </div>
</fieldset>

<fieldset class="not-prose quiz-question">
  <legend class="quiz-kicker">Question 2</legend>
  <p class="quiz-prompt">The last fully in-person San Jose dub dub before everything became a browser tab was W.W.D.C. 2019. At the Thursday night Bash in Discovery Meadow, which band took the stage and turned a field of badge-wearing developers into the world's most type-safe alt-rock crowd?</p>
  <div class="quiz-options">
    <label class="quiz-option"><input type="radio" name="q12" value="A" /><span><strong>A:</strong> Panic! At The Disco</span></label>
    <label class="quiz-option"><input type="radio" name="q12" value="B" /><span><strong>B:</strong> Weezer</span></label>
    <label class="quiz-option"><input type="radio" name="q12" value="C" /><span><strong>C:</strong> Fall Out Boy</span></label>
    <label class="quiz-option"><input type="radio" name="q12" value="D" /><span><strong>D:</strong> The Killers</span></label>
  </div>
</fieldset>

<fieldset class="not-prose quiz-question">
  <legend class="quiz-kicker">Question 3</legend>
  <p class="quiz-prompt">Before John Ternus was demoing titanium and starring in everyone's Apple succession takes, his Penn senior project already had very Apple-coded hardware-meets-human-need feel. What did he build?</p>
  <div class="quiz-options">
    <label class="quiz-option"><input type="radio" name="q13" value="A" /><span><strong>A:</strong> A mechanical feeding arm controlled with head movements</span></label>
    <label class="quiz-option"><input type="radio" name="q13" value="B" /><span><strong>B:</strong> A prototype Cinema Display stand that folded flat enough for a backpack</span></label>
    <label class="quiz-option"><input type="radio" name="q13" value="C" /><span><strong>C:</strong> A haptic swim coach that buzzed when you drifted out of your lane</span></label>
    <label class="quiz-option"><input type="radio" name="q13" value="D" /><span><strong>D:</strong> A VR headset cooling rig made from PowerBook parts</span></label>
  </div>
</fieldset>

<fieldset class="not-prose quiz-question">
  <legend class="quiz-kicker">Question 4</legend>
  <p class="quiz-prompt">This year's pre-W.W.D.C. tea-leaf reading has not been subtle: glowing Swift birds, Siri rings and now Apple DNS archaeology. A few weeks before the keynote, which dormant Apple subdomain kicked off another round of "oh, so it is AI season" chatter?</p>
  <div class="quiz-options">
    <label class="quiz-option"><input type="radio" name="q14" value="A" /><span><strong>A:</strong> brightup.apple.com</span></label>
    <label class="quiz-option"><input type="radio" name="q14" value="B" /><span><strong>B:</strong> siri.apple.com</span></label>
    <label class="quiz-option"><input type="radio" name="q14" value="C" /><span><strong>C:</strong> genai.apple.com</span></label>
    <label class="quiz-option"><input type="radio" name="q14" value="D" /><span><strong>D:</strong> ask.apple.com</span></label>
  </div>
</fieldset>

<fieldset class="not-prose quiz-question">
  <legend class="quiz-kicker">Wildcard</legend>
  <p class="quiz-prompt">The Apple Design Awards are basically Cupertino's annual "yes, we noticed your pixels" ceremony. This year's finalists span six categories, but one of these is not on the 2026 list. Which one is the beautiful impostor?</p>
  <div class="quiz-options">
    <label class="quiz-option"><input type="radio" name="q15" value="A" /><span><strong>A:</strong> Inclusivity</span></label>
    <label class="quiz-option"><input type="radio" name="q15" value="B" /><span><strong>B:</strong> Interaction</span></label>
    <label class="quiz-option"><input type="radio" name="q15" value="C" /><span><strong>C:</strong> Spatial Computing</span></label>
    <label class="quiz-option"><input type="radio" name="q15" value="D" /><span><strong>D:</strong> Visuals and Graphics</span></label>
    <label class="quiz-option quiz-option-skip"><input type="radio" name="q15" value="skip" /><span><strong>Skip:</strong> <span class="quiz-skip-copy">I am a coward.</span></span></label>
  </div>
</fieldset>

<div class="flex gap-x-6 items-center">
<p>👉</p>
<button class="rounded-md bg-indigo-50 px-4 py-2 text-sm font-semibold text-indigo-600 shadow-sm hover:bg-indigo-100" onclick="calculateScore()">Submit Quiz</button>
<p>👈</p>
</div>

<script>
var quizShareText = "";
var quizShareUrl = "https://swiftjectivec.com/WWDC-2026-The-Pregame-Quiz/";

function calculateScore() {
    var correctAnswers = {
        q1: "B",
        q2: "B",
        q3: "D",
        q4: "A",
        q5: "C",  // Wildcard question for Round 1
        q6: "C",
        q7: "C",
        q8: "D",
        q9: "C",
        q10: "A", // Wildcard question for Round 2
        q11: "A",
        q12: "B",
        q13: "A",
        q14: "C",
        q15: "C"  // Wildcard question for Round 3
    };

    var scoreSimple = 0;
    var scoreDetailed = 0;
    var incorrectAnswers = [];
    var checkboxes = document.querySelectorAll('input[type="radio"]');
    function isWildcard(questionName) {
        return questionName === "q5" || questionName === "q10" || questionName === "q15";
    }

    checkboxes.forEach(function(checkbox) {
        var questionName = checkbox.name;
        var isSkipped = checkbox.value === "skip";
        if (checkbox.checked && checkbox.value === correctAnswers[questionName]) {
            scoreSimple++;
            if (isWildcard(questionName)) {
                scoreDetailed += 4; // Wildcard question correct
            } else if (questionName === "q1" || questionName === "q2" || questionName === "q3" || questionName === "q4") {
                scoreDetailed += 1; // Round 1 questions
            } else if (questionName === "q6" || questionName === "q7" || questionName === "q8" || questionName === "q9") {
                scoreDetailed += 2; // Round 2 questions
            } else if (questionName === "q11" || questionName === "q12" || questionName === "q13" || questionName === "q14") {
                scoreDetailed += 3; // Round 3 questions
            }
        } else if (checkbox.checked && isWildcard(questionName) && !isSkipped && checkbox.value !== correctAnswers[questionName]) {
            scoreDetailed -= 2; // Wildcard question incorrect
        } 

        if (checkbox.checked && !isSkipped && checkbox.value !== correctAnswers[questionName]) {
            var round, roundNumber, questionNumber;
            var qNumber = parseInt(questionName.replace("q", ""));
            if (qNumber <= 5) {
                round = "Round One";
                roundNumber = 1;
                questionNumber = qNumber;
            } else if (qNumber <= 10) {
                round = "Round Two";
                roundNumber = 2;
                questionNumber = qNumber - 5;
            } else {
                round = "Round Three";
                roundNumber = 3;
                questionNumber = qNumber - 10;
            }
            incorrectAnswers.push({
                question: round + ": Question " + questionNumber,
                shortQuestion: "R" + roundNumber + " Q" + questionNumber,
                correctAnswer: correctAnswers[questionName]
            });
        }
    });

    function selectedAnswer(questionName) {
        var selected = document.querySelector('input[name="' + questionName + '"]:checked');
        return selected ? selected.value : null;
    }

    function buildRoundResult(roundName, questions) {
        var correctCount = 0;
        var marks = questions.map(function(questionName) {
            var answer = selectedAnswer(questionName);
            var isSkipped = answer === "skip";
            var isCorrect = answer === correctAnswers[questionName];
            if (isCorrect) {
                correctCount++;
            }

            if (isSkipped) {
                return "➖";
            }

            return isCorrect ? "✅" : "🚫";
        });

        return {
            name: roundName,
            marks: marks.join(""),
            correctCount: correctCount,
            shareText: roundName + ": " + marks.join("") + " (" + correctCount + "/5)"
        };
    }

    var roundResults = [
        buildRoundResult("Round 1", ["q1", "q2", "q3", "q4", "q5"]),
        buildRoundResult("Round 2", ["q6", "q7", "q8", "q9", "q10"]),
        buildRoundResult("Round 3", ["q11", "q12", "q13", "q14", "q15"])
    ];

    var shareText = [
        "Swiftjective-C W.W.D.C. 2026 Pregame Quiz",
        "Score: " + scoreDetailed + "/36",
        "",
        roundResults[0].shareText,
        roundResults[1].shareText,
        roundResults[2].shareText,
        "",
        "Can you beat me before #WWDC26?"
    ].join("\n");
    quizShareText = shareText;

    document.getElementById('quizScoreValue').innerText = scoreDetailed;
    document.getElementById('quizCorrectSummary').innerText = scoreSimple + "/15";
    document.getElementById('quizMissedBadge').innerText = incorrectAnswers.length === 0 ? "0" : incorrectAnswers.length;
    document.getElementById('quizRoundSummary').innerHTML = roundResults.map(function(result) {
        return '<div class="grid grid-cols-[4.5rem_minmax(0,1fr)_auto] items-center gap-3 px-3 py-3 sm:grid-cols-[5rem_minmax(0,1fr)_auto]">' +
            '<div class="text-base/6 font-semibold text-zinc-900 dark:text-zinc-100 sm:text-sm/5">' + result.name + '</div>' +
            '<div class="min-w-0 text-lg leading-6 sm:text-base">' + result.marks + '</div>' +
            '<div class="text-base/6 font-semibold tabular-nums text-zinc-600 dark:text-zinc-300 sm:text-sm/5">' + result.correctCount + '/5</div>' +
            '</div>';
    }).join("");

    if (incorrectAnswers.length > 0) {
        document.getElementById('quizMissedDetails').classList.remove('hidden');
        document.getElementById('quizMissedAnswers').innerHTML = incorrectAnswers.map(function(item) {
            return '<li class="flex items-center justify-between gap-2 rounded-md bg-white px-3 py-2 text-base/6 text-zinc-700 ring-1 ring-zinc-200 dark:bg-zinc-950 dark:text-zinc-200 dark:ring-zinc-700 sm:text-sm/5">' +
                '<span class="font-semibold">' + item.shortQuestion + '</span>' +
                '<span class="text-zinc-500 dark:text-zinc-400">Answer ' + item.correctAnswer + '</span>' +
                '</li>';
        }).join("");
    } else {
        document.getElementById('quizMissedDetails').classList.add('hidden');
        document.getElementById('quizMissedAnswers').innerHTML = "";
    }

    document.getElementById('quizSharePreview').innerText = shareText;
    document.getElementById('quizShareStatus').innerText = "";
    document.getElementById('quizResultModal').classList.remove('hidden');
    document.body.classList.add('overflow-hidden');
}

function closeQuizModal() {
    document.getElementById('quizResultModal').classList.add('hidden');
    document.body.classList.remove('overflow-hidden');
}

function shareQuizOnX() {
    var xPostUrl = "https://x.com/intent/post?text=" + encodeURIComponent(quizShareText + "\n" + quizShareUrl);
    window.open(xPostUrl, "_blank", "noopener,noreferrer");
}

function shareQuizWithSheet() {
    var sharePayload = {
        title: "Swiftjective-C W.W.D.C. 2026 Pregame Quiz",
        text: quizShareText,
        url: quizShareUrl
    };
    var fallbackText = quizShareText + "\n" + quizShareUrl;
    var shareStatus = document.getElementById('quizShareStatus');

    if (navigator.share) {
        navigator.share(sharePayload).then(function() {
            shareStatus.innerText = "";
        }).catch(function(error) {
            if (error.name !== "AbortError") {
                copyQuizResult(fallbackText, shareStatus, "Share sheet failed. Result copied instead.");
            }
        });
        return;
    }

    copyQuizResult(fallbackText, shareStatus, "Share sheet is not available here. Result copied instead.");
}

function copyQuizResult(text, shareStatus, statusText) {
    if (navigator.clipboard && navigator.clipboard.writeText) {
        navigator.clipboard.writeText(text).then(function() {
            shareStatus.innerText = statusText;
        });
        return;
    }

    window.prompt("Copy your result:", text);
}

document.addEventListener('keydown', function(event) {
  if (!document.getElementById('quizResultModal').classList.contains('hidden') && event.key === 'Escape') {
    closeQuizModal();
  }
});
</script>

<!-- Quiz result sorta modal, nicer than an alert -->
<div id="quizResultModal" class="fixed inset-0 z-50 flex items-end justify-center bg-white/40 px-3 py-3 backdrop-blur-md dark:bg-zinc-950/50 sm:items-center sm:px-4 hidden">
  <div class="not-prose relative flex max-h-[calc(100dvh-1.5rem)] w-full max-w-lg flex-col overflow-hidden rounded-t-2xl bg-white text-left shadow-2xl dark:bg-zinc-900 sm:rounded-2xl">
    <div class="flex items-start justify-between gap-4 border-b border-zinc-950/10 px-5 py-4 dark:border-white/10">
      <div>
        <p class="text-base/6 font-medium text-indigo-600 dark:text-indigo-300 sm:text-sm/5">W.W.D.C. Pregame Quiz</p>
        <h2 class="text-2xl font-semibold tracking-tight text-balance text-zinc-950 dark:text-zinc-50 sm:text-xl">Quiz Results</h2>
      </div>
      <button onclick="closeQuizModal()" class="relative inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-xl leading-none text-zinc-500 hover:bg-zinc-100 hover:text-zinc-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-100" aria-label="Close">
        <span class="pointer-fine:hidden absolute left-1/2 top-1/2 h-12 w-12 -translate-x-1/2 -translate-y-1/2" aria-hidden="true"></span>
        <span aria-hidden="true">&times;</span>
      </button>
    </div>

    <div class="overflow-y-auto px-5 py-4">
      <div class="grid grid-cols-3 divide-x divide-zinc-950/10 rounded-lg border border-zinc-950/10 dark:divide-white/10 dark:border-white/10">
        <div class="px-3 py-3">
          <p class="text-base/6 text-zinc-500 dark:text-zinc-400 sm:text-sm/5">Score</p>
          <p class="mt-1 whitespace-nowrap text-zinc-950 dark:text-zinc-50">
            <span id="quizScoreValue" class="text-3xl font-semibold tracking-tight tabular-nums">0</span>
            <span class="text-base/6 text-zinc-500 dark:text-zinc-400 sm:text-sm/5">/36</span>
          </p>
        </div>
        <div class="px-3 py-3">
          <p class="text-base/6 text-zinc-500 dark:text-zinc-400 sm:text-sm/5">Correct</p>
          <p id="quizCorrectSummary" class="mt-2 text-lg/6 font-semibold tabular-nums text-zinc-950 dark:text-zinc-50 sm:text-base/6">0/15</p>
        </div>
        <div class="px-3 py-3">
          <p class="text-base/6 text-zinc-500 dark:text-zinc-400 sm:text-sm/5">Missed</p>
          <p id="quizMissedBadge" class="mt-2 text-lg/6 font-semibold tabular-nums text-zinc-950 dark:text-zinc-50 sm:text-base/6">0</p>
        </div>
      </div>

      <div class="mt-4">
        <h3 class="text-base/6 font-semibold text-zinc-950 dark:text-zinc-50 sm:text-sm/5">Round breakdown</h3>
        <div id="quizRoundSummary" class="mt-2 divide-y divide-zinc-950/10 rounded-lg border border-zinc-950/10 dark:divide-white/10 dark:border-white/10"></div>
      </div>

      <details id="quizMissedDetails" class="mt-4 border-t border-zinc-950/10 pt-3 dark:border-white/10">
        <summary class="cursor-pointer list-none text-base/6 font-semibold text-zinc-950 dark:text-zinc-50 sm:text-sm/5">Review missed answers</summary>
        <ul id="quizMissedAnswers" class="mt-3 grid max-h-36 gap-2 overflow-y-auto sm:grid-cols-2"></ul>
      </details>

      <div class="mt-4 border-t border-zinc-950/10 pt-3 dark:border-white/10">
        <h3 class="text-base/6 font-semibold text-zinc-950 dark:text-zinc-50 sm:text-sm/5">Share preview</h3>
        <div id="quizSharePreview" class="mt-2 max-h-32 overflow-y-auto rounded-lg bg-zinc-50 p-3 font-mono text-base/7 text-zinc-800 ring-1 ring-zinc-950/10 dark:bg-zinc-800/60 dark:text-zinc-100 dark:ring-white/10 sm:text-sm/6"></div>
      </div>
    </div>

    <div class="grid gap-2 border-t border-zinc-950/10 bg-white px-5 py-4 dark:border-white/10 dark:bg-zinc-900 sm:grid-cols-2 sm:gap-3">
      <button id="quizShareXButton" type="button" onclick="shareQuizOnX()" class="inline-flex min-h-11 items-center justify-center rounded-lg bg-black px-4 py-2.5 text-base/6 font-semibold text-white hover:bg-zinc-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-950 dark:bg-white dark:text-black dark:hover:bg-zinc-200 sm:min-h-10 sm:text-sm/5">Share on X</button>
      <button id="quizShareSheetButton" type="button" onclick="shareQuizWithSheet()" class="inline-flex min-h-11 items-center justify-center rounded-lg bg-indigo-50 px-4 py-2.5 text-base/6 font-semibold text-indigo-700 hover:bg-indigo-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 dark:bg-indigo-400/10 dark:text-indigo-200 dark:hover:bg-indigo-400/20 sm:min-h-10 sm:text-sm/5">Share.Sheet()</button>
      <p id="quizShareStatus" class="min-h-6 text-center text-base/6 text-zinc-500 dark:text-zinc-400 sm:col-span-2 sm:min-h-0 sm:text-sm/5" aria-live="polite"></p>
    </div>

  </div>
</div>

<h3 id="answer-key">Answer Key</h3>

<p><b>Round 1:</b></p>

<ol>
  <li>B. The Mobile Software Competition Act. Apple says iOS 26.2 introduced the Japan changes to comply with the MSCA. <a href="https://developer.apple.com/support/app-distribution-in-japan/">1</a></li>
  <li>B. Guideline 1.2, User-Generated Content. Random or anonymous chat now gets pulled under that umbrella. <a href="https://developer.apple.com/news/?id=d75yllv4">2</a></li>
  <li>D. Apps in a bundle no longer need to support the same platform as the primary app for bundles created on February 17, 2026 or later. <a href="https://developer.apple.com/help/app-store-connect/release-notes/">3</a></li>
  <li>A. The United States and Singapore. Outside those two storefronts, monthly subscriptions can now have a 12-month commitment. <a href="https://developer.apple.com/help/app-store-connect/release-notes/">3</a></li>
  <li>Wildcard: C. Promo codes for In-App Purchases are no longer supported. Pour one out for the old tiny-code economy. <a href="https://developer.apple.com/help/app-store-connect/release-notes/">3</a></li>
</ol>

<p><b>Round 2:</b></p>

<ol>
  <li>C. 256GB. The 17e starts with double the previous generation’s entry storage. <a href="https://www.apple.com/newsroom/2026/03/apple-introduces-iphone-17e/">4</a></li>
  <li>C. A18 Pro. Yes, the footnote really does say preproduction MacBook Neo systems with Apple A18 Pro. <a href="https://www.apple.com/newsroom/2026/03/macbook-neo-iphone-17e-ipad-air-with-m4-and-more-are-now-available/">5</a></li>
  <li>D. N1 and C1X. Wi-Fi 7, Bluetooth 6, Thread and cellular modem trivia all in one tidy pair. <a href="https://www.apple.com/newsroom/2026/03/apple-introduces-the-new-ipad-air-powered-by-m4/">6</a></li>
  <li>C. Sage. Apple lists MacBook Neo in silver, blush, citrus and indigo, which means Sage is the very tasteful impostor. <a href="https://www.apple.com/newsroom/2026/03/macbook-neo-iphone-17e-ipad-air-with-m4-and-more-are-now-available/">5</a></li>
  <li>Wildcard: A. A lime and a lemon. Sometimes product marketing is just a tiny citrus FaceTime call on main. <a href="https://www.tiktok.com/@apple/video/7613820709629971725">7</a>, <a href="https://www.reddit.com/r/apple/comments/1rmv3zr/apple_is_on_tiktok_posting_unhinged_videos_about/">8</a></li>
</ol>

<p><b>Round 3:</b></p>

<ol>
  <li>A. San Pedro Square Market. The 2026 pre-W.W.D.C. community gathering listing puts the ritual right at 87 N San Pedro St, patio and all. <a href="https://luma.com/trtbdihd">13</a></li>
  <li>B. Weezer. W.W.D.C. 2019’s Bash at Discovery Meadow featured the multi-platinum alt-rock heroes themselves. <a href="https://9to5mac.com/2019/06/06/wwdc-bash-2019/">9</a></li>
  <li>A. A mechanical feeding arm controlled with head movements. Ternus’s Penn senior project was built for people with quadriplegia, which is vastly more interesting than another succession answer. <a href="https://en.wikipedia.org/wiki/John_Ternus">10</a></li>
  <li>C. genai.apple.com. A dormant Apple subdomain with “genai” in the name is exactly the kind of nothing that becomes pre-W.W.D.C. something. <a href="https://www.macrumors.com/2026/05/23/apple-gen-ai-subdomain/">11</a></li>
  <li>Wildcard: C. Spatial Computing. It sounds like it should be a category, but the 2026 list is Delight and Fun, Inclusivity, Innovation, Interaction, Social Impact, and Visuals and Graphics. <a href="https://developer.apple.com/design/awards/">12</a></li>
</ol>]]></content><author><name>Jordan Morgan</name></author><category term="Trivia" /><summary type="html"><![CDATA[The twelfth annual W.W.D.C. pregame quiz is here, built entirely from 2026 Apple developer chaos.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://swiftjectivec.com/assets/images/pregamequiz_2026.png" /><media:content medium="image" url="https://swiftjectivec.com/assets/images/pregamequiz_2026.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">WWDC in a 2026 Landscape</title><link href="https://swiftjectivec.com/WWDC-Wish-List-2026/" rel="alternate" type="text/html" title="WWDC in a 2026 Landscape" /><published>2026-05-16T00:00:00-05:00</published><updated>2026-05-16T00:00:00-05:00</updated><id>https://swiftjectivec.com/WWDC-Wish-List-2026</id><content type="html" xml:base="https://swiftjectivec.com/WWDC-Wish-List-2026/"><![CDATA[<p>This W.W.D.C. <em>will</em> probably be unlike any other. Things are going a bit wild lately, and even ardent A.I. detractors would likely concede that’s undeniably true now. The way we work compared to a year ago is drastically different. And, even six months ago? Might as well be a lifetime.</p>

<p>Cupertino &amp; Friends™️ would do well to adapt to a rapidly changing landscape. I’m hopeful they will, too, if recent trends are any indication. We received <a href="https://www.swiftjectivec.com/Agentic-Coding-Codex-Claude-Code-in-Xcode/">agentic support in Xcode</a> not long ago, something typically reserved #ForTheKeynote.</p>

<p>In the world of 2026, here’s what I’d love to see:</p>

<ul>
  <li><strong>The usual fare:</strong> Video sessions, in-depth deep dives, the works. Let’s not forget that Apple has done a fantastic job at packaging these now. Closed captions, links to demo projects, and other relevant materials are all tidy and available in the developer app. But in today’s world, stopping here would be a missed opportunity.</li>
  <li><strong>Skills:</strong> Consider this — each dub dub session which presents a new API comes packaged with a skill. That would be incredible. In fact, Apple has kinda sorta already <a href="https://mastodon.social/@steipete/114982119853883518">done this</a>, but it was tucked away deep in the file system. Don’t make me poke around, link a skill with each dub dub session.</li>
  <li><strong>MCP:</strong> Depending on who you ask, MCPs are amazing or terrible token wasters on their way out. Regardless, I’d love a direct, official line to Apple’s docs. For now, ex-Apple employee and NSHipter stalwart <a href="https://x.com/mattt">Mattt</a> has the best solution with <a href="https://sosumi.ai">sosumi</a>.</li>
  <li><strong>Xcode:</strong> And, Xcode should lean into agentic engineering, whatever that looks like. I still use an IDE quite frequently, and that’s certainly true of iOS development. There is not one piece of software I could ship that hasn’t had my fingerprints on it. Little tweaks, the UI and UX, all layers I am unwilling to concede to AI.</li>
</ul>

<p>In short, I hope Apple helps me do two things: adopt the latest APIs, and do that quickly. All of the above would help.</p>

<p>Code and implementation were always the time sink. They don’t have to be anymore. If using agents puts you in a state of torpor, that’s a shame. Instead, use them to get into a loop of build, review, and refine as quickly as you can so that you can make your apps the absolute best they can be. Let me stand up a working draft, and then let me make it great.</p>

<p>Until next time ✌️</p>]]></content><author><name>Jordan Morgan</name></author><category term="The Indie Dev Diaries" /><summary type="html"><![CDATA[So, dub dub beckons. Here's what I'm hoping to see.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://swiftjectivec.com/assets/images/logo.png" /><media:content medium="image" url="https://swiftjectivec.com/assets/images/logo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>