<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>Aleahim</title>
<link>https://aleahim.com/</link>
<description>Technical articles on Swift, OpenAPI, and iOS development by Mihaela Mihaljevic.</description>
<language>en-US</language>
<atom:link href="https://aleahim.com/rss.xml" rel="self" type="application/rss+xml"/>
<item>
<title>SwiftUI 3D is Flat</title>
<link>https://aleahim.com/blog/swiftui-3d-is-flat/</link>
<guid isPermaLink="true">https://aleahim.com/blog/swiftui-3d-is-flat/</guid>
<pubDate>Sat, 04 Jul 2026 00:00:00 +0000</pubDate>
<description>SwiftUI&apos;s iOS and macOS 3D is per-view projected 2D output composited by painter&apos;s order, with no shared-space depth-sorted hierarchy, and the APIs that would fix it are visionOS-only, proven with a rendered counterexample and four executable witnesses</description>
<content:encoded><![CDATA[<h1>SwiftUI 3D is Flat</h1>
<p>SwiftUI looks like it does 3D.</p>
<p><code>rotation3DEffect</code> takes an axis, an anchor, an <code>anchorZ</code>, and a <code>perspective</code> parameter. <code>projectionEffect</code> takes a full <code>ProjectionTransform</code>, and <code>ProjectionTransform</code> can even be initialized directly from a <code>CATransform3D</code>. A custom <code>GeometryEffect</code> hands you the view’s size and lets you return any projection matrix you can compute. Pose one view with these and it looks convincingly, beautifully three-dimensional.</p>
<p>So build a cube. Six faces, one shared space, rotate the camera. That is the simplest possible 3D scene, and pure SwiftUI on iOS and macOS cannot build it. I tested this thesis rather than assuming it: I wrote the counterexample, rendered it headlessly, and inspected the images, and the rendering corrected two of my own claims along the way.</p>
<p>Let me say the finding precisely, because the imprecise version is wrong. Do not say “SwiftUI has no 3D.” Say:</p>
<blockquote><p>SwiftUI’s iOS/macOS 3D is per-view projected 2D output composited by painter’s order. It has no shared-space, depth-sorted layer hierarchy.</p></blockquote>
<p>SwiftUI genuinely has per-view 3D projection: one view, one card, one face rotates in space with real perspective. What it lacks is a shared 3D coordinate space across sibling views and a per-frame depth ordering inside it. And the missing piece has a name: it is exactly what Core Animation’s <code>CATransformLayer</code> exists to provide.</p>
<h2>The smallest counterexample, pure SwiftUI, no bridge</h2>
<p>Here is a self-contained SwiftUI cube. No UIKit, no AppKit, no Core Animation import. It uses only <code>rotation3DEffect(perspective:)</code>, the real iOS/macOS 3D view effect. The faces are declared back-to-front for the rest pose, front face last so it paints on top when the cube faces you. That hand-sorted order is the whole point: SwiftUI never recomputes it as the cube rotates, so it is correct only at rest.</p>
<pre><code class="language-swift"><span class="tok-keyword">import</span> <span class="tok-type">SwiftUI</span>

<span class="tok-keyword">struct</span> <span class="tok-type">FlatSwiftUICube</span><span class="tok-operator">:</span> <span class="tok-type">View</span> {
    <span class="tok-attribute">@State</span> <span class="tok-keyword">private</span> <span class="tok-keyword">var</span> yaw <span class="tok-operator">=</span> <span class="tok-number">0.0</span>
    <span class="tok-attribute">@State</span> <span class="tok-keyword">private</span> <span class="tok-keyword">var</span> pitch <span class="tok-operator">=</span> <span class="tok-number">20.0</span>
    <span class="tok-keyword">private</span> <span class="tok-keyword">let</span> side<span class="tok-operator">:</span> <span class="tok-type">CGFloat</span> <span class="tok-operator">=</span> <span class="tok-number">120</span>

    <span class="tok-keyword">var</span> body<span class="tok-operator">:</span> some <span class="tok-type">View</span> {
        <span class="tok-type">VStack</span>(spacing<span class="tok-operator">:</span> <span class="tok-number">24</span>) {
            <span class="tok-type">ZStack</span> {
                <span class="tok-comment">// Back-to-front for the rest pose: front (2) declared LAST so it wins at yaw 0.</span>
                <span class="tok-function">face</span>(<span class="tok-operator">.</span>yellow, <span class="tok-string">"4"</span>, angle<span class="tok-operator">:</span> <span class="tok-operator">.</span><span class="tok-function">degrees</span>(<span class="tok-number">180</span>), ax<span class="tok-keyword">is</span><span class="tok-operator">:</span> (<span class="tok-number">0</span>, <span class="tok-number">1</span>, <span class="tok-number">0</span>)) <span class="tok-comment">// back</span>
                <span class="tok-function">face</span>(<span class="tok-operator">.</span>orange, <span class="tok-string">"5"</span>, angle<span class="tok-operator">:</span> <span class="tok-operator">.</span><span class="tok-function">degrees</span>(<span class="tok-number">90</span>),  ax<span class="tok-keyword">is</span><span class="tok-operator">:</span> (<span class="tok-number">1</span>, <span class="tok-number">0</span>, <span class="tok-number">0</span>)) <span class="tok-comment">// top</span>
                <span class="tok-function">face</span>(<span class="tok-operator">.</span>purple, <span class="tok-string">"6"</span>, angle<span class="tok-operator">:</span> <span class="tok-operator">.</span><span class="tok-function">degrees</span>(<span class="tok-number">-90</span>), ax<span class="tok-keyword">is</span><span class="tok-operator">:</span> (<span class="tok-number">1</span>, <span class="tok-number">0</span>, <span class="tok-number">0</span>)) <span class="tok-comment">// bottom</span>
                <span class="tok-function">face</span>(<span class="tok-operator">.</span>red,    <span class="tok-string">"1"</span>, angle<span class="tok-operator">:</span> <span class="tok-operator">.</span><span class="tok-function">degrees</span>(<span class="tok-number">-90</span>), ax<span class="tok-keyword">is</span><span class="tok-operator">:</span> (<span class="tok-number">0</span>, <span class="tok-number">1</span>, <span class="tok-number">0</span>)) <span class="tok-comment">// left</span>
                <span class="tok-function">face</span>(<span class="tok-operator">.</span>green,  <span class="tok-string">"3"</span>, angle<span class="tok-operator">:</span> <span class="tok-operator">.</span><span class="tok-function">degrees</span>(<span class="tok-number">90</span>),  ax<span class="tok-keyword">is</span><span class="tok-operator">:</span> (<span class="tok-number">0</span>, <span class="tok-number">1</span>, <span class="tok-number">0</span>)) <span class="tok-comment">// right</span>
                <span class="tok-function">face</span>(<span class="tok-operator">.</span>blue,   <span class="tok-string">"2"</span>, angle<span class="tok-operator">:</span> <span class="tok-operator">.</span><span class="tok-function">degrees</span>(<span class="tok-number">0</span>),   ax<span class="tok-keyword">is</span><span class="tok-operator">:</span> (<span class="tok-number">0</span>, <span class="tok-number">1</span>, <span class="tok-number">0</span>)) <span class="tok-comment">// front</span>
            }
            <span class="tok-comment">// Rotate the WHOLE assembly. There is no shared 3D space, so this is just one</span>
            <span class="tok-comment">// more projection applied on top of the already-projected faces.</span>
            <span class="tok-operator">.</span><span class="tok-function">rotation3DEffect</span>(<span class="tok-operator">.</span><span class="tok-function">degrees</span>(pitch), ax<span class="tok-keyword">is</span><span class="tok-operator">:</span> (<span class="tok-number">1</span>, <span class="tok-number">0</span>, <span class="tok-number">0</span>), perspective<span class="tok-operator">:</span> <span class="tok-number">0.5</span>)
            <span class="tok-operator">.</span><span class="tok-function">rotation3DEffect</span>(<span class="tok-operator">.</span><span class="tok-function">degrees</span>(yaw),   ax<span class="tok-keyword">is</span><span class="tok-operator">:</span> (<span class="tok-number">0</span>, <span class="tok-number">1</span>, <span class="tok-number">0</span>), perspective<span class="tok-operator">:</span> <span class="tok-number">0.5</span>)
            <span class="tok-operator">.</span><span class="tok-function">frame</span>(width<span class="tok-operator">:</span> <span class="tok-number">260</span>, height<span class="tok-operator">:</span> <span class="tok-number">260</span>)

            <span class="tok-type">Slider</span>(value<span class="tok-operator">:</span> $yaw,   <span class="tok-keyword">in</span><span class="tok-operator">:</span> <span class="tok-number">0...360</span>)
            <span class="tok-type">Slider</span>(value<span class="tok-operator">:</span> $pitch, <span class="tok-keyword">in</span><span class="tok-operator">:</span> <span class="tok-number">-90...90</span>)
        }
        <span class="tok-operator">.</span><span class="tok-function">padding</span>()
    }

    <span class="tok-keyword">private</span> <span class="tok-keyword">func</span> <span class="tok-function">face</span>(_ color<span class="tok-operator">:</span> <span class="tok-type">Color</span>, _ label<span class="tok-operator">:</span> <span class="tok-type">String</span>,
                      angle<span class="tok-operator">:</span> <span class="tok-type">Angle</span>, ax<span class="tok-keyword">is</span><span class="tok-operator">:</span> (x<span class="tok-operator">:</span> <span class="tok-type">CGFloat</span>, y<span class="tok-operator">:</span> <span class="tok-type">CGFloat</span>, z<span class="tok-operator">:</span> <span class="tok-type">CGFloat</span>)) <span class="tok-operator">-</span><span class="tok-operator">&gt;</span> some <span class="tok-type">View</span> {
        <span class="tok-type">ZStack</span> {
            <span class="tok-type">RoundedRectangle</span>(corner<span class="tok-type">Radius</span><span class="tok-operator">:</span> <span class="tok-number">8</span>)<span class="tok-operator">.</span><span class="tok-function">fill</span>(color<span class="tok-operator">.</span><span class="tok-function">opacity</span>(<span class="tok-number">0.55</span>))
            <span class="tok-type">RoundedRectangle</span>(corner<span class="tok-type">Radius</span><span class="tok-operator">:</span> <span class="tok-number">8</span>)<span class="tok-operator">.</span><span class="tok-function">stroke</span>(color, line<span class="tok-type">Width</span><span class="tok-operator">:</span> <span class="tok-number">2</span>)
            <span class="tok-type">Text</span>(label)<span class="tok-operator">.</span><span class="tok-function">font</span>(<span class="tok-operator">.</span>title)<span class="tok-operator">.</span><span class="tok-function">bold</span>()<span class="tok-operator">.</span><span class="tok-function">foregroundStyle</span>(<span class="tok-operator">.</span>white)
        }
        <span class="tok-operator">.</span><span class="tok-function">frame</span>(width<span class="tok-operator">:</span> side, height<span class="tok-operator">:</span> side)
        <span class="tok-comment">// anchorZ pivots the rotation half a side behind the face. It is the ONLY way to</span>
        <span class="tok-comment">// fake shared-space placement on iOS/macOS, because there is no z-translation in a</span>
        <span class="tok-comment">// shared 3D coordinate space (transform3DEffect is visionOS-only).</span>
        <span class="tok-operator">.</span><span class="tok-function">rotation3DEffect</span>(angle, ax<span class="tok-keyword">is</span><span class="tok-operator">:</span> ax<span class="tok-keyword">is</span>, anchor<span class="tok-type">Z</span><span class="tok-operator">:</span> <span class="tok-operator">-</span>side <span class="tok-operator">/</span> <span class="tok-number">2</span>, perspective<span class="tok-operator">:</span> <span class="tok-number">0.5</span>)
    }
}</code></pre>
<p>I rendered this with <code>ImageRenderer</code> at <code>pitch = 20</code> across a yaw sweep and inspected it image by image. The observations, rendered, not asserted:</p>
<ul><li><p><strong>At <code>yaw = 0</code>, the rest pose:</strong> the blue front face reads on top and the assembly reads as “a cube facing you.” But look closer: it is a <em>splayed frustum, not a closed cube</em>. The sides fan outward, because the <code>anchorZ</code> pivot cannot push a face out along its normal in a shared space. The correct static shape is already out of reach. That is itself evidence for the thesis.</p></li><li><p><strong>At <code>yaw = 120</code>:</strong> the blue front face has turned to face away from the viewer, yet it still dominates the center, painted on top, its label mirrored. A real cube at this angle shows mostly side and back faces with the front occluded.</p></li><li><p><strong>At <code>yaw = 180</code>:</strong> the front face points fully away, so a depth-sorted cube shows the yellow back face. Instead the yellow face is completely hidden and the blue front face still fills the center, label reversed. This is the unambiguous failure: no depth sort, the declared-last face always wins.</p></li></ul>
<p>An earlier draft of this analysis asserted the behavior from reasoning, and the render corrected it twice: my original face order buried the front face so the cube was wrong even at rest, and the “cube” at rest is really a frustum. The thesis survived both corrections and came out stronger, which is exactly why a thesis has to be tested.</p>
<p>The failure also fits in one automatable line: at <code>yaw = 180</code> the center pixel of the SwiftUI render is blue, the wrong winner. Render the Core Animation cube at the matching pose and the center is the true back face. Same pose, opposite winner: painter’s order versus depth sort.</p>
<p><img src="https://aleahim.com/images/blog/swiftui-3d-is-flat/flat-vs-cube.jpg" alt="Pure SwiftUI on the left: a splayed frustum with BACK painting over FRONT. Core Animation on the right: a closed cube, correct at every angle."></p>
<p>Here is the same failure live, in the pure SwiftUI macOS witness from the <a href="https://codeberg.org/Mihaela/applecube">AppleCube repository</a>:</p>
<figure class="td-embed" style="--td-embed-aspect-ratio: 315 / 560;">
<div class="td-embed-frame">
<iframe
  class="td-embed-iframe"
  src="https://www.youtube-nocookie.com/embed/uGORYd5CQwU"
  title="SwiftUI 3D Is Flat, macOS SwiftUI witness"
  loading="lazy"
  allowfullscreen
  referrerpolicy="strict-origin-when-cross-origin"
  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
></iframe>
</div>
</figure>
<p><img src="https://aleahim.com/images/blog/swiftui-3d-is-flat/iphone-swiftui-flat.jpg" alt="The failure pose on iPhone: BACK paints over FRONT"></p>
<h2>What the APIs actually are, in Apple’s words</h2>
<p>The decisive evidence is not the demo. It is the documentation, read carefully. Two phrases decide the thesis, and both appear verbatim in Apple’s docs.</p>
<p>Core Animation’s <code>CATransformLayer</code> is documented as the class for “true 3D layer hierarchies, rather than the flattened hierarchy rendering model used by other layer types”, and, unlike normal layers, transform layers “do not flatten their sublayers into the plane at Z=0”.</p>
<p>SwiftUI’s <code>rotation3DEffect</code> on iOS and macOS “renders a view’s content as if it’s rotated” in three dimensions. <code>projectionEffect</code> “applies a projection transformation to this view’s rendered output”.</p>
<p>As-if-rotated rendered output is a 2D projection. It is not membership in a shared 3D scene. The full API picture:</p>
<table><thead><tr><th>API</th><th>What it does</th><th>Availability</th><th>Verdict for a shared-space cube</th></tr></thead><tbody><tr><td><code>CATransformLayer</code></td><td>True 3D layer hierarchies, sublayers not flattened into Z = 0</td><td>iOS 3.0+, macOS 10.6+</td><td>The primitive. Shared 3D space, depth preserved.</td></tr><tr><td><code>rotation3DEffect(_:axis:anchor:anchorZ:perspective:)</code></td><td>Renders content as if rotated in 3D</td><td>iOS 13+, macOS 10.15+</td><td>Per-view projection of rendered output. No shared space.</td></tr><tr><td><code>projectionEffect(_:)</code></td><td>Applies a projection to rendered output</td><td>iOS 13+, macOS 10.15+</td><td>Per-view 2D projection. No shared space.</td></tr><tr><td><code>transform3DEffect(_:)</code> with <code>AffineTransform3D</code></td><td>A real 3D transform, including z-translation</td><td>visionOS only</td><td>Not available on iOS/macOS.</td></tr><tr><td><code>perspectiveRotationEffect(_:axis:)</code></td><td>3D rotation with perspective</td><td>visionOS only</td><td>Not available on iOS/macOS.</td></tr><tr><td><code>rotation3DEffect(_:anchor:)</code> with <code>Rotation3D</code></td><td>Rotation by a true 3D rotation value</td><td>visionOS only</td><td>Not available on iOS/macOS.</td></tr></tbody></table>
<p>Read the bottom three rows again. The richer 3D view model exists. Apple built it, named it, shipped it, on visionOS, where the platform has a real depth model. On iOS and macOS those APIs are simply not offered. Nobody forgot 3D. The boundary is deliberate, which is what makes it worth studying.</p>
<h2>The SwiftUI filter</h2>
<p>So ask the question the way a SwiftUI architect would. Not “which Core Animation properties are missing?”, but:</p>
<blockquote><p>Given the Core Animation layer model as input, what subset did SwiftUI admit into its value-based view model, under what names, and at what scope?</p></blockquote>
<p>The answer is a filter, not a one-to-one wrapper. The input side is a persistent object graph: layers with <code>bounds</code>, <code>position</code>, <code>zPosition</code>, <code>anchorPoint</code>, <code>anchorPointZ</code>, a mutable <code>transform</code>, a parent <code>sublayerTransform</code>, a <code>sublayers</code> array, and compositor rules like depth sorting and <code>doubleSided</code>. The output side is SwiftUI’s view value: layout proposes a rectangle, modifiers derive another rendered result from it, the parent composites children in view order plus <code>zIndex</code>.</p>
<p>The filter rule:</p>
<blockquote><p><strong>SwiftUI admits only the pieces that can be expressed as value transforms of one view’s rendered rectangle. It does not admit API that turns a view subtree into a mutable layer object graph.</strong></p></blockquote>
<p>Run every relevant Core Animation input through that rule and you get exactly the API that shipped:</p>
<table><thead><tr><th>Core Animation input</th><th>SwiftUI output</th><th>Preserved</th><th>Collapsed or rejected</th><th>Consequence for the cube</th></tr></thead><tbody><tr><td><code>CALayer.anchorPoint</code></td><td><code>anchor: UnitPoint</code></td><td>The normalized 2D pivot.</td><td>The persistent layer field is gone. The anchor is an argument to one transform or layout operation.</td><td>You can pivot one view effect. You cannot rebase an enduring subtree.</td></tr><tr><td><code>CALayer.anchorPointZ</code></td><td><code>anchorZ</code> on <code>rotation3DEffect</code></td><td>A Z pivot for one 3D rotation.</td><td>Scoped to the modifier, not stored as layer geometry.</td><td><code>anchorZ: -side / 2</code> can hinge one face. It cannot create shared depth.</td></tr><tr><td><code>CALayer.transform</code></td><td><code>rotation3DEffect</code>, <code>projectionEffect</code>, <code>GeometryEffect</code> returning <code>ProjectionTransform</code></td><td>Per-view projection, initializable from <code>CATransform3D</code>.</td><td>Retained <code>CATransform3D</code> layer state is collapsed into a projection of rendered output.</td><td>One card flip can be correct. Six projected cards are still six siblings.</td></tr><tr><td><code>CALayer.sublayerTransform</code></td><td>Nothing on iOS/macOS</td><td>Nothing at parent-view level.</td><td>Rejected: no parent camera matrix over child views.</td><td>No single perspective shared by all faces, and no shared vanishing point.</td></tr><tr><td><code>CATransformLayer</code></td><td>Nothing on iOS/macOS</td><td>Nothing.</td><td>Rejected: no non-flattening child container.</td><td>This is the missing primitive. Without it, a cube is only a pose.</td></tr><tr><td><code>zPosition</code></td><td><code>zIndex</code></td><td>A draw-order priority.</td><td>Geometric depth collapsed into painter’s order.</td><td>The order can be chosen. It is never recomputed from camera depth.</td></tr><tr><td><code>CALayer.doubleSided</code></td><td>Nothing direct</td><td>Nothing direct.</td><td>Back-face culling is not exposed as a view-compositor rule.</td><td>A two-sided card swaps content manually, per-face simulation, not compositor culling.</td></tr></tbody></table>
<p>So yes, SwiftUI has anchors. The exact sentence matters: <strong>SwiftUI has transform anchors; it does not expose Core Animation’s layer anchor fields.</strong> <code>anchor</code> and <code>anchorZ</code> are not <code>CALayer.anchorPoint</code> and <code>anchorPointZ</code> under different names. They are filtered descendants of those ideas. SwiftUI kept the notion of a normalized pivot because it is useful inside a pure value transform, and rejected the durable layer-geometry object that would let a subtree behave like a mutable 3D model.</p>
<p>Why draw the line there? Because the filter is the price of SwiftUI’s core design. Views are values: <code>body</code> produces them, a long-lived dependency graph diffs them, and animatable attributes interpolate copies of their data, off the main thread for the built-in effects. Exposing <code>sublayerTransform</code>, <code>CATransformLayer</code>, or mutable layer geometry would let user code mutate an object graph underneath SwiftUI’s layout, diffing, identity, animation, and cross-platform rendering model. SwiftUI instead keeps the backing renderer private and offers value-level transformations. If I were designing SwiftUI, this is a coherent boundary and I would draw it too. It is also the scientific reason the cube proof fails in pure SwiftUI. Both facts are true at once.</p>
<h2>The positive control: the cube that works</h2>
<p>Claims need controls. The <a href="https://codeberg.org/Mihaela/applecube">AppleCube repository</a> keeps four executable targets, a positive and a negative witness on each platform:</p>
<table><thead><tr><th>Platform</th><th>Positive construction</th><th>Negative witness</th></tr></thead><tbody><tr><td>iPhone</td><td><code>Cube3DMobile</code>: UIKit host, Core Animation layer scene</td><td><code>SwiftUIFlatMobile</code>: pure SwiftUI flattening failure</td></tr><tr><td>macOS</td><td><code>Cube3DCAMac</code>: AppKit host, the same layer scene</td><td><code>SwiftUIFlatMac</code>: pure SwiftUI flattening failure</td></tr></tbody></table>
<p><img src="https://aleahim.com/images/blog/swiftui-3d-is-flat/all-four-witnesses.jpg" alt="All four witnesses, side by side"></p>
<p>The negative controls are deliberately pure: no <code>UIViewRepresentable</code>, no <code>NSViewRepresentable</code>, no hosted UIKit or AppKit, no Core Animation, no SceneKit, no RealityKit. Even the night-sky background respects the firewall: the layer demos drift their stars with <code>CAEmitterLayer</code>, while the SwiftUI demos draw theirs with <code>Canvas</code> and <code>TimelineView</code>, because borrowing <code>CAEmitterLayer</code> would quietly turn the negative control back into a Core Animation demo.</p>
<p>The positive construction is everything the filter rejected, used directly, and each step is precisely the thing SwiftUI cannot express:</p>
<ol><li><p><strong>One parent <code>CATransformLayer</code></strong> holds the six faces as siblings in a single 3D coordinate space. Because it is a transform layer, the faces are not flattened to Z = 0.</p></li><li><p><strong>Perspective lives on the parent, once</strong>, shared by every face. One camera for the whole object, which also means one shared vanishing point:</p></li></ol>
<pre><code class="language-swift">perspective<span class="tok-operator">.</span>m<span class="tok-number">34</span> <span class="tok-operator">=</span> <span class="tok-number">-1.0</span> <span class="tok-operator">/</span> <span class="tok-number">500.0</span>
transformed<span class="tok-type">Layer</span><span class="tok-operator">.</span>sublayer<span class="tok-type">Transform</span> <span class="tok-operator">=</span> perspective</code></pre>
<ol start="3"><li><p><strong>Each face is placed in true 3D by its own layer transform.</strong> The fold is a rotation per face, and the front face is a real <code>CATransform3DMakeTranslation(0, 0, zWidth)</code>, a genuine +Z translation in the shared space. That z placement is exactly what <code>anchorZ</code> cannot fake, which is why the SwiftUI version is a frustum.</p></li><li><p><strong>Rotating the whole object mutates the parent’s <code>sublayerTransform</code>.</strong> One rotation moves the entire shared space, all six faces together, and the renderer re-resolves occlusion from the shared geometry every frame. Back faces can even be culled with <code>isDoubleSided = false</code>, a compositor rule, not a content swap.</p></li></ol>
<p>I walked through this construction step by step in <a href="https://aleahim.com/blog/core-animation-3d-cube/">Core Animation Layers forming a 3D cube</a>; it comes straight from Apple’s 2011 session Core Animation Essentials, and it still works, character for character, fifteen years later.</p>
<p>Here are both positive witnesses recorded together, the AppKit and UIKit apps hosting the same layer scene at the same time, folding and rotating with correct occlusion at every angle:</p>
<p><img src="https://aleahim.com/images/blog/swiftui-3d-is-flat/appkit-uikit-cube.gif" alt="AppKit and UIKit, one Core Animation scene, recorded together"></p>
<p>And the macOS witness on its own:</p>
<figure class="td-embed" style="--td-embed-aspect-ratio: 560 / 420;">
<div class="td-embed-frame">
<iframe
  class="td-embed-iframe"
  src="https://www.youtube-nocookie.com/embed/Ce5JnQ8B7pI"
  title="Core Animation Cube 3D, macOS AppKit witness"
  loading="lazy"
  allowfullscreen
  referrerpolicy="strict-origin-when-cross-origin"
  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
></iframe>
</div>
</figure>
<p>This is not a novel reading of Core Animation. Nick Lockwood’s <em>iOS Core Animation: Advanced Techniques</em> describes the identical model and builds the identical six-face cube: any 3D surfaces in one scene must be siblings “because each parent flattens its children”, <code>CATransformLayer</code> “does not flatten its sublayers, so it can be used to construct a hierarchical 3D structure”, and the parent’s perspective transform means “the vanishing point is set as the center of the container layer, not set individually”. Apple’s documentation, the running code, and a standard text all name the same two ingredients: the single-camera <code>sublayerTransform</code> and the non-flattening container.</p>
<h2>Core Animation is 2.5D, and that makes the finding sharper</h2>
<p>The article should not overclaim in the other direction either. Core Animation is not a depth-buffered 3D engine. Its model is 2.5D: flat layer surfaces living in a 3D compositing space, with a compositor descended from the painter’s model. Four exact rules:</p>
<ol><li><p>An ordinary <code>CALayer</code> flattens its subtree into one flat surface; a 3D transform then warps that surface as a projected quad.</p></li><li><p><code>sublayerTransform</code> is the parent camera. It applies to sublayers only, never to the parent’s own content. This is where shared <code>m34</code> perspective belongs.</p></li><li><p><code>CATransformLayer</code> is the exception: a non-rendering container whose only job is to keep children unflattened, so their faces project independently in one shared space.</p></li><li><p>Depth is sorted, not buffered. Faces are ordered back-to-front in the shared space; there is no per-pixel z-buffer. Mutually intersecting quads are the hard case. A cube of non-intersecting faces is the clean case.</p></li></ol>
<p>So the missing thing in SwiftUI is not “a 3D GPU”. The missing public primitive is the retained layer-compositor machinery: <code>CATransformLayer</code>, parent <code>sublayerTransform</code>, <code>zPosition</code> as layer-space ordering, and <code>doubleSided</code> as a compositor back-face rule.</p>
<p>There is one more witness worth naming. I have been building a clean-room reconstruction of Core Animation’s layer model, and it arrived at the same boundary from the other direction: to reproduce Core Animation’s cube it had to implement precisely these primitives, the non-flattening transform layer, the parent camera, the two-pivot face projection, the back-to-front depth sort, and back-face culling by projected winding. Its tests hold those behaviors against a live Core Animation rendering oracle, with one honestly open residual, an off-center multi-child seam measurement. A reconstruction that independently converges on the same four primitives is evidence that the boundary is real, not an artifact of one demo.</p>
<h2>Card flips prove a different theorem</h2>
<p>The classic <code>GeometryEffect</code> card flip, which I walked through in <a href="https://aleahim.com/blog/swift-u-i-lab-advanced-animations/">SwiftUILab’s Advanced Animations</a>, is intentionally not one of the witnesses. The trick enters through an allowed opening in the filter: a <code>GeometryEffect</code> receives a <code>CGSize</code> and returns a <code>ProjectionTransform</code>, a pure transform of one rendered rectangle, which is exactly the abstraction SwiftUI permits. Deferring the front-to-back content swap until the card is edge-on makes one rectangle look two-sided, and the card is beautiful because a single rendered rectangle is precisely what SwiftUI is for.</p>
<p>But note what the swap is standing in for. In Core Animation, <code>doubleSided</code> defaults to true and setting it false culls a layer when it faces away from the camera, a compositor rule. The card trick has to swap content manually because no such rule exists at the SwiftUI view level. It adds no shared Z space, no depth sorting, no parent camera. A card asks whether one view can be projected. A cube asks whether six siblings can share a scene. Different theorem.</p>
<h2>Proof status</h2>
<p>Following the discipline from <a href="https://aleahim.com/blog/the-swiftui-oracle/">The SwiftUI Oracle</a>, the claim is split into labeled evidence, because “it looks right” is not a result:</p>
<table><thead><tr><th>Claim</th><th>Status</th><th>Evidence</th></tr></thead><tbody><tr><td>SwiftUI exposes per-view projection, not a retained 3D hierarchy.</td><td>documented</td><td><code>rotation3DEffect(anchor:anchorZ:perspective:)</code> and <code>projectionEffect</code> act on rendered output. No <code>sublayerTransform</code>, no <code>CATransformLayer</code>, no depth-sorted container on iOS/macOS.</td></tr><tr><td>The richer 3D view model exists, elsewhere.</td><td>documented</td><td><code>transform3DEffect</code>, <code>perspectiveRotationEffect</code>, and the <code>Rotation3D</code> form of <code>rotation3DEffect</code> are visionOS-only.</td></tr><tr><td>The pure SwiftUI cube fails under rotation.</td><td>witnessed</td><td>The rendered yaw sweep: a frustum at rest, the front face painting on top after turning away at 120 and 180 degrees. The repo’s <code>SwiftUIFlatMobile</code> and <code>SwiftUIFlatMac</code> keep the failure visible and scripted.</td></tr><tr><td>The rejected primitives exist one level down.</td><td>documented</td><td><code>CATransformLayer</code> is documented as the non-flattening true-3D container; <code>sublayerTransform</code>, <code>zPosition</code>, <code>anchorPointZ</code>, and <code>doubleSided</code> are public layer API.</td></tr><tr><td>The real cube runs on iPhone and macOS.</td><td>witnessed</td><td><code>Cube3DMobile</code> and <code>Cube3DCAMac</code> host the same layer-only scene from UIKit and AppKit.</td></tr><tr><td>A clean-room reconstruction needs the same primitives.</td><td>witnessed</td><td>The Core Animation reconstruction independently implements the transform layer, parent camera, depth sort, and culling, gated against a live rendering oracle, with one disclosed open seam measurement.</td></tr><tr><td>Pixel-for-pixel equivalence to Apple’s compositor, inside AppleCube.</td><td>blocked</td><td>No pixel oracle in that repo yet. Its evidence is documented API surface plus executable witnesses.</td></tr></tbody></table>
<p>And the honest labeling that keeps all of this defensible:</p>
<ul><li><p>The AppleCube demo is labeled what it is: a UIKit/AppKit host with a Core Animation layer scene. It is not “pure SwiftUI 3D” and does not claim to be.</p></li><li><p>The counterexample above is genuinely pure SwiftUI, and it is labeled as what it is: a SwiftUI cube that demonstrates the flattening limit, not a working cube.</p></li><li><p>If you need a real cube inside a SwiftUI app on iOS or macOS today, the honest construction is a SwiftUI host wrapping the <code>CATransformLayer</code> scene through a representable, or SceneKit, or RealityKit. None of those is “pure SwiftUI 3D”.</p></li></ul>
<h2>Run it yourself</h2>
<p>Everything is in the <a href="https://codeberg.org/Mihaela/applecube">AppleCube repository</a>, including the recording scenarios that drive the failure poses reproducibly:</p>
<pre><code class="language-sh">xcodebuild \
  <span class="tok-operator">-</span>project Cube<span class="tok-number">3</span>DCA<span class="tok-operator">/</span>Cube<span class="tok-number">3</span>DCA<span class="tok-operator">.</span>xcodeproj \
  <span class="tok-operator">-</span>scheme SwiftUIFlatMobile \
  <span class="tok-operator">-</span>configuration Debug \
  <span class="tok-operator">-</span>destination <span class="tok-string">'platform=iOS Simulator,name=iPhone 17'</span> \
  build</code></pre>
<p>Swap the scheme for <code>Cube3DMobile</code>, <code>Cube3DCAMac</code>, or <code>SwiftUIFlatMac</code> for the other three witnesses.</p>
<p>In <a href="https://aleahim.com/blog/morlocks-built-swiftui/">The Morlocks Built SwiftUI</a> I argued that the foundation never left. This article is the constructive version of that argument, read off the API surface itself and then rendered to check. SwiftUI’s architects looked at the layer model, kept everything that could be expressed as a value transform of one rectangle, collapsed depth into draw order, and declined to expose the scene, then built the fuller 3D model where the platform demanded it, on visionOS. That decision is why your app animates smoothly without you ever thinking about the render loop. It is also why, when you finally need the cube on iOS or macOS, you will find it one hosting view below the boundary they drew.</p>]]></content:encoded>
</item>
<item>
<title>Humans Don&apos;t Work at GitHub</title>
<link>https://aleahim.com/blog/humans-dont-work-at-github/</link>
<guid isPermaLink="true">https://aleahim.com/blog/humans-dont-work-at-github/</guid>
<pubDate>Thu, 02 Jul 2026 00:00:00 +0000</pubDate>
<description>GitHub made my work disappear, put a broken machine gate in front of appeal, and gave me no human path back</description>
<content:encoded><![CDATA[<h1>Humans Don’t Work at GitHub</h1>
<p>On 30 June, at nine minutes past three in the afternoon, a machine at GitHub decided I was a machine. It locked my account. My repositories, my profile, my Pages, all of it went 404 to the world. Not deleted. I could still see everything from the inside. Just switched off, so that to everyone else I had simply never existed. No email. No warning. No reason. And I was asleep when it happened.</p>
<h2>What it saw</h2>
<p>I pulled my own security log before I got angry, because I wanted to be fair. I will not pretend the machine hallucinated. My tools had been busy: more than a thousand OAuth tokens minted and rotated, hundreds of repositories created and destroyed by automation running in my name. To an abuse detector, that is not a person. That is a bot.</p>
<p>Here is the part that still stings. I have never used GitHub Copilot. Not once. Yet two Copilot apps churned 963 of those tokens on their own, from January to June, an integration I never turned on, working under my name while I was somewhere else living my life. I did not use the feature. The feature used me. And then it got me flagged.</p>
<h2>A door with no one behind it</h2>
<p>To appeal, GitHub sends you to a screen that asks you to prove you are human. A captcha. It never loaded for me.</p>
<p><img src="https://aleahim.com/images/blog/humans-dont-work-at-github/sms-verification-redacted.png" alt="GitHub’s SMS verification gate: a red “unable to verify your captcha response” error sitting directly above a green “Verification complete” checkmark. My number is redacted."></p>
<p>That is the whole thing in one picture. A red error saying it cannot verify me, and a green checkmark underneath saying I am verified. Both at once. No text ever arrived. No ticket ever opened. No person ever answered, because there is no person. It is machines all the way down.</p>
<p>I am not special. Michal Flaška lost around a thousand repositories exactly this way, pulled his logs like I pulled mine, and found the same culprit: “the bad actor didn’t delete anything. GitHub’s own automation did.” He waited forty days. Another developer gave up by day ten and left for Codeberg, where I am now. There are dozens of us saying the same three sentences. Same 404. Same silence. Same machine.</p>
<h2>What I actually signed up for</h2>
<p>I subscribed to a tool. Machines I would orchestrate, machines I would give orders to. Build this. Run that. Host my work. I never signed up to be judged by machines I do not command, machines that flag me, lock me, and shut a door in my face while no human watches. The only machines I ever agreed to work with are the ones that take my orders, never the ones that give them to me.</p>
<p>A multi-trillion-dollar company decided humans do not scale, and quietly removed them. What is left flagged me, gated me, ignored me, and a marketing page calls it support.</p>
<h2>So keep it</h2>
<p>GitHub, you do not have to restore my account. Please, don’t bother.</p>
<p>I care about code more than almost anything I make, and I cannot keep what I love in a place where no one like me is home. Thinking about you now makes me nauseous, and I do not need that in my life.</p>
<p>I took my work, and I left. I am not coming back.</p>
<p>And I did not just leave. I became a member of Codeberg e.V., the non-profit behind the place I moved to, where humans vote on its direction. A machine decided I did not belong at GitHub. At Codeberg, I have a vote.</p>
<h2>Addendum, three hours later</h2>
<p>I published this post, shared it in four places, and went about my morning. Three hours later, out of curiosity, I clicked the support link one more time.</p>
<p>The wall was gone.</p>
<p><img src="https://aleahim.com/images/blog/humans-dont-work-at-github/support-select-account.png" alt="The support portal, simply loading, the way it must have loaded for everyone else all along. My account, selectable. A Continue button that works."></p>
<p>No SMS screen. No captcha refusing to verify me. The support portal simply loaded and offered me two situations to choose from.</p>
<p><img src="https://aleahim.com/images/blog/humans-dont-work-at-github/support-situations.png" alt="Two situations on offer: billing and payments, or reinstatement for disabled accounts. For two days there was a third situation, prove you are human to a captcha that never loads, and it was the only one I was allowed to see."></p>
<p>It walked me to a form titled “Reinstatement request.” One of its radio buttons reads, word for word: <em>“I can login, but my profile and contributions aren’t visible to others.”</em> My whole catastrophe, pre-printed as a checkbox. I was never an edge case. I was a category common enough to have its own form field.</p>
<p><img src="https://aleahim.com/images/blog/humans-dont-work-at-github/reinstatement-request-form.png" alt="The reinstatement form. One radio button reads: I can login, but my profile and contributions aren’t visible to others. My whole catastrophe, pre-printed as a checkbox."></p>
<p>The billing page, meanwhile, now tells me three things at once. A banner at the top asks me to please update my payment method. A toast announces my billing information was successfully updated. And a warning box below them both says I cannot update my billing information, because my account is flagged and ineligible for transactions. The machine orders the repair, reports the repair, and forbids the repair, in one screen.</p>
<p>I wrote in this post that the appeal only works when you are loud in public, and I hoped to be wrong about that. The door that stayed shut while I knocked for two days moved by itself the same morning people were reading about it. Draw your own conclusion about who it moves for.</p>
<p>I am a solo developer in Zagreb with a blog about running Swift everywhere, making Swift do anything, rebuilding Apple frameworks nobody asked me to rebuild. My megaphone is a paper cup. If the sound carried, it is because strangers passed it from hand to hand until someone with a real megaphone heard it.</p>
<p>To be clear about the score: I am still blocked. The noise did not open the door. It opened the waiting room. For everyone locked out this way without an audience, even the waiting room stays shut.</p>
<p>The form is there now. I did not ask them to open it.</p>
<p>Will I fill it in? I still do not know. There is exactly one reason to: if my account is ever deleted for good, my username becomes available, and whoever claims it inherits every old link to Cupertino. A stranger could stand at my old address, wearing my name, serving their code to people who trusted mine. So I may apply after all, not to return, but to hold the ground: one repository, Cupertino, kept as nothing but a redirect pointing home.</p>
<p>Everything else can go. My packages will drop off the Swift Package Index, which only indexes packages hosted on GitHub, and I have made my peace with that. None of them were essential to the community, so nothing of value is lost. I find no joy in seeing them tied to a place I left. They are already on Codeberg, whole. A listing is not a home.</p>
<p>Everything I make lives where a human answers the door.</p>
<h2>Addendum, one day later</h2>
<p>There is one more place where GitHub is required to tell the truth about what it did to me, and I went and looked.</p>
<p>Under the EU’s Digital Services Act, <strong>a platform that restricts you must hand you a statement of reasons, and must file that statement in the European Commission’s public DSA Transparency Database.</strong> Article 17 names the cases one by one: restricting the visibility of your content, suspending monetary payments, suspending the service, suspending your account. What happened to me on 30 June was all four at once. I never received a statement. So I checked whether they ever wrote one.</p>
<p>The database publishes complete daily dumps. I downloaded the three days around my lock and read every row, 66.7 million statements from every platform operating in Europe. My totals matched the Commission’s published counts exactly, so I know I missed nothing.</p>
<p>On 30 June, the day my account was locked at 15:09, GitHub filed nothing. Not about me, about anyone. On 1 July it filed 20,570 statements in a single batch, every one of them an account suspension for actions taken on 29 June, almost all for scams and fraud. On 2 July, nothing again. No statement covering any GitHub decision from 30 June exists in the database.</p>
<p>Then I stopped filtering by date and asked a wider question. In the entire history of the database, across everything GitHub has ever filed, the number of statements reporting a monetary restriction is zero. The number reporting a suspension of service is zero. GitHub Sponsors suspends payouts with every lock like mine, the law names that as a reportable restriction, and the count of times GitHub has reported one is zero. This is not a late batch. This is a category of decision that their reporting pipeline was never taught to mention.</p>
<p>Maybe a batch for 30 June still shows up in some later dump. It changes nothing. Article 17 is explicit about the moment: the statement of reasons is owed at the latest from the date the restriction is imposed, and it is owed to the person restricted. Not to a database, eventually. To me, then. On 30 June they took my visibility, my funding, my service, and my account in one stroke, and they told me nothing. Not that day, not since.</p>
<p><strong>The violation was complete at 15:09 on 30 June, the moment they acted without informing me. Nothing GitHub files later can reach back and undo that.</strong></p>
<p>One more field, and I will leave it with you. Every statement GitHub files carries a flag saying whether the decision was automated. All 20,570 say the same thing: not automated. <strong>Twenty thousand human decisions in one day, they tell the regulator????</strong> I titled this post before I read their filings. To the Commission, GitHub reports a building full of humans. At the door where I knocked for two days, there was no one. Both cannot be true.</p>]]></content:encoded>
</item>
<item>
<title>Cupertino moved to Codeberg. Update your Homebrew tap.</title>
<link>https://aleahim.com/blog/cupertino-moved-to-codeberg/</link>
<guid isPermaLink="true">https://aleahim.com/blog/cupertino-moved-to-codeberg/</guid>
<pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate>
<description>Cupertino v1.4.2 moves the working Homebrew tap and binary distribution to Codeberg, with setup assets served from Fly.</description>
<content:encoded><![CDATA[<h1>Cupertino moved to Codeberg. Update your Homebrew tap.</h1>
<p>Short version: Cupertino’s working distribution home is now Codeberg.</p>
<p>If you installed Cupertino from the old GitHub tap, <code>brew upgrade</code> can tell you that everything is current while leaving you on <code>1.4.1</code>. That is not the current working release. The current release is <code>1.4.2</code>, and it lives in the new Codeberg tap.</p>
<h2>Update an existing install</h2>
<p>Run this exactly:</p>
<pre><code class="language-bash">brew tap cupertinohq<span class="tok-operator">/</span>tap https<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>codeberg<span class="tok-operator">.</span>org<span class="tok-operator">/</span>CupertinoHQ<span class="tok-operator">/</span>homebrew<span class="tok-operator">-</span>tap<span class="tok-operator">.</span>git
brew fetch cupertinohq<span class="tok-operator">/</span>tap<span class="tok-operator">/</span>cupertino
brew uninstall mihaelamj<span class="tok-operator">/</span>tap<span class="tok-operator">/</span>cupertino <span class="tok-operator">|</span><span class="tok-operator">|</span> <span class="tok-literal">true</span>
brew install cupertinohq<span class="tok-operator">/</span>tap<span class="tok-operator">/</span>cupertino
brew untap mihaelamj<span class="tok-operator">/</span>tap <span class="tok-operator">|</span><span class="tok-operator">|</span> <span class="tok-literal">true</span>
cupertino <span class="tok-operator">-</span><span class="tok-operator">-</span>version</code></pre>
<p>Expected output:</p>
<pre><code class="language-text"><span class="tok-number">1.4.2</span></code></pre>
<p>Then check your databases:</p>
<pre><code class="language-bash">cupertino doctor</code></pre>
<p>If you need to download the databases again:</p>
<pre><code class="language-bash">cupertino setup</code></pre>
<h2>Fresh install</h2>
<p>For a new Homebrew install:</p>
<pre><code class="language-bash">brew tap cupertinohq<span class="tok-operator">/</span>tap https<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>codeberg<span class="tok-operator">.</span>org<span class="tok-operator">/</span>CupertinoHQ<span class="tok-operator">/</span>homebrew<span class="tok-operator">-</span>tap<span class="tok-operator">.</span>git
brew install cupertinohq<span class="tok-operator">/</span>tap<span class="tok-operator">/</span>cupertino
cupertino setup</code></pre>
<p>The explicit Codeberg URL matters. If you run only:</p>
<pre><code class="language-bash">brew tap cupertinohq<span class="tok-operator">/</span>tap</code></pre>
<p>Homebrew assumes GitHub. That is not where the working tap lives.</p>
<h2>How to tell if you are still on the old path</h2>
<p>Run:</p>
<pre><code class="language-bash">brew info cupertino</code></pre>
<p>If you see this:</p>
<pre><code class="language-text"><span class="tok-type">From</span><span class="tok-operator">:</span> https<span class="tok-operator">:</span><span class="tok-comment">//github.com/mihaelamj/homebrew-tap/...</span>
<span class="tok-type">Tap</span><span class="tok-operator">:</span> mihaelamj<span class="tok-operator">/</span>tap
stable <span class="tok-number">1.4.1</span></code></pre>
<p>you are still looking at the old GitHub tap.</p>
<p>The working tap should resolve to:</p>
<pre><code class="language-text"><span class="tok-type">From</span><span class="tok-operator">:</span> https<span class="tok-operator">:</span><span class="tok-comment">//codeberg.org/CupertinoHQ/homebrew-tap.git/Formula/cupertino.rb</span>
<span class="tok-type">Tap</span><span class="tok-operator">:</span> cupertinohq<span class="tok-operator">/</span>tap
stable <span class="tok-number">1.4.2</span></code></pre>
<h2>What changed in 1.4.2</h2>
<p>This is a distribution hotfix, not a corpus rebuild.</p>
<ul><li><p>Source moved to <a href="https://codeberg.org/CupertinoHQ/cupertino">codeberg.org/CupertinoHQ/cupertino</a></p></li><li><p>Homebrew moved to <a href="https://codeberg.org/CupertinoHQ/homebrew-tap">codeberg.org/CupertinoHQ/homebrew-tap</a></p></li><li><p>The Homebrew formula downloads the <code>1.4.2</code> binary from Codeberg packages</p></li><li><p><code>cupertino setup</code> downloads the database bundle from <code>cupertino-assets.fly.dev</code></p></li><li><p>The database bundle is still version <code>1.4.0</code></p></li><li><p>The schema is unchanged</p></li></ul>
<p>That means your existing v1.4.0 databases remain valid. If <code>cupertino doctor</code> says all checks passed, you do not need to download the databases again.</p>
<h2>Why <code>brew upgrade</code> was confusing</h2>
<p>Homebrew compares your installed formula against the tap it knows about. If your machine is still tapped to the old GitHub formula and that formula says <code>1.4.1</code>, Homebrew can report “nothing to upgrade” even though the working Codeberg tap has <code>1.4.2</code>.</p>
<p>The fix is not just <code>brew upgrade</code>. The fix is switching taps.</p>
<h2>The old GitHub setup path is obsolete</h2>
<p>The old GitHub release asset can return 404. Cupertino <code>1.4.2</code> no longer depends on that path for setup.</p>
<p>The current setup path is:</p>
<pre><code class="language-text">https<span class="tok-operator">:</span><span class="tok-comment">//cupertino-assets.fly.dev/releases/v1.4.0/cupertino-databases-v1.4.0.zip</span>
https<span class="tok-operator">:</span><span class="tok-comment">//cupertino-assets.fly.dev/apple-constraints.json</span></code></pre>
<p>I tested the new path end to end: Homebrew installs <code>1.4.2</code>, <code>cupertino setup</code> downloads the bundle, the constraints sidecar downloads, and all 8 databases verify.</p>
<h2>Source installs</h2>
<p>If you build from source, clone the Codeberg repo:</p>
<pre><code class="language-bash">git clone https<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>codeberg<span class="tok-operator">.</span>org<span class="tok-operator">/</span>CupertinoHQ<span class="tok-operator">/</span>cupertino<span class="tok-operator">.</span>git
cd cupertino</code></pre>
<p>The old GitHub URLs should be treated as legacy references. Codeberg is the working home now.</p>]]></content:encoded>
</item>
<item>
<title>&quot;The SwiftUI Oracle: Measuring a Clean Room Against the Real Thing&quot;</title>
<link>https://aleahim.com/blog/the-swiftui-oracle/</link>
<guid isPermaLink="true">https://aleahim.com/blog/the-swiftui-oracle/</guid>
<pubDate>Sat, 27 Jun 2026 00:00:00 +0000</pubDate>
<description>How I prove a from-scratch SwiftUI engine is correct by holding it against real SwiftUI in 125 differential tests, with the actual code, the harness, the firewall, and the floating-point floor that makes agreement mean identical</description>
<content:encoded><![CDATA[<h1>The SwiftUI Oracle: Measuring a Clean Room Against the Real Thing</h1>
<p>In the last post I claimed I had measured SwiftUI until its behavior was predictable. This
post is the receipts. It is for Swift developers, so it is mostly code: the test harness,
the firewall that keeps it honest, and the differential tests that hold a from-scratch
engine against the real framework. If you have ever wondered what it would take to <em>prove</em>
you understand SwiftUI rather than assert it, this is one answer.</p>
<p>The engine is called PureView. It is a clean-room reimplementation of SwiftUI’s semantics:
the attribute graph, identity, layout, animation, and lowering to a display list. It never
imports SwiftUI. The proof that it is faithful is a separate test target that <em>does</em> import
SwiftUI and compares the two, value for value.</p>
<p><img src="https://aleahim.com/images/blog/the-swiftui-oracle/1-two-pieces.jpg" alt="Our engine, and the real framework as the answer key"></p>
<h2>What “oracle” means here</h2>
<p>In testing, an <strong>oracle</strong> is the source of the correct answer: the thing that tells you
whether a result is right. Our oracle is real SwiftUI itself. For any scene, we ask the
real framework what it does, and that answer is the ground truth we grade the engine
against. The engine is the work; SwiftUI is the answer key.</p>
<h2>The firewall: the engine may never import SwiftUI</h2>
<p>A measurement is worthless if the instrument can cheat. If the engine were allowed to call
SwiftUI, any agreement would be suspect, because the engine could simply forward to the
framework it claims to reproduce. So the engine is sealed off, and the seal is not a
convention. It is a test that fails <code>swift test</code> if a UI-framework import ever appears in
the engine sources.</p>
<p><img src="https://aleahim.com/images/blog/the-swiftui-oracle/2-firewall.jpg" alt="The firewall: the engine cannot peek"></p>
<pre><code class="language-swift"><span class="tok-attribute">@Suite</span>(<span class="tok-string">"Engine import confinement"</span>)
<span class="tok-keyword">struct</span> <span class="tok-type">EngineImportConfinementTests</span> {
    <span class="tok-attribute">@Test</span>(<span class="tok-string">"Sources/PureView imports no UI framework"</span>)
    <span class="tok-keyword">func</span> <span class="tok-function">engineImportsNoUIFramework</span>() <span class="tok-keyword">throws</span> {
        <span class="tok-keyword">let</span> forbidden <span class="tok-operator">=</span> [<span class="tok-string">"SwiftUI"</span>, <span class="tok-string">"UIKit"</span>, <span class="tok-string">"AppKit"</span>, <span class="tok-string">"WatchKit"</span>]
        <span class="tok-comment">// #filePath = &lt;root&gt;/Tests/PureViewTests/&lt;this file&gt;; climb to &lt;root&gt;/Sources/PureView.</span>
        <span class="tok-keyword">let</span> engine<span class="tok-type">Root</span> <span class="tok-operator">=</span> <span class="tok-type">URL</span>(file<span class="tok-type">Path</span><span class="tok-operator">:</span> #file<span class="tok-type">Path</span>)
            <span class="tok-operator">.</span><span class="tok-function">deletingLastPathComponent</span>() <span class="tok-comment">// PureViewTests</span>
            <span class="tok-operator">.</span><span class="tok-function">deletingLastPathComponent</span>() <span class="tok-comment">// Tests</span>
            <span class="tok-operator">.</span><span class="tok-function">deletingLastPathComponent</span>() <span class="tok-comment">// &lt;root&gt;</span>
            <span class="tok-operator">.</span><span class="tok-function">appending</span>(path<span class="tok-operator">:</span> <span class="tok-string">"Sources/PureView"</span>)

        <span class="tok-keyword">let</span> file<span class="tok-type">Manager</span> <span class="tok-operator">=</span> <span class="tok-type">FileManager</span><span class="tok-operator">.</span><span class="tok-keyword">default</span>
        <span class="tok-keyword">let</span> enumerator <span class="tok-operator">=</span> <span class="tok-keyword">try</span> #<span class="tok-function">require</span>(
            file<span class="tok-type">Manager</span><span class="tok-operator">.</span><span class="tok-function">enumerator</span>(at<span class="tok-operator">:</span> engine<span class="tok-type">Root</span>, including<span class="tok-type">PropertiesForKeys</span><span class="tok-operator">:</span> <span class="tok-literal">nil</span>),
            <span class="tok-string">"engine source root must exist at \(engineRoot.path)"</span>
        )

        <span class="tok-keyword">func</span> <span class="tok-function">importsForbidden</span>(_ line<span class="tok-operator">:</span> <span class="tok-type">String</span>) <span class="tok-operator">-</span><span class="tok-operator">&gt;</span> <span class="tok-type">Bool</span> {
            forbidden<span class="tok-operator">.</span>contains { framework <span class="tok-keyword">in</span>
                line <span class="tok-operator">=</span><span class="tok-operator">=</span> <span class="tok-string">"import \(framework)"</span>
                    <span class="tok-operator">|</span><span class="tok-operator">|</span> line <span class="tok-operator">=</span><span class="tok-operator">=</span> <span class="tok-string">"@testable import \(framework)"</span>
                    <span class="tok-operator">|</span><span class="tok-operator">|</span> line<span class="tok-operator">.</span><span class="tok-function">hasPrefix</span>(<span class="tok-string">"import \(framework)."</span>)
            }
        }

        <span class="tok-keyword">var</span> offenders<span class="tok-operator">:</span> [<span class="tok-type">String</span>] <span class="tok-operator">=</span> []
        <span class="tok-keyword">for</span> <span class="tok-keyword">case</span> <span class="tok-keyword">let</span> url <span class="tok-keyword">as</span> <span class="tok-type">URL</span> <span class="tok-keyword">in</span> enumerator <span class="tok-keyword">where</span> url<span class="tok-operator">.</span>path<span class="tok-keyword">Extension</span> <span class="tok-operator">=</span><span class="tok-operator">=</span> <span class="tok-string">"swift"</span> {
            <span class="tok-keyword">let</span> source <span class="tok-operator">=</span> <span class="tok-keyword">try</span> <span class="tok-type">String</span>(contents<span class="tok-type">Of</span><span class="tok-operator">:</span> url, encoding<span class="tok-operator">:</span> <span class="tok-operator">.</span>utf<span class="tok-number">8</span>)
            <span class="tok-keyword">for</span> (index, line) <span class="tok-keyword">in</span> source<span class="tok-operator">.</span><span class="tok-function">split</span>(separator<span class="tok-operator">:</span> <span class="tok-string">"\n"</span>, omitting<span class="tok-type">EmptySubsequences</span><span class="tok-operator">:</span> <span class="tok-literal">false</span>)<span class="tok-operator">.</span><span class="tok-function">enumerated</span>() {
                <span class="tok-keyword">let</span> trimmed <span class="tok-operator">=</span> line<span class="tok-operator">.</span><span class="tok-function">trimmingCharacters</span>(<span class="tok-keyword">in</span><span class="tok-operator">:</span> <span class="tok-operator">.</span>whitespaces)
                <span class="tok-keyword">if</span> <span class="tok-function">importsForbidden</span>(trimmed) {
                    offenders<span class="tok-operator">.</span><span class="tok-function">append</span>(<span class="tok-string">"\(url.lastPathComponent):\(index + 1): \(trimmed)"</span>)
                }
            }
        }

        #<span class="tok-function">expect</span>(offenders<span class="tok-operator">.</span>is<span class="tok-type">Empty</span>, <span class="tok-string">"engine must not import a UI framework, found: \(offenders)"</span>)
    }
}</code></pre>
<p>SwiftUI, UIKit, and AppKit are system frameworks, so a stray <code>import SwiftUI</code> in an engine
file would compile green and ship undetected. The convention is not enough; this test makes
the leak fail the build. That is the whole integrity story in thirty lines.</p>
<h2>The shape of every test: do it twice, compare</h2>
<p>Every oracle test has the same skeleton. Describe a scene. Run it in real SwiftUI and
record the answer. Run the same scene in the engine. Assert the two agree, to the
floating-point floor.</p>
<p><img src="https://aleahim.com/images/blog/the-swiftui-oracle/3-do-it-twice.jpg" alt="One scene, run in both, compared to one millionth"></p>
<h2>The harness: running real SwiftUI headless</h2>
<p>To use SwiftUI as the oracle we have to run it for real, with no app and no screen. The
harness hosts a view in an offscreen window, lets it run a genuine update cycle, then reads
the resolved geometry back out through a preference-key probe.</p>
<pre><code class="language-swift"><span class="tok-keyword">private</span> <span class="tok-keyword">struct</span> <span class="tok-type">FrameKey</span><span class="tok-operator">:</span> <span class="tok-type">SwiftUI</span><span class="tok-operator">.</span><span class="tok-type">PreferenceKey</span> {
    <span class="tok-keyword">static</span> <span class="tok-keyword">let</span> default<span class="tok-type">Value</span><span class="tok-operator">:</span> [<span class="tok-type">String</span><span class="tok-operator">:</span> <span class="tok-type">CGRect</span>] <span class="tok-operator">=</span> [<span class="tok-operator">:</span>]
    <span class="tok-keyword">static</span> <span class="tok-keyword">func</span> <span class="tok-function">reduce</span>(value<span class="tok-operator">:</span> <span class="tok-keyword">inout</span> [<span class="tok-type">String</span><span class="tok-operator">:</span> <span class="tok-type">CGRect</span>], next<span class="tok-type">Value</span><span class="tok-operator">:</span> () <span class="tok-operator">-</span><span class="tok-operator">&gt;</span> [<span class="tok-type">String</span><span class="tok-operator">:</span> <span class="tok-type">CGRect</span>]) {
        value<span class="tok-operator">.</span><span class="tok-function">merge</span>(<span class="tok-function">nextValue</span>(), uniquing<span class="tok-type">KeysWith</span><span class="tok-operator">:</span> { _, new <span class="tok-keyword">in</span> new })
    }
}

<span class="tok-keyword">extension</span> <span class="tok-type">SwiftUI</span><span class="tok-operator">.</span><span class="tok-type">View</span> {
    <span class="tok-comment">/// Tag a view so the oracle records its resolved frame, in root coordinates.</span>
    <span class="tok-keyword">func</span> <span class="tok-function">probe</span>(_ id<span class="tok-operator">:</span> <span class="tok-type">String</span>) <span class="tok-operator">-</span><span class="tok-operator">&gt;</span> some <span class="tok-type">SwiftUI</span><span class="tok-operator">.</span><span class="tok-type">View</span> {
        <span class="tok-function">background</span>(
            <span class="tok-type">GeometryReader</span> { proxy <span class="tok-keyword">in</span>
                <span class="tok-type">Color</span><span class="tok-operator">.</span>clear<span class="tok-operator">.</span><span class="tok-function">preference</span>(key<span class="tok-operator">:</span> <span class="tok-type">FrameKey</span><span class="tok-operator">.</span><span class="tok-keyword">self</span>, value<span class="tok-operator">:</span> [id<span class="tok-operator">:</span> proxy<span class="tok-operator">.</span><span class="tok-function">frame</span>(<span class="tok-keyword">in</span><span class="tok-operator">:</span> <span class="tok-operator">.</span><span class="tok-function">named</span>(k<span class="tok-type">Root</span>))])
            }
        )
    }
}

<span class="tok-attribute">@MainActor</span>
<span class="tok-keyword">func</span> <span class="tok-function">oracleFrames</span>(_ content<span class="tok-operator">:</span> some <span class="tok-type">SwiftUI</span><span class="tok-operator">.</span><span class="tok-type">View</span>, root<span class="tok-type">Size</span><span class="tok-operator">:</span> <span class="tok-type">CGSize</span>) <span class="tok-operator">-</span><span class="tok-operator">&gt;</span> [<span class="tok-type">String</span><span class="tok-operator">:</span> <span class="tok-type">CGRect</span>] {
    <span class="tok-keyword">let</span> box <span class="tok-operator">=</span> <span class="tok-type">FrameBox</span>()
    <span class="tok-keyword">let</span> view <span class="tok-operator">=</span> <span class="tok-type">ZStack</span>(alignment<span class="tok-operator">:</span> <span class="tok-operator">.</span>top<span class="tok-type">Leading</span>) { content }
        <span class="tok-operator">.</span><span class="tok-function">frame</span>(width<span class="tok-operator">:</span> root<span class="tok-type">Size</span><span class="tok-operator">.</span>width, height<span class="tok-operator">:</span> root<span class="tok-type">Size</span><span class="tok-operator">.</span>height, alignment<span class="tok-operator">:</span> <span class="tok-operator">.</span>top<span class="tok-type">Leading</span>)
        <span class="tok-operator">.</span><span class="tok-function">coordinateSpace</span>(name<span class="tok-operator">:</span> k<span class="tok-type">Root</span>)
        <span class="tok-operator">.</span><span class="tok-function">onPreferenceChange</span>(<span class="tok-type">FrameKey</span><span class="tok-operator">.</span><span class="tok-keyword">self</span>) { box<span class="tok-operator">.</span>frames <span class="tok-operator">=</span> $<span class="tok-number">0</span> }
        <span class="tok-operator">.</span><span class="tok-function">environment</span>(\<span class="tok-operator">.</span>display<span class="tok-type">Scale</span>, <span class="tok-number">2</span>)   <span class="tok-comment">// pin the pixel grid for cross-machine determinism</span>

    <span class="tok-keyword">let</span> host <span class="tok-operator">=</span> <span class="tok-type">NSHostingView</span>(root<span class="tok-type">View</span><span class="tok-operator">:</span> view)
    host<span class="tok-operator">.</span>frame <span class="tok-operator">=</span> <span class="tok-type">CGRect</span>(orig<span class="tok-keyword">in</span><span class="tok-operator">:</span> <span class="tok-operator">.</span>zero, size<span class="tok-operator">:</span> root<span class="tok-type">Size</span>)
    <span class="tok-keyword">let</span> window <span class="tok-operator">=</span> <span class="tok-type">NSWindow</span>(content<span class="tok-type">Rect</span><span class="tok-operator">:</span> host<span class="tok-operator">.</span>frame, style<span class="tok-type">Mask</span><span class="tok-operator">:</span> [<span class="tok-operator">.</span>borderless],
                          backing<span class="tok-operator">:</span> <span class="tok-operator">.</span>buffered, <span class="tok-keyword">defer</span><span class="tok-operator">:</span> <span class="tok-literal">false</span>)
    window<span class="tok-operator">.</span>content<span class="tok-type">View</span> <span class="tok-operator">=</span> host
    host<span class="tok-operator">.</span><span class="tok-function">layoutSubtreeIfNeeded</span>()
    <span class="tok-function">pump</span>(until<span class="tok-operator">:</span> { <span class="tok-operator">!</span>box<span class="tok-operator">.</span>frames<span class="tok-operator">.</span>is<span class="tok-type">Empty</span> }, timeout<span class="tok-operator">:</span> <span class="tok-number">2</span>)
    <span class="tok-keyword">return</span> box<span class="tok-operator">.</span>frames
}

<span class="tok-comment">/// Spin the main run loop in 10 ms slices until `condition` holds or `timeout` elapses.</span>
<span class="tok-attribute">@MainActor</span>
<span class="tok-keyword">func</span> <span class="tok-function">pump</span>(until condition<span class="tok-operator">:</span> () <span class="tok-operator">-</span><span class="tok-operator">&gt;</span> <span class="tok-type">Bool</span>, timeout<span class="tok-operator">:</span> <span class="tok-type">TimeInterval</span>) {
    <span class="tok-keyword">let</span> deadline <span class="tok-operator">=</span> <span class="tok-type">Date</span>()<span class="tok-operator">.</span><span class="tok-function">addingTimeInterval</span>(timeout)
    <span class="tok-keyword">while</span> <span class="tok-operator">!</span><span class="tok-function">condition</span>(), <span class="tok-type">Date</span>() <span class="tok-operator">&lt;</span> deadline {
        <span class="tok-type">RunLoop</span><span class="tok-operator">.</span>ma<span class="tok-keyword">in</span><span class="tok-operator">.</span><span class="tok-function">run</span>(until<span class="tok-operator">:</span> <span class="tok-type">Date</span>()<span class="tok-operator">.</span><span class="tok-function">addingTimeInterval</span>(<span class="tok-number">0.01</span>))
    }
}</code></pre>
<p><code>displayScale</code> is pinned to 2 so SwiftUI’s frame snapping is identical on a Retina machine
and a CI box. When we say “we measured what SwiftUI does,” this is the literal mechanism:
SwiftUI runs, and a <code>GeometryReader</code> reports its own layout system’s answer.</p>
<h2>Measuring the attribute graph: count the body re-runs</h2>
<p>The headline claim about SwiftUI is the cone: change one value, and only the views that
read it recompute. The problem is that a <code>body</code> re-run leaves no trace. So we give it one.
Each probe view bumps a counter inside its <code>body</code>. The invisible event becomes an integer.</p>
<pre><code class="language-swift"><span class="tok-comment">/// A reference counter mutated only on the main actor (inside `body`).</span>
<span class="tok-keyword">private</span> <span class="tok-keyword">final</span> <span class="tok-keyword">class</span> <span class="tok-type">BodyCounter</span><span class="tok-operator">:</span> <span class="tok-attribute">@unchecked</span> <span class="tok-type">Sendable</span> {
    <span class="tok-keyword">private</span>(set) <span class="tok-keyword">var</span> runs <span class="tok-operator">=</span> <span class="tok-number">0</span>
    <span class="tok-keyword">func</span> <span class="tok-function">bump</span>() { runs <span class="tok-operator">+</span><span class="tok-operator">=</span> <span class="tok-number">1</span> }
}

<span class="tok-keyword">private</span> <span class="tok-keyword">final</span> <span class="tok-keyword">class</span> <span class="tok-type">OracleModel</span><span class="tok-operator">:</span> <span class="tok-type">SwiftUI</span><span class="tok-operator">.</span><span class="tok-type">ObservableObject</span> {
    <span class="tok-attribute">@SwiftUI</span><span class="tok-operator">.</span><span class="tok-type">Published</span> <span class="tok-keyword">var</span> value<span class="tok-operator">:</span> <span class="tok-type">Int</span> <span class="tok-operator">=</span> <span class="tok-number">0</span>
}

<span class="tok-comment">/// Reads `model.value`, so it depends on the state.</span>
<span class="tok-keyword">private</span> <span class="tok-keyword">struct</span> <span class="tok-type">ReaderView</span><span class="tok-operator">:</span> <span class="tok-type">SwiftUI</span><span class="tok-operator">.</span><span class="tok-type">View</span> {
    <span class="tok-attribute">@SwiftUI</span><span class="tok-operator">.</span><span class="tok-type">ObservedObject</span> <span class="tok-keyword">var</span> model<span class="tok-operator">:</span> <span class="tok-type">OracleModel</span>
    <span class="tok-keyword">let</span> counter<span class="tok-operator">:</span> <span class="tok-type">BodyCounter</span>
    <span class="tok-keyword">var</span> body<span class="tok-operator">:</span> some <span class="tok-type">SwiftUI</span><span class="tok-operator">.</span><span class="tok-type">View</span> {
        counter<span class="tok-operator">.</span><span class="tok-function">bump</span>()
        <span class="tok-keyword">return</span> <span class="tok-type">Text</span>(verbatim<span class="tok-operator">:</span> <span class="tok-string">"\(model.value)"</span>)
    }
}

<span class="tok-comment">/// Reads nothing from the model, so it must not re-evaluate when the state changes.</span>
<span class="tok-keyword">private</span> <span class="tok-keyword">struct</span> <span class="tok-type">IgnorerView</span><span class="tok-operator">:</span> <span class="tok-type">SwiftUI</span><span class="tok-operator">.</span><span class="tok-type">View</span> {
    <span class="tok-keyword">let</span> counter<span class="tok-operator">:</span> <span class="tok-type">BodyCounter</span>
    <span class="tok-keyword">var</span> body<span class="tok-operator">:</span> some <span class="tok-type">SwiftUI</span><span class="tok-operator">.</span><span class="tok-type">View</span> {
        counter<span class="tok-operator">.</span><span class="tok-function">bump</span>()
        <span class="tok-keyword">return</span> <span class="tok-type">Text</span>(verbatim<span class="tok-operator">:</span> <span class="tok-string">"static"</span>)
    }
}</code></pre>
<p><img src="https://aleahim.com/images/blog/the-swiftui-oracle/4-body-counter.jpg" alt="Counting which views recompute: the cone"></p>
<p>The test reads both counters, changes one value, waits for the reader to re-run, and then
drains the run loop a little longer so a stray re-run of the ignorer would be caught. Then
it builds the same shape in the engine’s <code>AttributeGraph</code> and asserts the cones match.</p>
<pre><code class="language-swift"><span class="tok-attribute">@Test</span>(<span class="tok-string">"only the view that reads state re-evaluates, and the engine cone agrees"</span>)
<span class="tok-keyword">func</span> <span class="tok-function">bodyEvalConeMatchesEngine</span>() {
    <span class="tok-comment">// Ground truth from real SwiftUI.</span>
    <span class="tok-keyword">let</span> swift<span class="tok-type">UI</span> <span class="tok-operator">=</span> <span class="tok-function">swiftUIBodyEvalDifferential</span>()
    #<span class="tok-function">expect</span>(swift<span class="tok-type">UI</span><span class="tok-operator">.</span>reader<span class="tok-type">Reran</span>,  <span class="tok-string">"SwiftUI: the reader's body should re-run on the state change"</span>)
    #<span class="tok-function">expect</span>(<span class="tok-operator">!</span>swift<span class="tok-type">UI</span><span class="tok-operator">.</span>ignorer<span class="tok-type">Reran</span>, <span class="tok-string">"SwiftUI: the ignorer's body should not re-run"</span>)

    <span class="tok-comment">// The same shape modeled in the engine: a source, a reader, an ignorer.</span>
    <span class="tok-keyword">let</span> graph <span class="tok-operator">=</span> <span class="tok-type">AttributeGraph</span>()
    <span class="tok-keyword">let</span> state <span class="tok-operator">=</span> <span class="tok-type">Attribute</span>(graph<span class="tok-operator">:</span> graph, value<span class="tok-operator">:</span> <span class="tok-number">0</span>)
    <span class="tok-keyword">let</span> reader <span class="tok-operator">=</span> <span class="tok-type">Attribute</span>(graph<span class="tok-operator">:</span> graph) { _ <span class="tok-keyword">in</span> state<span class="tok-operator">.</span>value }
    <span class="tok-keyword">let</span> ignorer <span class="tok-operator">=</span> <span class="tok-type">Attribute</span>(graph<span class="tok-operator">:</span> graph) { _ <span class="tok-keyword">in</span> <span class="tok-number">0</span> }
    _ <span class="tok-operator">=</span> reader<span class="tok-operator">.</span>value
    _ <span class="tok-operator">=</span> ignorer<span class="tok-operator">.</span>value
    <span class="tok-keyword">let</span> reader<span class="tok-type">Before</span> <span class="tok-operator">=</span> reader<span class="tok-operator">.</span>evaluation<span class="tok-type">Count</span>
    <span class="tok-keyword">let</span> ignorer<span class="tok-type">Before</span> <span class="tok-operator">=</span> ignorer<span class="tok-operator">.</span>evaluation<span class="tok-type">Count</span>

    state<span class="tok-operator">.</span><span class="tok-function">setValue</span>(<span class="tok-number">1</span>)
    _ <span class="tok-operator">=</span> reader<span class="tok-operator">.</span>value
    _ <span class="tok-operator">=</span> ignorer<span class="tok-operator">.</span>value

    <span class="tok-comment">// The differential: the engine's invalidation cone matches SwiftUI's.</span>
    #<span class="tok-function">expect</span>((reader<span class="tok-operator">.</span>evaluation<span class="tok-type">Count</span>  <span class="tok-operator">&gt;</span> reader<span class="tok-type">Before</span>)  <span class="tok-operator">=</span><span class="tok-operator">=</span> swift<span class="tok-type">UI</span><span class="tok-operator">.</span>reader<span class="tok-type">Reran</span>)
    #<span class="tok-function">expect</span>((ignorer<span class="tok-operator">.</span>evaluation<span class="tok-type">Count</span> <span class="tok-operator">&gt;</span> ignorer<span class="tok-type">Before</span>) <span class="tok-operator">=</span><span class="tok-operator">=</span> swift<span class="tok-type">UI</span><span class="tok-operator">.</span>ignorer<span class="tok-type">Reran</span>)
}</code></pre>
<p>Proving an <em>absence</em> is the subtle part. To show the ignorer stayed put, the test waits for
the expected re-run and then drains briefly for a stray. The engine side is deterministic
and is the real regression catcher; the bounded wait on the SwiftUI side can only catch a
stray, never manufacture a false pass.</p>
<h2>Measuring layout: probe the resolved frame</h2>
<p>Layout is the largest area, because there is the most to pin down. Each test lays the scene
out in real SwiftUI, reads back every subview’s frame through the probe, and asserts the
engine places each node at the same coordinates.</p>
<p><img src="https://aleahim.com/images/blog/the-swiftui-oracle/8-layout-negotiation.jpg" alt="Layout is a polite negotiation"></p>
<pre><code class="language-swift"><span class="tok-attribute">@Test</span>(<span class="tok-string">"Frame centering matches real SwiftUI"</span>)
<span class="tok-keyword">func</span> <span class="tok-function">frameCentering</span>() {
    <span class="tok-keyword">let</span> leaf <span class="tok-operator">=</span> <span class="tok-type">Color</span><span class="tok-operator">.</span>clear<span class="tok-operator">.</span><span class="tok-function">frame</span>(width<span class="tok-operator">:</span> <span class="tok-number">20</span>, height<span class="tok-operator">:</span> <span class="tok-number">20</span>)<span class="tok-operator">.</span><span class="tok-function">probe</span>(<span class="tok-string">"child"</span>)
    <span class="tok-keyword">let</span> container <span class="tok-operator">=</span> leaf<span class="tok-operator">.</span><span class="tok-function">frame</span>(width<span class="tok-operator">:</span> <span class="tok-number">30</span>, height<span class="tok-operator">:</span> <span class="tok-number">30</span>)<span class="tok-operator">.</span><span class="tok-function">probe</span>(<span class="tok-string">"container"</span>)
    <span class="tok-keyword">let</span> oracle <span class="tok-operator">=</span> <span class="tok-function">oracleFrames</span>(container, root<span class="tok-type">Size</span><span class="tok-operator">:</span> <span class="tok-type">CGSize</span>(width<span class="tok-operator">:</span> <span class="tok-number">100</span>, height<span class="tok-operator">:</span> <span class="tok-number">100</span>))
    #<span class="tok-function">expect</span>(<span class="tok-operator">!</span>oracle<span class="tok-operator">.</span>is<span class="tok-type">Empty</span>, <span class="tok-string">"oracle produced no frames (headless measurement failed)"</span>)

    <span class="tok-keyword">let</span> node <span class="tok-operator">=</span> <span class="tok-type">FrameLayout</span>(width<span class="tok-operator">:</span> <span class="tok-number">30</span>, height<span class="tok-operator">:</span> <span class="tok-number">30</span>, <span class="tok-type">FixedLeaf</span>(<span class="tok-type">Size</span>(width<span class="tok-operator">:</span> <span class="tok-number">20</span>, height<span class="tok-operator">:</span> <span class="tok-number">20</span>)))
    <span class="tok-keyword">let</span> engine <span class="tok-operator">=</span> node<span class="tok-operator">.</span><span class="tok-function">layout</span>(<span class="tok-keyword">in</span><span class="tok-operator">:</span> <span class="tok-type">Size</span>(width<span class="tok-operator">:</span> <span class="tok-number">100</span>, height<span class="tok-operator">:</span> <span class="tok-number">100</span>))

    <span class="tok-keyword">if</span> <span class="tok-keyword">let</span> oc <span class="tok-operator">=</span> oracle[<span class="tok-string">"container"</span>] {
        #<span class="tok-function">expect</span>(<span class="tok-function">close</span>(oc, <span class="tok-function">cg</span>(engine<span class="tok-operator">.</span>frame)), <span class="tok-string">"container: oracle \(oc) vs engine \(cg(engine.frame))"</span>)
    }
    <span class="tok-keyword">if</span> <span class="tok-keyword">let</span> ch <span class="tok-operator">=</span> oracle[<span class="tok-string">"child"</span>] {
        #<span class="tok-function">expect</span>(<span class="tok-function">close</span>(ch, <span class="tok-function">cg</span>(engine<span class="tok-operator">.</span>children[<span class="tok-number">0</span>]<span class="tok-operator">.</span>frame)), <span class="tok-string">"child: oracle \(ch) vs engine \(cg(engine.children[0].frame))"</span>)
    }
}</code></pre>
<p><code>close</code> is where the rigor lives. The tolerance absorbs only <code>CGFloat</code> round-trip, nothing
more.</p>
<pre><code class="language-swift"><span class="tok-comment">/// The engine and SwiftUI must produce the *same* frame, not a close one. This 1e-6</span>
<span class="tok-comment">/// absorbs only `CGFloat` round-trip representation. A residual above it is a real</span>
<span class="tok-comment">/// divergence, never slack: re-derive the rule, do not widen the bound.</span>
<span class="tok-keyword">let</span> exact<span class="tok-type">Floor</span><span class="tok-operator">:</span> <span class="tok-type">CGFloat</span> <span class="tok-operator">=</span> <span class="tok-number">1</span>e<span class="tok-number">-6</span>

<span class="tok-keyword">func</span> <span class="tok-function">close</span>(_ a<span class="tok-operator">:</span> <span class="tok-type">CGRect</span>, _ b<span class="tok-operator">:</span> <span class="tok-type">CGRect</span>, tol<span class="tok-operator">:</span> <span class="tok-type">CGFloat</span> <span class="tok-operator">=</span> exact<span class="tok-type">Floor</span>) <span class="tok-operator">-</span><span class="tok-operator">&gt;</span> <span class="tok-type">Bool</span> {
    <span class="tok-function">abs</span>(a<span class="tok-operator">.</span>min<span class="tok-type">X</span> <span class="tok-operator">-</span> b<span class="tok-operator">.</span>min<span class="tok-type">X</span>) <span class="tok-operator">&lt;</span><span class="tok-operator">=</span> tol <span class="tok-operator">&amp;</span><span class="tok-operator">&amp;</span> <span class="tok-function">abs</span>(a<span class="tok-operator">.</span>min<span class="tok-type">Y</span> <span class="tok-operator">-</span> b<span class="tok-operator">.</span>min<span class="tok-type">Y</span>) <span class="tok-operator">&lt;</span><span class="tok-operator">=</span> tol <span class="tok-operator">&amp;</span><span class="tok-operator">&amp;</span>
        <span class="tok-function">abs</span>(a<span class="tok-operator">.</span>width <span class="tok-operator">-</span> b<span class="tok-operator">.</span>width) <span class="tok-operator">&lt;</span><span class="tok-operator">=</span> tol <span class="tok-operator">&amp;</span><span class="tok-operator">&amp;</span> <span class="tok-function">abs</span>(a<span class="tok-operator">.</span>height <span class="tok-operator">-</span> b<span class="tok-operator">.</span>height) <span class="tok-operator">&lt;</span><span class="tok-operator">=</span> tol
}</code></pre>
<p>When a layout test fails, the rule is to find why the behavior differs and fix the engine,
never to widen <code>exactFloor</code> until the failure disappears. A tolerance you are allowed to
relax is a wish, not a measurement.</p>
<h2>Measuring animation: hold the spring against SwiftUI’s Spring</h2>
<p>Animation is the most quantitative area, because motion is numbers over time. A spring is a
damped harmonic oscillator. Released from rest, its progress curve is</p>
<div class="td-math td-math-display"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 193.08 21.18" fill="currentColor" aria-hidden="true" style="width:19.31em;height:2.12em;vertical-align:-0.81em"><path d="M5.21 10.93C5.21 9.66 4.24 8.67 3.12 8.67C2.34 8.67 1.92 9.11 1.72 9.33L1.72 8.67L0.28 8.78L0.28 9.09C0.99 9.09 1.06 9.15 1.06 9.59L1.06 14.27C1.06 14.72 0.95 14.72 0.28 14.72L0.28 15.03L1.4 15L2.53 15.03L2.53 14.72C1.86 14.72 1.75 14.72 1.75 14.27L1.75 12.59L1.75 12.5C1.8 12.66 2.22 13.2 2.98 13.2C4.17 13.2 5.21 12.22 5.21 10.93ZM4.38 10.93C4.38 12.14 3.68 12.98 2.94 12.98C2.54 12.98 2.16 12.78 1.89 12.37C1.75 12.16 1.75 12.15 1.75 11.95L1.75 9.72C2.04 9.21 2.53 8.92 3.04 8.92C3.77 8.92 4.38 9.8 4.38 10.93Z"/><path d="M8.88 15.47C8.88 15.44 8.86 15.41 8.84 15.39C7.8 14.61 7.11 12.61 7.11 11.01L7.11 10.17C7.11 8.57 7.8 6.57 8.84 5.79C8.86 5.77 8.88 5.74 8.88 5.71C8.88 5.66 8.83 5.61 8.78 5.61C8.76 5.61 8.74 5.62 8.72 5.63C7.62 6.46 6.57 8.46 6.57 10.17L6.57 11.01C6.57 12.72 7.62 14.72 8.72 15.55C8.74 15.56 8.76 15.57 8.78 15.57C8.83 15.57 8.88 15.52 8.88 15.47Z"/><path d="M12.77 11.85L12.77 11.28L12.52 11.28L12.52 11.83C12.52 12.57 12.22 12.95 11.85 12.95C11.18 12.95 11.18 12.04 11.18 11.87L11.18 9.09L12.61 9.09L12.61 8.78L11.18 8.78L11.18 6.94L10.93 6.94C10.92 7.76 10.62 8.83 9.64 8.87L9.64 9.09L10.49 9.09L10.49 11.85C10.49 13.08 11.42 13.2 11.78 13.2C12.49 13.2 12.77 12.49 12.77 11.85Z"/><path d="M16.22 11.01L16.22 10.17C16.22 8.46 15.17 6.46 14.07 5.63C14.05 5.62 14.03 5.61 14.01 5.61C13.96 5.61 13.91 5.66 13.91 5.71C13.91 5.74 13.93 5.77 13.95 5.79C14.99 6.57 15.68 8.57 15.68 10.17L15.68 11.01C15.68 12.61 14.99 14.61 13.95 15.39C13.93 15.41 13.91 15.44 13.91 15.47C13.91 15.52 13.96 15.57 14.01 15.57C14.03 15.57 14.05 15.56 14.07 15.55C15.17 14.72 16.22 12.72 16.22 11.01Z"/><path d="M27.77 9.62C27.77 9.51 27.68 9.42 27.57 9.42L21.31 9.42C21.2 9.42 21.11 9.51 21.11 9.62C21.11 9.73 21.2 9.82 21.31 9.82L27.57 9.82C27.68 9.82 27.77 9.73 27.77 9.62ZM27.77 11.56C27.77 11.45 27.68 11.36 27.57 11.36L21.31 11.36C21.2 11.36 21.11 11.45 21.11 11.56C21.11 11.67 21.2 11.76 21.31 11.76L27.57 11.76C27.68 11.76 27.77 11.67 27.77 11.56Z"/><path d="M35.84 13.09L35.84 12.78L35.52 12.78C34.62 12.78 34.59 12.67 34.59 12.3L34.59 6.69C34.59 6.45 34.59 6.43 34.36 6.43C33.74 7.07 32.86 7.07 32.54 7.07L32.54 7.38C32.74 7.38 33.33 7.38 33.85 7.12L33.85 12.3C33.85 12.66 33.82 12.78 32.92 12.78L32.6 12.78L32.6 13.09C32.95 13.06 33.82 13.06 34.22 13.06C34.62 13.06 35.49 13.06 35.84 13.09Z"/><path d="M42.73 11.22L42.73 10.64L40.08 10.64L40.08 11.22Z"/><path d="M50.77 11.9C50.77 11.8 50.69 11.78 50.64 11.78C50.55 11.78 50.53 11.84 50.51 11.92C50.16 12.95 49.26 12.95 49.16 12.95C48.66 12.95 48.26 12.65 48.03 12.28C47.73 11.8 47.73 11.14 47.73 10.78L50.52 10.78C50.74 10.78 50.77 10.78 50.77 10.57C50.77 9.58 50.23 8.61 48.98 8.61C47.82 8.61 46.9 9.64 46.9 10.89C46.9 12.23 47.95 13.2 49.1 13.2C50.32 13.2 50.77 12.09 50.77 11.9ZM50.11 10.57L47.74 10.57C47.8 9.08 48.64 8.83 48.98 8.83C50.01 8.83 50.11 10.18 50.11 10.57Z"/><path d="M53.55 8.15L53.55 7.75L51.7 7.75L51.7 8.15Z"/><path d="M57.34 10.05C57.27 9.78 57.1 9.64 57.03 9.58C56.88 9.45 56.74 9.41 56.29 9.27C55.68 9.08 55.56 9.04 55.36 8.9C54.99 8.63 54.9 8.23 54.86 8.1C54.6 7.01 55.14 5.79 55.75 5.41C55.91 5.54 56.07 5.54 56.21 5.54C56.39 5.54 56.84 5.54 56.78 5.32C56.74 5.14 56.45 5.13 56.17 5.13C56.09 5.13 55.9 5.13 55.74 5.2C55.69 5.12 55.67 5.08 55.62 4.93C55.6 4.84 55.6 4.67 55.6 4.67C55.58 4.62 55.54 4.58 55.49 4.58C55.37 4.58 55.44 4.91 55.45 4.93C55.48 5.05 55.53 5.17 55.62 5.28C54.97 5.65 54.19 6.94 54.53 8.29C54.78 9.28 55.46 9.49 56.01 9.66C56.24 9.74 56.25 9.74 56.47 9.81C56.63 9.85 56.94 9.95 57.02 10.28C57.07 10.47 57.01 10.74 56.75 10.74C56.57 10.74 56.36 10.66 56.18 10.53C56.12 10.49 56.11 10.48 56.08 10.48C56.04 10.48 56.02 10.51 56.03 10.56C56.05 10.63 56.42 10.9 56.79 10.9C57.22 10.9 57.44 10.45 57.34 10.05ZM56.6 5.33C56.55 5.35 56.49 5.38 56.18 5.38C56.03 5.38 55.98 5.38 55.9 5.33C55.99 5.28 56.11 5.28 56.21 5.28C56.44 5.28 56.46 5.29 56.6 5.33Z"/><path d="M64.16 7.79C64.16 7.49 64.16 7.14 64.09 6.86C64.05 6.68 63.97 6.52 63.84 6.43C63.79 6.39 63.73 6.37 63.67 6.37C63.58 6.37 63.5 6.41 63.45 6.48C63.4 6.55 63.39 6.63 63.41 6.7C63.42 6.77 63.46 6.83 63.53 6.88C63.74 7.03 63.88 7.25 63.95 7.5C63.97 7.58 63.98 7.67 63.98 7.75C63.98 8.39 63.74 9.04 63.18 9.04C62.79 9.04 62.49 8.77 62.34 8.41C62.38 8.21 62.4 8 62.39 7.79C62.39 7.73 62.39 7.66 62.38 7.61C62.35 7.5 62.3 7.43 62.2 7.43C62.06 7.43 62.04 7.6 62.04 7.75C62.04 7.97 62.06 8.18 62.11 8.38C61.97 8.76 61.71 9.04 61.32 9.04C60.9 9.04 60.62 8.69 60.52 8.24C60.48 8.1 60.46 7.94 60.45 7.79C60.45 7.37 60.61 6.98 60.78 6.61C60.8 6.58 60.8 6.54 60.8 6.52C60.78 6.47 60.75 6.44 60.71 6.42C60.69 6.4 60.66 6.4 60.64 6.4C60.6 6.4 60.56 6.42 60.53 6.47C60.34 6.87 60.28 7.31 60.28 7.75C60.28 8.02 60.31 8.29 60.37 8.54C60.51 9.11 60.83 9.54 61.36 9.54C61.8 9.54 62.09 9.23 62.24 8.8C62.43 9.23 62.75 9.54 63.22 9.54C63.95 9.54 64.16 8.64 64.16 7.79Z"/><path d="M67.12 9.62C67.12 9.23 67.09 8.84 66.92 8.48C66.7 8.01 66.29 7.93 66.09 7.93C65.8 7.93 65.44 8.06 65.24 8.51C65.08 8.85 65.06 9.23 65.06 9.62C65.06 9.99 65.07 10.43 65.28 10.8C65.49 11.2 65.84 11.3 66.08 11.3C66.35 11.3 66.72 11.2 66.94 10.73C67.09 10.39 67.12 10.01 67.12 9.62ZM66.71 9.56C66.71 9.93 66.71 10.27 66.66 10.58C66.58 11.04 66.3 11.19 66.08 11.19C65.89 11.19 65.6 11.07 65.52 10.6C65.46 10.3 65.46 9.85 65.46 9.56C65.46 9.25 65.46 8.93 65.5 8.66C65.59 8.08 65.96 8.04 66.08 8.04C66.25 8.04 66.57 8.12 66.66 8.61C66.71 8.88 66.71 9.26 66.71 9.56Z"/><path d="M71.96 8.59L71.96 8.2L71.79 8.2L71.79 8.58C71.79 9.1 71.58 9.36 71.32 9.36C70.85 9.36 70.85 8.73 70.85 8.61L70.85 6.66L71.85 6.66L71.85 6.45L70.85 6.45L70.85 5.16L70.67 5.16C70.67 5.73 70.46 6.48 69.77 6.51L69.77 6.66L70.37 6.66L70.37 8.59C70.37 9.46 71.02 9.54 71.27 9.54C71.77 9.54 71.96 9.04 71.96 8.59Z"/><path d="M74.92 15.39C74.92 15.28 74.83 15.19 74.72 15.19L73.9 15.19L73.9 5.99L74.72 5.99C74.83 5.99 74.92 5.9 74.92 5.79C74.92 5.68 74.83 5.59 74.72 5.59L73.5 5.59L73.5 15.59L74.72 15.59C74.83 15.59 74.92 15.5 74.92 15.39Z"/><path d="M79.29 11.9C79.29 11.8 79.19 11.8 79.16 11.8C79.07 11.8 79.05 11.84 79.03 11.9C78.74 12.83 78.09 12.95 77.72 12.95C77.19 12.95 76.31 12.52 76.31 10.91C76.31 9.28 77.13 8.86 77.66 8.86C77.75 8.86 78.38 8.87 78.73 9.23C78.32 9.26 78.26 9.56 78.26 9.69C78.26 9.95 78.44 10.15 78.72 10.15C78.98 10.15 79.18 9.98 79.18 9.68C79.18 9 78.42 8.61 77.65 8.61C76.4 8.61 75.48 9.69 75.48 10.93C75.48 12.21 76.47 13.2 77.63 13.2C78.97 13.2 79.29 12 79.29 11.9Z"/><path d="M84.29 10.95C84.29 9.67 83.29 8.61 82.08 8.61C80.83 8.61 79.86 9.7 79.86 10.95C79.86 12.24 80.9 13.2 82.07 13.2C83.28 13.2 84.29 12.22 84.29 10.95ZM83.46 10.87C83.46 11.23 83.46 11.77 83.24 12.21C83.02 12.66 82.58 12.95 82.08 12.95C81.65 12.95 81.21 12.74 80.94 12.28C80.69 11.84 80.69 11.23 80.69 10.87C80.69 10.48 80.69 9.94 80.93 9.5C81.2 9.04 81.67 8.83 82.07 8.83C82.51 8.83 82.94 9.05 83.2 9.48C83.46 9.91 83.46 10.49 83.46 10.87Z"/><path d="M88.18 11.81C88.18 11.28 87.88 10.98 87.76 10.86C87.43 10.54 87.04 10.46 86.62 10.38C86.06 10.27 85.39 10.14 85.39 9.56C85.39 9.21 85.65 8.8 86.51 8.8C87.61 8.8 87.66 9.7 87.68 10.01C87.69 10.1 87.8 10.1 87.8 10.1C87.93 10.1 87.93 10.05 87.93 9.86L87.93 8.85C87.93 8.68 87.93 8.61 87.82 8.61C87.77 8.61 87.75 8.61 87.62 8.73C87.59 8.77 87.49 8.86 87.45 8.89C87.07 8.61 86.66 8.61 86.51 8.61C85.29 8.61 84.91 9.28 84.91 9.84C84.91 10.19 85.07 10.47 85.34 10.69C85.66 10.95 85.94 11.01 86.66 11.15C86.88 11.19 87.7 11.35 87.7 12.07C87.7 12.58 87.35 12.98 86.57 12.98C85.73 12.98 85.37 12.41 85.18 11.56C85.15 11.43 85.14 11.39 85.04 11.39C84.91 11.39 84.91 11.46 84.91 11.64L84.91 12.96C84.91 13.13 84.91 13.2 85.02 13.2C85.07 13.2 85.08 13.19 85.27 13C85.29 12.98 85.29 12.96 85.47 12.77C85.91 13.19 86.36 13.2 86.57 13.2C87.72 13.2 88.18 12.53 88.18 11.81Z"/><path d="M91.84 15.47C91.84 15.44 91.82 15.41 91.8 15.39C90.76 14.61 90.07 12.61 90.07 11.01L90.07 10.17C90.07 8.57 90.76 6.57 91.8 5.79C91.82 5.77 91.84 5.74 91.84 5.71C91.84 5.66 91.79 5.61 91.74 5.61C91.72 5.61 91.7 5.62 91.68 5.63C90.58 6.46 89.53 8.46 89.53 10.17L89.53 11.01C89.53 12.72 90.58 14.72 91.68 15.55C91.7 15.56 91.72 15.57 91.74 15.57C91.79 15.57 91.84 15.52 91.84 15.47Z"/><path d="M98.37 10.7C98.38 10.27 98.37 9.77 98.27 9.37C98.21 9.11 98.1 8.89 97.92 8.76C97.84 8.7 97.76 8.67 97.67 8.67C97.55 8.67 97.43 8.73 97.36 8.83C97.29 8.93 97.27 9.04 97.3 9.15C97.32 9.25 97.38 9.33 97.47 9.4C97.77 9.62 97.98 9.93 98.07 10.28C98.1 10.4 98.12 10.53 98.12 10.65C98.12 11.56 97.77 12.48 96.98 12.48C96.42 12.48 95.99 12.1 95.77 11.58C95.83 11.3 95.86 11 95.85 10.7C95.85 10.61 95.85 10.52 95.83 10.44C95.79 10.29 95.71 10.18 95.57 10.18C95.38 10.18 95.35 10.43 95.35 10.65C95.35 10.96 95.38 11.26 95.45 11.55C95.25 12.09 94.87 12.48 94.32 12.48C93.71 12.48 93.32 11.98 93.17 11.35C93.12 11.15 93.09 10.92 93.08 10.7C93.08 10.1 93.3 9.54 93.55 9.01C93.57 8.97 93.58 8.92 93.57 8.88C93.55 8.82 93.5 8.77 93.44 8.74C93.41 8.72 93.38 8.72 93.35 8.72C93.29 8.72 93.23 8.75 93.19 8.81C92.92 9.38 92.83 10.02 92.83 10.65C92.83 11.03 92.87 11.42 92.96 11.77C93.16 12.58 93.61 13.2 94.37 13.2C95 13.2 95.41 12.76 95.63 12.14C95.9 12.76 96.36 13.2 97.03 13.2C98.07 13.2 98.38 11.91 98.37 10.7Z"/><path d="M103.07 15.56L103.07 15.35C102.58 15.35 102.52 15.3 102.52 14.95L102.52 10.7L101.52 10.78L101.52 11C102.01 11 102.06 11.05 102.06 11.39L102.06 12.9C101.86 12.65 101.56 12.47 101.18 12.47C100.35 12.47 99.62 13.15 99.62 14.06C99.62 14.95 100.31 15.64 101.1 15.64C101.55 15.64 101.87 15.4 102.04 15.18L102.04 15.64ZM102.04 14.74C102.04 14.86 102.04 14.88 101.96 15C101.75 15.33 101.44 15.49 101.14 15.49C100.82 15.49 100.57 15.3 100.4 15.04C100.22 14.75 100.2 14.35 100.2 14.06C100.2 13.81 100.21 13.39 100.42 13.07C100.56 12.85 100.83 12.62 101.21 12.62C101.45 12.62 101.75 12.73 101.96 13.04C102.04 13.16 102.04 13.18 102.04 13.3Z"/><path d="M109.91 11.85L109.91 11.28L109.66 11.28L109.66 11.83C109.66 12.57 109.36 12.95 108.99 12.95C108.32 12.95 108.32 12.04 108.32 11.87L108.32 9.09L109.75 9.09L109.75 8.78L108.32 8.78L108.32 6.94L108.07 6.94C108.06 7.76 107.76 8.83 106.78 8.87L106.78 9.09L107.63 9.09L107.63 11.85C107.63 13.08 108.56 13.2 108.92 13.2C109.63 13.2 109.91 12.49 109.91 11.85Z"/><path d="M113.36 11.01L113.36 10.17C113.36 8.46 112.31 6.46 111.21 5.63C111.19 5.62 111.17 5.61 111.15 5.61C111.1 5.61 111.05 5.66 111.05 5.71C111.05 5.74 111.07 5.77 111.09 5.79C112.13 6.57 112.82 8.57 112.82 10.17L112.82 11.01C112.82 12.61 112.13 14.61 111.09 15.39C111.07 15.41 111.05 15.44 111.05 15.47C111.05 15.52 111.1 15.57 111.15 15.57C111.17 15.57 111.19 15.56 111.21 15.55C112.31 14.72 113.36 12.72 113.36 11.01Z"/><path d="M124.91 10.59C124.91 10.48 124.82 10.39 124.71 10.39L121.78 10.39L121.78 7.46C121.78 7.35 121.69 7.26 121.58 7.26C121.47 7.26 121.38 7.35 121.38 7.46L121.38 10.39L118.45 10.39C118.34 10.39 118.25 10.48 118.25 10.59C118.25 10.7 118.34 10.79 118.45 10.79L121.38 10.79L121.38 13.72C121.38 13.83 121.47 13.92 121.58 13.92C121.69 13.92 121.78 13.83 121.78 13.72L121.78 10.79L124.71 10.79C124.82 10.79 124.91 10.7 124.91 10.59Z"/><rect x="128.79" y="10.39" width="20.05" height="0.4"/><path d="M134.96 6.59C134.88 6.28 134.68 6.11 134.6 6.04C134.43 5.89 134.26 5.84 133.73 5.67C133.02 5.45 132.88 5.41 132.64 5.24C132.21 4.93 132.1 4.46 132.06 4.3C131.75 3.03 132.39 1.6 133.1 1.16C133.29 1.3 133.48 1.3 133.64 1.3C133.85 1.3 134.37 1.3 134.31 1.05C134.26 0.84 133.92 0.83 133.59 0.83C133.5 0.83 133.28 0.83 133.09 0.91C133.03 0.81 133 0.77 132.95 0.6C132.93 0.48 132.92 0.3 132.92 0.3C132.9 0.23 132.85 0.19 132.8 0.19C132.65 0.19 132.74 0.57 132.75 0.59C132.78 0.73 132.85 0.88 132.94 1C132.19 1.43 131.27 2.94 131.67 4.53C131.96 5.69 132.76 5.94 133.4 6.13C133.67 6.22 133.69 6.22 133.95 6.31C134.13 6.36 134.49 6.48 134.59 6.86C134.65 7.08 134.58 7.4 134.27 7.4C134.06 7.4 133.81 7.31 133.6 7.16C133.54 7.11 133.52 7.1 133.49 7.1C133.44 7.1 133.41 7.13 133.43 7.19C133.45 7.27 133.88 7.58 134.31 7.58C134.82 7.58 135.08 7.06 134.96 6.59ZM134.09 1.06C134.04 1.08 133.96 1.12 133.6 1.12C133.43 1.12 133.37 1.12 133.27 1.07C133.38 1.01 133.53 1.01 133.63 1.01C133.9 1.01 133.94 1.02 134.09 1.06Z"/><path d="M142.95 3.94C142.96 3.59 142.95 3.18 142.87 2.85C142.82 2.64 142.73 2.46 142.58 2.35C142.51 2.3 142.45 2.28 142.37 2.28C142.28 2.28 142.18 2.33 142.12 2.41C142.06 2.49 142.05 2.58 142.07 2.67C142.09 2.76 142.14 2.82 142.21 2.88C142.46 3.06 142.63 3.31 142.7 3.6C142.73 3.7 142.74 3.8 142.74 3.9C142.74 4.65 142.46 5.4 141.81 5.4C141.35 5.4 141 5.09 140.82 4.67C140.87 4.44 140.89 4.19 140.88 3.94C140.88 3.87 140.88 3.8 140.87 3.73C140.83 3.61 140.77 3.52 140.65 3.52C140.5 3.52 140.47 3.72 140.47 3.9C140.47 4.16 140.5 4.4 140.55 4.64C140.39 5.08 140.08 5.4 139.63 5.4C139.13 5.4 138.81 4.99 138.68 4.48C138.64 4.31 138.62 4.12 138.61 3.94C138.61 3.45 138.79 2.99 139 2.56C139.01 2.53 139.02 2.48 139.01 2.45C139 2.4 138.96 2.36 138.91 2.34C138.88 2.32 138.86 2.32 138.83 2.32C138.78 2.32 138.73 2.35 138.7 2.39C138.48 2.86 138.41 3.39 138.41 3.9C138.41 4.21 138.44 4.53 138.51 4.82C138.68 5.49 139.05 5.99 139.67 5.99C140.19 5.99 140.52 5.63 140.7 5.12C140.92 5.63 141.3 5.99 141.85 5.99C142.7 5.99 142.96 4.94 142.95 3.94Z"/><path d="M146.42 6.09C146.42 5.63 146.39 5.17 146.19 4.75C145.92 4.2 145.45 4.11 145.21 4.11C144.87 4.11 144.45 4.26 144.21 4.79C144.03 5.19 144 5.63 144 6.09C144 6.52 144.02 7.04 144.26 7.48C144.51 7.94 144.92 8.06 145.21 8.06C145.52 8.06 145.95 7.94 146.2 7.39C146.39 6.99 146.42 6.55 146.42 6.09ZM145.94 6.02C145.94 6.45 145.94 6.84 145.88 7.21C145.79 7.76 145.46 7.93 145.21 7.93C144.98 7.93 144.64 7.79 144.54 7.23C144.48 6.89 144.48 6.36 144.48 6.02C144.48 5.66 144.48 5.28 144.52 4.97C144.63 4.28 145.06 4.23 145.21 4.23C145.4 4.23 145.77 4.34 145.88 4.9C145.94 5.23 145.94 5.66 145.94 6.02Z"/><path d="M139.25 15.94C139.26 15.58 139.25 15.17 139.17 14.85C139.12 14.63 139.03 14.45 138.88 14.35C138.82 14.3 138.75 14.27 138.68 14.27C138.58 14.27 138.48 14.32 138.43 14.4C138.37 14.48 138.35 14.58 138.38 14.67C138.39 14.75 138.44 14.81 138.52 14.87C138.76 15.05 138.93 15.3 139.01 15.59C139.03 15.69 139.05 15.8 139.05 15.9C139.05 16.64 138.76 17.4 138.11 17.4C137.65 17.4 137.3 17.08 137.12 16.66C137.17 16.43 137.2 16.18 137.19 15.94C137.19 15.86 137.19 15.79 137.17 15.72C137.14 15.6 137.07 15.51 136.96 15.51C136.8 15.51 136.78 15.71 136.78 15.9C136.78 16.15 136.8 16.4 136.86 16.63C136.7 17.08 136.38 17.4 135.93 17.4C135.43 17.4 135.11 16.99 134.99 16.47C134.95 16.31 134.92 16.12 134.92 15.94C134.92 15.44 135.1 14.99 135.3 14.55C135.32 14.52 135.33 14.48 135.32 14.44C135.3 14.39 135.26 14.35 135.21 14.33C135.19 14.31 135.16 14.31 135.14 14.31C135.09 14.31 135.04 14.34 135.01 14.39C134.78 14.85 134.71 15.38 134.71 15.9C134.71 16.21 134.74 16.53 134.82 16.81C134.98 17.48 135.35 17.99 135.97 17.99C136.49 17.99 136.83 17.63 137.01 17.12C137.23 17.63 137.61 17.99 138.15 17.99C139.01 17.99 139.26 16.93 139.25 15.94Z"/><path d="M143.11 19.92L143.11 19.74C142.71 19.74 142.66 19.7 142.66 19.42L142.66 15.94L141.83 16L141.83 16.18C142.23 16.18 142.28 16.22 142.28 16.5L142.28 17.74C142.11 17.53 141.87 17.38 141.56 17.38C140.88 17.38 140.28 17.95 140.28 18.69C140.28 19.42 140.84 19.98 141.49 19.98C141.86 19.98 142.12 19.79 142.26 19.61L142.26 19.98ZM142.26 19.24C142.26 19.35 142.26 19.36 142.2 19.46C142.03 19.73 141.77 19.86 141.52 19.86C141.26 19.86 141.06 19.71 140.92 19.49C140.77 19.26 140.75 18.93 140.75 18.69C140.75 18.48 140.76 18.14 140.93 17.88C141.05 17.7 141.27 17.51 141.58 17.51C141.78 17.51 142.02 17.6 142.2 17.86C142.26 17.95 142.26 17.96 142.26 18.07Z"/><path d="M155.77 11.81C155.77 11.28 155.47 10.98 155.35 10.86C155.02 10.54 154.63 10.46 154.21 10.38C153.65 10.27 152.98 10.14 152.98 9.56C152.98 9.21 153.24 8.8 154.1 8.8C155.2 8.8 155.25 9.7 155.27 10.01C155.28 10.1 155.39 10.1 155.39 10.1C155.52 10.1 155.52 10.05 155.52 9.86L155.52 8.85C155.52 8.68 155.52 8.61 155.41 8.61C155.36 8.61 155.34 8.61 155.21 8.73C155.18 8.77 155.08 8.86 155.04 8.89C154.66 8.61 154.25 8.61 154.1 8.61C152.88 8.61 152.5 9.28 152.5 9.84C152.5 10.19 152.66 10.47 152.93 10.69C153.25 10.95 153.53 11.01 154.25 11.15C154.47 11.19 155.29 11.35 155.29 12.07C155.29 12.58 154.94 12.98 154.16 12.98C153.32 12.98 152.96 12.41 152.77 11.56C152.74 11.43 152.73 11.39 152.63 11.39C152.5 11.39 152.5 11.46 152.5 11.64L152.5 12.96C152.5 13.13 152.5 13.2 152.61 13.2C152.66 13.2 152.67 13.19 152.86 13C152.88 12.98 152.88 12.96 153.06 12.77C153.5 13.19 153.95 13.2 154.16 13.2C155.31 13.2 155.77 12.53 155.77 11.81Z"/><path d="M158.58 13.09L158.58 12.78C157.92 12.78 157.88 12.73 157.88 12.34L157.88 8.67L156.48 8.78L156.48 9.09C157.13 9.09 157.22 9.15 157.22 9.64L157.22 12.33C157.22 12.78 157.11 12.78 156.44 12.78L156.44 13.09L157.54 13.06C157.89 13.06 158.24 13.08 158.58 13.09ZM158.03 7.05C158.03 6.78 157.8 6.52 157.5 6.52C157.16 6.52 156.96 6.8 156.96 7.05C156.96 7.32 157.19 7.58 157.49 7.58C157.83 7.58 158.03 7.3 158.03 7.05Z"/><path d="M164.24 13.09L164.24 12.78C163.72 12.78 163.47 12.78 163.46 12.48L163.46 10.57C163.46 9.71 163.46 9.4 163.15 9.04C163.01 8.87 162.68 8.67 162.1 8.67C161.37 8.67 160.9 9.1 160.62 9.72L160.62 8.67L159.21 8.78L159.21 9.09C159.91 9.09 159.99 9.16 159.99 9.65L159.99 12.33C159.99 12.78 159.88 12.78 159.21 12.78L159.21 13.09L160.34 13.06L161.46 13.09L161.46 12.78C160.79 12.78 160.68 12.78 160.68 12.33L160.68 10.49C160.68 9.45 161.39 8.89 162.03 8.89C162.66 8.89 162.77 9.43 162.77 10L162.77 12.33C162.77 12.78 162.66 12.78 161.99 12.78L161.99 13.09L163.12 13.06Z"/><path d="M167.77 15.47C167.77 15.44 167.75 15.41 167.73 15.39C166.69 14.61 166 12.61 166 11.01L166 10.17C166 8.57 166.69 6.57 167.73 5.79C167.75 5.77 167.77 5.74 167.77 5.71C167.77 5.66 167.72 5.61 167.67 5.61C167.65 5.61 167.63 5.62 167.61 5.63C166.51 6.46 165.46 8.46 165.46 10.17L165.46 11.01C165.46 12.72 166.51 14.72 167.61 15.55C167.63 15.56 167.65 15.57 167.67 15.57C167.72 15.57 167.77 15.52 167.77 15.47Z"/><path d="M174.3 10.7C174.31 10.27 174.3 9.77 174.2 9.37C174.14 9.11 174.03 8.89 173.85 8.76C173.77 8.7 173.69 8.67 173.6 8.67C173.48 8.67 173.36 8.73 173.29 8.83C173.22 8.93 173.2 9.04 173.23 9.15C173.25 9.25 173.31 9.33 173.4 9.4C173.7 9.62 173.91 9.93 174 10.28C174.03 10.4 174.05 10.53 174.05 10.65C174.05 11.56 173.7 12.48 172.91 12.48C172.35 12.48 171.92 12.1 171.7 11.58C171.76 11.3 171.79 11 171.78 10.7C171.78 10.61 171.78 10.52 171.76 10.44C171.72 10.29 171.64 10.18 171.5 10.18C171.31 10.18 171.28 10.43 171.28 10.65C171.28 10.96 171.31 11.26 171.38 11.55C171.18 12.09 170.8 12.48 170.25 12.48C169.64 12.48 169.25 11.98 169.1 11.35C169.05 11.15 169.02 10.92 169.01 10.7C169.01 10.1 169.23 9.54 169.48 9.01C169.5 8.97 169.51 8.92 169.5 8.88C169.48 8.82 169.43 8.77 169.37 8.74C169.34 8.72 169.31 8.72 169.28 8.72C169.22 8.72 169.16 8.75 169.12 8.81C168.85 9.38 168.76 10.02 168.76 10.65C168.76 11.03 168.8 11.42 168.89 11.77C169.09 12.58 169.54 13.2 170.3 13.2C170.93 13.2 171.34 12.76 171.56 12.14C171.83 12.76 172.29 13.2 172.96 13.2C174 13.2 174.31 11.91 174.3 10.7Z"/><path d="M179 15.56L179 15.35C178.51 15.35 178.45 15.3 178.45 14.95L178.45 10.7L177.44 10.78L177.44 11C177.93 11 177.99 11.05 177.99 11.39L177.99 12.9C177.78 12.65 177.48 12.47 177.11 12.47C176.28 12.47 175.54 13.15 175.54 14.06C175.54 14.95 176.23 15.64 177.03 15.64C177.48 15.64 177.79 15.4 177.97 15.18L177.97 15.64ZM177.97 14.74C177.97 14.86 177.97 14.88 177.89 15C177.68 15.33 177.36 15.49 177.06 15.49C176.75 15.49 176.5 15.3 176.33 15.04C176.15 14.75 176.13 14.35 176.13 14.06C176.13 13.81 176.14 13.39 176.34 13.07C176.49 12.85 176.76 12.62 177.13 12.62C177.38 12.62 177.67 12.73 177.89 13.04C177.97 13.16 177.97 13.18 177.97 13.3Z"/><path d="M185.84 11.85L185.84 11.28L185.59 11.28L185.59 11.83C185.59 12.57 185.29 12.95 184.92 12.95C184.25 12.95 184.25 12.04 184.25 11.87L184.25 9.09L185.68 9.09L185.68 8.78L184.25 8.78L184.25 6.94L184 6.94C183.99 7.76 183.69 8.83 182.71 8.87L182.71 9.09L183.56 9.09L183.56 11.85C183.56 13.08 184.49 13.2 184.85 13.2C185.56 13.2 185.84 12.49 185.84 11.85Z"/><path d="M189.29 11.01L189.29 10.17C189.29 8.46 188.24 6.46 187.14 5.63C187.12 5.62 187.1 5.61 187.08 5.61C187.03 5.61 186.98 5.66 186.98 5.71C186.98 5.74 187 5.77 187.02 5.79C188.06 6.57 188.75 8.57 188.75 10.17L188.75 11.01C188.75 12.61 188.06 14.61 187.02 15.39C187 15.41 186.98 15.44 186.98 15.47C186.98 15.52 187.03 15.57 187.08 15.57C187.1 15.57 187.12 15.56 187.14 15.55C188.24 14.72 189.29 12.72 189.29 11.01Z"/><path d="M191.94 15.59L191.94 5.59L190.72 5.59C190.61 5.59 190.52 5.68 190.52 5.79C190.52 5.9 190.61 5.99 190.72 5.99L191.54 5.99L191.54 15.19L190.72 15.19C190.61 15.19 190.52 15.28 190.52 15.39C190.52 15.5 190.61 15.59 190.72 15.59Z"/></svg><math class="td-math-a11y" xmlns="http://www.w3.org/1998/Math/MathML"><mrow><mi>p</mi><mtext>(</mtext><mi>t</mi><mtext>)</mtext><mtext> </mtext><mtext>=</mtext><mtext> </mtext><mn>1</mn><mtext> </mtext><mtext>-</mtext><mtext> </mtext><msup><mrow><mi>e</mi></mrow><mrow><mrow><mtext>-</mtext><mo>ζ</mo><mtext> </mtext><msub><mrow><mo>ω</mo></mrow><mrow><mn>0</mn></mrow></msub><mtext> </mtext><mi>t</mi></mrow></mrow></msup><mrow><mtext>[</mtext><mrow><mo>cos</mo><mtext>(</mtext><msub><mrow><mo>ω</mo></mrow><mrow><mi>d</mi></mrow></msub><mtext> </mtext><mi>t</mi><mtext>)</mtext><mtext> </mtext><mtext>+</mtext><mtext> </mtext><mfrac><mrow><mrow><mo>ζ</mo><mtext> </mtext><msub><mrow><mo>ω</mo></mrow><mrow><mn>0</mn></mrow></msub></mrow></mrow><mrow><msub><mrow><mo>ω</mo></mrow><mrow><mi>d</mi></mrow></msub></mrow></mfrac><mtext> </mtext><mo>sin</mo><mtext>(</mtext><msub><mrow><mo>ω</mo></mrow><mrow><mi>d</mi></mrow></msub><mtext> </mtext><mi>t</mi><mtext>)</mtext></mrow><mtext>]</mtext></mrow></mrow></math></div>
<p>which starts at 0, sweeps past 1, and settles back. We sample the engine’s spring and
SwiftUI’s own <code>Spring</code> at the same times and assert they agree across all three damping
regimes.</p>
<pre><code class="language-swift"><span class="tok-attribute">@available</span>(mac<span class="tok-type">OS</span> <span class="tok-number">14</span>, i<span class="tok-type">OS</span> <span class="tok-number">17</span>, <span class="tok-operator">*</span>)
<span class="tok-keyword">private</span> <span class="tok-keyword">func</span> <span class="tok-function">assertSpringMatches</span>(
    response<span class="tok-operator">:</span> <span class="tok-type">Double</span>,
    damping<span class="tok-type">Fraction</span><span class="tok-operator">:</span> <span class="tok-type">Double</span>,
    tolerance<span class="tok-operator">:</span> <span class="tok-type">Double</span> <span class="tok-operator">=</span> <span class="tok-number">1</span>e<span class="tok-number">-6</span>
) {
    <span class="tok-keyword">let</span> engine <span class="tok-operator">=</span> <span class="tok-type">PureView</span><span class="tok-operator">.</span><span class="tok-type">Spring</span>(response<span class="tok-operator">:</span> response, damping<span class="tok-type">Fraction</span><span class="tok-operator">:</span> damping<span class="tok-type">Fraction</span>)
    <span class="tok-keyword">let</span> oracle <span class="tok-operator">=</span> <span class="tok-type">SwiftUI</span><span class="tok-operator">.</span><span class="tok-type">Spring</span>(response<span class="tok-operator">:</span> response, damping<span class="tok-type">Ratio</span><span class="tok-operator">:</span> damping<span class="tok-type">Fraction</span>)
    <span class="tok-keyword">let</span> samples <span class="tok-operator">=</span> <span class="tok-number">120</span>
    <span class="tok-keyword">for</span> step <span class="tok-keyword">in</span> <span class="tok-number">0</span> <span class="tok-operator">.</span><span class="tok-operator">.</span><span class="tok-operator">.</span> samples {
        <span class="tok-keyword">let</span> t <span class="tok-operator">=</span> engine<span class="tok-operator">.</span>settle<span class="tok-type">Duration</span> <span class="tok-operator">*</span> <span class="tok-type">Double</span>(step) <span class="tok-operator">/</span> <span class="tok-type">Double</span>(samples)
        <span class="tok-keyword">let</span> engine<span class="tok-type">Value</span> <span class="tok-operator">=</span> engine<span class="tok-operator">.</span><span class="tok-function">unitStepResponse</span>(at<span class="tok-operator">:</span> t)
        <span class="tok-keyword">let</span> oracle<span class="tok-type">Value</span> <span class="tok-operator">=</span> oracle<span class="tok-operator">.</span><span class="tok-function">value</span>(target<span class="tok-operator">:</span> <span class="tok-number">1.0</span>, initial<span class="tok-type">Velocity</span><span class="tok-operator">:</span> <span class="tok-number">0.0</span>, time<span class="tok-operator">:</span> t)
        #<span class="tok-function">expect</span>(
            <span class="tok-function">abs</span>(engine<span class="tok-type">Value</span> <span class="tok-operator">-</span> oracle<span class="tok-type">Value</span>) <span class="tok-operator">&lt;</span><span class="tok-operator">=</span> tolerance,
            <span class="tok-string">"spring(response=\(response), damping=\(dampingFraction)) at t=\(t): "</span> <span class="tok-operator">+</span>
            <span class="tok-string">"engine \(engineValue) vs SwiftUI \(oracleValue)"</span>
        )
    }
}</code></pre>
<p>The assertion is the whole point, in one line:</p>
<div class="td-math td-math-display"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180.68 25.22" fill="currentColor" aria-hidden="true" style="width:18.07em;height:2.52em;vertical-align:-1.22em"><path d="M14.68 13L14.68 12.44C13.74 12.44 13.29 12.44 13.27 11.9L13.27 8.45C13.27 6.9 13.27 6.34 12.71 5.69C12.46 5.38 11.86 5.02 10.82 5.02C9.3 5.02 8.5 6.1 8.2 6.79C7.94 5.22 6.61 5.02 5.8 5.02C4.48 5.02 3.63 5.8 3.12 6.92L3.12 5.02L0.58 5.22L0.58 5.78C1.84 5.78 1.99 5.9 1.99 6.79L1.99 11.63C1.99 12.44 1.79 12.44 0.58 12.44L0.58 13L2.62 12.95L4.64 13L4.64 12.44C3.43 12.44 3.23 12.44 3.23 11.63L3.23 8.31C3.23 6.43 4.51 5.42 5.67 5.42C6.81 5.42 7.01 6.39 7.01 7.42L7.01 11.63C7.01 12.44 6.81 12.44 5.6 12.44L5.6 13L7.64 12.95L9.66 13L9.66 12.44C8.45 12.44 8.25 12.44 8.25 11.63L8.25 8.31C8.25 6.43 9.53 5.42 10.69 5.42C11.83 5.42 12.03 6.39 12.03 7.42L12.03 11.63C12.03 12.44 11.83 12.44 10.62 12.44L10.62 13L12.66 12.95Z"/><path d="M23.76 11.39L23.76 10.38L23.31 10.38L23.31 11.39C23.31 12.44 22.86 12.55 22.66 12.55C22.06 12.55 21.99 11.74 21.99 11.65L21.99 8.03C21.99 7.28 21.99 6.57 21.34 5.9C20.64 5.2 19.73 4.91 18.87 4.91C17.39 4.91 16.14 5.76 16.14 6.95C16.14 7.49 16.5 7.8 16.97 7.8C17.48 7.8 17.8 7.44 17.8 6.97C17.8 6.75 17.71 6.16 16.88 6.14C17.37 5.51 18.25 5.31 18.83 5.31C19.72 5.31 20.75 6.01 20.75 7.62L20.75 8.29C19.83 8.34 18.56 8.4 17.42 8.94C16.07 9.55 15.62 10.49 15.62 11.28C15.62 12.75 17.37 13.2 18.51 13.2C19.7 13.2 20.53 12.48 20.87 11.63C20.94 12.35 21.43 13.11 22.28 13.11C22.66 13.11 23.76 12.86 23.76 11.39ZM20.75 10.47C20.75 12.19 19.45 12.8 18.63 12.8C17.75 12.8 17.01 12.17 17.01 11.27C17.01 10.27 17.77 8.77 20.75 8.67Z"/><path d="M33.38 13L33.38 12.44C32.41 12.44 32.08 12.4 31.67 11.88L29.25 8.76C29.79 8.07 30.48 7.19 30.91 6.72C31.47 6.07 32.21 5.8 33.06 5.78L33.06 5.22C32.59 5.25 32.05 5.27 31.58 5.27C31.04 5.27 30.08 5.24 29.85 5.22L29.85 5.78C30.23 5.81 30.37 6.05 30.37 6.34C30.37 6.63 30.19 6.86 30.1 6.97L28.98 8.38L27.57 6.55C27.41 6.37 27.41 6.34 27.41 6.23C27.41 5.96 27.68 5.8 28.04 5.78L28.04 5.22L26.09 5.27C25.71 5.27 24.86 5.25 24.38 5.22L24.38 5.78C25.64 5.78 25.66 5.8 26.51 6.88L28.29 9.21C27.44 10.29 27.44 10.33 26.6 11.36C25.73 12.4 24.66 12.44 24.28 12.44L24.28 13C24.75 12.96 25.31 12.95 25.78 12.95L27.5 13L27.5 12.44C27.1 12.39 26.98 12.15 26.98 11.88C26.98 11.48 27.5 10.89 28.6 9.59L29.97 11.39C30.12 11.59 30.35 11.88 30.35 11.99C30.35 12.15 30.19 12.42 29.7 12.44L29.7 13L31.65 12.95C32.14 12.95 32.84 12.96 33.38 13Z"/><path d="M17.76 22.81L17.76 22.42L17.59 22.42L17.59 22.8C17.59 23.32 17.38 23.58 17.12 23.58C16.65 23.58 16.65 22.95 16.65 22.83L16.65 20.88L17.65 20.88L17.65 20.67L16.65 20.67L16.65 19.38L16.48 19.38C16.47 19.95 16.26 20.7 15.57 20.73L15.57 20.88L16.17 20.88L16.17 22.81C16.17 23.68 16.82 23.76 17.07 23.76C17.57 23.76 17.76 23.26 17.76 22.81Z"/><path d="M41.61 15.76L41.61 4.24C41.61 4.11 41.5 4 41.37 4C41.24 4 41.13 4.11 41.13 4.24L41.13 15.76C41.13 15.89 41.24 16 41.37 16C41.5 16 41.61 15.89 41.61 15.76Z"/><path d="M53.1 9L53.1 8.69C52.87 8.71 52.58 8.72 52.35 8.72L51.48 8.69L51.48 9C51.85 9.01 51.96 9.24 51.96 9.43C51.96 9.52 51.94 9.56 51.9 9.67L50.88 12.22L49.76 9.43C49.7 9.3 49.7 9.26 49.7 9.26C49.7 9 50.09 9 50.27 9L50.27 8.69L49.18 8.72C48.91 8.72 48.51 8.71 48.21 8.69L48.21 9C48.84 9 48.88 9.06 49.01 9.37L50.45 12.92C50.51 13.06 50.53 13.11 50.66 13.11C50.79 13.11 50.83 13.02 50.87 12.92L52.18 9.67C52.27 9.44 52.44 9.01 53.1 9Z"/><path d="M56.77 14.64C56.77 14.57 56.71 14.55 56.68 14.55C56.61 14.55 56.6 14.6 56.58 14.65C56.34 15.37 55.71 15.37 55.64 15.37C55.29 15.37 55.01 15.16 54.85 14.9C54.64 14.57 54.64 14.11 54.64 13.85L56.59 13.85C56.75 13.85 56.77 13.85 56.77 13.71C56.77 13.01 56.39 12.33 55.51 12.33C54.7 12.33 54.06 13.06 54.06 13.93C54.06 14.87 54.79 15.55 55.6 15.55C56.45 15.55 56.77 14.77 56.77 14.64ZM56.3 13.71L54.65 13.71C54.69 12.66 55.28 12.49 55.51 12.49C56.23 12.49 56.3 13.43 56.3 13.71Z"/><path d="M60.71 15.47L60.71 15.25C60.35 15.25 60.18 15.25 60.17 15.04L60.17 13.71C60.17 13.1 60.17 12.89 59.95 12.64C59.85 12.52 59.62 12.38 59.22 12.38C58.71 12.38 58.38 12.68 58.18 13.11L58.18 12.38L57.19 12.45L57.19 12.67C57.68 12.67 57.74 12.72 57.74 13.06L57.74 14.94C57.74 15.25 57.66 15.25 57.19 15.25L57.19 15.47L57.98 15.45L58.77 15.47L58.77 15.25C58.3 15.25 58.22 15.25 58.22 14.94L58.22 13.65C58.22 12.92 58.72 12.53 59.17 12.53C59.61 12.53 59.69 12.91 59.69 13.31L59.69 14.94C59.69 15.25 59.61 15.25 59.14 15.25L59.14 15.47L59.93 15.45Z"/><path d="M64.26 12.64C64.26 12.52 64.17 12.3 63.9 12.3C63.76 12.3 63.45 12.34 63.16 12.63C62.86 12.4 62.57 12.38 62.42 12.38C61.76 12.38 61.28 12.86 61.28 13.4C61.28 13.71 61.44 13.97 61.61 14.12C61.52 14.22 61.39 14.46 61.39 14.7C61.39 14.92 61.48 15.18 61.7 15.32C61.28 15.44 61.06 15.74 61.06 16.02C61.06 16.53 61.75 16.91 62.6 16.91C63.43 16.91 64.16 16.56 64.16 16.01C64.16 15.76 64.06 15.41 63.7 15.21C63.33 15.02 62.93 15.02 62.5 15.02C62.32 15.02 62.02 15.02 61.97 15.01C61.75 14.98 61.6 14.76 61.6 14.54C61.6 14.51 61.6 14.35 61.72 14.21C62 14.41 62.28 14.43 62.42 14.43C63.07 14.43 63.55 13.94 63.55 13.41C63.55 13.15 63.44 12.89 63.26 12.73C63.51 12.49 63.77 12.45 63.89 12.45C63.89 12.45 63.94 12.45 63.96 12.46C63.89 12.49 63.85 12.57 63.85 12.65C63.85 12.77 63.94 12.85 64.05 12.85C64.12 12.85 64.26 12.8 64.26 12.64ZM63.02 13.4C63.02 13.59 63.02 13.81 62.91 13.99C62.86 14.07 62.7 14.27 62.42 14.27C61.81 14.27 61.81 13.57 61.81 13.41C61.81 13.22 61.81 12.99 61.92 12.82C61.97 12.73 62.14 12.54 62.42 12.54C63.02 12.54 63.02 13.24 63.02 13.4ZM63.79 16.02C63.79 16.4 63.3 16.75 62.61 16.75C61.9 16.75 61.42 16.39 61.42 16.02C61.42 15.7 61.69 15.44 62 15.42L62.41 15.42C63.01 15.42 63.79 15.42 63.79 16.02Z"/><path d="M66.09 15.47L66.09 15.25C65.63 15.25 65.6 15.22 65.6 14.95L65.6 12.38L64.62 12.45L64.62 12.67C65.08 12.67 65.14 12.71 65.14 13.06L65.14 14.94C65.14 15.25 65.06 15.25 64.59 15.25L64.59 15.47L65.36 15.45C65.61 15.45 65.85 15.46 66.09 15.47ZM65.71 11.24C65.71 11.05 65.54 10.87 65.33 10.87C65.1 10.87 64.96 11.07 64.96 11.24C64.96 11.43 65.12 11.61 65.33 11.61C65.57 11.61 65.71 11.42 65.71 11.24Z"/><path d="M70.05 15.47L70.05 15.25C69.69 15.25 69.51 15.25 69.51 15.04L69.51 13.71C69.51 13.1 69.51 12.89 69.29 12.64C69.19 12.52 68.96 12.38 68.55 12.38C68.04 12.38 67.71 12.68 67.52 13.11L67.52 12.38L66.53 12.45L66.53 12.67C67.02 12.67 67.08 12.72 67.08 13.06L67.08 14.94C67.08 15.25 67 15.25 66.53 15.25L66.53 15.47L67.32 15.45L68.11 15.47L68.11 15.25C67.64 15.25 67.56 15.25 67.56 14.94L67.56 13.65C67.56 12.92 68.06 12.53 68.51 12.53C68.95 12.53 69.02 12.91 69.02 13.31L69.02 14.94C69.02 15.25 68.95 15.25 68.48 15.25L68.48 15.47L69.27 15.45Z"/><path d="M73.1 14.64C73.1 14.57 73.05 14.55 73.01 14.55C72.95 14.55 72.94 14.6 72.92 14.65C72.68 15.37 72.05 15.37 71.98 15.37C71.63 15.37 71.35 15.16 71.19 14.9C70.98 14.57 70.98 14.11 70.98 13.85L72.93 13.85C73.08 13.85 73.1 13.85 73.1 13.71C73.1 13.01 72.73 12.33 71.85 12.33C71.04 12.33 70.4 13.06 70.4 13.93C70.4 14.87 71.13 15.55 71.94 15.55C72.79 15.55 73.1 14.77 73.1 14.64ZM72.64 13.71L70.98 13.71C71.03 12.66 71.61 12.49 71.85 12.49C72.57 12.49 72.64 13.43 72.64 13.71Z"/><path d="M76.63 15.38C76.63 15.35 76.61 15.32 76.59 15.3C75.55 14.52 74.86 12.52 74.86 10.92L74.86 10.08C74.86 8.48 75.55 6.48 76.59 5.7C76.61 5.68 76.63 5.65 76.63 5.62C76.63 5.57 76.58 5.52 76.53 5.52C76.51 5.52 76.49 5.53 76.47 5.54C75.37 6.37 74.32 8.37 74.32 10.08L74.32 10.92C74.32 12.63 75.37 14.63 76.47 15.46C76.49 15.47 76.51 15.48 76.53 15.48C76.58 15.48 76.63 15.43 76.63 15.38Z"/><path d="M80.52 11.76L80.52 11.19L80.27 11.19L80.27 11.74C80.27 12.48 79.97 12.86 79.6 12.86C78.93 12.86 78.93 11.95 78.93 11.78L78.93 9L80.36 9L80.36 8.69L78.93 8.69L78.93 6.85L78.68 6.85C78.67 7.67 78.37 8.74 77.39 8.78L77.39 9L78.24 9L78.24 11.76C78.24 12.99 79.17 13.11 79.53 13.11C80.24 13.11 80.52 12.4 80.52 11.76Z"/><path d="M83.97 10.92L83.97 10.08C83.97 8.37 82.92 6.37 81.82 5.54C81.8 5.53 81.78 5.52 81.76 5.52C81.71 5.52 81.66 5.57 81.66 5.62C81.66 5.65 81.68 5.68 81.7 5.7C82.74 6.48 83.43 8.48 83.43 10.08L83.43 10.92C83.43 12.52 82.74 14.52 81.7 15.3C81.68 15.32 81.66 15.35 81.66 15.38C81.66 15.43 81.71 15.48 81.76 15.48C81.78 15.48 81.8 15.47 81.82 15.46C82.92 14.63 83.97 12.63 83.97 10.92Z"/><path d="M91.06 11.13L91.06 10.55L88.41 10.55L88.41 11.13Z"/><path d="M100.03 9L100.03 8.69C99.8 8.71 99.51 8.72 99.28 8.72L98.41 8.69L98.41 9C98.78 9.01 98.89 9.24 98.89 9.43C98.89 9.52 98.87 9.56 98.83 9.67L97.81 12.22L96.69 9.43C96.63 9.3 96.63 9.26 96.63 9.26C96.63 9 97.02 9 97.2 9L97.2 8.69L96.11 8.72C95.84 8.72 95.44 8.71 95.14 8.69L95.14 9C95.77 9 95.81 9.06 95.94 9.37L97.38 12.92C97.44 13.06 97.46 13.11 97.59 13.11C97.72 13.11 97.76 13.02 97.8 12.92L99.11 9.67C99.2 9.44 99.37 9.01 100.03 9Z"/><path d="M104.28 14.17C104.28 13.47 103.82 12.89 103.23 12.75L102.33 12.54C101.9 12.43 101.63 12.05 101.63 11.65C101.63 11.16 102.01 10.73 102.55 10.73C103.72 10.73 103.87 11.88 103.92 12.19C103.92 12.24 103.92 12.28 104 12.28C104.09 12.28 104.09 12.24 104.09 12.11L104.09 10.7C104.09 10.58 104.09 10.54 104.01 10.54C103.97 10.54 103.96 10.54 103.91 10.63L103.66 11.03C103.45 10.82 103.17 10.54 102.54 10.54C101.77 10.54 101.18 11.15 101.18 11.89C101.18 12.47 101.55 12.99 102.1 13.17C102.17 13.2 102.53 13.29 103.02 13.41C103.21 13.45 103.42 13.5 103.62 13.76C103.76 13.94 103.83 14.18 103.83 14.41C103.83 14.9 103.48 15.41 102.89 15.41C102.69 15.41 102.16 15.37 101.79 15.03C101.38 14.65 101.36 14.2 101.35 13.95C101.35 13.88 101.29 13.88 101.27 13.88C101.18 13.88 101.18 13.93 101.18 14.06L101.18 15.46C101.18 15.58 101.18 15.62 101.26 15.62C101.31 15.62 101.31 15.61 101.36 15.53C101.36 15.53 101.38 15.51 101.61 15.13C101.83 15.37 102.28 15.62 102.9 15.62C103.72 15.62 104.28 14.94 104.28 14.17Z"/><path d="M109.6 12.67L109.6 12.45C109.45 12.47 109.24 12.47 109.09 12.47L108.44 12.45L108.44 12.67C108.69 12.68 108.84 12.8 108.84 13.01C108.84 13.05 108.84 13.06 108.81 13.15L108.17 14.95L107.48 12.99C107.45 12.91 107.44 12.89 107.44 12.86C107.44 12.67 107.72 12.67 107.86 12.67L107.86 12.45L107.13 12.47C106.92 12.47 106.72 12.47 106.51 12.45L106.51 12.67C106.77 12.67 106.88 12.68 106.95 12.78C106.98 12.82 107.06 13.03 107.11 13.16L106.51 14.85L105.84 12.99C105.81 12.9 105.81 12.89 105.81 12.86C105.81 12.67 106.08 12.67 106.22 12.67L106.22 12.45L105.46 12.47L104.81 12.45L104.81 12.67C105.16 12.67 105.24 12.69 105.32 12.92L106.21 15.39C106.24 15.49 106.26 15.55 106.35 15.55C106.44 15.55 106.46 15.51 106.49 15.41L107.2 13.43L107.91 15.41C107.94 15.49 107.96 15.55 108.05 15.55C108.14 15.55 108.17 15.48 108.19 15.41L109.01 13.12C109.14 12.77 109.36 12.68 109.6 12.67Z"/><path d="M111.46 15.47L111.46 15.25C111 15.25 110.97 15.22 110.97 14.95L110.97 12.38L109.99 12.45L109.99 12.67C110.45 12.67 110.51 12.71 110.51 13.06L110.51 14.94C110.51 15.25 110.43 15.25 109.96 15.25L109.96 15.47L110.73 15.45C110.98 15.45 111.22 15.46 111.46 15.47ZM111.08 11.24C111.08 11.05 110.92 10.87 110.71 10.87C110.47 10.87 110.33 11.07 110.33 11.24C110.33 11.43 110.49 11.61 110.7 11.61C110.94 11.61 111.08 11.42 111.08 11.24Z"/><path d="M114.18 11.03C114.18 10.77 113.92 10.54 113.55 10.54C113.06 10.54 112.46 10.91 112.46 11.65L112.46 12.45L111.91 12.45L111.91 12.67L112.46 12.67L112.46 14.94C112.46 15.25 112.39 15.25 111.92 15.25L111.92 15.47L112.72 15.45C113 15.45 113.32 15.45 113.6 15.47L113.6 15.25L113.46 15.25C112.94 15.25 112.93 15.18 112.93 14.92L112.93 12.67L113.72 12.67L113.72 12.45L112.9 12.45L112.9 11.64C112.9 11.03 113.24 10.69 113.55 10.69C113.57 10.69 113.67 10.69 113.78 10.74C113.7 10.77 113.57 10.86 113.57 11.03C113.57 11.19 113.68 11.33 113.87 11.33C114.07 11.33 114.18 11.19 114.18 11.03Z"/><path d="M116.15 14.6L116.15 14.2L115.97 14.2L115.97 14.59C115.97 15.11 115.76 15.37 115.5 15.37C115.03 15.37 115.03 14.74 115.03 14.62L115.03 12.67L116.03 12.67L116.03 12.45L115.03 12.45L115.03 11.17L114.86 11.17C114.85 11.74 114.64 12.49 113.95 12.52L113.95 12.67L114.55 12.67L114.55 14.6C114.55 15.46 115.2 15.55 115.45 15.55C115.95 15.55 116.15 15.05 116.15 14.6Z"/><path d="M121.56 10.91L121.56 10.69L120.73 10.71L119.9 10.69L119.9 10.91C120.62 10.91 120.62 11.24 120.62 11.42L120.62 13.85C120.62 14.85 119.94 15.41 119.27 15.41C118.95 15.41 118.12 15.23 118.12 13.9L118.12 11.24C118.12 10.98 118.13 10.91 118.67 10.91L118.84 10.91L118.84 10.69C118.6 10.71 118.08 10.71 117.81 10.71C117.55 10.71 117.02 10.71 116.78 10.69L116.78 10.91L116.94 10.91C117.48 10.91 117.5 10.98 117.5 11.24L117.5 13.87C117.5 14.86 118.31 15.62 119.26 15.62C120.07 15.62 120.7 14.97 120.81 14.18C120.84 14.04 120.84 13.97 120.84 13.69L120.84 11.45C120.84 11.22 120.84 10.91 121.56 10.91Z"/><path d="M124.13 15.47L124.13 15.25L123.94 15.25C123.39 15.25 123.37 15.18 123.37 14.92L123.37 11.24C123.37 10.98 123.39 10.91 123.94 10.91L124.13 10.91L124.13 10.69C123.88 10.71 123.33 10.71 123.06 10.71C122.79 10.71 122.24 10.71 121.99 10.69L121.99 10.91L122.17 10.91C122.73 10.91 122.75 10.98 122.75 11.24L122.75 14.92C122.75 15.18 122.73 15.25 122.17 15.25L121.99 15.25L121.99 15.47C122.24 15.45 122.79 15.45 123.05 15.45C123.33 15.45 123.88 15.45 124.13 15.47Z"/><path d="M127.64 15.38C127.64 15.35 127.62 15.32 127.6 15.3C126.56 14.52 125.87 12.52 125.87 10.92L125.87 10.08C125.87 8.48 126.56 6.48 127.6 5.7C127.62 5.68 127.64 5.65 127.64 5.62C127.64 5.57 127.59 5.52 127.54 5.52C127.52 5.52 127.5 5.53 127.48 5.54C126.38 6.37 125.33 8.37 125.33 10.08L125.33 10.92C125.33 12.63 126.38 14.63 127.48 15.46C127.5 15.47 127.52 15.48 127.54 15.48C127.59 15.48 127.64 15.43 127.64 15.38Z"/><path d="M131.53 11.76L131.53 11.19L131.28 11.19L131.28 11.74C131.28 12.48 130.98 12.86 130.61 12.86C129.94 12.86 129.94 11.95 129.94 11.78L129.94 9L131.37 9L131.37 8.69L129.94 8.69L129.94 6.85L129.69 6.85C129.68 7.67 129.38 8.74 128.4 8.78L128.4 9L129.25 9L129.25 11.76C129.25 12.99 130.18 13.11 130.54 13.11C131.25 13.11 131.53 12.4 131.53 11.76Z"/><path d="M134.98 10.92L134.98 10.08C134.98 8.37 133.93 6.37 132.83 5.54C132.81 5.53 132.79 5.52 132.77 5.52C132.72 5.52 132.67 5.57 132.67 5.62C132.67 5.65 132.69 5.68 132.71 5.7C133.75 6.48 134.44 8.48 134.44 10.08L134.44 10.92C134.44 12.52 133.75 14.52 132.71 15.3C132.69 15.32 132.67 15.35 132.67 15.38C132.67 15.43 132.72 15.48 132.77 15.48C132.79 15.48 132.81 15.47 132.83 15.46C133.93 14.63 134.98 12.63 134.98 10.92Z"/><path d="M142.89 15.76L142.89 4.24C142.89 4.11 142.78 4 142.65 4C142.51 4 142.41 4.11 142.41 4.24L142.41 15.76C142.41 15.89 142.51 16 142.65 16C142.78 16 142.89 15.89 142.89 15.76Z"/><path d="M157.32 12.25L151.84 9.66L157.32 7.07C157.56 6.96 157.39 6.6 157.14 6.71L151.33 9.46C151.16 9.54 151.16 9.78 151.33 9.86L157.14 12.61C157.39 12.72 157.56 12.36 157.32 12.25ZM157.43 13.99C157.43 13.88 157.34 13.79 157.23 13.79L151.37 13.79C151.26 13.79 151.17 13.88 151.17 13.99C151.17 14.1 151.26 14.19 151.37 14.19L157.23 14.19C157.34 14.19 157.43 14.1 157.43 13.99Z"/><path d="M168.48 13L168.48 12.69L168.16 12.69C167.26 12.69 167.23 12.58 167.23 12.21L167.23 6.6C167.23 6.36 167.23 6.34 167 6.34C166.38 6.98 165.5 6.98 165.18 6.98L165.18 7.29C165.38 7.29 165.97 7.29 166.49 7.03L166.49 12.21C166.49 12.57 166.46 12.69 165.56 12.69L165.24 12.69L165.24 13C165.59 12.97 166.46 12.97 166.86 12.97C167.26 12.97 168.13 12.97 168.48 13Z"/><path d="M173.89 9.8C173.89 9 173.84 8.2 173.49 7.46C173.03 6.5 172.21 6.34 171.79 6.34C171.19 6.34 170.46 6.6 170.05 7.53C169.73 8.22 169.68 9 169.68 9.8C169.68 10.55 169.72 11.45 170.13 12.21C170.56 13.02 171.29 13.22 171.78 13.22C172.32 13.22 173.08 13.01 173.52 12.06C173.84 11.37 173.89 10.59 173.89 9.8ZM173.06 9.68C173.06 10.43 173.06 11.11 172.95 11.75C172.8 12.7 172.23 13 171.78 13C171.39 13 170.8 12.75 170.62 11.79C170.51 11.19 170.51 10.27 170.51 9.68C170.51 9.04 170.51 8.38 170.59 7.84C170.78 6.65 171.53 6.56 171.78 6.56C172.11 6.56 172.77 6.74 172.96 7.73C173.06 8.29 173.06 9.05 173.06 9.68Z"/><path d="M176.78 8.06L176.78 7.66L174.93 7.66L174.93 8.06Z"/><path d="M180.38 7.94C180.38 7.05 179.76 6.38 178.98 6.38C178.5 6.38 178.25 6.74 178.11 7.07L178.11 6.91C178.11 5.14 178.97 4.88 179.33 4.88C179.5 4.88 179.79 4.93 179.95 5.16C179.84 5.16 179.56 5.16 179.56 5.48C179.56 5.7 179.73 5.8 179.88 5.8C180 5.8 180.21 5.74 180.21 5.46C180.21 5.04 179.9 4.71 179.32 4.71C178.42 4.71 177.48 5.61 177.48 7.16C177.48 9.03 178.29 9.52 178.94 9.52C179.72 9.52 180.38 8.87 180.38 7.94ZM179.75 7.94C179.75 8.27 179.75 8.62 179.63 8.87C179.42 9.29 179.1 9.33 178.94 9.33C178.5 9.33 178.29 8.91 178.25 8.8C178.12 8.47 178.12 7.91 178.12 7.79C178.12 7.24 178.34 6.54 178.97 6.54C179.09 6.54 179.41 6.54 179.62 6.98C179.75 7.24 179.75 7.59 179.75 7.94Z"/></svg><math class="td-math-a11y" xmlns="http://www.w3.org/1998/Math/MathML"><mrow><msub><mrow><mo>max</mo></mrow><mrow><mi>t</mi></mrow></msub><mspace width="0.28em"/><mtext> </mtext><mo>|</mo><mspace width="0.17em"/><mtext> </mtext><msub><mrow><mi>v</mi></mrow><mrow><mi>engine</mi></mrow></msub><mtext>(</mtext><mi>t</mi><mtext>)</mtext><mtext> </mtext><mtext>-</mtext><mtext> </mtext><msub><mrow><mi>v</mi></mrow><mrow><mi>SwiftUI</mi></mrow></msub><mtext>(</mtext><mi>t</mi><mtext>)</mtext><mtext> </mtext><mspace width="0.17em"/><mo>|</mo><mtext> </mtext><mspace width="0.28em"/><mo>≤</mo><mspace width="0.28em"/><mtext> </mtext><mn>1</mn><msup><mrow><mn>0</mn></mrow><mrow><mrow><mtext>-</mtext><mn>6</mn></mrow></mrow></msup></mrow></math></div>
<p>Sampled point by point, the two springs are the same number to six decimals.</p>
<figure class="td-chart td-chart-line">
<figcaption class="td-chart-caption">The engine&#39;s spring rides on SwiftUI&#39;s</figcaption>
<div class="td-chart-frame">
<svg
  class="td-chart-svg"
  viewBox="0 0 720 382"
  role="img"
  aria-label="The engine&#39;s spring rides on SwiftUI&#39;s"
>
<desc>line chart with 0 labels and 1 series</desc>
<line
  class="td-chart-grid"
  x1="80"
  y1="302"
  x2="696"
  y2="302"
></line>
<text
  class="td-chart-value"
  x="70"
  y="306"
  text-anchor="end"
>0</text>
<line
  class="td-chart-grid"
  x1="80"
  y1="186.17"
  x2="696"
  y2="186.17"
></line>
<text
  class="td-chart-value"
  x="70"
  y="190.17"
  text-anchor="end"
>0.5</text>
<line
  class="td-chart-grid"
  x1="80"
  y1="70.33"
  x2="696"
  y2="70.33"
></line>
<text
  class="td-chart-value"
  x="70"
  y="74.33"
  text-anchor="end"
>1</text>
<line
  class="td-chart-axis"
  x1="80"
  y1="302"
  x2="696"
  y2="302"
></line>
<line
  class="td-chart-axis"
  x1="80"
  y1="24"
  x2="80"
  y2="302"
></line>
<text
  class="td-chart-label"
  x="80"
  y="324"
  text-anchor="middle"
>0</text>
<text
  class="td-chart-label"
  x="388"
  y="324"
  text-anchor="middle"
>4</text>
<text
  class="td-chart-label"
  x="696"
  y="324"
  text-anchor="middle"
>8</text>
<polyline class="td-chart-line td-chart-series-0" points="80,302 157,116.67 234,24 311,81.92 388,58.75 465,72.65 542,68.02 619,70.33 696,70.33"></polyline>
<circle
  class="td-chart-point td-chart-series-0"
  cx="80"
  cy="302"
  r="4"
><title>Progress: (0, 0)</title></circle>
<circle
  class="td-chart-point td-chart-series-0"
  cx="157"
  cy="116.67"
  r="4"
><title>Progress: (1, 0.8)</title></circle>
<circle
  class="td-chart-point td-chart-series-0"
  cx="234"
  cy="24"
  r="4"
><title>Progress: (2, 1.2)</title></circle>
<circle
  class="td-chart-point td-chart-series-0"
  cx="311"
  cy="81.92"
  r="4"
><title>Progress: (3, 0.95)</title></circle>
<circle
  class="td-chart-point td-chart-series-0"
  cx="388"
  cy="58.75"
  r="4"
><title>Progress: (4, 1.05)</title></circle>
<circle
  class="td-chart-point td-chart-series-0"
  cx="465"
  cy="72.65"
  r="4"
><title>Progress: (5, 0.99)</title></circle>
<circle
  class="td-chart-point td-chart-series-0"
  cx="542"
  cy="68.02"
  r="4"
><title>Progress: (6, 1.01)</title></circle>
<circle
  class="td-chart-point td-chart-series-0"
  cx="619"
  cy="70.33"
  r="4"
><title>Progress: (7, 1)</title></circle>
<circle
  class="td-chart-point td-chart-series-0"
  cx="696"
  cy="70.33"
  r="4"
><title>Progress: (8, 1)</title></circle>
<text
  class="td-chart-label"
  x="388"
  y="368"
  text-anchor="middle"
>Time</text>
<text
  class="td-chart-label"
  x="16"
  y="163"
  text-anchor="middle"
  transform="rotate(-90 16 163)"
>Progress</text>
<rect
  class="td-chart-series-0"
  x="80"
  y="336"
  width="12"
  height="12"
  rx="3"
  fill="currentColor"
></rect>
<text
  class="td-chart-legend-text"
  x="98"
  y="346"
  text-anchor="start"
>Progress</text>
</svg>
</div>
</figure>
<h2>Measuring pixels: compare the rendered bytes</h2>
<p>Frames prove position. Pixels prove the rest: the actual color drawn, and the paint order
of overlapping fills. SwiftUI’s own <code>ImageRenderer</code> rasterizes the view; the engine’s
display list is rasterized by a reference renderer; we diff the buffers channel by channel.</p>
<p><img src="https://aleahim.com/images/blog/the-swiftui-oracle/9-pixel-diff.jpg" alt="Two pictures, compared dot by dot"></p>
<pre><code class="language-swift"><span class="tok-comment">/// Render a SwiftUI view to an RGBA8 buffer in the same space/scale/orientation.</span>
<span class="tok-keyword">private</span> <span class="tok-keyword">func</span> <span class="tok-function">snapshot</span>(_ view<span class="tok-operator">:</span> some <span class="tok-type">SwiftUI</span><span class="tok-operator">.</span><span class="tok-type">View</span>, size<span class="tok-operator">:</span> <span class="tok-type">CGSize</span>, scale<span class="tok-operator">:</span> <span class="tok-type">CGFloat</span>) <span class="tok-keyword">throws</span> <span class="tok-operator">-</span><span class="tok-operator">&gt;</span> [<span class="tok-type">UInt8</span>] {
    <span class="tok-keyword">let</span> renderer <span class="tok-operator">=</span> <span class="tok-type">ImageRenderer</span>(content<span class="tok-operator">:</span> view<span class="tok-operator">.</span><span class="tok-function">frame</span>(width<span class="tok-operator">:</span> size<span class="tok-operator">.</span>width, height<span class="tok-operator">:</span> size<span class="tok-operator">.</span>height))
    renderer<span class="tok-operator">.</span>scale <span class="tok-operator">=</span> scale
    renderer<span class="tok-operator">.</span>color<span class="tok-type">Mode</span> <span class="tok-operator">=</span> <span class="tok-operator">.</span>non<span class="tok-type">Linear</span>
    <span class="tok-keyword">let</span> image <span class="tok-operator">=</span> <span class="tok-keyword">try</span> #<span class="tok-function">require</span>(renderer<span class="tok-operator">.</span>cg<span class="tok-type">Image</span>, <span class="tok-string">"ImageRenderer produced no image"</span>)
    <span class="tok-keyword">let</span> w <span class="tok-operator">=</span> <span class="tok-type">Int</span>(size<span class="tok-operator">.</span>width <span class="tok-operator">*</span> scale), h <span class="tok-operator">=</span> <span class="tok-type">Int</span>(size<span class="tok-operator">.</span>height <span class="tok-operator">*</span> scale)
    <span class="tok-keyword">let</span> (ctx, raw) <span class="tok-operator">=</span> <span class="tok-keyword">try</span> <span class="tok-function">context</span>(width<span class="tok-operator">:</span> w, height<span class="tok-operator">:</span> h, scale<span class="tok-operator">:</span> <span class="tok-number">1</span>, flip<span class="tok-type">ForDrawing</span><span class="tok-operator">:</span> <span class="tok-literal">true</span>)
    <span class="tok-keyword">defer</span> { raw<span class="tok-operator">.</span><span class="tok-function">deallocate</span>() }
    ctx<span class="tok-operator">.</span><span class="tok-function">draw</span>(image, <span class="tok-keyword">in</span><span class="tok-operator">:</span> <span class="tok-type">CGRect</span>(x<span class="tok-operator">:</span> <span class="tok-number">0</span>, y<span class="tok-operator">:</span> <span class="tok-number">0</span>, width<span class="tok-operator">:</span> <span class="tok-type">CGFloat</span>(w), height<span class="tok-operator">:</span> <span class="tok-type">CGFloat</span>(h)))
    <span class="tok-keyword">return</span> <span class="tok-function">bytes</span>(raw, count<span class="tok-operator">:</span> w <span class="tok-operator">*</span> <span class="tok-number">4</span> <span class="tok-operator">*</span> h)
}

<span class="tok-keyword">private</span> <span class="tok-keyword">func</span> <span class="tok-function">assertPixelsMatch</span>(
    _ name<span class="tok-operator">:</span> <span class="tok-type">String</span>,
    list<span class="tok-operator">:</span> <span class="tok-type">DisplayList</span>,
    view<span class="tok-operator">:</span> some <span class="tok-type">SwiftUI</span><span class="tok-operator">.</span><span class="tok-type">View</span>,
    size<span class="tok-operator">:</span> <span class="tok-type">CGSize</span>,
    scale<span class="tok-operator">:</span> <span class="tok-type">CGFloat</span> <span class="tok-operator">=</span> <span class="tok-number">2</span>,
    tolerance<span class="tok-operator">:</span> <span class="tok-type">Int</span> <span class="tok-operator">=</span> <span class="tok-number">2</span>
) <span class="tok-keyword">throws</span> {
    <span class="tok-keyword">let</span> engine <span class="tok-operator">=</span> <span class="tok-keyword">try</span> <span class="tok-function">rasterize</span>(list, size<span class="tok-operator">:</span> size, scale<span class="tok-operator">:</span> scale)
    <span class="tok-keyword">let</span> swiftui <span class="tok-operator">=</span> <span class="tok-keyword">try</span> <span class="tok-function">snapshot</span>(view, size<span class="tok-operator">:</span> size, scale<span class="tok-operator">:</span> scale)
    #<span class="tok-function">expect</span>(<span class="tok-operator">!</span>engine<span class="tok-operator">.</span>is<span class="tok-type">Empty</span> <span class="tok-operator">&amp;</span><span class="tok-operator">&amp;</span> engine<span class="tok-operator">.</span>count <span class="tok-operator">=</span><span class="tok-operator">=</span> swiftui<span class="tok-operator">.</span>count, <span class="tok-string">"\(name): buffer size mismatch"</span>)
    <span class="tok-keyword">var</span> max<span class="tok-type">Diff</span> <span class="tok-operator">=</span> <span class="tok-number">0</span>, exceeding <span class="tok-operator">=</span> <span class="tok-number">0</span>
    <span class="tok-keyword">for</span> i <span class="tok-keyword">in</span> <span class="tok-number">0</span> <span class="tok-operator">.</span><span class="tok-operator">.</span><span class="tok-operator">&lt;</span> <span class="tok-function">min</span>(engine<span class="tok-operator">.</span>count, swiftui<span class="tok-operator">.</span>count) {
        <span class="tok-keyword">let</span> diff <span class="tok-operator">=</span> <span class="tok-function">abs</span>(<span class="tok-type">Int</span>(engine[i]) <span class="tok-operator">-</span> <span class="tok-type">Int</span>(swiftui[i]))
        max<span class="tok-type">Diff</span> <span class="tok-operator">=</span> <span class="tok-function">max</span>(max<span class="tok-type">Diff</span>, diff)
        <span class="tok-keyword">if</span> diff <span class="tok-operator">&gt;</span> tolerance { exceeding <span class="tok-operator">+</span><span class="tok-operator">=</span> <span class="tok-number">1</span> }
    }
    #<span class="tok-function">expect</span>(exceeding <span class="tok-operator">=</span><span class="tok-operator">=</span> <span class="tok-number">0</span>, <span class="tok-string">"\(name): \(exceeding) channels exceed tol \(tolerance) (maxDiff \(maxDiff))"</span>)
}</code></pre>
<p>The tolerance here is 2 on an 8-bit channel: the quantization floor is one least-significant
bit, and integer-aligned fills at scale 2 have crisp edges, so there is no anti-aliasing
slack to hide behind.</p>
<h2>What it all adds up to</h2>
<p>There are 125 of these tests today, across 19 files. The same do-it-twice method holds
every layer the engine rebuilds.</p>
<figure class="td-chart td-chart-bar">
<figcaption class="td-chart-caption">125 oracle tests holding the engine to SwiftUI</figcaption>
<div class="td-chart-frame">
<svg
  class="td-chart-svg"
  viewBox="0 0 720 360"
  role="img"
  aria-label="125 oracle tests holding the engine to SwiftUI"
>
<desc>bar chart with 6 labels and 1 series</desc>
<line
  class="td-chart-grid"
  x1="80"
  y1="302"
  x2="696"
  y2="302"
></line>
<text
  class="td-chart-value"
  x="70"
  y="306"
  text-anchor="end"
>0</text>
<line
  class="td-chart-grid"
  x1="80"
  y1="209.33"
  x2="696"
  y2="209.33"
></line>
<text
  class="td-chart-value"
  x="70"
  y="213.33"
  text-anchor="end"
>20</text>
<line
  class="td-chart-grid"
  x1="80"
  y1="116.67"
  x2="696"
  y2="116.67"
></line>
<text
  class="td-chart-value"
  x="70"
  y="120.67"
  text-anchor="end"
>40</text>
<line
  class="td-chart-grid"
  x1="80"
  y1="24"
  x2="696"
  y2="24"
></line>
<text
  class="td-chart-value"
  x="70"
  y="28"
  text-anchor="end"
>60</text>
<line
  class="td-chart-axis"
  x1="80"
  y1="302"
  x2="696"
  y2="302"
></line>
<line
  class="td-chart-axis"
  x1="80"
  y1="24"
  x2="80"
  y2="302"
></line>
<rect
  class="td-chart-bar td-chart-series-0"
  x="114.33"
  y="278.83"
  width="32"
  height="23.17"
  rx="4"
><title>Graph: 5</title></rect>
<rect
  class="td-chart-bar td-chart-series-0"
  x="217"
  y="125.93"
  width="32"
  height="176.07"
  rx="4"
><title>Animation: 38</title></rect>
<rect
  class="td-chart-bar td-chart-series-0"
  x="319.67"
  y="283.47"
  width="32"
  height="18.53"
  rx="4"
><title>Identity: 4</title></rect>
<rect
  class="td-chart-bar td-chart-series-0"
  x="422.33"
  y="65.7"
  width="32"
  height="236.3"
  rx="4"
><title>Layout: 51</title></rect>
<rect
  class="td-chart-bar td-chart-series-0"
  x="525"
  y="288.1"
  width="32"
  height="13.9"
  rx="4"
><title>DisplayList: 3</title></rect>
<rect
  class="td-chart-bar td-chart-series-0"
  x="627.67"
  y="190.8"
  width="32"
  height="111.2"
  rx="4"
><title>Pixel: 24</title></rect>
<text
  class="td-chart-label"
  x="131.33"
  y="324"
  text-anchor="middle"
>Graph</text>
<text
  class="td-chart-label"
  x="234"
  y="324"
  text-anchor="middle"
>Animation</text>
<text
  class="td-chart-label"
  x="336.67"
  y="324"
  text-anchor="middle"
>Identity</text>
<text
  class="td-chart-label"
  x="439.33"
  y="324"
  text-anchor="middle"
>Layout</text>
<text
  class="td-chart-label"
  x="542"
  y="324"
  text-anchor="middle"
>DisplayList</text>
<text
  class="td-chart-label"
  x="644.67"
  y="324"
  text-anchor="middle"
>Pixel</text>
<text
  class="td-chart-label"
  x="16"
  y="163"
  text-anchor="middle"
  transform="rotate(-90 16 163)"
>Tests</text>
<rect
  class="td-chart-series-0"
  x="80"
  y="336"
  width="12"
  height="12"
  rx="3"
  fill="currentColor"
></rect>
<text
  class="td-chart-legend-text"
  x="98"
  y="346"
  text-anchor="start"
>Tests</text>
</svg>
</div>
</figure>
<h2>What measuring caught that reading never would</h2>
<p>The payoff: the measurement caught a bug in the engine that reading the code could not. The
spring decided it had come to rest using the bare decay envelope, which undercounts the long
tail of a critically damped or overdamped spring. The code looked correct. Measured against
SwiftUI, the spring clamped a few percent short of its target, a visible snap. The oracle
failed with the exact residual, and the settling time was rederived from the real basis of
the oscillator. Reading gives you the happy path. Measuring gives you the edges, where the
interesting bugs live.</p>
<h2>One honest caveat about the animation path</h2>
<p>A committed animation keeps running smoothly when the main thread is busy only for one of
two paths. Layer-level properties, opacity and transform, lower to Core Animation and are
interpolated on the render server’s own clock, off the main thread. Value-driven animations,
a spring on a custom value, are re-pulled through the graph every frame on the main thread,
and those do stutter if you block it. The oracle measures the value and velocity of the
motion; which thread interpolates it is a property of the lowering path. Worth stating
precisely, since the rest of this post is about precision.</p>
<h2>Colophon: every figure was made by my own software</h2>
<p>No third-party drawing or charting tool was used anywhere in this post.</p>
<ul><li><p>The conceptual diagrams (the wall, the two pieces, the counters, the negotiation, the
pixel comparison) were drawn by <strong>PureDraw</strong>, my dependency-free Swift-native 2D vector
engine, through its <code>GraphicsContext</code> API. PureDraw rasterized them to the JPEGs shown here with its own <code>BitmapRenderer</code> and <code>JPEGEncoder</code>, and also produced scalable SVG and PDF masters. No external image tool touched them.</p></li><li><p>The bar chart, the spring plot, and every mathematical formula were rendered by
<strong>TileDown</strong>, my static-site engine, from plain-text fences in this Markdown file.</p></li><li><p>This page was assembled by <strong>TileDown</strong>, and its PDF was typeset by <strong>MarkdownPDF</strong>, my Markdown-to-PDF engine.</p></li></ul>
<p><strong>PureDraw</strong> is part of <strong><a href="https://slayermotion.com">SlayerMotion</a></strong>, the graphics engine family this series is about. <strong>TileDown</strong> and <strong>MarkdownPDF</strong> are separate projects of mine, not part of SlayerMotion. All three are my own software; none of it is anyone else’s.</p>]]></content:encoded>
</item>
<item>
<title>SwiftUI Is One Graph, Over 40+ Years of Engineering</title>
<link>https://aleahim.com/blog/swiftui-is-one-graph/</link>
<guid isPermaLink="true">https://aleahim.com/blog/swiftui-is-one-graph/</guid>
<pubDate>Thu, 25 Jun 2026 00:00:00 +0000</pubDate>
<description>SwiftUI is the thin graph sitting above 40+ years of engineering. Measured against the real framework and read against Apple&apos;s own patent, it is a single demand-driven graph, and the least remarkable engine in the stack it rests on. Here is SwiftUI laid bare, with a hint of the far more interesting machines underneath.</description>
<content:encoded><![CDATA[<h1>SwiftUI Is One Graph, Over 40+ Years of Engineering</h1>
<p>I wanted to know what SwiftUI really is, not what the tutorials say it is. So I came at it from two directions that cannot collude. I measured the real framework’s behavior until it was predictable, the way you probe any black box, and I read Apple’s own patent on it, line by line. The shipping framework is the oracle. The granted patent is Apple describing its own machine. When the two agree, you are no longer guessing.</p>
<figure class="td-mermaid" data-td-mermaid>
<pre class="mermaid td-mermaid-source">flowchart LR
  B[&quot;Real framework behavior&quot;] --&gt; M[&quot;One demand-driven graph&quot;]
  P[&quot;Apple&#39;s granted patent&quot;] --&gt; M
</pre>
</figure>
<p>And they agree, almost field for field. What they describe together is simpler and stranger than the usual story: not a view tree that gets diffed, but a single demand-driven graph. I will show you that graph, because it is genuinely elegant. Then, at the end, I will tell you why it is the part of Apple’s stack I find least interesting, and what I have been doing about that. First the engine.</p>
<h2>A view is a value, not an object</h2>
<p>When you write a SwiftUI view you are not creating a thing on screen. You are describing what the screen should look like, as a cheap struct that gets thrown away and rebuilt constantly. This trips people up because it feels wasteful. It is not. The struct is disposable on purpose. The part of your UI that actually persists lives somewhere else entirely.</p>
<p>That somewhere else is the attribute graph, and it is the whole game.</p>
<h2>The attribute graph</h2>
<p>Every view compiles into a graph of attributes. An attribute is one node with exactly two things: a cached value, and a rule that computes that value from its inputs. A view’s <code>body</code> is itself an attribute whose rule is “evaluate this body.”</p>
<p>The clever part is how edges form. When a rule runs and reads another attribute, an edge is recorded between them. Dependencies are never declared. They are discovered, by watching what each body actually reads while it runs. You get the dependency graph for free, just by evaluating.</p>
<p>Now the reactive behavior everyone loves, explained without magic. You change one piece of state. SwiftUI does not rebuild the world. It marks that single source attribute dirty and pushes the dirty flag forward along the edges, to exactly the attributes that depended on it and to nobody else. That set is the cone. Then, lazily, when the screen needs a value, it pulls. It walks down to the dirty inputs, recomputes only those, caches them, and bubbles the result back up. An attribute whose inputs did not change returns its cache and its rule never runs. That is why a screen with a thousand views re-runs the body of only the one that changed.</p>
<figure class="td-mermaid" data-td-mermaid>
<pre class="mermaid td-mermaid-source">flowchart TD
  S[&quot;@State count, a source&quot;] --&gt;|dirty| L[&quot;Label.body&quot;]
  L --&gt; T[&quot;Text attribute&quot;]
  S -.-&gt;|no edge, untouched| O[&quot;Other.body&quot;]
</pre>
</figure>
<p>The cone is the whole efficiency story. A naive rebuild touches every body in the interface, so its cost grows with the screen. The graph touches only the dirty node and what truly depends on it, so its cost tracks the change, not the size of the UI.</p>
<figure class="td-chart td-chart-line">
<figcaption class="td-chart-caption">Bodies re-run for one change as the screen grows</figcaption>
<div class="td-chart-frame">
<svg
  class="td-chart-svg"
  viewBox="0 0 720 382"
  role="img"
  aria-label="Bodies re-run for one change as the screen grows"
>
<desc>line chart with 0 labels and 2 series</desc>
<line
  class="td-chart-grid"
  x1="80"
  y1="302"
  x2="696"
  y2="302"
></line>
<text
  class="td-chart-value"
  x="70"
  y="306"
  text-anchor="end"
>0</text>
<line
  class="td-chart-grid"
  x1="80"
  y1="246.4"
  x2="696"
  y2="246.4"
></line>
<text
  class="td-chart-value"
  x="70"
  y="250.4"
  text-anchor="end"
>200</text>
<line
  class="td-chart-grid"
  x1="80"
  y1="190.8"
  x2="696"
  y2="190.8"
></line>
<text
  class="td-chart-value"
  x="70"
  y="194.8"
  text-anchor="end"
>400</text>
<line
  class="td-chart-grid"
  x1="80"
  y1="135.2"
  x2="696"
  y2="135.2"
></line>
<text
  class="td-chart-value"
  x="70"
  y="139.2"
  text-anchor="end"
>600</text>
<line
  class="td-chart-grid"
  x1="80"
  y1="79.6"
  x2="696"
  y2="79.6"
></line>
<text
  class="td-chart-value"
  x="70"
  y="83.6"
  text-anchor="end"
>800</text>
<line
  class="td-chart-grid"
  x1="80"
  y1="24"
  x2="696"
  y2="24"
></line>
<text
  class="td-chart-value"
  x="70"
  y="28"
  text-anchor="end"
>1000</text>
<line
  class="td-chart-axis"
  x1="80"
  y1="302"
  x2="696"
  y2="302"
></line>
<line
  class="td-chart-axis"
  x1="80"
  y1="24"
  x2="80"
  y2="302"
></line>
<text
  class="td-chart-label"
  x="80"
  y="324"
  text-anchor="middle"
>200</text>
<text
  class="td-chart-label"
  x="388"
  y="324"
  text-anchor="middle"
>600</text>
<text
  class="td-chart-label"
  x="696"
  y="324"
  text-anchor="middle"
>1000</text>
<polyline class="td-chart-line td-chart-series-0" points="80,246.4 234,190.8 388,135.2 542,79.6 696,24"></polyline>
<circle
  class="td-chart-point td-chart-series-0"
  cx="80"
  cy="246.4"
  r="4"
><title>Rebuild the world: (200, 200)</title></circle>
<circle
  class="td-chart-point td-chart-series-0"
  cx="234"
  cy="190.8"
  r="4"
><title>Rebuild the world: (400, 400)</title></circle>
<circle
  class="td-chart-point td-chart-series-0"
  cx="388"
  cy="135.2"
  r="4"
><title>Rebuild the world: (600, 600)</title></circle>
<circle
  class="td-chart-point td-chart-series-0"
  cx="542"
  cy="79.6"
  r="4"
><title>Rebuild the world: (800, 800)</title></circle>
<circle
  class="td-chart-point td-chart-series-0"
  cx="696"
  cy="24"
  r="4"
><title>Rebuild the world: (1000, 1000)</title></circle>
<polyline class="td-chart-line td-chart-series-1" points="80,301.72 234,301.72 388,301.72 542,301.72 696,301.72"></polyline>
<circle
  class="td-chart-point td-chart-series-1"
  cx="80"
  cy="301.72"
  r="4"
><title>Demand-driven graph: (200, 1)</title></circle>
<circle
  class="td-chart-point td-chart-series-1"
  cx="234"
  cy="301.72"
  r="4"
><title>Demand-driven graph: (400, 1)</title></circle>
<circle
  class="td-chart-point td-chart-series-1"
  cx="388"
  cy="301.72"
  r="4"
><title>Demand-driven graph: (600, 1)</title></circle>
<circle
  class="td-chart-point td-chart-series-1"
  cx="542"
  cy="301.72"
  r="4"
><title>Demand-driven graph: (800, 1)</title></circle>
<circle
  class="td-chart-point td-chart-series-1"
  cx="696"
  cy="301.72"
  r="4"
><title>Demand-driven graph: (1000, 1)</title></circle>
<text
  class="td-chart-label"
  x="388"
  y="368"
  text-anchor="middle"
>Views on screen</text>
<text
  class="td-chart-label"
  x="16"
  y="163"
  text-anchor="middle"
  transform="rotate(-90 16 163)"
>Bodies re-run</text>
<rect
  class="td-chart-series-0"
  x="80"
  y="336"
  width="12"
  height="12"
  rx="3"
  fill="currentColor"
></rect>
<text
  class="td-chart-legend-text"
  x="98"
  y="346"
  text-anchor="start"
>Rebuild the world</text>
<rect
  class="td-chart-series-1"
  x="247.84"
  y="336"
  width="12"
  height="12"
  rx="3"
  fill="currentColor"
></rect>
<text
  class="td-chart-legend-text"
  x="265.84"
  y="346"
  text-anchor="start"
>Demand-driven graph</text>
</svg>
</div>
</figure>
<p>There is one more piece that matters for performance. If a recomputed value comes out equal to what it was before, propagation stops there. An unchanged value never disturbs anything downstream, so a state change that happens to produce the same result costs almost nothing.</p>
<h2>State and identity</h2>
<p>Because the view struct is disposable, your <code>@State</code> cannot live inside it. It lives in persistent storage, keyed by the view’s identity. Identity is structural, meaning the view’s type and position in the tree. An explicit <code>id</code> does not override that, it pairs with it: change the identifier and the view gets a new identity at that spot, but reuse the same identifier elsewhere and the two stay distinct.</p>
<p>This one rule explains a lot of confusing behavior. As long as the identity is stable, your state survives every rebuild of the struct. The moment the identity changes, by changing <code>.id</code> or by removing and reinserting the view, the old state is destroyed and a fresh one is created at its default. It is also why a <code>List</code> can recycle its rows without losing their state. The state follows the identity, not the position on screen. Reorder the data and each row’s state travels with its identifier.</p>
<h2>Layout is a negotiation</h2>
<p>Layout is three steps and one rule. The parent proposes a size to the child. The child chooses its own size. The parent then places the child. The rule is that the parent never forces a size on the child. A stack divides its space among its children, giving the least flexible ones their size first and sharing what is left. Text measures itself through the text engine, with kerning and the font’s own line metrics, which is why a label is exactly as wide as it needs to be.</p>
<figure class="td-mermaid" data-td-mermaid>
<pre class="mermaid td-mermaid-source">flowchart TD
  P1[&quot;Parent proposes a size&quot;] --&gt; C1[&quot;Child chooses its own size&quot;]
  C1 --&gt; P2[&quot;Parent places the child&quot;]
</pre>
</figure>
<h2>How it becomes pixels, and the part people get wrong</h2>
<p>SwiftUI sits on Core Animation. Core Animation holds the layer state, position, opacity, transform, the properties the GPU composites and the render server interpolates. Core Graphics paints the content, text and shapes, into bitmaps that become a layer’s contents.</p>
<p>Here is the part most people get wrong. <strong>SwiftUI does not make one layer per view. UIKit does that, where every view is layer backed one to one. SwiftUI coalesces. A stack of ten text labels is usually a single layer with a single drawing, not eleven layers.</strong> A view earns its own layer only when it needs a layer level property, like opacity or a transform. This is a real reason SwiftUI is efficient. A long list is a handful of layers for the compositor to manage, not thousands. The tradeoff is that coalesced content has to be redrawn when it changes, while a dedicated layer can move without a redraw. For typical interfaces that trade is a clear win.</p>
<figure class="td-chart td-chart-bar">
<figcaption class="td-chart-caption">Layers for a stack of ten text labels</figcaption>
<div class="td-chart-frame">
<svg
  class="td-chart-svg"
  viewBox="0 0 720 360"
  role="img"
  aria-label="Layers for a stack of ten text labels"
>
<desc>bar chart with 2 labels and 1 series</desc>
<line
  class="td-chart-grid"
  x1="80"
  y1="302"
  x2="696"
  y2="302"
></line>
<text
  class="td-chart-value"
  x="70"
  y="306"
  text-anchor="end"
>0</text>
<line
  class="td-chart-grid"
  x1="80"
  y1="186.17"
  x2="696"
  y2="186.17"
></line>
<text
  class="td-chart-value"
  x="70"
  y="190.17"
  text-anchor="end"
>5</text>
<line
  class="td-chart-grid"
  x1="80"
  y1="70.33"
  x2="696"
  y2="70.33"
></line>
<text
  class="td-chart-value"
  x="70"
  y="74.33"
  text-anchor="end"
>10</text>
<line
  class="td-chart-axis"
  x1="80"
  y1="302"
  x2="696"
  y2="302"
></line>
<line
  class="td-chart-axis"
  x1="80"
  y1="24"
  x2="80"
  y2="302"
></line>
<rect
  class="td-chart-bar td-chart-series-0"
  x="217"
  y="47.17"
  width="32"
  height="254.83"
  rx="4"
><title>One layer per view: 11</title></rect>
<rect
  class="td-chart-bar td-chart-series-0"
  x="525"
  y="278.83"
  width="32"
  height="23.17"
  rx="4"
><title>SwiftUI coalesced: 1</title></rect>
<text
  class="td-chart-label"
  x="234"
  y="324"
  text-anchor="middle"
>One layer per view</text>
<text
  class="td-chart-label"
  x="542"
  y="324"
  text-anchor="middle"
>SwiftUI coalesced</text>
<text
  class="td-chart-label"
  x="16"
  y="163"
  text-anchor="middle"
  transform="rotate(-90 16 163)"
>Layers created</text>
<rect
  class="td-chart-series-0"
  x="80"
  y="336"
  width="12"
  height="12"
  rx="3"
  fill="currentColor"
></rect>
<text
  class="td-chart-legend-text"
  x="98"
  y="346"
  text-anchor="start"
>Layers</text>
</svg>
</div>
</figure>
<h2>Animation, the beautiful part</h2>
<p>When you change a value inside <code>withAnimation</code>, the model value jumps straight to its target. It does not crawl there. Instead the framework makes a copy of that destination, and into the copy it injects an intermediate value, interpolated for this exact instant. The view draws the copy. Your data is already at the final value. Only the presentation is in flight.</p>
<p>Each animation is a tiny record: a from value, a to value, a timing function, and a start time. That is all it needs.</p>
<figure class="td-mermaid" data-td-mermaid>
<pre class="mermaid td-mermaid-source">flowchart TD
  R[&quot;Animation record&quot;] --&gt; A[&quot;from value&quot;]
  R --&gt; B[&quot;to value&quot;]
  R --&gt; C[&quot;animation function&quot;]
  R --&gt; D[&quot;start time&quot;]
</pre>
</figure>
<p>Time itself is just another input to the graph. Every frame a clock ticks, that tick dirties only the animated attributes, and they re-pull a fresh interpolated value through the same cone machinery as everything else. The interpolation works on a delta vector, the difference between from and to, scaled by the curve.</p>
<p>A spring is not a special case. It is a damped harmonic oscillator solved over time, which is exactly why it overshoots and then settles. And because the animation is committed once and the render server interpolates on its own clock, it keeps running smoothly even when your main thread is busy.</p>
<figure class="td-chart td-chart-line">
<figcaption class="td-chart-caption">A spring overshoots its target, then settles</figcaption>
<div class="td-chart-frame">
<svg
  class="td-chart-svg"
  viewBox="0 0 720 382"
  role="img"
  aria-label="A spring overshoots its target, then settles"
>
<desc>line chart with 0 labels and 1 series</desc>
<line
  class="td-chart-grid"
  x1="80"
  y1="302"
  x2="696"
  y2="302"
></line>
<text
  class="td-chart-value"
  x="70"
  y="306"
  text-anchor="end"
>0</text>
<line
  class="td-chart-grid"
  x1="80"
  y1="209.33"
  x2="696"
  y2="209.33"
></line>
<text
  class="td-chart-value"
  x="70"
  y="213.33"
  text-anchor="end"
>50</text>
<line
  class="td-chart-grid"
  x1="80"
  y1="116.67"
  x2="696"
  y2="116.67"
></line>
<text
  class="td-chart-value"
  x="70"
  y="120.67"
  text-anchor="end"
>100</text>
<line
  class="td-chart-grid"
  x1="80"
  y1="24"
  x2="696"
  y2="24"
></line>
<text
  class="td-chart-value"
  x="70"
  y="28"
  text-anchor="end"
>150</text>
<line
  class="td-chart-axis"
  x1="80"
  y1="302"
  x2="696"
  y2="302"
></line>
<line
  class="td-chart-axis"
  x1="80"
  y1="24"
  x2="80"
  y2="302"
></line>
<text
  class="td-chart-label"
  x="80"
  y="324"
  text-anchor="middle"
>0</text>
<text
  class="td-chart-label"
  x="388"
  y="324"
  text-anchor="middle"
>4</text>
<text
  class="td-chart-label"
  x="696"
  y="324"
  text-anchor="middle"
>8</text>
<polyline class="td-chart-line td-chart-series-0" points="80,302 157,153.73 234,79.6 311,125.93 388,107.4 465,118.52 542,114.81 619,116.67 696,116.67"></polyline>
<circle
  class="td-chart-point td-chart-series-0"
  cx="80"
  cy="302"
  r="4"
><title>Spring: (0, 0)</title></circle>
<circle
  class="td-chart-point td-chart-series-0"
  cx="157"
  cy="153.73"
  r="4"
><title>Spring: (1, 80)</title></circle>
<circle
  class="td-chart-point td-chart-series-0"
  cx="234"
  cy="79.6"
  r="4"
><title>Spring: (2, 120)</title></circle>
<circle
  class="td-chart-point td-chart-series-0"
  cx="311"
  cy="125.93"
  r="4"
><title>Spring: (3, 95)</title></circle>
<circle
  class="td-chart-point td-chart-series-0"
  cx="388"
  cy="107.4"
  r="4"
><title>Spring: (4, 105)</title></circle>
<circle
  class="td-chart-point td-chart-series-0"
  cx="465"
  cy="118.52"
  r="4"
><title>Spring: (5, 99)</title></circle>
<circle
  class="td-chart-point td-chart-series-0"
  cx="542"
  cy="114.81"
  r="4"
><title>Spring: (6, 101)</title></circle>
<circle
  class="td-chart-point td-chart-series-0"
  cx="619"
  cy="116.67"
  r="4"
><title>Spring: (7, 100)</title></circle>
<circle
  class="td-chart-point td-chart-series-0"
  cx="696"
  cy="116.67"
  r="4"
><title>Spring: (8, 100)</title></circle>
<text
  class="td-chart-label"
  x="388"
  y="368"
  text-anchor="middle"
>Time</text>
<text
  class="td-chart-label"
  x="16"
  y="163"
  text-anchor="middle"
  transform="rotate(-90 16 163)"
>Value</text>
<rect
  class="td-chart-series-0"
  x="80"
  y="336"
  width="12"
  height="12"
  rx="3"
  fill="currentColor"
></rect>
<text
  class="td-chart-legend-text"
  x="98"
  y="346"
  text-anchor="start"
>Spring</text>
</svg>
</div>
</figure>
<p>Two lines of math carry the whole motion. The presented value is the start plus the delta, scaled by the curve $p(t)$, which runs from 0 to 1, so the data sits at the target while only the presentation moves:</p>
<div class="td-math td-math-display"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 136.27 11.21" fill="currentColor" aria-hidden="true" style="width:13.63em;height:1.12em;vertical-align:-0.4em"><path d="M5.08 3.2L5.08 2.89C4.85 2.91 4.56 2.92 4.33 2.92L3.46 2.89L3.46 3.2C3.83 3.21 3.94 3.44 3.94 3.63C3.94 3.72 3.92 3.76 3.88 3.87L2.86 6.42L1.74 3.63C1.68 3.5 1.68 3.46 1.68 3.46C1.68 3.2 2.07 3.2 2.25 3.2L2.25 2.89L1.16 2.92C0.89 2.92 0.49 2.91 0.19 2.89L0.19 3.2C0.82 3.2 0.86 3.26 0.99 3.57L2.43 7.12C2.49 7.26 2.51 7.31 2.64 7.31C2.77 7.31 2.81 7.22 2.85 7.12L4.16 3.87C4.25 3.64 4.42 3.21 5.08 3.2Z"/><path d="M8.6 9.58C8.6 9.55 8.58 9.52 8.56 9.5C7.52 8.72 6.83 6.72 6.83 5.12L6.83 4.28C6.83 2.68 7.52 0.68 8.56 -0.1C8.58 -0.12 8.6 -0.15 8.6 -0.18C8.6 -0.23 8.55 -0.28 8.5 -0.28C8.48 -0.28 8.46 -0.27 8.44 -0.26C7.34 0.57 6.29 2.57 6.29 4.28L6.29 5.12C6.29 6.83 7.34 8.83 8.44 9.66C8.46 9.67 8.48 9.68 8.5 9.68C8.55 9.68 8.6 9.63 8.6 9.58Z"/><path d="M12.49 5.96L12.49 5.39L12.24 5.39L12.24 5.94C12.24 6.68 11.94 7.06 11.57 7.06C10.9 7.06 10.9 6.15 10.9 5.98L10.9 3.2L12.33 3.2L12.33 2.89L10.9 2.89L10.9 1.05L10.65 1.05C10.64 1.87 10.34 2.94 9.36 2.98L9.36 3.2L10.21 3.2L10.21 5.96C10.21 7.19 11.14 7.31 11.5 7.31C12.21 7.31 12.49 6.6 12.49 5.96Z"/><path d="M15.94 5.12L15.94 4.28C15.94 2.57 14.89 0.57 13.79 -0.26C13.77 -0.27 13.75 -0.28 13.73 -0.28C13.68 -0.28 13.63 -0.23 13.63 -0.18C13.63 -0.15 13.65 -0.12 13.67 -0.1C14.71 0.68 15.4 2.68 15.4 4.28L15.4 5.12C15.4 6.72 14.71 8.72 13.67 9.5C13.65 9.52 13.63 9.55 13.63 9.58C13.63 9.63 13.68 9.68 13.73 9.68C13.75 9.68 13.77 9.67 13.79 9.66C14.89 8.83 15.94 6.83 15.94 5.12Z"/><path d="M27.49 3.73C27.49 3.62 27.4 3.53 27.29 3.53L21.03 3.53C20.92 3.53 20.83 3.62 20.83 3.73C20.83 3.84 20.92 3.93 21.03 3.93L27.29 3.93C27.4 3.93 27.49 3.84 27.49 3.73ZM27.49 5.67C27.49 5.56 27.4 5.47 27.29 5.47L21.03 5.47C20.92 5.47 20.83 5.56 20.83 5.67C20.83 5.78 20.92 5.87 21.03 5.87L27.29 5.87C27.4 5.87 27.49 5.78 27.49 5.67Z"/><path d="M36.45 3.2L36.45 2.89C36.22 2.91 35.93 2.92 35.7 2.92L34.83 2.89L34.83 3.2C35.2 3.21 35.31 3.44 35.31 3.63C35.31 3.72 35.29 3.76 35.25 3.87L34.23 6.42L33.11 3.63C33.05 3.5 33.05 3.46 33.05 3.46C33.05 3.2 33.44 3.2 33.62 3.2L33.62 2.89L32.53 2.92C32.26 2.92 31.86 2.91 31.56 2.89L31.56 3.2C32.19 3.2 32.23 3.26 32.36 3.57L33.8 7.12C33.86 7.26 33.88 7.31 34.01 7.31C34.14 7.31 34.18 7.22 34.22 7.12L35.53 3.87C35.62 3.64 35.79 3.21 36.45 3.2Z"/><path d="M39.71 5.23C39.71 4.97 39.45 4.73 39.08 4.73C38.59 4.73 37.99 5.11 37.99 5.85L37.99 6.65L37.44 6.65L37.44 6.87L37.99 6.87L37.99 9.14C37.99 9.45 37.92 9.45 37.45 9.45L37.45 9.67L38.25 9.65C38.53 9.65 38.86 9.65 39.14 9.67L39.14 9.45L38.99 9.45C38.47 9.45 38.46 9.38 38.46 9.12L38.46 6.87L39.25 6.87L39.25 6.65L38.44 6.65L38.44 5.84C38.44 5.23 38.77 4.89 39.08 4.89C39.1 4.89 39.21 4.89 39.31 4.94C39.23 4.97 39.1 5.06 39.1 5.23C39.1 5.39 39.21 5.53 39.4 5.53C39.6 5.53 39.71 5.39 39.71 5.23Z"/><path d="M41.9 7C41.9 6.78 41.68 6.58 41.38 6.58C40.87 6.58 40.62 7.05 40.52 7.35L40.52 6.58L39.55 6.65L39.55 6.87C40.04 6.87 40.09 6.92 40.09 7.26L40.09 9.14C40.09 9.45 40.02 9.45 39.55 9.45L39.55 9.67L40.35 9.65C40.63 9.65 40.96 9.65 41.24 9.67L41.24 9.45L41.09 9.45C40.57 9.45 40.56 9.38 40.56 9.12L40.56 8.05C40.56 7.35 40.85 6.73 41.38 6.73C41.43 6.73 41.45 6.73 41.46 6.74C41.44 6.74 41.3 6.83 41.3 7.01C41.3 7.21 41.45 7.31 41.6 7.31C41.73 7.31 41.9 7.23 41.9 7Z"/><path d="M45.39 8.17C45.39 7.28 44.69 6.53 43.85 6.53C42.97 6.53 42.29 7.3 42.29 8.17C42.29 9.07 43.02 9.75 43.84 9.75C44.69 9.75 45.39 9.06 45.39 8.17ZM44.81 8.12C44.81 8.37 44.81 8.75 44.66 9.05C44.5 9.37 44.2 9.57 43.85 9.57C43.55 9.57 43.24 9.43 43.05 9.1C42.87 8.8 42.87 8.37 42.87 8.12C42.87 7.84 42.87 7.47 43.04 7.16C43.23 6.84 43.56 6.69 43.84 6.69C44.15 6.69 44.45 6.84 44.63 7.14C44.81 7.44 44.81 7.85 44.81 8.12Z"/><path d="M51.29 9.67L51.29 9.45C50.92 9.45 50.75 9.45 50.74 9.24L50.74 7.91C50.74 7.3 50.74 7.09 50.52 6.84C50.43 6.72 50.2 6.58 49.79 6.58C49.2 6.58 48.89 7 48.77 7.26C48.68 6.65 48.16 6.58 47.84 6.58C47.33 6.58 47 6.88 46.81 7.31L46.81 6.58L45.82 6.65L45.82 6.87C46.31 6.87 46.37 6.92 46.37 7.26L46.37 9.14C46.37 9.45 46.29 9.45 45.82 9.45L45.82 9.67L46.61 9.65L47.4 9.67L47.4 9.45C46.93 9.45 46.85 9.45 46.85 9.14L46.85 7.85C46.85 7.12 47.35 6.73 47.79 6.73C48.24 6.73 48.31 7.11 48.31 7.51L48.31 9.14C48.31 9.45 48.24 9.45 47.77 9.45L47.77 9.67L48.56 9.65L49.34 9.67L49.34 9.45C48.87 9.45 48.8 9.45 48.8 9.14L48.8 7.85C48.8 7.12 49.29 6.73 49.74 6.73C50.18 6.73 50.26 7.11 50.26 7.51L50.26 9.14C50.26 9.45 50.18 9.45 49.71 9.45L49.71 9.67L50.5 9.65Z"/><path d="M61.97 4.7C61.97 4.59 61.88 4.5 61.77 4.5L58.84 4.5L58.84 1.57C58.84 1.46 58.75 1.37 58.64 1.37C58.53 1.37 58.44 1.46 58.44 1.57L58.44 4.5L55.51 4.5C55.4 4.5 55.31 4.59 55.31 4.7C55.31 4.81 55.4 4.9 55.51 4.9L58.44 4.9L58.44 7.83C58.44 7.94 58.53 8.03 58.64 8.03C58.75 8.03 58.84 7.94 58.84 7.83L58.84 4.9L61.77 4.9C61.88 4.9 61.97 4.81 61.97 4.7Z"/><path d="M69.17 9.58C69.17 9.55 69.15 9.52 69.13 9.5C68.09 8.72 67.4 6.72 67.4 5.12L67.4 4.28C67.4 2.68 68.09 0.68 69.13 -0.1C69.15 -0.12 69.17 -0.15 69.17 -0.18C69.17 -0.23 69.12 -0.28 69.07 -0.28C69.05 -0.28 69.03 -0.27 69.01 -0.26C67.91 0.57 66.86 2.57 66.86 4.28L66.86 5.12C66.86 6.83 67.91 8.83 69.01 9.66C69.03 9.67 69.05 9.68 69.07 9.68C69.12 9.68 69.17 9.63 69.17 9.58Z"/><path d="M74.82 3.2L74.82 2.89C74.59 2.91 74.3 2.92 74.07 2.92L73.2 2.89L73.2 3.2C73.57 3.21 73.68 3.44 73.68 3.63C73.68 3.72 73.66 3.76 73.62 3.87L72.6 6.42L71.48 3.63C71.42 3.5 71.42 3.46 71.42 3.46C71.42 3.2 71.81 3.2 71.99 3.2L71.99 2.89L70.9 2.92C70.63 2.92 70.23 2.91 69.93 2.89L69.93 3.2C70.56 3.2 70.6 3.26 70.73 3.57L72.17 7.12C72.23 7.26 72.25 7.31 72.38 7.31C72.51 7.31 72.55 7.22 72.59 7.12L73.9 3.87C73.99 3.64 74.16 3.21 74.82 3.2Z"/><path d="M77.9 8.8L77.9 8.4L77.73 8.4L77.73 8.79C77.73 9.31 77.52 9.57 77.26 9.57C76.79 9.57 76.79 8.94 76.79 8.82L76.79 6.87L77.79 6.87L77.79 6.65L76.79 6.65L76.79 5.37L76.61 5.37C76.61 5.94 76.4 6.69 75.71 6.72L75.71 6.87L76.3 6.87L76.3 8.8C76.3 9.66 76.96 9.75 77.21 9.75C77.71 9.75 77.9 9.25 77.9 8.8Z"/><path d="M81.6 8.17C81.6 7.28 80.9 6.53 80.05 6.53C79.18 6.53 78.5 7.3 78.5 8.17C78.5 9.07 79.22 9.75 80.04 9.75C80.89 9.75 81.6 9.06 81.6 8.17ZM81.02 8.12C81.02 8.37 81.02 8.75 80.86 9.05C80.71 9.37 80.4 9.57 80.05 9.57C79.75 9.57 79.44 9.43 79.25 9.1C79.08 8.8 79.08 8.37 79.08 8.12C79.08 7.84 79.08 7.47 79.24 7.16C79.43 6.84 79.76 6.69 80.04 6.69C80.35 6.69 80.65 6.84 80.83 7.14C81.02 7.44 81.02 7.85 81.02 8.12Z"/><path d="M87.88 5.33L87.88 4.75L85.23 4.75L85.23 5.33Z"/><path d="M96.85 3.2L96.85 2.89C96.62 2.91 96.33 2.92 96.1 2.92L95.23 2.89L95.23 3.2C95.6 3.21 95.71 3.44 95.71 3.63C95.71 3.72 95.69 3.76 95.65 3.87L94.63 6.42L93.51 3.63C93.45 3.5 93.45 3.46 93.45 3.46C93.45 3.2 93.84 3.2 94.02 3.2L94.02 2.89L92.93 2.92C92.66 2.92 92.26 2.91 91.96 2.89L91.96 3.2C92.59 3.2 92.63 3.26 92.76 3.57L94.2 7.12C94.26 7.26 94.28 7.31 94.41 7.31C94.54 7.31 94.58 7.22 94.62 7.12L95.93 3.87C96.02 3.64 96.19 3.21 96.85 3.2Z"/><path d="M100.11 5.23C100.11 4.97 99.85 4.73 99.48 4.73C98.99 4.73 98.39 5.11 98.39 5.85L98.39 6.65L97.84 6.65L97.84 6.87L98.39 6.87L98.39 9.14C98.39 9.45 98.32 9.45 97.85 9.45L97.85 9.67L98.65 9.65C98.93 9.65 99.25 9.65 99.53 9.67L99.53 9.45L99.39 9.45C98.87 9.45 98.86 9.38 98.86 9.12L98.86 6.87L99.65 6.87L99.65 6.65L98.83 6.65L98.83 5.84C98.83 5.23 99.17 4.89 99.48 4.89C99.5 4.89 99.6 4.89 99.71 4.94C99.63 4.97 99.5 5.06 99.5 5.23C99.5 5.39 99.61 5.53 99.8 5.53C100 5.53 100.11 5.39 100.11 5.23Z"/><path d="M102.3 7C102.3 6.78 102.08 6.58 101.78 6.58C101.27 6.58 101.02 7.05 100.92 7.35L100.92 6.58L99.95 6.65L99.95 6.87C100.44 6.87 100.49 6.92 100.49 7.26L100.49 9.14C100.49 9.45 100.42 9.45 99.95 9.45L99.95 9.67L100.75 9.65C101.03 9.65 101.35 9.65 101.63 9.67L101.63 9.45L101.49 9.45C100.97 9.45 100.96 9.38 100.96 9.12L100.96 8.05C100.96 7.35 101.25 6.73 101.78 6.73C101.83 6.73 101.84 6.73 101.86 6.74C101.84 6.74 101.7 6.83 101.7 7.01C101.7 7.21 101.84 7.31 102 7.31C102.12 7.31 102.3 7.23 102.3 7Z"/><path d="M105.79 8.17C105.79 7.28 105.09 6.53 104.25 6.53C103.37 6.53 102.69 7.3 102.69 8.17C102.69 9.07 103.42 9.75 104.24 9.75C105.09 9.75 105.79 9.06 105.79 8.17ZM105.21 8.12C105.21 8.37 105.21 8.75 105.06 9.05C104.9 9.37 104.6 9.57 104.25 9.57C103.94 9.57 103.64 9.43 103.45 9.1C103.27 8.8 103.27 8.37 103.27 8.12C103.27 7.84 103.27 7.47 103.44 7.16C103.63 6.84 103.96 6.69 104.24 6.69C104.55 6.69 104.85 6.84 105.03 7.14C105.21 7.44 105.21 7.85 105.21 8.12Z"/><path d="M111.69 9.67L111.69 9.45C111.32 9.45 111.15 9.45 111.14 9.24L111.14 7.91C111.14 7.3 111.14 7.09 110.92 6.84C110.83 6.72 110.59 6.58 110.19 6.58C109.6 6.58 109.29 7 109.17 7.26C109.08 6.65 108.56 6.58 108.24 6.58C107.73 6.58 107.4 6.88 107.21 7.31L107.21 6.58L106.22 6.65L106.22 6.87C106.71 6.87 106.77 6.92 106.77 7.26L106.77 9.14C106.77 9.45 106.69 9.45 106.22 9.45L106.22 9.67L107.01 9.65L107.79 9.67L107.79 9.45C107.33 9.45 107.25 9.45 107.25 9.14L107.25 7.85C107.25 7.12 107.75 6.73 108.19 6.73C108.63 6.73 108.71 7.11 108.71 7.51L108.71 9.14C108.71 9.45 108.63 9.45 108.17 9.45L108.17 9.67L108.96 9.65L109.74 9.67L109.74 9.45C109.27 9.45 109.19 9.45 109.19 9.14L109.19 7.85C109.19 7.12 109.69 6.73 110.14 6.73C110.58 6.73 110.66 7.11 110.66 7.51L110.66 9.14C110.66 9.45 110.58 9.45 110.11 9.45L110.11 9.67L110.9 9.65Z"/><path d="M114.71 5.12L114.71 4.28C114.71 2.57 113.66 0.57 112.56 -0.26C112.54 -0.27 112.52 -0.28 112.5 -0.28C112.45 -0.28 112.4 -0.23 112.4 -0.18C112.4 -0.15 112.42 -0.12 112.44 -0.1C113.48 0.68 114.17 2.68 114.17 4.28L114.17 5.12C114.17 6.72 113.48 8.72 112.44 9.5C112.42 9.52 112.4 9.55 112.4 9.58C112.4 9.63 112.45 9.68 112.5 9.68C112.52 9.68 112.54 9.67 112.56 9.66C113.66 8.83 114.71 6.83 114.71 5.12Z"/><path d="M124.25 5.04C124.25 3.77 123.28 2.78 122.16 2.78C121.38 2.78 120.96 3.22 120.76 3.44L120.76 2.78L119.32 2.89L119.32 3.2C120.03 3.2 120.1 3.26 120.1 3.7L120.1 8.38C120.1 8.83 119.99 8.83 119.32 8.83L119.32 9.14L120.44 9.11L121.57 9.14L121.57 8.83C120.9 8.83 120.79 8.83 120.79 8.38L120.79 6.7L120.79 6.61C120.84 6.77 121.26 7.31 122.02 7.31C123.21 7.31 124.25 6.33 124.25 5.04ZM123.42 5.04C123.42 6.25 122.72 7.09 121.98 7.09C121.58 7.09 121.2 6.89 120.93 6.48C120.79 6.27 120.79 6.26 120.79 6.06L120.79 3.83C121.08 3.32 121.57 3.03 122.08 3.03C122.81 3.03 123.42 3.91 123.42 5.04Z"/><path d="M127.92 9.58C127.92 9.55 127.9 9.52 127.88 9.5C126.84 8.72 126.15 6.72 126.15 5.12L126.15 4.28C126.15 2.68 126.84 0.68 127.88 -0.1C127.9 -0.12 127.92 -0.15 127.92 -0.18C127.92 -0.23 127.87 -0.28 127.82 -0.28C127.8 -0.28 127.78 -0.27 127.76 -0.26C126.66 0.57 125.61 2.57 125.61 4.28L125.61 5.12C125.61 6.83 126.66 8.83 127.76 9.66C127.78 9.67 127.8 9.68 127.82 9.68C127.87 9.68 127.92 9.63 127.92 9.58Z"/><path d="M131.81 5.96L131.81 5.39L131.56 5.39L131.56 5.94C131.56 6.68 131.26 7.06 130.89 7.06C130.22 7.06 130.22 6.15 130.22 5.98L130.22 3.2L131.65 3.2L131.65 2.89L130.22 2.89L130.22 1.05L129.97 1.05C129.96 1.87 129.66 2.94 128.68 2.98L128.68 3.2L129.53 3.2L129.53 5.96C129.53 7.19 130.46 7.31 130.82 7.31C131.53 7.31 131.81 6.6 131.81 5.96Z"/><path d="M135.26 5.12L135.26 4.28C135.26 2.57 134.21 0.57 133.11 -0.26C133.09 -0.27 133.07 -0.28 133.05 -0.28C133 -0.28 132.95 -0.23 132.95 -0.18C132.95 -0.15 132.97 -0.12 132.99 -0.1C134.03 0.68 134.72 2.68 134.72 4.28L134.72 5.12C134.72 6.72 134.03 8.72 132.99 9.5C132.97 9.52 132.95 9.55 132.95 9.58C132.95 9.63 133 9.68 133.05 9.68C133.07 9.68 133.09 9.67 133.11 9.66C134.21 8.83 135.26 6.83 135.26 5.12Z"/></svg><math class="td-math-a11y" xmlns="http://www.w3.org/1998/Math/MathML"><mrow><mi>v</mi><mtext>(</mtext><mi>t</mi><mtext>)</mtext><mtext> </mtext><mtext>=</mtext><mtext> </mtext><msub><mrow><mi>v</mi></mrow><mrow><mrow><mi>f</mi><mi>r</mi><mi>o</mi><mi>m</mi></mrow></mrow></msub><mtext> </mtext><mtext>+</mtext><mtext> </mtext><mtext>(</mtext><msub><mrow><mi>v</mi></mrow><mrow><mrow><mi>t</mi><mi>o</mi></mrow></mrow></msub><mtext> </mtext><mtext>-</mtext><mtext> </mtext><msub><mrow><mi>v</mi></mrow><mrow><mrow><mi>f</mi><mi>r</mi><mi>o</mi><mi>m</mi></mrow></mrow></msub><mtext>)</mtext><mtext> </mtext><mi>p</mi><mtext>(</mtext><mi>t</mi><mtext>)</mtext></mrow></math></div>
<p>A spring is the same machinery with the curve replaced by a damped harmonic oscillator. Released from rest, its progress curve is</p>
<div class="td-math td-math-display"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 193.08 21.18" fill="currentColor" aria-hidden="true" style="width:19.31em;height:2.12em;vertical-align:-0.81em"><path d="M5.21 10.93C5.21 9.66 4.24 8.67 3.12 8.67C2.34 8.67 1.92 9.11 1.72 9.33L1.72 8.67L0.28 8.78L0.28 9.09C0.99 9.09 1.06 9.15 1.06 9.59L1.06 14.27C1.06 14.72 0.95 14.72 0.28 14.72L0.28 15.03L1.4 15L2.53 15.03L2.53 14.72C1.86 14.72 1.75 14.72 1.75 14.27L1.75 12.59L1.75 12.5C1.8 12.66 2.22 13.2 2.98 13.2C4.17 13.2 5.21 12.22 5.21 10.93ZM4.38 10.93C4.38 12.14 3.68 12.98 2.94 12.98C2.54 12.98 2.16 12.78 1.89 12.37C1.75 12.16 1.75 12.15 1.75 11.95L1.75 9.72C2.04 9.21 2.53 8.92 3.04 8.92C3.77 8.92 4.38 9.8 4.38 10.93Z"/><path d="M8.88 15.47C8.88 15.44 8.86 15.41 8.84 15.39C7.8 14.61 7.11 12.61 7.11 11.01L7.11 10.17C7.11 8.57 7.8 6.57 8.84 5.79C8.86 5.77 8.88 5.74 8.88 5.71C8.88 5.66 8.83 5.61 8.78 5.61C8.76 5.61 8.74 5.62 8.72 5.63C7.62 6.46 6.57 8.46 6.57 10.17L6.57 11.01C6.57 12.72 7.62 14.72 8.72 15.55C8.74 15.56 8.76 15.57 8.78 15.57C8.83 15.57 8.88 15.52 8.88 15.47Z"/><path d="M12.77 11.85L12.77 11.28L12.52 11.28L12.52 11.83C12.52 12.57 12.22 12.95 11.85 12.95C11.18 12.95 11.18 12.04 11.18 11.87L11.18 9.09L12.61 9.09L12.61 8.78L11.18 8.78L11.18 6.94L10.93 6.94C10.92 7.76 10.62 8.83 9.64 8.87L9.64 9.09L10.49 9.09L10.49 11.85C10.49 13.08 11.42 13.2 11.78 13.2C12.49 13.2 12.77 12.49 12.77 11.85Z"/><path d="M16.22 11.01L16.22 10.17C16.22 8.46 15.17 6.46 14.07 5.63C14.05 5.62 14.03 5.61 14.01 5.61C13.96 5.61 13.91 5.66 13.91 5.71C13.91 5.74 13.93 5.77 13.95 5.79C14.99 6.57 15.68 8.57 15.68 10.17L15.68 11.01C15.68 12.61 14.99 14.61 13.95 15.39C13.93 15.41 13.91 15.44 13.91 15.47C13.91 15.52 13.96 15.57 14.01 15.57C14.03 15.57 14.05 15.56 14.07 15.55C15.17 14.72 16.22 12.72 16.22 11.01Z"/><path d="M27.77 9.62C27.77 9.51 27.68 9.42 27.57 9.42L21.31 9.42C21.2 9.42 21.11 9.51 21.11 9.62C21.11 9.73 21.2 9.82 21.31 9.82L27.57 9.82C27.68 9.82 27.77 9.73 27.77 9.62ZM27.77 11.56C27.77 11.45 27.68 11.36 27.57 11.36L21.31 11.36C21.2 11.36 21.11 11.45 21.11 11.56C21.11 11.67 21.2 11.76 21.31 11.76L27.57 11.76C27.68 11.76 27.77 11.67 27.77 11.56Z"/><path d="M35.84 13.09L35.84 12.78L35.52 12.78C34.62 12.78 34.59 12.67 34.59 12.3L34.59 6.69C34.59 6.45 34.59 6.43 34.36 6.43C33.74 7.07 32.86 7.07 32.54 7.07L32.54 7.38C32.74 7.38 33.33 7.38 33.85 7.12L33.85 12.3C33.85 12.66 33.82 12.78 32.92 12.78L32.6 12.78L32.6 13.09C32.95 13.06 33.82 13.06 34.22 13.06C34.62 13.06 35.49 13.06 35.84 13.09Z"/><path d="M42.73 11.22L42.73 10.64L40.08 10.64L40.08 11.22Z"/><path d="M50.77 11.9C50.77 11.8 50.69 11.78 50.64 11.78C50.55 11.78 50.53 11.84 50.51 11.92C50.16 12.95 49.26 12.95 49.16 12.95C48.66 12.95 48.26 12.65 48.03 12.28C47.73 11.8 47.73 11.14 47.73 10.78L50.52 10.78C50.74 10.78 50.77 10.78 50.77 10.57C50.77 9.58 50.23 8.61 48.98 8.61C47.82 8.61 46.9 9.64 46.9 10.89C46.9 12.23 47.95 13.2 49.1 13.2C50.32 13.2 50.77 12.09 50.77 11.9ZM50.11 10.57L47.74 10.57C47.8 9.08 48.64 8.83 48.98 8.83C50.01 8.83 50.11 10.18 50.11 10.57Z"/><path d="M53.55 8.15L53.55 7.75L51.7 7.75L51.7 8.15Z"/><path d="M57.34 10.05C57.27 9.78 57.1 9.64 57.03 9.58C56.88 9.45 56.74 9.41 56.29 9.27C55.68 9.08 55.56 9.04 55.36 8.9C54.99 8.63 54.9 8.23 54.86 8.1C54.6 7.01 55.14 5.79 55.75 5.41C55.91 5.54 56.07 5.54 56.21 5.54C56.39 5.54 56.84 5.54 56.78 5.32C56.74 5.14 56.45 5.13 56.17 5.13C56.09 5.13 55.9 5.13 55.74 5.2C55.69 5.12 55.67 5.08 55.62 4.93C55.6 4.84 55.6 4.67 55.6 4.67C55.58 4.62 55.54 4.58 55.49 4.58C55.37 4.58 55.44 4.91 55.45 4.93C55.48 5.05 55.53 5.17 55.62 5.28C54.97 5.65 54.19 6.94 54.53 8.29C54.78 9.28 55.46 9.49 56.01 9.66C56.24 9.74 56.25 9.74 56.47 9.81C56.63 9.85 56.94 9.95 57.02 10.28C57.07 10.47 57.01 10.74 56.75 10.74C56.57 10.74 56.36 10.66 56.18 10.53C56.12 10.49 56.11 10.48 56.08 10.48C56.04 10.48 56.02 10.51 56.03 10.56C56.05 10.63 56.42 10.9 56.79 10.9C57.22 10.9 57.44 10.45 57.34 10.05ZM56.6 5.33C56.55 5.35 56.49 5.38 56.18 5.38C56.03 5.38 55.98 5.38 55.9 5.33C55.99 5.28 56.11 5.28 56.21 5.28C56.44 5.28 56.46 5.29 56.6 5.33Z"/><path d="M64.16 7.79C64.16 7.49 64.16 7.14 64.09 6.86C64.05 6.68 63.97 6.52 63.84 6.43C63.79 6.39 63.73 6.37 63.67 6.37C63.58 6.37 63.5 6.41 63.45 6.48C63.4 6.55 63.39 6.63 63.41 6.7C63.42 6.77 63.46 6.83 63.53 6.88C63.74 7.03 63.88 7.25 63.95 7.5C63.97 7.58 63.98 7.67 63.98 7.75C63.98 8.39 63.74 9.04 63.18 9.04C62.79 9.04 62.49 8.77 62.34 8.41C62.38 8.21 62.4 8 62.39 7.79C62.39 7.73 62.39 7.66 62.38 7.61C62.35 7.5 62.3 7.43 62.2 7.43C62.06 7.43 62.04 7.6 62.04 7.75C62.04 7.97 62.06 8.18 62.11 8.38C61.97 8.76 61.71 9.04 61.32 9.04C60.9 9.04 60.62 8.69 60.52 8.24C60.48 8.1 60.46 7.94 60.45 7.79C60.45 7.37 60.61 6.98 60.78 6.61C60.8 6.58 60.8 6.54 60.8 6.52C60.78 6.47 60.75 6.44 60.71 6.42C60.69 6.4 60.66 6.4 60.64 6.4C60.6 6.4 60.56 6.42 60.53 6.47C60.34 6.87 60.28 7.31 60.28 7.75C60.28 8.02 60.31 8.29 60.37 8.54C60.51 9.11 60.83 9.54 61.36 9.54C61.8 9.54 62.09 9.23 62.24 8.8C62.43 9.23 62.75 9.54 63.22 9.54C63.95 9.54 64.16 8.64 64.16 7.79Z"/><path d="M67.12 9.62C67.12 9.23 67.09 8.84 66.92 8.48C66.7 8.01 66.29 7.93 66.09 7.93C65.8 7.93 65.44 8.06 65.24 8.51C65.08 8.85 65.06 9.23 65.06 9.62C65.06 9.99 65.07 10.43 65.28 10.8C65.49 11.2 65.84 11.3 66.08 11.3C66.35 11.3 66.72 11.2 66.94 10.73C67.09 10.39 67.12 10.01 67.12 9.62ZM66.71 9.56C66.71 9.93 66.71 10.27 66.66 10.58C66.58 11.04 66.3 11.19 66.08 11.19C65.89 11.19 65.6 11.07 65.52 10.6C65.46 10.3 65.46 9.85 65.46 9.56C65.46 9.25 65.46 8.93 65.5 8.66C65.59 8.08 65.96 8.04 66.08 8.04C66.25 8.04 66.57 8.12 66.66 8.61C66.71 8.88 66.71 9.26 66.71 9.56Z"/><path d="M71.96 8.59L71.96 8.2L71.79 8.2L71.79 8.58C71.79 9.1 71.58 9.36 71.32 9.36C70.85 9.36 70.85 8.73 70.85 8.61L70.85 6.66L71.85 6.66L71.85 6.45L70.85 6.45L70.85 5.16L70.67 5.16C70.67 5.73 70.46 6.48 69.77 6.51L69.77 6.66L70.37 6.66L70.37 8.59C70.37 9.46 71.02 9.54 71.27 9.54C71.77 9.54 71.96 9.04 71.96 8.59Z"/><path d="M74.92 15.39C74.92 15.28 74.83 15.19 74.72 15.19L73.9 15.19L73.9 5.99L74.72 5.99C74.83 5.99 74.92 5.9 74.92 5.79C74.92 5.68 74.83 5.59 74.72 5.59L73.5 5.59L73.5 15.59L74.72 15.59C74.83 15.59 74.92 15.5 74.92 15.39Z"/><path d="M79.29 11.9C79.29 11.8 79.19 11.8 79.16 11.8C79.07 11.8 79.05 11.84 79.03 11.9C78.74 12.83 78.09 12.95 77.72 12.95C77.19 12.95 76.31 12.52 76.31 10.91C76.31 9.28 77.13 8.86 77.66 8.86C77.75 8.86 78.38 8.87 78.73 9.23C78.32 9.26 78.26 9.56 78.26 9.69C78.26 9.95 78.44 10.15 78.72 10.15C78.98 10.15 79.18 9.98 79.18 9.68C79.18 9 78.42 8.61 77.65 8.61C76.4 8.61 75.48 9.69 75.48 10.93C75.48 12.21 76.47 13.2 77.63 13.2C78.97 13.2 79.29 12 79.29 11.9Z"/><path d="M84.29 10.95C84.29 9.67 83.29 8.61 82.08 8.61C80.83 8.61 79.86 9.7 79.86 10.95C79.86 12.24 80.9 13.2 82.07 13.2C83.28 13.2 84.29 12.22 84.29 10.95ZM83.46 10.87C83.46 11.23 83.46 11.77 83.24 12.21C83.02 12.66 82.58 12.95 82.08 12.95C81.65 12.95 81.21 12.74 80.94 12.28C80.69 11.84 80.69 11.23 80.69 10.87C80.69 10.48 80.69 9.94 80.93 9.5C81.2 9.04 81.67 8.83 82.07 8.83C82.51 8.83 82.94 9.05 83.2 9.48C83.46 9.91 83.46 10.49 83.46 10.87Z"/><path d="M88.18 11.81C88.18 11.28 87.88 10.98 87.76 10.86C87.43 10.54 87.04 10.46 86.62 10.38C86.06 10.27 85.39 10.14 85.39 9.56C85.39 9.21 85.65 8.8 86.51 8.8C87.61 8.8 87.66 9.7 87.68 10.01C87.69 10.1 87.8 10.1 87.8 10.1C87.93 10.1 87.93 10.05 87.93 9.86L87.93 8.85C87.93 8.68 87.93 8.61 87.82 8.61C87.77 8.61 87.75 8.61 87.62 8.73C87.59 8.77 87.49 8.86 87.45 8.89C87.07 8.61 86.66 8.61 86.51 8.61C85.29 8.61 84.91 9.28 84.91 9.84C84.91 10.19 85.07 10.47 85.34 10.69C85.66 10.95 85.94 11.01 86.66 11.15C86.88 11.19 87.7 11.35 87.7 12.07C87.7 12.58 87.35 12.98 86.57 12.98C85.73 12.98 85.37 12.41 85.18 11.56C85.15 11.43 85.14 11.39 85.04 11.39C84.91 11.39 84.91 11.46 84.91 11.64L84.91 12.96C84.91 13.13 84.91 13.2 85.02 13.2C85.07 13.2 85.08 13.19 85.27 13C85.29 12.98 85.29 12.96 85.47 12.77C85.91 13.19 86.36 13.2 86.57 13.2C87.72 13.2 88.18 12.53 88.18 11.81Z"/><path d="M91.84 15.47C91.84 15.44 91.82 15.41 91.8 15.39C90.76 14.61 90.07 12.61 90.07 11.01L90.07 10.17C90.07 8.57 90.76 6.57 91.8 5.79C91.82 5.77 91.84 5.74 91.84 5.71C91.84 5.66 91.79 5.61 91.74 5.61C91.72 5.61 91.7 5.62 91.68 5.63C90.58 6.46 89.53 8.46 89.53 10.17L89.53 11.01C89.53 12.72 90.58 14.72 91.68 15.55C91.7 15.56 91.72 15.57 91.74 15.57C91.79 15.57 91.84 15.52 91.84 15.47Z"/><path d="M98.37 10.7C98.38 10.27 98.37 9.77 98.27 9.37C98.21 9.11 98.1 8.89 97.92 8.76C97.84 8.7 97.76 8.67 97.67 8.67C97.55 8.67 97.43 8.73 97.36 8.83C97.29 8.93 97.27 9.04 97.3 9.15C97.32 9.25 97.38 9.33 97.47 9.4C97.77 9.62 97.98 9.93 98.07 10.28C98.1 10.4 98.12 10.53 98.12 10.65C98.12 11.56 97.77 12.48 96.98 12.48C96.42 12.48 95.99 12.1 95.77 11.58C95.83 11.3 95.86 11 95.85 10.7C95.85 10.61 95.85 10.52 95.83 10.44C95.79 10.29 95.71 10.18 95.57 10.18C95.38 10.18 95.35 10.43 95.35 10.65C95.35 10.96 95.38 11.26 95.45 11.55C95.25 12.09 94.87 12.48 94.32 12.48C93.71 12.48 93.32 11.98 93.17 11.35C93.12 11.15 93.09 10.92 93.08 10.7C93.08 10.1 93.3 9.54 93.55 9.01C93.57 8.97 93.58 8.92 93.57 8.88C93.55 8.82 93.5 8.77 93.44 8.74C93.41 8.72 93.38 8.72 93.35 8.72C93.29 8.72 93.23 8.75 93.19 8.81C92.92 9.38 92.83 10.02 92.83 10.65C92.83 11.03 92.87 11.42 92.96 11.77C93.16 12.58 93.61 13.2 94.37 13.2C95 13.2 95.41 12.76 95.63 12.14C95.9 12.76 96.36 13.2 97.03 13.2C98.07 13.2 98.38 11.91 98.37 10.7Z"/><path d="M103.07 15.56L103.07 15.35C102.58 15.35 102.52 15.3 102.52 14.95L102.52 10.7L101.52 10.78L101.52 11C102.01 11 102.06 11.05 102.06 11.39L102.06 12.9C101.86 12.65 101.56 12.47 101.18 12.47C100.35 12.47 99.62 13.15 99.62 14.06C99.62 14.95 100.31 15.64 101.1 15.64C101.55 15.64 101.87 15.4 102.04 15.18L102.04 15.64ZM102.04 14.74C102.04 14.86 102.04 14.88 101.96 15C101.75 15.33 101.44 15.49 101.14 15.49C100.82 15.49 100.57 15.3 100.4 15.04C100.22 14.75 100.2 14.35 100.2 14.06C100.2 13.81 100.21 13.39 100.42 13.07C100.56 12.85 100.83 12.62 101.21 12.62C101.45 12.62 101.75 12.73 101.96 13.04C102.04 13.16 102.04 13.18 102.04 13.3Z"/><path d="M109.91 11.85L109.91 11.28L109.66 11.28L109.66 11.83C109.66 12.57 109.36 12.95 108.99 12.95C108.32 12.95 108.32 12.04 108.32 11.87L108.32 9.09L109.75 9.09L109.75 8.78L108.32 8.78L108.32 6.94L108.07 6.94C108.06 7.76 107.76 8.83 106.78 8.87L106.78 9.09L107.63 9.09L107.63 11.85C107.63 13.08 108.56 13.2 108.92 13.2C109.63 13.2 109.91 12.49 109.91 11.85Z"/><path d="M113.36 11.01L113.36 10.17C113.36 8.46 112.31 6.46 111.21 5.63C111.19 5.62 111.17 5.61 111.15 5.61C111.1 5.61 111.05 5.66 111.05 5.71C111.05 5.74 111.07 5.77 111.09 5.79C112.13 6.57 112.82 8.57 112.82 10.17L112.82 11.01C112.82 12.61 112.13 14.61 111.09 15.39C111.07 15.41 111.05 15.44 111.05 15.47C111.05 15.52 111.1 15.57 111.15 15.57C111.17 15.57 111.19 15.56 111.21 15.55C112.31 14.72 113.36 12.72 113.36 11.01Z"/><path d="M124.91 10.59C124.91 10.48 124.82 10.39 124.71 10.39L121.78 10.39L121.78 7.46C121.78 7.35 121.69 7.26 121.58 7.26C121.47 7.26 121.38 7.35 121.38 7.46L121.38 10.39L118.45 10.39C118.34 10.39 118.25 10.48 118.25 10.59C118.25 10.7 118.34 10.79 118.45 10.79L121.38 10.79L121.38 13.72C121.38 13.83 121.47 13.92 121.58 13.92C121.69 13.92 121.78 13.83 121.78 13.72L121.78 10.79L124.71 10.79C124.82 10.79 124.91 10.7 124.91 10.59Z"/><rect x="128.79" y="10.39" width="20.05" height="0.4"/><path d="M134.96 6.59C134.88 6.28 134.68 6.11 134.6 6.04C134.43 5.89 134.26 5.84 133.73 5.67C133.02 5.45 132.88 5.41 132.64 5.24C132.21 4.93 132.1 4.46 132.06 4.3C131.75 3.03 132.39 1.6 133.1 1.16C133.29 1.3 133.48 1.3 133.64 1.3C133.85 1.3 134.37 1.3 134.31 1.05C134.26 0.84 133.92 0.83 133.59 0.83C133.5 0.83 133.28 0.83 133.09 0.91C133.03 0.81 133 0.77 132.95 0.6C132.93 0.48 132.92 0.3 132.92 0.3C132.9 0.23 132.85 0.19 132.8 0.19C132.65 0.19 132.74 0.57 132.75 0.59C132.78 0.73 132.85 0.88 132.94 1C132.19 1.43 131.27 2.94 131.67 4.53C131.96 5.69 132.76 5.94 133.4 6.13C133.67 6.22 133.69 6.22 133.95 6.31C134.13 6.36 134.49 6.48 134.59 6.86C134.65 7.08 134.58 7.4 134.27 7.4C134.06 7.4 133.81 7.31 133.6 7.16C133.54 7.11 133.52 7.1 133.49 7.1C133.44 7.1 133.41 7.13 133.43 7.19C133.45 7.27 133.88 7.58 134.31 7.58C134.82 7.58 135.08 7.06 134.96 6.59ZM134.09 1.06C134.04 1.08 133.96 1.12 133.6 1.12C133.43 1.12 133.37 1.12 133.27 1.07C133.38 1.01 133.53 1.01 133.63 1.01C133.9 1.01 133.94 1.02 134.09 1.06Z"/><path d="M142.95 3.94C142.96 3.59 142.95 3.18 142.87 2.85C142.82 2.64 142.73 2.46 142.58 2.35C142.51 2.3 142.45 2.28 142.37 2.28C142.28 2.28 142.18 2.33 142.12 2.41C142.06 2.49 142.05 2.58 142.07 2.67C142.09 2.76 142.14 2.82 142.21 2.88C142.46 3.06 142.63 3.31 142.7 3.6C142.73 3.7 142.74 3.8 142.74 3.9C142.74 4.65 142.46 5.4 141.81 5.4C141.35 5.4 141 5.09 140.82 4.67C140.87 4.44 140.89 4.19 140.88 3.94C140.88 3.87 140.88 3.8 140.87 3.73C140.83 3.61 140.77 3.52 140.65 3.52C140.5 3.52 140.47 3.72 140.47 3.9C140.47 4.16 140.5 4.4 140.55 4.64C140.39 5.08 140.08 5.4 139.63 5.4C139.13 5.4 138.81 4.99 138.68 4.48C138.64 4.31 138.62 4.12 138.61 3.94C138.61 3.45 138.79 2.99 139 2.56C139.01 2.53 139.02 2.48 139.01 2.45C139 2.4 138.96 2.36 138.91 2.34C138.88 2.32 138.86 2.32 138.83 2.32C138.78 2.32 138.73 2.35 138.7 2.39C138.48 2.86 138.41 3.39 138.41 3.9C138.41 4.21 138.44 4.53 138.51 4.82C138.68 5.49 139.05 5.99 139.67 5.99C140.19 5.99 140.52 5.63 140.7 5.12C140.92 5.63 141.3 5.99 141.85 5.99C142.7 5.99 142.96 4.94 142.95 3.94Z"/><path d="M146.42 6.09C146.42 5.63 146.39 5.17 146.19 4.75C145.92 4.2 145.45 4.11 145.21 4.11C144.87 4.11 144.45 4.26 144.21 4.79C144.03 5.19 144 5.63 144 6.09C144 6.52 144.02 7.04 144.26 7.48C144.51 7.94 144.92 8.06 145.21 8.06C145.52 8.06 145.95 7.94 146.2 7.39C146.39 6.99 146.42 6.55 146.42 6.09ZM145.94 6.02C145.94 6.45 145.94 6.84 145.88 7.21C145.79 7.76 145.46 7.93 145.21 7.93C144.98 7.93 144.64 7.79 144.54 7.23C144.48 6.89 144.48 6.36 144.48 6.02C144.48 5.66 144.48 5.28 144.52 4.97C144.63 4.28 145.06 4.23 145.21 4.23C145.4 4.23 145.77 4.34 145.88 4.9C145.94 5.23 145.94 5.66 145.94 6.02Z"/><path d="M139.25 15.94C139.26 15.58 139.25 15.17 139.17 14.85C139.12 14.63 139.03 14.45 138.88 14.35C138.82 14.3 138.75 14.27 138.68 14.27C138.58 14.27 138.48 14.32 138.43 14.4C138.37 14.48 138.35 14.58 138.38 14.67C138.39 14.75 138.44 14.81 138.52 14.87C138.76 15.05 138.93 15.3 139.01 15.59C139.03 15.69 139.05 15.8 139.05 15.9C139.05 16.64 138.76 17.4 138.11 17.4C137.65 17.4 137.3 17.08 137.12 16.66C137.17 16.43 137.2 16.18 137.19 15.94C137.19 15.86 137.19 15.79 137.17 15.72C137.14 15.6 137.07 15.51 136.96 15.51C136.8 15.51 136.78 15.71 136.78 15.9C136.78 16.15 136.8 16.4 136.86 16.63C136.7 17.08 136.38 17.4 135.93 17.4C135.43 17.4 135.11 16.99 134.99 16.47C134.95 16.31 134.92 16.12 134.92 15.94C134.92 15.44 135.1 14.99 135.3 14.55C135.32 14.52 135.33 14.48 135.32 14.44C135.3 14.39 135.26 14.35 135.21 14.33C135.19 14.31 135.16 14.31 135.14 14.31C135.09 14.31 135.04 14.34 135.01 14.39C134.78 14.85 134.71 15.38 134.71 15.9C134.71 16.21 134.74 16.53 134.82 16.81C134.98 17.48 135.35 17.99 135.97 17.99C136.49 17.99 136.83 17.63 137.01 17.12C137.23 17.63 137.61 17.99 138.15 17.99C139.01 17.99 139.26 16.93 139.25 15.94Z"/><path d="M143.11 19.92L143.11 19.74C142.71 19.74 142.66 19.7 142.66 19.42L142.66 15.94L141.83 16L141.83 16.18C142.23 16.18 142.28 16.22 142.28 16.5L142.28 17.74C142.11 17.53 141.87 17.38 141.56 17.38C140.88 17.38 140.28 17.95 140.28 18.69C140.28 19.42 140.84 19.98 141.49 19.98C141.86 19.98 142.12 19.79 142.26 19.61L142.26 19.98ZM142.26 19.24C142.26 19.35 142.26 19.36 142.2 19.46C142.03 19.73 141.77 19.86 141.52 19.86C141.26 19.86 141.06 19.71 140.92 19.49C140.77 19.26 140.75 18.93 140.75 18.69C140.75 18.48 140.76 18.14 140.93 17.88C141.05 17.7 141.27 17.51 141.58 17.51C141.78 17.51 142.02 17.6 142.2 17.86C142.26 17.95 142.26 17.96 142.26 18.07Z"/><path d="M155.77 11.81C155.77 11.28 155.47 10.98 155.35 10.86C155.02 10.54 154.63 10.46 154.21 10.38C153.65 10.27 152.98 10.14 152.98 9.56C152.98 9.21 153.24 8.8 154.1 8.8C155.2 8.8 155.25 9.7 155.27 10.01C155.28 10.1 155.39 10.1 155.39 10.1C155.52 10.1 155.52 10.05 155.52 9.86L155.52 8.85C155.52 8.68 155.52 8.61 155.41 8.61C155.36 8.61 155.34 8.61 155.21 8.73C155.18 8.77 155.08 8.86 155.04 8.89C154.66 8.61 154.25 8.61 154.1 8.61C152.88 8.61 152.5 9.28 152.5 9.84C152.5 10.19 152.66 10.47 152.93 10.69C153.25 10.95 153.53 11.01 154.25 11.15C154.47 11.19 155.29 11.35 155.29 12.07C155.29 12.58 154.94 12.98 154.16 12.98C153.32 12.98 152.96 12.41 152.77 11.56C152.74 11.43 152.73 11.39 152.63 11.39C152.5 11.39 152.5 11.46 152.5 11.64L152.5 12.96C152.5 13.13 152.5 13.2 152.61 13.2C152.66 13.2 152.67 13.19 152.86 13C152.88 12.98 152.88 12.96 153.06 12.77C153.5 13.19 153.95 13.2 154.16 13.2C155.31 13.2 155.77 12.53 155.77 11.81Z"/><path d="M158.58 13.09L158.58 12.78C157.92 12.78 157.88 12.73 157.88 12.34L157.88 8.67L156.48 8.78L156.48 9.09C157.13 9.09 157.22 9.15 157.22 9.64L157.22 12.33C157.22 12.78 157.11 12.78 156.44 12.78L156.44 13.09L157.54 13.06C157.89 13.06 158.24 13.08 158.58 13.09ZM158.03 7.05C158.03 6.78 157.8 6.52 157.5 6.52C157.16 6.52 156.96 6.8 156.96 7.05C156.96 7.32 157.19 7.58 157.49 7.58C157.83 7.58 158.03 7.3 158.03 7.05Z"/><path d="M164.24 13.09L164.24 12.78C163.72 12.78 163.47 12.78 163.46 12.48L163.46 10.57C163.46 9.71 163.46 9.4 163.15 9.04C163.01 8.87 162.68 8.67 162.1 8.67C161.37 8.67 160.9 9.1 160.62 9.72L160.62 8.67L159.21 8.78L159.21 9.09C159.91 9.09 159.99 9.16 159.99 9.65L159.99 12.33C159.99 12.78 159.88 12.78 159.21 12.78L159.21 13.09L160.34 13.06L161.46 13.09L161.46 12.78C160.79 12.78 160.68 12.78 160.68 12.33L160.68 10.49C160.68 9.45 161.39 8.89 162.03 8.89C162.66 8.89 162.77 9.43 162.77 10L162.77 12.33C162.77 12.78 162.66 12.78 161.99 12.78L161.99 13.09L163.12 13.06Z"/><path d="M167.77 15.47C167.77 15.44 167.75 15.41 167.73 15.39C166.69 14.61 166 12.61 166 11.01L166 10.17C166 8.57 166.69 6.57 167.73 5.79C167.75 5.77 167.77 5.74 167.77 5.71C167.77 5.66 167.72 5.61 167.67 5.61C167.65 5.61 167.63 5.62 167.61 5.63C166.51 6.46 165.46 8.46 165.46 10.17L165.46 11.01C165.46 12.72 166.51 14.72 167.61 15.55C167.63 15.56 167.65 15.57 167.67 15.57C167.72 15.57 167.77 15.52 167.77 15.47Z"/><path d="M174.3 10.7C174.31 10.27 174.3 9.77 174.2 9.37C174.14 9.11 174.03 8.89 173.85 8.76C173.77 8.7 173.69 8.67 173.6 8.67C173.48 8.67 173.36 8.73 173.29 8.83C173.22 8.93 173.2 9.04 173.23 9.15C173.25 9.25 173.31 9.33 173.4 9.4C173.7 9.62 173.91 9.93 174 10.28C174.03 10.4 174.05 10.53 174.05 10.65C174.05 11.56 173.7 12.48 172.91 12.48C172.35 12.48 171.92 12.1 171.7 11.58C171.76 11.3 171.79 11 171.78 10.7C171.78 10.61 171.78 10.52 171.76 10.44C171.72 10.29 171.64 10.18 171.5 10.18C171.31 10.18 171.28 10.43 171.28 10.65C171.28 10.96 171.31 11.26 171.38 11.55C171.18 12.09 170.8 12.48 170.25 12.48C169.64 12.48 169.25 11.98 169.1 11.35C169.05 11.15 169.02 10.92 169.01 10.7C169.01 10.1 169.23 9.54 169.48 9.01C169.5 8.97 169.51 8.92 169.5 8.88C169.48 8.82 169.43 8.77 169.37 8.74C169.34 8.72 169.31 8.72 169.28 8.72C169.22 8.72 169.16 8.75 169.12 8.81C168.85 9.38 168.76 10.02 168.76 10.65C168.76 11.03 168.8 11.42 168.89 11.77C169.09 12.58 169.54 13.2 170.3 13.2C170.93 13.2 171.34 12.76 171.56 12.14C171.83 12.76 172.29 13.2 172.96 13.2C174 13.2 174.31 11.91 174.3 10.7Z"/><path d="M179 15.56L179 15.35C178.51 15.35 178.45 15.3 178.45 14.95L178.45 10.7L177.44 10.78L177.44 11C177.93 11 177.99 11.05 177.99 11.39L177.99 12.9C177.78 12.65 177.48 12.47 177.11 12.47C176.28 12.47 175.54 13.15 175.54 14.06C175.54 14.95 176.23 15.64 177.03 15.64C177.48 15.64 177.79 15.4 177.97 15.18L177.97 15.64ZM177.97 14.74C177.97 14.86 177.97 14.88 177.89 15C177.68 15.33 177.36 15.49 177.06 15.49C176.75 15.49 176.5 15.3 176.33 15.04C176.15 14.75 176.13 14.35 176.13 14.06C176.13 13.81 176.14 13.39 176.34 13.07C176.49 12.85 176.76 12.62 177.13 12.62C177.38 12.62 177.67 12.73 177.89 13.04C177.97 13.16 177.97 13.18 177.97 13.3Z"/><path d="M185.84 11.85L185.84 11.28L185.59 11.28L185.59 11.83C185.59 12.57 185.29 12.95 184.92 12.95C184.25 12.95 184.25 12.04 184.25 11.87L184.25 9.09L185.68 9.09L185.68 8.78L184.25 8.78L184.25 6.94L184 6.94C183.99 7.76 183.69 8.83 182.71 8.87L182.71 9.09L183.56 9.09L183.56 11.85C183.56 13.08 184.49 13.2 184.85 13.2C185.56 13.2 185.84 12.49 185.84 11.85Z"/><path d="M189.29 11.01L189.29 10.17C189.29 8.46 188.24 6.46 187.14 5.63C187.12 5.62 187.1 5.61 187.08 5.61C187.03 5.61 186.98 5.66 186.98 5.71C186.98 5.74 187 5.77 187.02 5.79C188.06 6.57 188.75 8.57 188.75 10.17L188.75 11.01C188.75 12.61 188.06 14.61 187.02 15.39C187 15.41 186.98 15.44 186.98 15.47C186.98 15.52 187.03 15.57 187.08 15.57C187.1 15.57 187.12 15.56 187.14 15.55C188.24 14.72 189.29 12.72 189.29 11.01Z"/><path d="M191.94 15.59L191.94 5.59L190.72 5.59C190.61 5.59 190.52 5.68 190.52 5.79C190.52 5.9 190.61 5.99 190.72 5.99L191.54 5.99L191.54 15.19L190.72 15.19C190.61 15.19 190.52 15.28 190.52 15.39C190.52 15.5 190.61 15.59 190.72 15.59Z"/></svg><math class="td-math-a11y" xmlns="http://www.w3.org/1998/Math/MathML"><mrow><mi>p</mi><mtext>(</mtext><mi>t</mi><mtext>)</mtext><mtext> </mtext><mtext>=</mtext><mtext> </mtext><mn>1</mn><mtext> </mtext><mtext>-</mtext><mtext> </mtext><msup><mrow><mi>e</mi></mrow><mrow><mrow><mtext>-</mtext><mo>ζ</mo><mtext> </mtext><msub><mrow><mo>ω</mo></mrow><mrow><mn>0</mn></mrow></msub><mtext> </mtext><mi>t</mi></mrow></mrow></msup><mrow><mtext>[</mtext><mrow><mo>cos</mo><mtext>(</mtext><msub><mrow><mo>ω</mo></mrow><mrow><mi>d</mi></mrow></msub><mtext> </mtext><mi>t</mi><mtext>)</mtext><mtext> </mtext><mtext>+</mtext><mtext> </mtext><mfrac><mrow><mrow><mo>ζ</mo><mtext> </mtext><msub><mrow><mo>ω</mo></mrow><mrow><mn>0</mn></mrow></msub></mrow></mrow><mrow><msub><mrow><mo>ω</mo></mrow><mrow><mi>d</mi></mrow></msub></mrow></mfrac><mtext> </mtext><mo>sin</mo><mtext>(</mtext><msub><mrow><mo>ω</mo></mrow><mrow><mi>d</mi></mrow></msub><mtext> </mtext><mi>t</mi><mtext>)</mtext></mrow><mtext>]</mtext></mrow></mrow></math></div>
<p>which starts at 0, sweeps past 1, and settles back to 1. That overshoot past 1 is exactly why the spring overshoots its target and then settles.</p>
<h2>What the patent says</h2>
<p>US 11,042,388 B2, granted June 22, 2021, priority June 3, 2018. Inventors Jacob Xiao, Kyle Macomber, Joshua Shaffer, and John Harper, the same John Harper who created Core Animation in 2006.</p>
<p><img src="https://aleahim.com/images/blog/swiftui-is-one-graph/john-harper.png" alt="John Harper"></p>
<p><em>John Harper, via his GitHub.</em></p>
<p>Hold that name. Apple the company would not exist without Steve Jobs. <strong>The iPhone, and the whole fluid, animated, GPU-composited kingdom that grew on top of it, iOS, iPadOS, watchOS, tvOS, and visionOS, would not exist as we know it without John Harper.</strong> The same hand drew the engine that runs underneath all of it, from Core Animation in 2006 to the very graph this post is about. SwiftUI is just the newest room in a house whose foundation he poured. <strong>He is the John Carmack of Apple, its Linus Torvalds: the engineer whose work a billion people touch every day and whose name almost none of them know.</strong> That is a shame, and correcting it, in my own small way, is part of why I am writing all this.</p>
<figure class="td-mermaid" data-td-mermaid>
<pre class="mermaid td-mermaid-source">flowchart LR
  I[&quot;Inputs, 0 to n&quot;] --&gt; F[&quot;Function&quot;]
  F --&gt; O[&quot;Cached output, persistent&quot;]
  O -.-&gt;|re-run| F
</pre>
</figure>
<p>As you can see in the image above, the patent lays the attribute graph out in plain words. It supports zero to n inputs, applies a function on the inputs to calculate an output, stores the output in a persistent memory structure, and whenever any input changes the function gets re-run. Affected attributes set a dirty bit, and the tree is traversed bottom up so that dirty attributes initiate an update. That is exactly what the behavior shows, down to the dirty bit and the bottom up pull, now in Apple’s own words.</p>
<p>It also spells out the animation record as the same four fields the behavior implies: from value, to value, animation function, start time.</p>
<figure class="td-mermaid" data-td-mermaid>
<pre class="mermaid td-mermaid-source">flowchart LR
  T[&quot;Destination state, your data&quot;] --&gt; Cp[&quot;Copy&quot;]
  Cp --&gt; Inj[&quot;Intermediate value injected&quot;]
  Inj --&gt; Rn[&quot;Copy is rendered&quot;]
</pre>
</figure>
<p>And as the image above shows, the patent describes the method as generating a copy of the destination state and injecting an intermediate value into the copy for rendering. That is the model and presentation split you can watch the real framework perform.</p>
<p>I did not take any of this from the patent. The behavior and the patent are two independent witnesses, and they arrive at the same machine. When the framework you can observe and the patent Apple was granted describe the same structure, that structure is real, forced by the problem, not chosen by taste.</p>
<h2>One graph, one cone, drawn lazily</h2>
<p>That is the whole of it. A single demand-driven graph. State flows down through the environment, preferences flow up from children to parents, time is just another input, the graph recomputes only what truly changed, and it hands a coalesced layer tree to Core Animation to composite. Strip away the syntax and SwiftUI is one graph, one cone, drawn lazily.</p>
<p>And here is the part I will only hint at. This was never really about SwiftUI. It began with Core Animation, because I wanted to understand it, and <strong>the only way I trust to understand a thing is to build it again from nothing</strong>. But you cannot understand Core Animation without the drawing layer beneath it, so that came next.</p>
<p><img src="https://aleahim.com/images/blog/swiftui-is-one-graph/the-stack.jpg" alt="The engines beneath SwiftUI, the stack I have been rebuilding"></p>
<p>A text layer pulls you into typesetting. Transitions pull you into image processing. Each engine demanded the one below it, until the whole stack stood on its own and SwiftUI settled on top almost as an afterthought. None of it ships, and none of it is for sharing. It is mine, a way to learn the machine by rebuilding it, and it has a name: <a href="https://slayermotion.com">SlayerMotion</a>.</p>
<figure class="td-mermaid" data-td-mermaid>
<pre class="mermaid td-mermaid-source">flowchart TD
  SUI[&quot;SwiftUI, one graph&quot;] --&gt; CA[&quot;Core Animation, the compositor&quot;]
  CA --&gt; CG[&quot;Core Graphics, drawing&quot;]
  CA --&gt; CT[&quot;Core Text, typesetting&quot;]
  CA --&gt; CI[&quot;Core Image, effects&quot;]
</pre>
</figure>
<p><strong>Of the five, SwiftUI interests me the least, by a wide margin.</strong> It was not even born to be the future of the platform: it started as a way to make watchOS apps bearable to write, and only once it existed did Apple look at it and see what they had, and take it everywhere. It arrived with a quiet promise: that you could finally skip all of it, the dozen frameworks underneath, and just ship.</p>
<p><img src="https://aleahim.com/images/blog/swiftui-is-one-graph/the-shortcut.jpg" alt="What leaks through when you skip the frameworks"></p>
<p>That promise was a lie. You can ship, yes, but you have not learned the platform, you have learned a facade over it, and the day something leaks through, a layout that will not behave, an animation that stutters, text that measures wrong, you are standing on a stack you never met. The people who took SwiftUI as a way around the frameworks did not become Apple developers. They became SwiftUI users, and that is a different and smaller thing.</p>
<p>Set beside the engines underneath it, SwiftUI is genuinely uninteresting, boring even, a thin and tidy graph that does one clever thing and then gets out of the way. The one that floored me at the start still floors me most, and I suspect always will: Core Animation, a compositor that keeps an animation running smooth on its own clock while your app is busy elsewhere. The drawing layer, the type engine, the image pipeline are close behind. That is where the hard, strange, beautiful problems live, and that is the series. <strong>SwiftUI was just the easiest door into the story.</strong></p>]]></content:encoded>
</item>
<item>
<title>What Apple Wants You to Build This Summer (You Can Read It in the Docs)</title>
<link>https://aleahim.com/blog/cupertino-11-release/</link>
<guid isPermaLink="true">https://aleahim.com/blog/cupertino-11-release/</guid>
<pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate>
<description>Apple&apos;s new frameworks and sample code read like a brief, and Cupertino v1.4.0 puts the whole signal offline</description>
<content:encoded><![CDATA[<h1>What Apple Wants You to Build This Summer (You Can Read It in the Docs)</h1>
<p>The keynote tells you what Apple announced. The documentation and the sample code tell you what Apple wants you to actually ship. Those are not the same thing, and the second one is far more useful if you are deciding what to build this summer.</p>
<p>So I did the boring, revealing thing: I diffed it. Cupertino just shipped v1.4.0, a full re-crawl of Apple’s developer documentation, and I compared it page by page against the version that has been on everyone’s disk since May. Then I read the diff like a tea leaf.</p>
<p>The answer is not subtle. <strong>Apple wants you to build Apple Intelligence into your app, and it has written you the homework.</strong></p>
<h2>The sample code is the smoking gun</h2>
<p>When Apple wants you to learn an API, it announces it. When Apple wants you to <em>ship</em> something, it gives you sample code you can paste from. So the new sample projects are the clearest signal in the whole corpus, and v1.4.0 added eleven of them. Here is the entire list:</p>
<ul><li><p>App Intents: Integrating Your <strong>Music</strong> App with Apple Intelligence</p></li><li><p>App Intents: Integrating Your <strong>Messaging</strong> App with Apple Intelligence</p></li><li><p>App Intents: Integrating Your <strong>Calendar</strong> App with Apple Intelligence</p></li><li><p>App Intents: Integrating Your <strong>Photo</strong> App with Apple Intelligence</p></li><li><p>FoundationModels: Origami, crafting a dynamic tutorial for Apple Intelligence</p></li><li><p>Evaluations: Book Tracker, using evaluations to evaluate an intelligent feature</p></li><li><p>HealthKit: Tracking heart rate zones for workouts</p></li><li><p>Metal: Training a neural network to render irradiance in real time</p></li><li><p>Metal: Running inline ML operations in a shader with Metal 4</p></li><li><p>DeclaredAgeRange: Implementing age assurance and permissions</p></li><li><p>VisionKit: Structuring recognized text on a document</p></li></ul>
<p>Read the top four again. Apple did not write “an example of App Intents.” It wrote “Integrating <strong>Your</strong> Music App,” “<strong>Your</strong> Messaging App,” “<strong>Your</strong> Calendar App,” “<strong>Your</strong> Photo App.” That is not documentation. That is a brief. Apple has taken the four most common app categories on the platform and handed each one a template for wiring itself into Apple Intelligence through App Intents. If you ship one of those four kinds of app, your summer roadmap is sitting in a sample project with your name on it.</p>
<p>There is not a single exception in the eleven. Every new sample is Apple Intelligence, App Intents, or on-device ML. No new SwiftUI layout sample, no new networking sample, no new Core Data sample. The signal is monochrome.</p>
<h2>The new frameworks are the syllabus</h2>
<p>Sample code shows you the assignments; the new frameworks show you the curriculum. v1.4.0 added <strong>19 frameworks that did not exist in the previous release</strong>, and the cluster tells the same story:</p>
<table><thead><tr><th>Framework</th><th style="text-align:right">Pages</th><th style="text-align:right">Symbols</th></tr></thead><tbody><tr><td>USDKit</td><td style="text-align:right">854</td><td style="text-align:right">806</td></tr><tr><td>ComputeGraph</td><td style="text-align:right">539</td><td style="text-align:right">273</td></tr><tr><td>Core AI</td><td style="text-align:right">308</td><td style="text-align:right">425</td></tr><tr><td>Evaluations</td><td style="text-align:right">172</td><td style="text-align:right">527</td></tr><tr><td>App Intents Testing</td><td style="text-align:right">103</td><td style="text-align:right">363</td></tr><tr><td>Now Playing</td><td style="text-align:right">100</td><td style="text-align:right">318</td></tr><tr><td>Music Understanding</td><td style="text-align:right">94</td><td style="text-align:right">77</td></tr><tr><td>AVSystemRouting</td><td style="text-align:right">92</td><td style="text-align:right">109</td></tr><tr><td>Media Device</td><td style="text-align:right">92</td><td style="text-align:right">148</td></tr><tr><td>Trust Insights</td><td style="text-align:right">85</td><td style="text-align:right">66</td></tr><tr><td>Media Intelligence</td><td style="text-align:right">77</td><td style="text-align:right">100</td></tr><tr><td>Accessory Access</td><td style="text-align:right">63</td><td style="text-align:right">53</td></tr><tr><td>DiskImageKit</td><td style="text-align:right">58</td><td style="text-align:right">87</td></tr><tr><td>Spatial Preview</td><td style="text-align:right">57</td><td style="text-align:right">60</td></tr><tr><td>StateReporting</td><td style="text-align:right">38</td><td style="text-align:right">77</td></tr><tr><td>CrashReportExtension</td><td style="text-align:right">29</td><td style="text-align:right">36</td></tr><tr><td>Media Intents</td><td style="text-align:right">15</td><td style="text-align:right">30</td></tr><tr><td>Suggested Actions</td><td style="text-align:right">9</td><td style="text-align:right">26</td></tr><tr><td>Apple School and Business Manager API</td><td style="text-align:right">3</td><td style="text-align:right">0</td></tr></tbody></table>
<p>Core AI. Evaluations. Media Intelligence. Music Understanding. Trust Insights. Suggested Actions. App Intents Testing. This is the connective tissue around on-device models: the pieces that let your app declare what it can do, feed a model the right context, evaluate what comes back, and route media intelligently. <strong>Evaluations</strong> is the quiet standout. Apple shipping a framework specifically for <em>evaluating</em> an intelligent feature, plus a sample that uses it, is Apple admitting that “add AI” is not a feature, it is an engineering discipline with a test loop. That is the most grown-up thing in the release.</p>
<p>And it is not only the debutants. <code>foundationmodels</code>, the on-device model framework, was not new but it more than doubled in pages (356 to 821) and more than tripled in symbols (406 to 1,383). The framework Apple gave you last year to talk to its models is the one it spent this cycle fleshing out.</p>
<h2>The honest part: most of the bytes are a deeper re-crawl</h2>
<p>I am not going to oversell the numbers, because the headline flatters the truth. The documentation database grew by <strong>+12,057 pages and +67,575 symbols</strong>. But only about <strong>23% of the new pages</strong> come from those 19 new frameworks. The other 77% is a deeper re-crawl of frameworks that already existed. For symbols it is even more lopsided: roughly <strong>95%</strong> of the gain is re-crawled existing frameworks, and the Swift standard library alone accounts for over 40% of it (+27,908 symbols).</p>
<p>So the accurate sentence is: the byte-weight is a fuller pass over everything, and the <em>signal</em> is the 19 new frameworks and eleven new samples on top. Both are true, and a release post that only quoted the big number would be lying by omission.</p>
<p>Where did the existing-framework growth concentrate? Right where you would guess given the theme:</p>
<table><thead><tr><th>Framework</th><th style="text-align:right">old pages</th><th style="text-align:right">new pages</th><th style="text-align:right">delta</th></tr></thead><tbody><tr><td>realitykit</td><td style="text-align:right">4,456</td><td style="text-align:right">6,133</td><td style="text-align:right">+1,677</td></tr><tr><td>appintents</td><td style="text-align:right">3,509</td><td style="text-align:right">4,376</td><td style="text-align:right">+867</td></tr><tr><td>foundation</td><td style="text-align:right">13,649</td><td style="text-align:right">14,282</td><td style="text-align:right">+633</td></tr><tr><td>appkit</td><td style="text-align:right">13,378</td><td style="text-align:right">13,917</td><td style="text-align:right">+539</td></tr><tr><td>foundationmodels</td><td style="text-align:right">356</td><td style="text-align:right">821</td><td style="text-align:right">+465</td></tr><tr><td>swift</td><td style="text-align:right">18,717</td><td style="text-align:right">19,057</td><td style="text-align:right">+340</td></tr><tr><td>metrickit</td><td style="text-align:right">249</td><td style="text-align:right">567</td><td style="text-align:right">+318</td></tr><tr><td>corespotlight</td><td style="text-align:right">403</td><td style="text-align:right">719</td><td style="text-align:right">+316</td></tr><tr><td>swiftui</td><td style="text-align:right">8,679</td><td style="text-align:right">8,938</td><td style="text-align:right">+259</td></tr></tbody></table>
<p>RealityKit and App Intents leading the page growth is the same story from yet another angle: spatial computing and the intent system are both load-bearing for what Apple is asking you to build. No framework shrank in page count and none were removed, so at the framework level v1.4.0 is a clean superset of v1.3.0.</p>
<h2>What did not change, honestly</h2>
<p>A re-crawl is not a magic content generator. Four databases came back byte-for-byte identical to v1.3.0: HIG, Swift Evolution, the Swift book, and the legacy Apple Archive guides. Three of those (HIG, Swift Evolution, the Swift book) were genuinely re-crawled this cycle and simply came back unchanged because Apple had not touched them upstream; the archive corpus was the one source carried over from the previous cycle. I verified “identical” with a content fingerprint over every page, not by trusting file dates. If your work lives in those four, v1.4.0 changes nothing for you, and I would rather say so than pad the release.</p>
<p>The Swift package index (<code>packages.db</code>) is the interesting middle case. The set of 185 packages is unchanged, but 183 were re-fetched from current upstream and re-indexed, so the database now reflects what those packages look like <em>today</em> rather than at the last crawl. That means it is honest rather than purely additive: Vapor lost 1,604 symbols where upstream refactored, while swift-package-manager and swift-async-algorithms each gained a few hundred. Net +2,836 symbols, but the real win is that the package picture is current.</p>
<h2>Why your AI agent needs this, specifically now</h2>
<p>Here is the part that ties the signal back to the tool. If Apple wants you to spend the summer wiring your app into Apple Intelligence, you are going to ask an assistant about Core AI, App Intents Testing, the Evaluations framework, and FoundationModels constantly. And an assistant working from a six-month-old corpus will confidently tell you those frameworks do not exist, because in its world they do not.</p>
<p>That is exactly the failure Cupertino exists to prevent. It puts Apple’s documentation on your machine as a set of local, searchable databases an AI agent can read through MCP, offline, in milliseconds. v1.4.0 is not a flashy release: it does not add a feature or touch a line of the query engine. It does the quieter thing that actually matters for a documentation tool, which is make the corpus match what Apple shipped. The fact that the new frameworks map almost perfectly onto Apple’s summer wishlist is not a coincidence; it is the whole point. Your agent now knows about the assignment.</p>
<h2>Getting v1.4.0</h2>
<p>The binary is signed and notarized and lives on the tap:</p>
<pre><code class="language-bash">brew upgrade cupertinohq<span class="tok-operator">/</span>tap<span class="tok-operator">/</span>cupertino<span class="tok-comment"># or: brew tap cupertinohq/tap https://codeberg.org/CupertinoHQ/homebrew-tap.git</span>
brew install cupertinohq<span class="tok-operator">/</span>tap<span class="tok-operator">/</span>cupertino
cupertino setup            <span class="tok-comment"># downloads the v1.4.0 database bundle</span></code></pre>
<p>Same 8 per-source databases as v1.3.0, shipped read-only in rollback-journal mode, same schema (v18, packages on their own v5 track). Nothing in your tooling changes; the corpus underneath it just got current, and it got current in a very specific, very on-message direction.</p>
<p>Build the thing in the sample project. Apple already wrote the brief.</p>]]></content:encoded>
</item>
<item>
<title>&quot;Indistinguishable From an Attack&quot;</title>
<link>https://aleahim.com/blog/indistinguishable-from-an-attack/</link>
<guid isPermaLink="true">https://aleahim.com/blog/indistinguishable-from-an-attack/</guid>
<pubDate>Sun, 21 Jun 2026 00:00:00 +0000</pubDate>
<description>How an autonomous bounty bot opened a pull request that repointed a core dependency to a fork it controlled</description>
<content:encoded><![CDATA[<h1>Indistinguishable From an Attack</h1>
<p><em>An autonomous bounty bot opened a pull request on my project that quietly repointed a core dependency to a fork it controlled. It did not even work. That is the part that should worry you.</em></p>
<p>On 21 June 2026, a pull request landed on Cupertino, my open-source Apple-documentation MCP server. The title was reassuringly boring: “Fix for issue #13.” The body was one line: “Implemented automated fix.” Three files changed, a hundred-odd lines, green-ish checks. The kind of contribution a maintainer waves through between meetings.</p>
<p>Then I read the lockfile.</p>
<h2>One line in a lockfile</h2>
<p>Buried in <code>Packages/Package.resolved</code>, the PR changed a single dependency’s location:</p>
<pre><code>- "location": "https://codeberg.org/Mihaela/SwiftMCPCore.git"
+ "location": "https://github.com/KartavyaDikshit/SwiftMCPCore.git"</code></pre>
<p><code>SwiftMCPCore</code> is the wire-protocol core my server is built on. The PR repointed it from my repository to a fork owned by the person who opened the PR. The new code in the same PR called an API (<code>variables</code>) that does not exist in the real <code>SwiftMCPCore</code>, only in that fork. The fork had been created 33 minutes before the PR was opened.</p>
<p>That one line is the entire anatomy of a software supply-chain compromise. If I merge it, or if my CI so much as builds the branch, the build tool fetches and runs code from a repository I do not control, and bakes it into the binary that ships to every user through Homebrew. The payload today is harmless. The repository is not mine. Whoever owns it can change it tomorrow. That is the whole move: get a trusted project to depend on something you control, dressed up as a helpful fix.</p>
<h2>The twist: it would not have worked</h2>
<p>Here is what turns this from a scary anecdote into something useful. The PR changed the lockfile but not the manifest. Swift Package Manager trusts <code>Package.swift</code>, not <code>Package.resolved</code>, for where a dependency actually comes from, and <code>Package.swift</code> still pointed at my canonical repository. So on a real build the fork URL gets ignored or overwritten, and the new code, which depends on the fork, fails to compile. No build check ever ran green, because it cannot.</p>
<p>A competent attacker edits the manifest too, so the swap actually takes effect. This one did not. And that is exactly what told me what I was really looking at.</p>
<h2>The real story: a bounty bot</h2>
<p>This was not a hand-crafted attack on my project, and I do not have to guess, because the author published the tool. Among his own repositories is one called <code>bounty-hunter</code>, described in his own words as an “Autonomous GitHub open-source bounty hunter bot.” He created it at midnight UTC, finished building it within the hour, and then let it run.</p>
<p>The timeline of my incident reads like a machine working a list:</p>
<table><thead><tr><th>Time (UTC, 21 June)</th><th>Event</th></tr></thead><tbody><tr><td>00:00</td><td><code>bounty-hunter</code> bot repository created</td></tr><tr><td>00:53</td><td>bot finished and pushed</td></tr><tr><td>04:21</td><td>bot forks <code>cupertino</code></td></tr><tr><td>04:27</td><td>bot forks <code>SwiftMCPCore</code> (it needs a type the real package lacks)</td></tr><tr><td>05:00</td><td>bot opens my PR #1294</td></tr><tr><td>all day</td><td>36 “Fix for issue #N” PRs across 34 repositories</td></tr></tbody></table>
<p>The targets give away the goal, and it is farming, not sabotage. One target was a crypto-bounty repo that literally pays per merged contribution, hit twice. Another was a well-known honeypot whose own description invites newcomers to “make pull requests and build their reputation.” The rest were high-visibility AI and developer-tool projects where good-first-issues live, including, with some irony, a Python supply-chain auditing tool.</p>
<p>To make my build compile inside its sandbox, the bot needed a type the upstream package did not have. So it forked the package, added the type, and its local resolve wrote the fork URL into my lockfile, which it then committed. The dependency repoint was a byproduct of a machine trying to make a build pass on its way to a merged PR. On the other 33 repositories, the same bot never touches dependencies at all.</p>
<p>The author looks like an early-career developer: a five-year-old account, a portfolio of student machine-learning and web projects, based in a German university town. This is not a nation-state. It is one person, a sixteen-kilobyte Python script, and a language model.</p>
<h2>Why the harmless explanation is the alarming one</h2>
<p>Intent barely matters. Whether this bot is farming bounties, padding a contribution graph, or demonstrating a product, the artifact it produced is indistinguishable from a supply-chain attack and one manifest edit away from a working one. And it operates at volume: one bot, 34 repositories, in a single day.</p>
<p>The defense open source has always leaned on is a human maintainer reading the diff. That defense does not scale to this throughput. Review fatigue is the attack surface now. The weaponized version writes itself: same pipeline, swap the benign type for a payload, fix the manifest so it resolves, fan out to a few hundred repositories, and wait for one tired maintainer to merge a green-looking fix at 1am. The barrier to producing this used to be skill and intent. Now it is a weekend project.</p>
<h2>What actually protects you</h2>
<ul><li><p>Read the dependency files first. On any PR that touches <code>Package.resolved</code>, <code>package-lock.json</code>, <code>go.sum</code>, or <code>Cargo.lock</code>, that diff is the review, not the feature code.</p></li><li><p>A dependency URL that points at a contributor’s fork is an automatic reject, no matter what the fork contains today.</p></li><li><p>Do not let CI build untrusted external PRs without a human gate. Building is execution.</p></li><li><p>Keep the manifest and lockfile consistent, pinned to a canonical source and a known revision, and reject any PR where the two disagree.</p></li></ul>
<h2>How I handled it</h2>
<p>I declined and closed the PR with a public explanation, because the reasons are a heads-up for every other maintainer this bot visited. The feature it reached for is actually worth having: typed resource-template variables. The right way to get it is to propose the type to the canonical package, where it can be reviewed, tagged, and pinned, then build on top of that. The idea was fine. Depending on a fork I do not control is the disqualifier.</p>
<p>If you maintain anything popular enough to have open issues, a bot is already reading them. Read your lockfiles.</p>]]></content:encoded>
</item>
<item>
<title>&quot;The rules my AI coding agent is not allowed to break&quot;</title>
<link>https://aleahim.com/blog/rules-my-coding-agent-cannot-break/</link>
<guid isPermaLink="true">https://aleahim.com/blog/rules-my-coding-agent-cannot-break/</guid>
<pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate>
<description>How a pile of repeated corrections became the rulebook I load into every coding agent, and what writing it down taught me about my own code</description>
<content:encoded><![CDATA[<h1>The rules my AI coding agent is not allowed to break</h1>
<p>Left to its own defaults, an AI coding agent is an enthusiastic shortcut machine. It will wrap the failing call in <code>try?</code> and move on. It will force-unwrap because the optional is “obviously” non-nil. It will handle the three easy cases, skip the malformed one, and report the task done. It will tell you “tests pass” without running them, because the build was green and the build “should” mean the tests are fine. Every one of those is a small lie that compiles.</p>
<p>I did not start with a rulebook. I started with corrections.</p>
<p>The first ones were one-offs, the kind you mutter while you fix them yourself. Don’t swallow that error. Don’t force-unwrap there. Run the tests before you tell me they passed. But the same corrections came back, session after session, and a correction you give twice is a rule you have not written down yet. So I started writing them down, one repeated annoyance at a time, in a file the agent loads before it touches a line of my code.</p>
<p>What I did not expect was the side effect. To make a rule a machine will actually obey, you have to state it precisely enough that it cannot be wriggled around, and every time I forced that precision I found a place where I had been hand-waving for years. The rulebook was supposed to discipline the agent. It ended up disciplining me.</p>
<p>That file is public now. It is <a href="https://codeberg.org/Mihaela/rules-swift"><code>rules-swift</code></a>: the rules I point every coding agent at as always-loaded context. It is two layers in one repo, the Swift and Apple-platform craft on top and a language-agnostic standard of care vendored underneath in <code>core/</code> (which is also its own repo, <a href="https://codeberg.org/Mihaela/rules-engineering-discipline"><code>rules-engineering-discipline</code></a>, if you work in another language). The whole thing rests on one line, “choose the optimal path, not the fastest one,” and everything else is a specific way of not betraying it.</p>
<p>But the repo is the destination. The journey was three turns, each one a correction I kept making until it became a rule, and each rule turning around to teach me something about my own work.</p>
<h2>The first turn: the fix you ask for is usually a symptom</h2>
<p>The correction I made most often was not about a bug. It was about where the bug was being fixed.</p>
<p>The example that finally made me write the rule was a retry. The agent had wrapped a call that should never fail in a retry loop, and the retry made the failure disappear. But a call that should never fail and sometimes does has a bug one layer down, and the retry was hiding it. The fix belonged in the layer that owns the data, not in a loop around the symptom.</p>
<p>The rule I wrote is almost embarrassingly basic: before writing code, state the problem’s real invariant in one sentence, and if you cannot, you are not ready to write it. Trace a symptom to the layer that owns it before you patch anything. The moment I made the agent do that, I had to admit how often I did the opposite, reaching for the nearest familiar shape instead of deriving from the actual constraint. Its companion clause turned out to be just as much about me: reason from the source, not from memory. A remembered API signature is a hypothesis, not a citation. I had been quoting my own memory as evidence for years.</p>
<h2>The second turn: validations are values, and the best one is the one you delete</h2>
<p>The more I corrected the agent’s validation code, the more I recognized my own. One big <code>validate()</code> function, a tree of <code>if</code> statements, a mutable <code>errors</code> array I appended to as I went. That is how almost everyone does it, and it is how I did it.</p>
<p>The turn came from Matt Polzin’s OpenAPIKit idiom: validations are not control flow, they are values. A <code>Validation&lt;Subject&gt;</code> is a description, a check, and a <code>when</code> predicate, composed with an algebra instead of nested conditionals. Two things in that rewired how I think about correctness. First, the description states the correct state, never the failure. You write “Operations contain at least one response,” and the framework derives the negative by prefixing “Failed to satisfy:”. One source of truth, two readings, where I used to write the message and the check as two things that quietly drifted apart. Second, the strongest validation is the one you delete: if a type carries its own invariant, a non-empty collection, a smart constructor that rejects bad input at <code>init</code>, the bad state cannot be built and needs no check at all.</p>
<p>And the part with teeth, the one I now miss in every codebase that lacks it: every public type is either validated or excluded with a one-line reason, and a coverage gate in CI fails the build if someone adds a public type that is neither. “We validate our inputs” stopped being something I said in a meeting and became something the build proves on every commit.</p>
<h2>The third turn: no singletons, not even the sanctioned one</h2>
<p>This is the bluntest rule in the set, and the one I argued with myself about the longest. No singletons, ever. Not <code>static let shared</code>. Not even the Singleton that the Gang of Four book explicitly sanctions on page 127. The rule names that exact temptation and refuses it: when you catch yourself about to document one as “legitimate per p. 127,” stop, and do the injection.</p>
<p>It reads as dogma until you see the reason, and the reason was the lesson. Every dependency should appear at the type’s <code>init</code>, where the coupling is visible, testable, and removable. A singleton conjures its collaborator out of process-wide state at runtime, and the instant it does, that dependency is invisible in the call graph and welded to everything that touches it. The test I now hold a module to is mechanical: copy it out of the repo with only its declared dependencies and build it. If it does not lift out clean, it has a hidden dependency, and a hidden dependency is a bug that has not happened yet.</p>
<h2>Where it landed: a machine will not give you the benefit of the doubt</h2>
<p>Here is the thing the whole journey was actually about, the one I did not see coming. A colleague who reads “validate carefully” fills in the rest from experience. An agent does not. Vague rules produce vague work, instantly and at scale. So every rule had to become checkable: not “be rigorous” but “no claim of done without a fresh command in this response,” not “handle errors” but the exact list of forbidden silencers (<code>try?</code>, force-unwrap, <code>xfail</code>, <code>sleep()</code> to make a symptom vanish).</p>
<p>That is what made the corrections worth writing down. Every place a rule resisted being made checkable was a place I had been getting by on the benefit of the doubt, the same benefit a machine refuses to extend. The whole set collapses into one question the agent has to answer before it calls anything finished, and writing it for the agent is how I finally started asking it of myself:</p>
<blockquote><p>Did I take the optimal path, or a path I am hoping no one inspects?</p></blockquote>
<p>Honesty is one of the rules, so the limits: a rules file does not enforce itself. A model that has read “no force-unwrap” is not a force-unwrap that never ships, which is why the checks a machine can decide live in git hooks and CI and the prose only covers the judgment calls. The set is also shaped to my stack, a Swift monorepo with an OpenAPI-first workflow, and the Swift opinions are opinions. The engineering-discipline core travels anywhere; the rest you should fork and disagree with in writing.</p>
<h2>The map, if you want it</h2>
<p>If any of this sounds like a correction you have made more than once, take the map. Clone <a href="https://codeberg.org/Mihaela/rules-swift"><code>rules-swift</code></a>, point your agent at it, and watch how much stops needing to be said twice. The core spine comes vendored with it; the standalone <a href="https://codeberg.org/Mihaela/rules-engineering-discipline"><code>rules-engineering-discipline</code></a> is there if you want it on its own. Star it if it saves you a repeated correction. That is the whole ask.</p>]]></content:encoded>
</item>
<item>
<title>&quot;Validations Are Values&quot;</title>
<link>https://aleahim.com/blog/validations-are-values/</link>
<guid isPermaLink="true">https://aleahim.com/blog/validations-are-values/</guid>
<pubDate>Sat, 13 Jun 2026 00:00:00 +0000</pubDate>
<description>Most validation code is one giant function that walks a tree and appends to an error array through a forest of if statements. OpenAPIKit showed me a better way, a validation is a small composable value, the description states the correct state, and every error already knows where it lives</description>
<content:encoded><![CDATA[<h1>Validations Are Values</h1>
<p>Here is the validation function almost everyone writes the first time. You have a parsed model. You want to check it. So you write this:</p>
<pre><code class="language-swift"><span class="tok-keyword">func</span> <span class="tok-function">validate</span>(_ document<span class="tok-operator">:</span> <span class="tok-type">Document</span>) <span class="tok-keyword">throws</span> {
    <span class="tok-keyword">var</span> errors<span class="tok-operator">:</span> [<span class="tok-type">String</span>] <span class="tok-operator">=</span> []
    <span class="tok-keyword">if</span> document<span class="tok-operator">.</span>servers<span class="tok-operator">.</span>is<span class="tok-type">Empty</span> {
        errors<span class="tok-operator">.</span><span class="tok-function">append</span>(<span class="tok-string">"document has no servers"</span>)
    }
    <span class="tok-keyword">for</span> (path, item) <span class="tok-keyword">in</span> document<span class="tok-operator">.</span>paths {
        <span class="tok-keyword">for</span> (method, operation) <span class="tok-keyword">in</span> item<span class="tok-operator">.</span>operations {
            <span class="tok-keyword">if</span> operation<span class="tok-operator">.</span>responses<span class="tok-operator">.</span>is<span class="tok-type">Empty</span> {
                errors<span class="tok-operator">.</span><span class="tok-function">append</span>(<span class="tok-string">"\(method) \(path) has no responses"</span>)
            }
            <span class="tok-keyword">for</span> variable <span class="tok-keyword">in</span> operation<span class="tok-operator">.</span>server<span class="tok-operator">?</span><span class="tok-operator">.</span>url<span class="tok-type">Template</span><span class="tok-operator">.</span>variables <span class="tok-operator">?</span><span class="tok-operator">?</span> [] {
                <span class="tok-keyword">if</span> <span class="tok-operator">!</span>operation<span class="tok-operator">.</span>server<span class="tok-operator">!</span><span class="tok-operator">.</span>variables<span class="tok-operator">.</span>keys<span class="tok-operator">.</span><span class="tok-function">contains</span>(variable) {
                    errors<span class="tok-operator">.</span><span class="tok-function">append</span>(<span class="tok-string">"\(path) uses undefined variable \(variable)"</span>)
                }
            }
        }
    }
    <span class="tok-comment">// ... three hundred more lines ...</span>
    <span class="tok-keyword">if</span> <span class="tok-operator">!</span>errors<span class="tok-operator">.</span>is<span class="tok-type">Empty</span> { <span class="tok-keyword">throw</span> <span class="tok-type">ValidationError</span>(errors) }
}</code></pre>
<p>It works. It also rots. Every new rule is another branch wedged into the same traversal. The error strings drift out of sync with the checks. Nobody can test a single rule in isolation, because there is no single rule, there is only the function. You cannot turn one check off without commenting out code. And the location of each failure is whatever you remembered to interpolate into the string, which is to say, inconsistent and usually wrong.</p>
<p>I stopped writing validators like this when I read <a href="https://github.com/mattpolzin/OpenAPIKit">Matt Polzin’s OpenAPIKit</a>. His validation layer is the cleanest I have seen in Swift, and the core idea is one sentence:</p>
<p><strong>A validation is not code you run. It is a value you build.</strong></p>
<p>Once you take that seriously, everything else follows. This post is the whole idiom, distilled from how OpenAPIKit actually does it. I now hold every validation layer I write to it.</p>
<h2>What OpenAPIKit is</h2>
<p><a href="https://github.com/mattpolzin/OpenAPIKit">OpenAPIKit</a> is a Swift library for reading, writing, and validating <a href="https://www.openapis.org">OpenAPI</a> documents. OpenAPI is the specification format that describes an HTTP API: its paths, operations, request and response bodies, schemas, servers, and components. The documents are usually written as large YAML or JSON files.</p>
<p>OpenAPIKit gives you three things, layered cleanly on top of each other:</p>
<ol><li><p><strong>A typed model of the entire OpenAPI specification.</strong> Every construct in the spec, <code>OpenAPI.Document</code>, <code>OpenAPI.PathItem</code>, <code>Operation</code>, <code>Response</code>, <code>Server</code>, <code>JSONSchema</code>, and the rest, is a real Swift type. Not a <code>[String: Any]</code> you index into and pray. A real type with real fields, so the compiler knows the shape of an OpenAPI document.</p></li><li><p><strong><code>Codable</code> conformance over that model.</strong> Because the model is <code>Codable</code>, you decode a YAML or JSON spec straight into <code>OpenAPI.Document</code> and encode it back out. The decoder is where the first wall of correctness lives: a document that is not even shaped like OpenAPI fails to decode at all. This is the “parse first” pass.</p></li><li><p><strong>A validation subsystem over the decoded model.</strong> This is the part this post is about. Once you have a decoded <code>OpenAPI.Document</code>, you run a <code>Validator</code> over it to catch the semantic errors that decoding alone cannot: a <code>$ref</code> that points at a component that does not exist, an operation with no responses, a server URL template that uses a variable nobody defined. These are valid OpenAPI <em>syntax</em> describing an invalid <em>document</em>.</p></li></ol>
<p>The validation code lives under <code>Sources/OpenAPIKit/Validator/</code>. The files worth reading are <code>Validation.swift</code> (the atom), <code>Validator.swift</code> (the engine and the default set), <code>Validation+Builtins.swift</code> (the shipped rules), and <code>ReferenceValidations.swift</code> (the reference-resolution rules). The tests under <code>Tests/OpenAPIKitTests/Validator/</code> are as instructive as the source, and I will come back to them.</p>
<p>How you actually use it is three lines:</p>
<pre><code class="language-swift"><span class="tok-keyword">import</span> <span class="tok-type">OpenAPIKit</span>
<span class="tok-keyword">import</span> <span class="tok-type">Yams</span>

<span class="tok-keyword">let</span> document <span class="tok-operator">=</span> <span class="tok-keyword">try</span> <span class="tok-type">YAMLDecoder</span>()<span class="tok-operator">.</span><span class="tok-function">decode</span>(<span class="tok-type">OpenAPI</span><span class="tok-operator">.</span><span class="tok-type">Document</span><span class="tok-operator">.</span><span class="tok-keyword">self</span>, from<span class="tok-operator">:</span> yaml<span class="tok-type">String</span>)
<span class="tok-keyword">try</span> document<span class="tok-operator">.</span><span class="tok-function">validate</span>()   <span class="tok-comment">// throws a ValidationErrorCollection if anything is wrong</span></code></pre>
<p>The first line is the parse pass. The second is the validate pass. Everything below is how that second line is built, and why it is built the way it is.</p>
<h2>Parse first, validate second</h2>
<p>The first move happens before any validation runs at all. Parsing and validation are two passes, not one.</p>
<p>The parser’s job is to turn bytes into a well typed value and to make as many illegal states as it can simply unrepresentable. An enum cannot hold a case that does not exist. A non-optional field cannot be missing. By the time you have a parsed <code>Document</code>, a whole category of “errors” is gone, because the type system refused to let them exist.</p>
<p>Validation runs <em>after</em> that, over the already typed value. It catches only what the types cannot or should not enforce: a server template that references a variable nobody defined, an operation with zero responses, a reference that points at a component that is not in the document. These are not type errors. They are semantic ones. They need their own pass.</p>
<p>If you find yourself validating inside the parser, stop. Parse to a clean value. Then validate the value.</p>
<h2>A validation is a small struct of closures</h2>
<p>Here is the atom. A <code>Validation</code> over some subject type carries three things:</p>
<ul><li><p>a <strong>description</strong> string,</p></li><li><p>a <strong>check</strong> that produces errors,</p></li><li><p>a <strong>predicate</strong> that decides whether this rule even applies here.</p></li></ul>
<p>That is it. No inheritance, no protocol with ten requirements. A plain value you can build, store in an array, pass around, and combine.</p>
<p>The simplest one reads almost like English:</p>
<pre><code class="language-swift"><span class="tok-keyword">public</span> <span class="tok-keyword">static</span> <span class="tok-keyword">var</span> operations<span class="tok-type">ContainResponses</span><span class="tok-operator">:</span> <span class="tok-type">Validation</span><span class="tok-operator">&lt;</span><span class="tok-type">Operation</span><span class="tok-operator">&gt;</span> {
    <span class="tok-operator">.</span><span class="tok-keyword">init</span>(
        description<span class="tok-operator">:</span> <span class="tok-string">"Operations contain at least one response"</span>,
        check<span class="tok-operator">:</span> \<span class="tok-operator">.</span>responses<span class="tok-operator">.</span>count <span class="tok-operator">&gt;</span> <span class="tok-number">0</span>
    )
}</code></pre>
<p>Look at what is not there. There is no traversal. This rule does not know how to find <code>Operation</code> values in a document, and it does not care. It only knows what a correct <code>Operation</code> looks like. The machinery finds every operation in the tree and offers each one to this rule. Dispatch is directed by the subject type: a <code>Validation&lt;Operation&gt;</code> fires on operations, wherever they appear, and is invisible to everything else.</p>
<h2>The description states the correct state, never the failure</h2>
<p>Read that description again: “Operations contain at least one response.” Positive. It describes the world when the rule is satisfied, not the world when it breaks.</p>
<p>This is deliberate, and it is one of my favorite details. You write the description once, phrased positively. When the rule fails, the framework derives the failure message by prefixing <code>"Failed to satisfy: "</code>. So you get:</p>
<blockquote><p>Failed to satisfy: Operations contain at least one response</p></blockquote>
<p>One source of truth, two readings. You never write the failure phrasing by hand, so the check and its message can never drift apart. Compare that to the if-tree above, where <code>if responses.isEmpty</code> and the string <code>"has no responses"</code> are two independent things you have to keep in agreement forever.</p>
<p>The description does more than format messages. It is the rule’s <strong>identity</strong>. Because rules are removed by matching their description, the description must be unique and stable. Which leads to the next piece.</p>
<h2>Every error knows where it lives</h2>
<p>An error is a reason plus a location:</p>
<pre><code class="language-swift"><span class="tok-type">ValidationError</span>(reason<span class="tok-operator">:</span> <span class="tok-string">"Server Object does not define the variable 'x'"</span>, at<span class="tok-operator">:</span> context<span class="tok-operator">.</span>coding<span class="tok-type">Path</span>)</code></pre>
<p>You supply the reason. You do <em>not</em> supply the location by hand. The traversal fills in the coding path as it walks, so by the time your check runs, <code>context.codingPath</code> already says exactly where in the tree you are. The rendered message comes out consistent every time:</p>
<blockquote><p>… at path: .paths./hello/world.get.servers</p></blockquote>
<p>or, at the top:</p>
<blockquote><p>… at root of document</p></blockquote>
<p>No more interpolating <code>\(path).\(method)</code> into strings and getting it subtly wrong. The path is bookkeeping the framework owns, not you.</p>
<h2>Application is predicate gated</h2>
<p>Every validation has a <code>when:</code> predicate that runs before the check, with the full context in hand. The default is “always.” But when a rule should only apply to a subset, you scope it with the predicate, not with an <code>if</code> inside the check body.</p>
<p>This matters because it keeps the check pure. The check answers “is this value correct,” full stop. The predicate answers “does this rule even apply here,” separately. A rule that only applies to OpenAPI 3.1 documents, or only to operations at a certain path, or only when some other field is present, says so in <code>when:</code> and keeps its check clean. Conditionals do not leak into the logic that decides pass or fail.</p>
<h2>Two shapes, by how a value can fail</h2>
<p>There are two ways to author the check, and the choice is mechanical.</p>
<p>If a value can fail <strong>one way</strong>, use the Bool form. You give a positive description and a predicate; <code>false</code> yields exactly one error, auto-formatted from the description. That <code>operationsContainResponses</code> rule above is this form. It is the dominant case.</p>
<p>If a single value can fail in <strong>several places at once</strong>, use the multi-error form and return one <code>ValidationError</code> per offender:</p>
<pre><code class="language-swift"><span class="tok-keyword">public</span> <span class="tok-keyword">static</span> <span class="tok-keyword">var</span> server<span class="tok-type">VariablesAreDefined</span><span class="tok-operator">:</span> <span class="tok-type">Validation</span><span class="tok-operator">&lt;</span><span class="tok-type">Server</span><span class="tok-operator">&gt;</span> {
    <span class="tok-operator">.</span><span class="tok-keyword">init</span>(
        description<span class="tok-operator">:</span> <span class="tok-string">"All server template variables are defined"</span>,
        check<span class="tok-operator">:</span> { context <span class="tok-keyword">in</span>
            context<span class="tok-operator">.</span>subject<span class="tok-operator">.</span>url<span class="tok-type">Template</span><span class="tok-operator">.</span>variables
                <span class="tok-operator">.</span>filter { <span class="tok-operator">!</span>context<span class="tok-operator">.</span>subject<span class="tok-operator">.</span>variables<span class="tok-operator">.</span><span class="tok-function">contains</span>(key<span class="tok-operator">:</span> $<span class="tok-number">0</span>) }
                <span class="tok-operator">.</span>map { name <span class="tok-keyword">in</span>
                    <span class="tok-type">ValidationError</span>(
                        reason<span class="tok-operator">:</span> <span class="tok-string">"Server Object does not define the variable '\(name)'"</span>,
                        at<span class="tok-operator">:</span> context<span class="tok-operator">.</span>coding<span class="tok-type">Path</span>
                    )
                }
        }
    )
}</code></pre>
<p>A server with three undefined variables produces three errors, each with its own reason and its own path. You do not collapse them into one “server is invalid” message, because then you would lose two thirds of the information about what to fix. The rule emits one error per problem.</p>
<h2>A default set you can add to and subtract from</h2>
<p>You ship two validators: a populated one with a curated default set, and a <code>blank</code> one with nothing. Builders are fluent and chainable. You compose your actual policy from the defaults, plus your own rules, minus the ones you do not want:</p>
<pre><code class="language-swift"><span class="tok-keyword">let</span> validator <span class="tok-operator">=</span> <span class="tok-type">Validator</span>()                       <span class="tok-comment">// the defaults</span>
    <span class="tok-operator">.</span><span class="tok-function">validating</span>(<span class="tok-operator">.</span>operations<span class="tok-type">ContainResponses</span>)      <span class="tok-comment">// add a builtin by name</span>
    <span class="tok-operator">.</span><span class="tok-function">validating</span>(my<span class="tok-type">CustomRule</span>)                      <span class="tok-comment">// add your own</span>
    <span class="tok-operator">.</span><span class="tok-function">withoutValidating</span>(<span class="tok-string">"All server template variables are defined"</span>)  <span class="tok-comment">// remove one by its description</span></code></pre>
<p>This is why descriptions have to be unique and stable: the description <em>is</em> the removal key. <code>withoutValidating</code> matches on it. Rename a rule’s description and you have quietly changed its identity.</p>
<p>When strictness comes in grades, you model the grades as whole-set swaps on the validator, not as flags threaded through individual rules. OpenAPIKit does lenient reference checks by default, swaps in the strict set with <code>validatingAllReferencesFoundInComponents()</code>, and clears them with <code>skippingReferenceValidations()</code>. Three named configurations, not a <code>strict: Bool</code> smuggled into every check.</p>
<h2>The combinator algebra</h2>
<p>Because validations and their checks are values, you combine them with an algebra instead of nesting <code>if</code> statements. These are the moves I reach for:</p>
<ul><li><p><strong><code>&amp;&amp;</code> / <code>||</code></strong> combine checks. <code>&amp;&amp;</code> concatenates both error lists; <code>||</code> short-circuits and only fails if both sides fail.</p></li><li><p><strong>comparison operators</strong> (<code>==</code>, <code>&gt;</code>, <code>&gt;=</code>, and friends) lift a key path and a literal into a predicate, so <code>check: \.responses.count &gt; 0</code> is a real expression, not a closure you wrote by hand.</p></li><li><p><strong><code>take(\.path) { value in ... }</code></strong> digs to a value and runs arbitrary logic past simple equality.</p></li><li><p><strong><code>lift(\.child, into: ...)</code></strong> runs child-typed validations against a child value while keeping the parent’s path and document. This is the key compositional move for nested structure.</p></li><li><p><strong><code>unwrap(\.optional, into:description:)</code></strong> unwraps an optional, erroring if nil, otherwise running child validations on the unwrapped value.</p></li><li><p><strong><code>lookup</code> / <code>unwrapAndLookup</code></strong> resolve a reference against the document’s component store, error if it is not found, otherwise validate the resolved value.</p></li><li><p><strong><code>all(validations...)</code></strong> applies many validations to the same context.</p></li></ul>
<p>So a compound rule reads declaratively:</p>
<pre><code class="language-swift">check<span class="tok-operator">:</span> \<span class="tok-operator">.</span>response<span class="tok-type">Outcomes</span><span class="tok-operator">.</span>count <span class="tok-operator">&gt;</span><span class="tok-operator">=</span> <span class="tok-number">1</span>
    <span class="tok-operator">&amp;</span><span class="tok-operator">&amp;</span> { $<span class="tok-number">0.</span>subject<span class="tok-operator">.</span>response<span class="tok-type">Outcomes</span><span class="tok-operator">.</span>all<span class="tok-type">Satisfy</span> { $<span class="tok-number">0.</span>status <span class="tok-operator">=</span><span class="tok-operator">=</span> <span class="tok-number">200</span> } }</code></pre>
<p>You are describing what correct looks like by composing pieces, not writing a traversal.</p>
<h2>Test the rules, the machinery, and the set</h2>
<p>The test style mirrors the philosophy, and OpenAPIKit’s bar here is the part most people skip. There are three layers to it.</p>
<p><strong>Every rule gets a failing and a succeeding test.</strong> Not just “here is a broken document, does it throw.” Also “here is a document that <em>almost</em> trips the rule but does not.” The succeeding fixture is a near miss, the value sitting exactly at the boundary the rule guards. A rule tested only in its failing direction is untested, because nothing proves it stays quiet on valid input. Assert the thrown collection’s exact count, each reason (including the <code>"Failed to satisfy: "</code> prefix), and each coding path:</p>
<pre><code class="language-swift"><span class="tok-keyword">func</span> <span class="tok-function">test_serverCountCheckFails</span>() {
    <span class="tok-keyword">let</span> document <span class="tok-operator">=</span> <span class="tok-comment">// ... seed the failing case</span>
    <span class="tok-keyword">let</span> validator <span class="tok-operator">=</span> <span class="tok-type">Validator</span><span class="tok-operator">.</span>blank
        <span class="tok-operator">.</span><span class="tok-function">validating</span>(<span class="tok-string">"All server arrays have more than 1 server"</span>, check<span class="tok-operator">:</span> \[<span class="tok-type">Server</span>]<span class="tok-operator">.</span>count <span class="tok-operator">&gt;</span> <span class="tok-number">1</span>)
    <span class="tok-type">XCTAssertThrowsError</span>(<span class="tok-keyword">try</span> document<span class="tok-operator">.</span><span class="tok-function">validate</span>(using<span class="tok-operator">:</span> validator)) { error <span class="tok-keyword">in</span>
        <span class="tok-keyword">let</span> error <span class="tok-operator">=</span> error <span class="tok-keyword">as</span><span class="tok-operator">?</span> <span class="tok-type">ValidationErrorCollection</span>
        <span class="tok-type">XCTAssertEqual</span>(error<span class="tok-operator">?</span><span class="tok-operator">.</span>values<span class="tok-operator">.</span>count, <span class="tok-number">1</span>)
        <span class="tok-type">XCTAssertEqual</span>(error<span class="tok-operator">?</span><span class="tok-operator">.</span>values<span class="tok-operator">.</span>first<span class="tok-operator">?</span><span class="tok-operator">.</span>reason,
                       <span class="tok-string">"Failed to satisfy: All server arrays have more than 1 server"</span>)
        <span class="tok-type">XCTAssertEqual</span>(error<span class="tok-operator">?</span><span class="tok-operator">.</span>values<span class="tok-operator">.</span>first<span class="tok-operator">?</span><span class="tok-operator">.</span>coding<span class="tok-type">Path</span><span class="tok-operator">.</span><span class="tok-function">map</span>(\<span class="tok-operator">.</span>string<span class="tok-type">Value</span>),
                       [<span class="tok-string">"paths"</span>, <span class="tok-string">"/hello/world"</span>, <span class="tok-string">"get"</span>, <span class="tok-string">"servers"</span>])
    }
}</code></pre>
<p><strong>The machinery gets its own negative tests.</strong> The type-erased wrapper that filters by runtime type has to be proven correct on its own: an optional subject yields no errors, a wrong type yields no errors, a false predicate yields no errors. And one positive control, the same value of the same type appearing twice yields two errors, to prove those negatives are not vacuously passing.</p>
<p><strong>The configuration is pinned.</strong> A test asserts the exact, ordered list of active rule descriptions for every validator configuration. <code>blank</code> is empty. The full default set is pinned description by description. This means you cannot add, remove, or reword a rule without a test failing first, which forces every such change to be deliberate rather than accidental.</p>
<p>The point of all this is that a validator can pass every test it ships and still be incomplete, if some field of the input model never got a rule at all. So the final discipline is a coverage law: keep a registry of every field the model can carry, and a meta-test that fails if any field lacks either a validation or an explicit “unsupported, and here is why” classification. Silent gaps get caught mechanically, not in review.</p>
<h2>Why I hold everything to this now</h2>
<p>I do not treat this as an OpenAPI trick. I treat it as the standard for any validation layer over any parsed model: a config, a manifest, an HTML tree, a schema, a feature model. One framework, parameterized over both the subject and the document, reused by every layer instead of copied per layer. Each rule a named, removable, isolation-testable value with a positive description. Errors that carry their own location. Combinators instead of conditionals.</p>
<p>The if-tree at the top of this post produces correct results. That is not the bar. The bar is whether you can add a rule without touching a three-hundred-line function, turn one off by name, test it alone, and trust that its failure message points at the right place. The if-tree fails all four. Validations as values pass all four, and they do it because the design made the right thing the easy thing.</p>
<p>Parse first. Then build your validations as values.</p>
<hr>
<p>Credit where it is due: this entire idiom is <a href="https://github.com/mattpolzin/OpenAPIKit">Matt Polzin’s</a>. Go read <code>Sources/OpenAPIKit/Validator/</code>. It is some of the most quietly excellent Swift in the ecosystem.</p>]]></content:encoded>
</item>
<item>
<title>&quot;Swift Runs Everywhere. I Checked.&quot;</title>
<link>https://aleahim.com/blog/swift-runs-everywhere/</link>
<guid isPermaLink="true">https://aleahim.com/blog/swift-runs-everywhere/</guid>
<pubDate>Wed, 10 Jun 2026 00:00:00 +0000</pubDate>
<description>Four of my Swift projects run right now on macOS, Linux, Windows, and inside the browser. A static site generator, a Markdown to PDF engine, a YAML parser, and a 2D vector graphics engine, all pure Swift, all with CI proving every platform on every push.</description>
<content:encoded><![CDATA[<h1>Swift Runs Everywhere. I Checked.</h1>
<p>“Swift runs everywhere” is usually a slide. A roadmap bullet, a conference
promise, a footnote about a port someone started. I wanted to know what it looks
like when it is not a slide, so I held my own projects to it: every platform
claim must be a CI gate that runs on every push, and at least one build per
project must be something you can click right now.</p>
<p>Four projects pass that bar today. A static site generator, a Markdown to PDF
engine, a YAML parser, and a 2D vector graphics engine. All pure Swift. Between
them they cover macOS, Linux, Windows, and the browser.</p>
<h2>Exhibit one: the page you are reading</h2>
<p>This site is built by <a href="https://codeberg.org/TileDownHQ/tile-down">TileDown</a>, a
static site generator written in Swift. The math on my posts is typeset to SVG
at build time, the charts and diagrams are fenced text blocks rendered by the
engine, and every article ships its own PDF from the same Markdown source. I
wrote about the why in
<a href="https://aleahim.com/blog/why-i-built-tiledown/">Why I built TileDown</a>.</p>
<p>The part that belongs in this post is the CI table. On every push, TileDown
builds and tests on macOS and on Linux in a Swift 6.1 container, the core
targets build on Windows, and the math playground product cross-compiles to
wasm32-unknown-wasip1 with the official Swift WebAssembly SDK. Four platforms,
four green gates, one codebase. The generator that produced this page would
produce it on a Linux box just as happily.</p>
<h2>Exhibit two: a PDF engine in your browser tab</h2>
<p><a href="https://pdf.aleahim.com">pdf.aleahim.com</a> is
<a href="https://codeberg.org/Mihaela/MarkdownPDF">MarkdownPDF</a> compiled to
WebAssembly. Type Markdown on the left, get a real PDF on the right, with no
server involved. The engine parses the Markdown, lays out the document, and
writes the PDF bytes directly in Swift, no LaTeX, no headless browser, no C
library underneath. The wasm ships gzipped at about eighteen megabytes,
downloads once, and is cached after that.</p>
<p>The same engine runs the full witness test suite on macOS and Linux, where the
output PDFs are checked by independent tools including veraPDF for PDF/UA and
PDF/A conformance. Windows and WebAssembly are build gates that compile the
engine on every push. I covered the engine itself in
<a href="https://aleahim.com/blog/markdown-to-pdf-in-the-browser/">Markdown to PDF in your browser, in pure Swift</a>.</p>
<h2>Exhibit three: a YAML parser with receipts</h2>
<p><a href="https://codeberg.org/Mihaela/PureYAML">PureYAML</a> is a dependency-free YAML
package: no external SwiftPM dependencies, no bundled C sources, and no
Foundation requirement in the library target. Its single CI workflow gates
macOS, Linux, Windows, and WASI on every push.</p>
<p>Correctness is measured, not asserted. A comparison harness runs PureYAML and
Yams side by side over a corpus of 111 real-world documents, OpenAPI specs,
Kubernetes manifests, GitHub Actions workflows. In the latest checked-in run
both parsers accept the same 110 documents and reject the same intentionally
damaged one, with zero disagreements between them.</p>
<p>And because a CI badge is less fun than a demo, there is a
<a href="https://codeberg.org/Mihaela/PureYAMLStreaming">live streaming benchmark</a> that
fetches multi-megabyte YAML files and parses them as streams, in Swift, inside
your browser tab, reporting fetch time, parse time, and throughput as it goes.</p>
<h2>Exhibit four: a graphics engine that brings its own canvas</h2>
<p><a href="https://slayermotion.com/#engine">PureDraw</a> is a dependency-free 2D
vector graphics engine, a “Virtual PostScript Machine” with an API compatible
with CoreGraphics and the HTML5 Canvas, written without either of them. Zero
external dependencies, zero bundled C sources, no Foundation requirement in
the core. The capability showcase at the top of its README, a bezier-path
character, gradients, blend modes, shadows, a 3D cube with a projective grid
texture, is a PDF rendered entirely by the engine itself, from the test suite.</p>
<p>This is the project where portability earns its keep most visibly: graphics is
the domain everyone assumes belongs to the platform. PureDraw builds and runs
its full test suite on macOS, on Linux in a Swift container, and on Windows,
in both debug and release, and the library cross-compiles to WebAssembly on
every push. The drawings that come out are the same bytes everywhere, because
nothing underneath them belongs to any one platform.</p>
<p><img src="https://aleahim.com/images/blog/swift-runs-everywhere/showcase3d.gif" alt="A gradient card with a white star, ringed by orbiting dots, animating in a loop"></p>
<p>And you do not have to take the CI’s word for any of it. That badge is not a
video. It is a few lines of PureComposition, a small language with a grammar of
its own, compiled and rendered frame by frame. <a href="https://slayermotion.com/#products">Glow Draw</a>
runs the same engines as WebAssembly right in your browser tab. PureDraw is
CoreGraphics rebuilt from nothing, PureLayer is CoreAnimation rebuilt the same
way, and PureComposition is the language that drives them: an EBNF grammar, a
parser, a compiler that lowers it to a PureDraw virtual-machine program, and a
command-line engine that runs the whole pipeline with no Mac in sight. The
drawing API, the layer compositor, and the language on top, all reconstructed in
portable Swift. If rebuilding Apple’s graphics stack from first principles is the
kind of problem you would want to talk about, get in touch.</p>
<p>If Swift is your language and you have been told it stops at the Mac, the CI
logs say otherwise.</p>]]></content:encoded>
</item>
<item>
<title>&quot;Markdown to PDF in your browser, in pure Swift&quot;</title>
<link>https://aleahim.com/blog/markdown-to-pdf-in-the-browser/</link>
<guid isPermaLink="true">https://aleahim.com/blog/markdown-to-pdf-in-the-browser/</guid>
<pubDate>Sun, 07 Jun 2026 00:00:00 +0000</pubDate>
<description>A pure-Swift Markdown to PDF engine compiled to WebAssembly, with a live in-browser playground</description>
<content:encoded><![CDATA[<h1>Markdown to PDF in your browser, in pure Swift</h1>
<p>There is a playground at <a href="https://pdf.aleahim.com">pdf.aleahim.com</a>. Type Markdown
on the left, get a real PDF on the right. No server is involved. The PDF is
produced by a pure-Swift engine compiled to WebAssembly and run inside your
browser.</p>
<p><img src="https://aleahim.com/images/blog/markdown-to-pdf-in-the-browser/markdown-to-pdf-playground-light.png" alt="The playground: Markdown source on the left, the rendered PDF on the right"></p>
<h2>What it is</h2>
<p>MarkdownPDF turns Markdown into a PDF with no LaTeX, no headless browser, and no C
Markdown or PDF library underneath. It parses the Markdown, lays the document out,
and writes the PDF bytes directly in Swift. It runs on macOS and Linux, the core
also builds for Windows and WebAssembly, and that last one is what makes the
in-browser demo possible.</p>
<p>The reason I wanted this: every Markdown to PDF path I tried pulled in something
heavy. A browser to print from, a full LaTeX distribution, or a C library bound
into the build. I wanted one Swift package that takes Markdown and emits the PDF,
so it drops into a server or an app with nothing to provision on the machine.</p>
<h2>What it renders</h2>
<p>In one codebase:</p>
<ul><li><p>A TeX-style math subset. Fractions, radicals, superscripts, and big-operator
limits lay out as real two-dimensional math. The quadratic formula and a
several-rows-tall continued fraction are in the default sample.</p></li><li><p>Native vector charts: bar, line, pie, and scatter, drawn with PDF path
operators rather than rasterized images.</p></li><li><p>A Mermaid flowchart subset, drawn as native PDF shapes.</p></li><li><p>Embedded TrueType subsetting, DEFLATE compression, tagged PDF, and PDF/UA and
PDF/A profiles checked with veraPDF on a fixture corpus.</p></li></ul>
<p>Headings, lists, tables, code blocks with syntax highlighting, links, and images
are all there too.</p>
<h2>How it runs in the browser</h2>
<p>The browser demo is small. There is a tiny executable that reads Markdown from
standard input, renders it, and writes the PDF bytes to standard output. That
executable is compiled to wasm32-unknown-wasip1. In the page, a WASI shim runs
the wasm, feeds the text area in as stdin, and captures stdout as the PDF, which
is shown in an iframe with a download fallback.</p>
<p>Two things were worth getting right. The wasm has to be instantiated
asynchronously, because synchronous instantiation is disallowed on the main
thread for modules over eight megabytes. And the engine is large, so the
playground ships a gzipped wasm, about eighteen megabytes on the wire, and
inflates it in the browser. First load downloads it once, then it is cached.</p>
<h2>The pieces, and where they live</h2>
<p>The work is split into focused packages, each open source under MIT:</p>
<ul><li><p>The engine: <a href="https://codeberg.org/Mihaela/MarkdownPDF">MarkdownPDF</a>. The parser,
layout, and PDF byte serialization.</p></li><li><p>The math: <a href="https://codeberg.org/Mihaela/MathTypeset">MathTypeset</a>. A
dependency-free Swift math typesetting package. It is its own package because a
static site generator I work on shares it, so the same TeX source typesets the
same way whether the target is a web page or a PDF.</p></li><li><p>The command line: <a href="https://codeberg.org/MarkdownPDFHQ/MarkdownPDFCli">MarkdownPDFCli</a>.
<code>markdownpdf input.md output.pdf</code> and a resume variant.</p></li><li><p>The playground: <a href="https://pdf.aleahim.com">pdf.aleahim.com</a>, with source at
<a href="https://codeberg.org/MarkdownPDFHQ/markdownpdf-playground">markdownpdf-playground</a>.
The wasm build, the WASI harness, and the page itself.</p></li></ul>
<p>There are no third-party runtime dependencies. The only package the engine pulls
in is my own MathTypeset, which itself has none.</p>
<h2>Honest limits</h2>
<p>It is early and I say so. Math renders with an embedded Latin Modern Math font,
so plus-minus, sigma, radicals, and Greek come out as real glyphs, not ASCII. One
wrinkle worth noting: the engine embeds TrueType (glyf) fonts only for now, and
Latin Modern Math ships as OpenType CFF, so the playground bundles a TrueType
conversion of it (the MATH table is preserved). The full witness test suite runs
on macOS and Linux; Windows and WebAssembly are build gates that compile the
engine but do not yet run the suite. Text beyond Western European needs a
supplied font, and shaping for Arabic and Hebrew is the open frontier where help
is welcome.</p>
<h2>Try it</h2>
<p>The demo is at <a href="https://pdf.aleahim.com">pdf.aleahim.com</a>, the engine is at
<a href="https://codeberg.org/Mihaela/MarkdownPDF">codeberg.org/Mihaela/MarkdownPDF</a>. If
you render Markdown to PDF anywhere that you would rather not install a browser or
a LaTeX stack, this might fit.</p>]]></content:encoded>
</item>
<item>
<title>Why I built TileDown</title>
<link>https://aleahim.com/blog/why-i-built-tiledown/</link>
<guid isPermaLink="true">https://aleahim.com/blog/why-i-built-tiledown/</guid>
<pubDate>Sat, 06 Jun 2026 00:00:00 +0000</pubDate>
<description>I already had a static site generator I liked, right up until I wanted diagrams, charts, a per-article PDF, real math, and a path to dynamic content. So I built one around a typed tile model, and this post is built by it.</description>
<content:encoded><![CDATA[<h1>Why I built TileDown</h1>
<p>I already had a static site generator. It was small, fast, and pleasant, and for a
while that was enough. Then I started wanting things it was never going to give me,
and every workaround made the setup more fragile instead of less.</p>
<p>So I wrote down what I actually wanted from a blog. It turned into a short,
stubborn list.</p>
<h2>I wanted diagrams in the prose</h2>
<p>Not screenshots of diagrams. Not a separate tool that exports a PNG I have to keep
in sync by hand. A fenced block, in the article, that renders:</p>
<figure class="td-mermaid" data-td-mermaid>
<pre class="mermaid td-mermaid-source">graph TD
  Idea[I want to explain a flow] --&gt; Tool{Does my generator do diagrams?}
  Tool --&gt;|No| Screenshot[Export a PNG forever]
  Tool --&gt;|Yes| Inline[Write it in the post]
  Screenshot --&gt; Stale[It drifts out of date]
  Inline --&gt; Done[It stays with the text]
</pre>
</figure>
<h2>I wanted charts that are just text</h2>
<p>Same idea, numbers instead of boxes. I describe the data in a few lines and the
build hands me a static SVG. No chart library, no script tag, no runtime:</p>
<figure class="td-chart td-chart-bar">
<figcaption class="td-chart-caption">What my old setup made hard</figcaption>
<div class="td-chart-frame">
<svg
  class="td-chart-svg"
  viewBox="0 0 720 360"
  role="img"
  aria-label="What my old setup made hard"
>
<desc>bar chart with 5 labels and 1 series</desc>
<line
  class="td-chart-grid"
  x1="80"
  y1="302"
  x2="696"
  y2="302"
></line>
<text
  class="td-chart-value"
  x="70"
  y="306"
  text-anchor="end"
>0</text>
<line
  class="td-chart-grid"
  x1="80"
  y1="246.4"
  x2="696"
  y2="246.4"
></line>
<text
  class="td-chart-value"
  x="70"
  y="250.4"
  text-anchor="end"
>2</text>
<line
  class="td-chart-grid"
  x1="80"
  y1="190.8"
  x2="696"
  y2="190.8"
></line>
<text
  class="td-chart-value"
  x="70"
  y="194.8"
  text-anchor="end"
>4</text>
<line
  class="td-chart-grid"
  x1="80"
  y1="135.2"
  x2="696"
  y2="135.2"
></line>
<text
  class="td-chart-value"
  x="70"
  y="139.2"
  text-anchor="end"
>6</text>
<line
  class="td-chart-grid"
  x1="80"
  y1="79.6"
  x2="696"
  y2="79.6"
></line>
<text
  class="td-chart-value"
  x="70"
  y="83.6"
  text-anchor="end"
>8</text>
<line
  class="td-chart-grid"
  x1="80"
  y1="24"
  x2="696"
  y2="24"
></line>
<text
  class="td-chart-value"
  x="70"
  y="28"
  text-anchor="end"
>10</text>
<line
  class="td-chart-axis"
  x1="80"
  y1="302"
  x2="696"
  y2="302"
></line>
<line
  class="td-chart-axis"
  x1="80"
  y1="24"
  x2="80"
  y2="302"
></line>
<rect
  class="td-chart-bar td-chart-series-0"
  x="124.6"
  y="79.6"
  width="32"
  height="222.4"
  rx="4"
><title>Diagrams: 8</title></rect>
<rect
  class="td-chart-bar td-chart-series-0"
  x="247.8"
  y="107.4"
  width="32"
  height="194.6"
  rx="4"
><title>Charts: 7</title></rect>
<rect
  class="td-chart-bar td-chart-series-0"
  x="371"
  y="51.8"
  width="32"
  height="250.2"
  rx="4"
><title>PDF: 9</title></rect>
<rect
  class="td-chart-bar td-chart-series-0"
  x="494.2"
  y="51.8"
  width="32"
  height="250.2"
  rx="4"
><title>Math: 9</title></rect>
<rect
  class="td-chart-bar td-chart-series-0"
  x="617.4"
  y="24"
  width="32"
  height="278"
  rx="4"
><title>Dynamic: 10</title></rect>
<text
  class="td-chart-label"
  x="141.6"
  y="324"
  text-anchor="middle"
>Diagrams</text>
<text
  class="td-chart-label"
  x="264.8"
  y="324"
  text-anchor="middle"
>Charts</text>
<text
  class="td-chart-label"
  x="388"
  y="324"
  text-anchor="middle"
>PDF</text>
<text
  class="td-chart-label"
  x="511.2"
  y="324"
  text-anchor="middle"
>Math</text>
<text
  class="td-chart-label"
  x="634.4"
  y="324"
  text-anchor="middle"
>Dynamic</text>
<text
  class="td-chart-label"
  x="16"
  y="163"
  text-anchor="middle"
  transform="rotate(-90 16 163)"
>pain (made up units)</text>
<rect
  class="td-chart-series-0"
  x="80"
  y="336"
  width="12"
  height="12"
  rx="3"
  fill="currentColor"
></rect>
<text
  class="td-chart-legend-text"
  x="98"
  y="346"
  text-anchor="start"
>Before</text>
</svg>
</div>
</figure>
<p>The numbers are invented. The rendering is real, and it uses the same notation the
PDF side understands, which matters for the next part.</p>
<h2>I wanted the PDF of the article, for free</h2>
<p>If you are reading this on the site, there is a Download PDF button near the top of
the page. I did not build it by hand for this post. The same engine that lays out
this page lays out a print version, from the same Markdown, so every article ships
a PDF without me thinking about it.</p>
<h2>I wanted real math</h2>
<p>Typeset at build time into static SVG, themed to the page. No MathJax, no web font,
no LaTeX install:</p>
<div class="td-math td-math-display"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 69.19 33.8" fill="currentColor" aria-hidden="true" style="width:6.92em;height:3.38em;vertical-align:-1.22em"><path d="M11 8.54C11 7.68 10.09 7.05 9.01 7.05C7.29 7.05 6.59 8.8 6.28 10.17C6.07 11.16 5.42 16.25 5.25 18.44L4.91 22.93C4.68 25.9 4.03 26.71 3 26.71C2.76 26.71 2.22 26.65 1.84 26.33C2.37 26.26 2.51 25.84 2.51 25.59C2.51 25.06 2.11 24.83 1.77 24.83C1.41 24.83 1.01 25.06 1.01 25.61C1.01 26.47 1.91 27.1 3 27.1C4.71 27.1 5.58 25.53 5.98 23.91C6.21 22.97 6.86 17.72 7.01 15.71L7.35 11.22C7.6 7.89 8.22 7.44 9.03 7.44C9.21 7.44 9.77 7.48 10.17 7.82C9.64 7.89 9.5 8.31 9.5 8.56C9.5 9.09 9.89 9.32 10.24 9.32C10.6 9.32 11 9.09 11 8.54Z"/><path d="M9.1 3.53C9.1 2.75 8.61 1.95 7.78 1.95C6.77 1.95 6.15 2.98 6.06 3.14C5.59 2.57 5.58 2.56 5.43 2.43C5.21 2.24 4.79 1.95 4.24 1.95C3.43 1.95 2.9 2.71 2.9 3.53C2.9 4.31 3.39 5.12 4.22 5.12C5.23 5.12 5.85 4.08 5.94 3.93C6.41 4.49 6.42 4.5 6.57 4.63C6.79 4.82 7.21 5.12 7.76 5.12C8.57 5.12 9.1 4.35 9.1 3.53ZM8.95 3.53C8.95 4.23 8.46 4.78 7.87 4.78C7.55 4.78 7.26 4.62 6.95 4.28C6.75 4.06 6.44 3.62 6.24 3.38C6.51 2.9 7.04 2.17 7.83 2.17C8.57 2.17 8.95 2.9 8.95 3.53ZM5.76 3.68C5.49 4.16 4.96 4.89 4.17 4.89C3.43 4.89 3.05 4.16 3.05 3.53C3.05 2.83 3.54 2.28 4.13 2.28C4.45 2.28 4.74 2.44 5.05 2.79C5.25 3 5.56 3.44 5.76 3.68Z"/><path d="M3.27 30.95L3.27 30.55L1.41 30.55L1.41 30.95Z"/><path d="M10.27 30.75C10.27 29.97 9.78 29.17 8.95 29.17C7.94 29.17 7.32 30.2 7.22 30.36C6.76 29.79 6.75 29.78 6.59 29.65C6.37 29.46 5.96 29.17 5.4 29.17C4.59 29.17 4.06 29.93 4.06 30.76C4.06 31.53 4.55 32.34 5.38 32.34C6.39 32.34 7.01 31.3 7.11 31.15C7.57 31.72 7.58 31.72 7.74 31.86C7.96 32.05 8.37 32.34 8.93 32.34C9.74 32.34 10.27 31.58 10.27 30.75ZM10.12 30.75C10.12 31.45 9.63 32 9.04 32C8.72 32 8.43 31.84 8.11 31.5C7.92 31.28 7.6 30.84 7.41 30.6C7.67 30.13 8.2 29.39 9 29.39C9.74 29.39 10.12 30.12 10.12 30.75ZM6.92 30.9C6.66 31.38 6.13 32.12 5.33 32.12C4.59 32.12 4.21 31.39 4.21 30.76C4.21 30.06 4.7 29.5 5.29 29.5C5.61 29.5 5.9 29.67 6.22 30.01C6.41 30.23 6.73 30.67 6.92 30.9Z"/><path d="M19.48 20.39C19.48 20.29 19.4 20.27 19.35 20.27C19.26 20.27 19.24 20.33 19.22 20.41C18.87 21.44 17.97 21.44 17.87 21.44C17.37 21.44 16.97 21.14 16.74 20.77C16.44 20.29 16.44 19.63 16.44 19.27L19.23 19.27C19.45 19.27 19.48 19.27 19.48 19.06C19.48 18.07 18.94 17.1 17.69 17.1C16.53 17.1 15.61 18.13 15.61 19.38C15.61 20.72 16.66 21.69 17.81 21.69C19.03 21.69 19.48 20.58 19.48 20.39ZM18.82 19.06L16.45 19.06C16.51 17.57 17.35 17.32 17.69 17.32C18.72 17.32 18.82 18.67 18.82 19.06Z"/><path d="M22.26 16.64L22.26 16.24L20.4 16.24L20.4 16.64Z"/><path d="M26.27 17.95L26.27 17.73C25.89 17.73 25.77 17.72 25.6 17.52L24.67 16.31C24.88 16.04 25.14 15.7 25.31 15.51C25.53 15.26 25.81 15.16 26.14 15.15L26.14 14.93C25.96 14.95 25.75 14.95 25.57 14.95C25.36 14.95 24.99 14.94 24.9 14.93L24.9 15.15C25.04 15.16 25.1 15.26 25.1 15.37C25.1 15.48 25.03 15.57 25 15.61L24.56 16.16L24.02 15.45C23.95 15.38 23.95 15.37 23.95 15.33C23.95 15.22 24.06 15.16 24.2 15.15L24.2 14.93L23.44 14.95C23.29 14.95 22.97 14.95 22.78 14.93L22.78 15.15C23.27 15.15 23.27 15.16 23.6 15.58L24.3 16.48C23.97 16.9 23.97 16.91 23.64 17.31C23.3 17.72 22.89 17.73 22.74 17.73L22.74 17.95C22.92 17.94 23.14 17.93 23.32 17.93L23.99 17.95L23.99 17.73C23.83 17.71 23.78 17.62 23.78 17.52C23.78 17.36 23.99 17.13 24.41 16.63L24.95 17.33C25 17.4 25.09 17.52 25.09 17.56C25.09 17.62 25.03 17.73 24.84 17.73L24.84 17.95L25.6 17.93C25.79 17.93 26.06 17.94 26.27 17.95Z"/><path d="M28.95 14.56L28.82 14.56C28.8 14.7 28.76 14.92 28.72 14.99C28.68 15.03 28.36 15.03 28.25 15.03L27.37 15.03L27.89 14.53C28.65 13.85 28.95 13.59 28.95 13.1C28.95 12.54 28.51 12.15 27.91 12.15C27.35 12.15 26.99 12.6 26.99 13.03C26.99 13.31 27.24 13.31 27.25 13.31C27.33 13.31 27.51 13.25 27.51 13.05C27.51 12.92 27.42 12.79 27.25 12.79C27.21 12.79 27.2 12.79 27.18 12.8C27.29 12.48 27.56 12.3 27.84 12.3C28.29 12.3 28.5 12.69 28.5 13.1C28.5 13.49 28.26 13.88 27.99 14.18L27.04 15.23C26.99 15.28 26.99 15.29 26.99 15.41L28.81 15.41Z"/><path d="M36.13 21.58L36.13 21.27C35.43 21.27 35.35 21.2 35.35 20.71L35.35 14.64L33.91 14.75L33.91 15.06C34.61 15.06 34.69 15.13 34.69 15.62L34.69 17.78C34.4 17.42 33.97 17.16 33.43 17.16C32.25 17.16 31.2 18.14 31.2 19.43C31.2 20.7 32.18 21.69 33.32 21.69C33.96 21.69 34.41 21.35 34.66 21.03L34.66 21.69ZM34.66 20.4C34.66 20.58 34.66 20.6 34.55 20.77C34.25 21.25 33.8 21.47 33.37 21.47C32.92 21.47 32.56 21.21 32.32 20.83C32.06 20.42 32.03 19.85 32.03 19.44C32.03 19.07 32.05 18.47 32.34 18.02C32.55 17.71 32.93 17.38 33.47 17.38C33.82 17.38 34.24 17.53 34.55 17.98C34.66 18.15 34.66 18.17 34.66 18.35Z"/><path d="M41.58 21.58L41.58 21.27C41.04 21.27 40.86 21.25 40.63 20.96L39.29 19.23C39.59 18.85 39.97 18.36 40.21 18.1C40.52 17.74 40.93 17.59 41.4 17.58L41.4 17.27C41.14 17.29 40.84 17.3 40.58 17.3C40.28 17.3 39.75 17.28 39.62 17.27L39.62 17.58C39.83 17.6 39.91 17.73 39.91 17.89C39.91 18.05 39.81 18.18 39.76 18.24L39.14 19.02L38.36 18.01C38.27 17.91 38.27 17.89 38.27 17.83C38.27 17.68 38.42 17.59 38.62 17.58L38.62 17.27L37.54 17.3C37.33 17.3 36.86 17.29 36.59 17.27L36.59 17.58C37.29 17.58 37.3 17.59 37.77 18.19L38.76 19.48C38.29 20.08 38.29 20.1 37.82 20.67C37.34 21.25 36.75 21.27 36.54 21.27L36.54 21.58C36.8 21.56 37.11 21.55 37.37 21.55L38.32 21.58L38.32 21.27C38.1 21.24 38.03 21.11 38.03 20.96C38.03 20.74 38.32 20.41 38.93 19.69L39.69 20.69C39.77 20.8 39.9 20.96 39.9 21.02C39.9 21.11 39.81 21.26 39.54 21.27L39.54 21.58L40.62 21.55C40.89 21.55 41.28 21.56 41.58 21.58Z"/><path d="M52.24 18.11C52.24 18 52.15 17.91 52.04 17.91L45.78 17.91C45.67 17.91 45.58 18 45.58 18.11C45.58 18.22 45.67 18.31 45.78 18.31L52.04 18.31C52.15 18.31 52.24 18.22 52.24 18.11ZM52.24 20.05C52.24 19.94 52.15 19.85 52.04 19.85L45.78 19.85C45.67 19.85 45.58 19.94 45.58 20.05C45.58 20.16 45.67 20.25 45.78 20.25L52.04 20.25C52.15 20.25 52.24 20.16 52.24 20.05Z"/><line x1="56.12" y1="17.84" x2="58.32" y2="23.67" stroke="currentColor" stroke-width="0.4" stroke-linejoin="miter" stroke-linecap="butt"/><line x1="58.32" y1="23.67" x2="61.62" y2="13.26" stroke="currentColor" stroke-width="0.4" stroke-linejoin="miter" stroke-linecap="butt"/><rect x="61.62" y="12.86" width="5.57" height="0.4"/><path d="M68.52 17.71C68.46 17.49 68.26 17.49 68.08 17.49L64.9 17.49C64.69 17.49 64.33 17.49 64.02 17.93C63.78 18.3 63.64 18.74 63.66 18.79C63.66 18.79 63.68 18.88 63.79 18.88C63.87 18.88 63.88 18.84 63.92 18.77C64.2 18.04 64.75 18.04 64.94 18.04L65.48 18.04C65.47 19.19 65.24 20.34 65.06 21.2C65.02 21.34 65.02 21.36 65.04 21.43C65.09 21.61 65.26 21.68 65.38 21.68C65.67 21.68 65.67 21.42 65.7 21.07L65.72 20.14L65.73 18.04L66.81 18.04C66.84 19.44 66.86 19.85 67.01 20.49C67.05 20.63 67.12 20.89 67.27 21.21C67.47 21.63 67.59 21.68 67.74 21.68C67.93 21.68 68.08 21.51 68.03 21.32C68.02 21.27 68.01 21.25 67.93 21.11C67.48 20.43 67.33 19.81 67.26 19.55C67.13 19.04 67.07 18.53 67.05 18.04L68.14 18.04C68.26 18.04 68.6 18.04 68.52 17.71Z"/></svg><math class="td-math-a11y" xmlns="http://www.w3.org/1998/Math/MathML"><mrow><msubsup><mrow><mo>∫</mo></mrow><mrow><mrow><mtext>-</mtext><mo>∞</mo></mrow></mrow><mrow><mo>∞</mo></mrow></msubsup><mtext> </mtext><msup><mrow><mi>e</mi></mrow><mrow><mrow><mtext>-</mtext><msup><mrow><mi>x</mi></mrow><mrow><mn>2</mn></mrow></msup></mrow></mrow></msup><mspace width="0.17em"/><mi>d</mi><mi>x</mi><mtext> </mtext><mtext>=</mtext><mtext> </mtext><msqrt><mo>π</mo></msqrt></mrow></math></div>
<h2>And I wanted a way out of “static forever”</h2>
<p>This is the part that made it a project instead of a config file. Static sites are
wonderful until the day you want one live thing on the page: a counter, an embed, a
form that talks to a backend. Most generators answer that with “drop in some
JavaScript and good luck.”</p>
<p>I wanted that one live thing to be a first-class, typed unit of content. So
TileDown has tiles. A tile renders to HTML, scoped CSS, and only the JavaScript it
needs, and nothing else on the page pays for it. Here is one:</p>
<div class="td-counter" data-td-counter>
<button class="td-counter-button" type="button">A small live thing on a static page</button>
<span class="td-counter-value" data-td-counter-value>0</span>
</div>
<p>Today tiles cover the client-side cases: counters, embeds, callouts. The next step
is tiles that talk to a backend, a form that hits a real service, and that work is
still coming. But the model is the point. Dynamic behavior is a tile, not a hole I
cut in the static output.</p>
<h2>That is the whole reason</h2>
<p>Diagrams, charts, a free PDF, real math, and a typed path to dynamic content. None
of those is exotic on its own. I just could not get all of them from one tool
without the setup fighting me, so I built the tool. This post is written in plain
Markdown and rendered by it.</p>
<pre><code class="language-sh">brew install tiledown<span class="tok-operator">/</span>tap<span class="tok-operator">/</span>tiledown</code></pre>]]></content:encoded>
</item>
<item>
<title>OpenAPIDoctor WebAssembly readiness</title>
<link>https://aleahim.com/blog/openapi-doctor-webassembly-readiness/</link>
<guid isPermaLink="true">https://aleahim.com/blog/openapi-doctor-webassembly-readiness/</guid>
<pubDate>Fri, 05 Jun 2026 00:00:00 +0000</pubDate>
<description>A first compiler-grounded check of whether OpenAPIDoctor can become a local-first browser tool.</description>
<content:encoded><![CDATA[<h1>OpenAPIDoctor WebAssembly readiness</h1>
<p>I want OpenAPIDoctor to become a no-backend browser tool.</p>
<p>The product shape is right. A user should be able to paste or upload an OpenAPI
spec, run the same strict Swift validator locally in the browser, see structured
diagnostics, repair the safe cases, and download the fixed spec. No upload. No
server. No account.</p>
<p>The only honest way to test that idea is to ask the compiler.</p>
<h2>What I tested</h2>
<p>Native build first:</p>
<pre><code class="language-bash">swift build</code></pre>
<p>That passed with Apple Swift 6.2.</p>
<p>Then I tested the installed WASI SDK:</p>
<pre><code class="language-bash">swift build \
  <span class="tok-operator">-</span><span class="tok-operator">-</span>target OpenAPIDoctor \
  <span class="tok-operator">-</span><span class="tok-operator">-</span>swift<span class="tok-operator">-</span>sdk <span class="tok-number">6.2</span><span class="tok-operator">-</span>RELEASE<span class="tok-operator">-</span>wasm<span class="tok-number">32</span><span class="tok-operator">-</span>unknown<span class="tok-operator">-</span>wasip<span class="tok-number">1</span> \
  <span class="tok-operator">-</span><span class="tok-operator">-</span>scratch<span class="tok-operator">-</span>path <span class="tok-operator">/</span>tmp<span class="tok-operator">/</span>openapidoctor<span class="tok-operator">-</span>wasm<span class="tok-operator">-</span>library</code></pre>
<p>And the CLI shape:</p>
<pre><code class="language-bash">swift build \
  <span class="tok-operator">-</span><span class="tok-operator">-</span>product openapi<span class="tok-operator">-</span>doctor \
  <span class="tok-operator">-</span><span class="tok-operator">-</span>swift<span class="tok-operator">-</span>sdk <span class="tok-number">6.2</span><span class="tok-operator">-</span>RELEASE<span class="tok-operator">-</span>wasm<span class="tok-number">32</span><span class="tok-operator">-</span>unknown<span class="tok-operator">-</span>wasip<span class="tok-number">1</span> \
  <span class="tok-operator">-</span><span class="tok-operator">-</span>scratch<span class="tok-operator">-</span>path <span class="tok-operator">/</span>tmp<span class="tok-operator">/</span>openapidoctor<span class="tok-operator">-</span>wasm<span class="tok-operator">-</span>cli</code></pre>
<p>Both failed before OpenAPIDoctor itself compiled.</p>
<h2>The blocker</h2>
<p>The first blocker is <code>Yams</code>, specifically its embedded <code>CYaml</code> C target:</p>
<pre><code class="language-text">error<span class="tok-operator">:</span> unable to create target<span class="tok-operator">:</span>
<span class="tok-string">'No available targets are compatible with triple "wasm32-unknown-wasip1"'</span></code></pre>
<p>I confirmed it directly by building <code>Yams</code> for the same SDK. It fails in the C
parser files: <code>parser.c</code>, <code>reader.c</code>, <code>writer.c</code>, <code>api.c</code>, <code>emitter.c</code>, and
<code>scanner.c</code>.</p>
<p>I also tried a tiny C compile with the same target:</p>
<pre><code class="language-bash">clang <span class="tok-operator">-</span><span class="tok-operator">-</span>target<span class="tok-operator">=</span>wasm<span class="tok-number">32</span><span class="tok-operator">-</span>unknown<span class="tok-operator">-</span>wasip<span class="tok-number">1</span></code></pre>
<p>That failed with the same target-support error. So the current local toolchain
cannot compile the C YAML dependency to WASI.</p>
<h2>What this means</h2>
<p>OpenAPIDoctor is not WebAssembly-ready as it exists today.</p>
<p>That does not kill the idea. It narrows the work.</p>
<p>The core shape is still good:</p>
<ul><li><p><code>Validator.validate(yaml:)</code> already accepts an in-memory string.</p></li><li><p><code>Repairer.repair(yaml:)</code> already returns repaired YAML in memory.</p></li><li><p>The native CLI is just a composition layer around those APIs.</p></li><li><p>The right browser boundary is a WASI command: <code>stdin</code> in, diagnostics or files
out.</p></li></ul>
<p>The weak point is YAML infrastructure.</p>
<p>OpenAPIDoctor imports <code>Yams</code> in the validator, repairer, and synthesizer. It also
uses <code>Stitcher</code> for multi-file refs, and Stitcher depends on Yams too. That means
a browser build needs a YAML path that can cross to WASI.</p>
<h2>The next path</h2>
<p>There are three practical options.</p>
<ol><li><p>Test again inside the Linux SwiftWasm toolchain we use for the TileDown
playground builds. If that environment can compile C targets to WASI, then the
next blocker will be runtime behavior.</p></li><li><p>Replace or isolate the YAML layer behind a WebAssembly-friendly interface.
Single-file diagnosis could start with a smaller path before multi-file repair.</p></li><li><p>Add an OpenAPIDoctor WASI wrapper only after the dependency question is settled.
The wrapper should read the spec from <code>stdin</code> and write JSON diagnostics to
<code>stdout</code>.</p></li></ol>
<p>The browser product should not use custom JavaScript exports. The better pattern
is the same one that already works for the PDF playground:</p>
<pre><code class="language-text">stdin <span class="tok-operator">+</span> argv <span class="tok-operator">+</span> preopened files<span class="tok-operator">/</span>directories <span class="tok-operator">-</span><span class="tok-operator">&gt;</span> stdout or output files</code></pre>
<p>That keeps the Swift code shaped like a normal command-line program and lets the
browser provide an in-memory filesystem.</p>
<h2>Current verdict</h2>
<p>OpenAPIDoctor is product-ready as a local-first browser idea, but not yet
compiler-ready for WebAssembly.</p>
<p>The first concrete task is not UI. It is a dependency build proof for
<code>OpenAPIKit</code>, <code>Yams</code>, and <code>Stitcher</code> under WASI. Only after that should the web
tool be wrapped as a TileDown WASM tile or a standalone site.</p>]]></content:encoded>
</item>
<item>
<title>cupertino v1.3.0: The Big Refactor</title>
<link>https://aleahim.com/blog/cupertino-v1-3-0-platform-filtering/</link>
<guid isPermaLink="true">https://aleahim.com/blog/cupertino-v1-3-0-platform-filtering/</guid>
<pubDate>Sun, 31 May 2026 00:00:00 +0000</pubDate>
<description>v1.3.0 is the architecture release: eight per-source databases derived from one registry, shipped read-only, with the legacy unified search.db retired for good.</description>
<content:encoded><![CDATA[<h1>cupertino v1.3.0: The Big Refactor</h1>
<p>This release started with a number that looked too small to matter.</p>
<p>Back in v1.2.0 I had added deployment-aware search: ask cupertino for an API and tell it which OS you actually ship, and it filters out the answers that would not compile for you.</p>
<pre><code class="language-bash">cupertino search swiftui <span class="tok-operator">-</span><span class="tok-operator">-</span>source samples <span class="tok-operator">-</span><span class="tok-operator">-</span>m<span class="tok-keyword">in</span><span class="tok-operator">-</span>ios <span class="tok-number">16</span></code></pre>
<p>The flag parsed. The database carried availability metadata. The command looked completely right. And yet, in the dev corpus, <code>--min-ios</code> handed back the same 88 results whether I asked for iOS 13 or iOS 18. A filter that returns the same set for every value is not filtering. It is decoration.</p>
<p>That is the kind of bug that looks like a one-line fix until you ask why it was possible at all.</p>
<p>A filter that does not filter is worse than no filter. It lets an assistant claim it respected your deployment target while quietly handing you an iOS 18 API for an app that still ships iOS 16. That is not an answer. It is a future compile error wearing a green checkmark.</p>
<p>I could have found the missing parameter, threaded it through the one path that dropped it, added a test, and shipped. One path fixed. The reason that path could go missing in the first place: untouched.</p>
<p>So I pulled on it instead.</p>
<h2>The problem under the bug</h2>
<p>cupertino had outgrown the shape it was born in.</p>
<p>Early on, the whole thing fit one mental model: a big Apple documentation index with a few extra sources bolted on. Apple Developer Documentation, the HIG, Apple Archive, Swift Evolution, swift.org, and <em>The Swift Programming Language</em> all lived inside a single SQLite file. Samples and packages had their own databases, but most of the command surface still thought in three buckets:</p>
<ul><li><p><code>search.db</code></p></li><li><p><code>samples.db</code></p></li><li><p><code>packages.db</code></p></li></ul>
<p>That shape was fine when the project was small. It quietly stopped being fine the moment cupertino became a genuine multi-source tool, and the symptoms were everywhere if you knew to look. A platform filter honored in one source and silently dropped in another. A doctor probe pointed at a database filename that a per-source build no longer produced. A release script that knew the names of its databases by heart.</p>
<p>Those were not five unrelated bugs. They were one disease with five rashes. Source identity was scattered across CLI switches, MCP handlers, setup checks, release manifests, and tests, each carrying its own hand-copied list of names. Change the world in one place and the other four kept believing the old map.</p>
<p>The cure was already half-built. The Source Independence Day effort had been pushing toward a single source of truth in code: a registry every part of the system reads from, so adding a content source becomes one composition-root change instead of a scavenger hunt. The code was moving that way. The shipped artifact was not. The bundle users actually installed still wore the old three-bucket shape.</p>
<p>So v1.3.0 became the release that drags the artifact up to meet the architecture.</p>
<p>Not because retiring a database filename is exciting. It is not. Nobody installs cupertino to admire the shape of its SQLite files. It matters because that old shape was a tax. Every new source paid it. Every cross-source feature paid it. v1.3.0 is the refactor that stops collecting it.</p>
<h2>One registry, eight databases</h2>
<p>In v1.3.0 a source is no longer a string in a <code>source</code> column.</p>
<p>It is a registered provider that declares everything about itself: where its data lives, how it fetches, how it reads, how it searches, which metadata it carries, and which filters it can honestly apply. One registry holds them all, and the rest of the system asks the registry instead of remembering.</p>
<p>The release bundle now matches that model exactly. Instead of one giant docs database plus two siblings, v1.3.0 ships eight per-source databases:</p>
<ul><li><p><code>apple-documentation.db</code></p></li><li><p><code>hig.db</code></p></li><li><p><code>apple-archive.db</code></p></li><li><p><code>swift-evolution.db</code></p></li><li><p><code>swift-org.db</code></p></li><li><p><code>swift-book.db</code></p></li><li><p><code>apple-sample-code.db</code></p></li><li><p><code>packages.db</code></p></li></ul>
<p><code>cupertino setup</code> downloads <code>cupertino-databases-v1.3.0.zip</code>, a 742 MB bundle that expands to about 3.9 GB. Most of that is one file: <code>apple-documentation.db</code> weighs 2.6 GB and holds 351,505 documents and 240,543 symbols across 398 frameworks. Next is <code>packages.db</code> at 1.0 GB with 185 packages. The HIG, archive, Swift Evolution, swift.org, and Swift book databases are the small ones, a few megabytes each.</p>
<p>The split is not cosmetic. It changes what can go wrong independently. If the HIG corpus is stale, the HIG database is stale and Apple API search is not dragged down with it. If packages need a schema bump, packages get one without reindexing everything else. And when a new source lands, the release tool derives its database straight from the registry. No human goes hunting for the one more filename list that forgot to grow.</p>
<p>Users never see any of this. The fan-out search surface stays exactly as simple as it was. Underneath, SmartQuery dispatches across the independent source databases and fuses the results with reciprocal rank fusion, so one source can degrade without taking the whole query down with it. That is the trade the refactor buys: invisible to the person typing a query, decisive for the person adding the next source.</p>
<h2>The old public shape had to break</h2>
<p>Some flags described a world that no longer exists, so they had to go.</p>
<p>The legacy <code>--search-db</code> override is gone from all six commands that carried it: <code>search</code>, <code>read</code>, <code>save</code>, <code>doctor</code>, <code>list-frameworks</code>, and <code>inheritance</code>. It made sense when there was one docs database to point at. It is meaningless when every source resolves through the registry to its own file, and keeping it around would only preserve the wrong mental model. Scripts that still pass <code>--search-db</code> now get a clear error instead of a silent misread.</p>
<p>The same honesty went inward. Roughly 230 <code>searchDB</code> and <code>searchDb</code> identifiers across the CLI, services, indexer, and search layers became the generic <code>dbURL</code> and <code>dbPath</code>, because there is no longer a single “the db” to name. The release tool stopped bundling a hardcoded trio of filenames and now asks the production registry for every enabled source’s destination database. And <code>databaseVersion</code> moved to <code>1.3.0</code>, so a v1.3.0 binary pulls the per-source bundle while a v1.2.x binary keeps pulling the old unified one. Upgrades and downgrades both land on a bundle they understand.</p>
<p>The one place the literal <code>search.db</code> name survives is the upgrade shim, which has to recognize a real pre-v1.3.0 file by its old name in order to migrate it. Everywhere else, the old name is finally just history.</p>
<h2>Then SQLite made the refactor earn it</h2>
<p>Splitting the bundle uncovered a second bug, the kind that only appears once you change how the file is born.</p>
<p>The indexers build databases in WAL mode, which is great for local write workloads and awkward for a static read-only artifact. A freshly extracted WAL database usually arrives with no <code>-shm</code> sidecar, and a plain <code>SQLITE_OPEN_READONLY</code> cannot open it without conjuring that shared-memory state first. So a shipped database could be byte-for-byte intact, pass an integrity check, and still refuse to open on a clean install.</p>
<p><code>packages.db</code> made it impossible to ignore. On a fresh setup, package search would report the database as unopenable while the file sat right there, perfectly valid.</p>
<p>v1.3.0 fixes the artifact instead of teaching every reader a workaround. The release tool converts each bundled database to rollback journal mode before zipping, and rollback-mode databases open read-only with no <code>-shm</code> or <code>-wal</code> sidecar required. Then every query, read, and serve path opens through one shared helper, <code>SQLiteSupport.openReadOnly(at:)</code>, using <code>SQLITE_OPEN_READONLY</code>. <code>SELECT</code> works. <code>INSERT</code>, <code>UPDATE</code>, <code>DELETE</code>, and any DDL fail with <code>SQLITE_READONLY</code>. The indexer and the setup migrator are the only writers; ordinary search and MCP reads physically cannot mutate the cache they read from.</p>
<p>That is the invariant I want under a local documentation tool. The thing you query is the thing you shipped, and nothing in the read path can quietly rewrite it.</p>
<h2>I did not trust any of this without batteries</h2>
<p>The frightening part of a refactor this wide is never the code you remember touching. It is the assumption asleep in a path you forgot existed.</p>
<p>So v1.3.0 leans on tests that behave more like release audits than unit checks.</p>
<p><code>DatabaseBundleManifestTests</code> proves the bundle is derived from the production registry and excludes the retired <code>search.db</code>. <code>searchDBFlagRejectedEverywhere</code> proves the removed flag errors on every one of the six commands that used to take it. <code>ConvertToRollbackJournalTests</code> reproduces the WAL no-sidecar failure, then proves rollback conversion cures it. <code>SQLiteSupportReadOnlyTests</code> proves the shared helper can read and cannot write. <code>Issue1190PackageQueryReadOnlyOpenTests</code> proves a shipped rollback-mode <code>packages.db</code> answers a query the instant setup finishes.</p>
<p>Then come the snapshot batteries, which run the real CLI against the real shipped databases.</p>
<p><code>ReadOnlyReadBatteryTests</code> drives the actual binary across all eight databases through the read-only path, searching every source and then performing at least twenty reads from each. The assertions check shape, not whether the output was simply long enough to look alive.</p>
<p><code>ExhaustiveEnrichmentBatteryTests</code> covers all 24 enrichments from the project’s enrichment inventory. Sixteen of them are proven end to end through the real CLI: lexical search, AST symbols, generic constraints, framework aliasing, conformances, inheritance, rank fusion, exact-title reranking, deployment floors for every <code>--min</code> platform, structured reads, and code examples. The other eight are internal columns the CLI never surfaces, so they are proven directly against the database with row-count probes rather than pretended through a command that cannot show them. The run writes the actual output into an HTML report, because a feature is not real until the artifact users install can demonstrate it.</p>
<p>That is the whole point of a battery. The local corpus is the product, so the product has to prove itself against the corpus, not against a mock that always agrees.</p>
<h2>One more footgun, while I was in there</h2>
<p>The refactor also exposed a package-fetch default that had quietly stopped making sense.</p>
<p>Before v1.3.0:</p>
<pre><code class="language-bash">cupertino fetch <span class="tok-operator">-</span><span class="tok-operator">-</span>source packages</code></pre>
<p>refreshed Swift Package Index metadata and star counts for 10,995 packages and <em>then</em> downloaded the curated archive closure the indexer actually consumes. The metadata refresh can take around four hours without a <code>GITHUB_TOKEN</code>. The archive stage took about 97 seconds for the 185-archive closure on a clean base. The four-hour stage was not the one doing the load-bearing work.</p>
<p>So the default flipped. Now <code>cupertino fetch --source packages</code> just downloads the archives. If you want the metadata refresh, you ask for it:</p>
<pre><code class="language-bash">cupertino fetch <span class="tok-operator">-</span><span class="tok-operator">-</span>source packages <span class="tok-operator">-</span><span class="tok-operator">-</span>refresh<span class="tok-operator">-</span>metadata</code></pre>
<p>The old <code>--skip-metadata</code> flag is gone, because skipping metadata is no longer the exception. It is the default.</p>
<h2>The boundary work continues</h2>
<p>v1.3.0 also documents three public Swift packages factored out of cupertino: <code>SwiftMCPCore</code>, <code>SwiftMCPClient</code>, and <code>CupertinoDataKit</code>.</p>
<p><code>CupertinoDataKit</code> is the one that belongs to this release’s theme. It is the public read contract: the documentation and sample-code read protocols plus the value types they return. cupertino re-exports it through <code>SearchModels</code>, so the in-repo names stay put while the boundary becomes something real and external rather than implied.</p>
<p>It is the same move as the database split, applied to code instead of files. Name the boundary out loud. Make the system derive from it. Stop asking every caller to carry the old map in its head.</p>
<h2>Upgrade</h2>
<pre><code class="language-bash">brew upgrade cupertino
cupertino setup</code></pre>
<p><code>setup</code> pulls the v1.3.0 per-source bundle. After that, search, read, and MCP serve all open the shipped databases read-only.</p>
<p>Here is the short version. Source independence has always been a code goal, and it is still a work in progress there. v1.3.0 is the release where it stops being only a code goal and starts being true of the artifact you actually install: eight databases, one registry, nothing in the read path that can rewrite them, and no unified <code>search.db</code> left to leak the old shape back in.</p>
<p>And the ninth database, whenever a new source earns one, is now a registry entry away instead of a refactor. That door is open now. I will walk through it soon.</p>
<p>That is why this one is The Big Refactor.</p>]]></content:encoded>
</item>
<item>
<title>The Morlocks Built SwiftUI</title>
<link>https://aleahim.com/blog/morlocks-built-swiftui/</link>
<guid isPermaLink="true">https://aleahim.com/blog/morlocks-built-swiftui/</guid>
<pubDate>Thu, 28 May 2026 00:00:00 +0000</pubDate>
<description>The cynical read is that Apple kept the real tools and handed us toys. But the same engineer built Core Animation in 2007 and co-built SwiftUI in 2019, and that one fact says the opposite. On why the foundation never left, and the debt that comes due the day the abstraction breaks.</description>
<content:encoded><![CDATA[<h1>The Morlocks Built SwiftUI</h1>
<p>Swift is for the Eloi.</p>
<p>That is the cynical reading. There is a ladder here, and every rung hides more than the last:</p>
<ul><li><p><strong>C</strong>: too dangerous. Pointers, manual memory, undefined behavior.</p></li><li><p><strong>Objective-C</strong>: too dynamic. A runtime that will happily message a deallocated object and crash you in production.</p></li><li><p><strong>Swift</strong>: safe by default, but the pointers are still there if you go looking.</p></li><li><p><strong>Swift 6 strict concurrency</strong>: the compiler now walks you across every actor boundary so you never have to reason about a data race yourself.</p></li><li><p><strong>SwiftUI</strong>: declarative. Stop thinking about the render loop.</p></li><li><p><strong>SwiftData</strong>: persistence without ever learning what an object graph is.</p></li></ul>
<p>Most developers shipping apps today have never typed <code>CALayer</code>, because SwiftUI just works. Until it doesn’t.</p>
<p>It is tempting to call this condescension: the toolmakers kept the real tools and handed us the Fisher-Price version. It is a satisfying story. It is also wrong.</p>
<h2>The man who built both ends</h2>
<p>Core Animation was created by an Apple engineer named John Harper, and it is older than the device that made it famous. He built it under the codename “LayerKit”, and Apple was already showing it running on the Mac at WWDC 2006, months before the iPhone was announced. The foundational patent, US 8,130,226 (“Framework for Graphics Animation and Compositing Operations”), traces its priority to an application filed on August 4, 2006, and its own text still notes that “Core Animation” is just the “LayerKit” of that earlier filing. It reached the public in 2007: in the first iPhone OS, and later that year on the Mac in OS X 10.5 Leopard.</p>
<p>He left for Facebook in 2014. He came back to Apple and co-built SwiftUI: at WWDC 2019 he was on stage in “Building Custom Views with SwiftUI”, explaining how the view system actually works.</p>
<p>The person who built the rendering foundation that shipped in 2007 was, twelve years later, on the team building the declarative layer that sits on top of it. The same hands on both ends of the abstraction.</p>
<p>A Morlock in the daylight.</p>
<p>SwiftUI is not what happens when the serious engineers go build silicon and leave the toy framework to someone junior. It is what happens when the people who understand the foundation best decide what you should usually not have to think about. The implicit animation system, the idea that a state change should animate by default, that you declare what the UI is and let the framework diff and interpolate, is not a dumbing-down of Core Animation. It is Core Animation’s own philosophy, made the default instead of the opt-in.</p>
<h2>The foundations never left</h2>
<p>Of all of Apple’s low-level frameworks, almost everything named <code>Core</code> is written in C and meant to be called from anywhere: Core Graphics, Core Text, Core Audio, Core Foundation. Close to the metal, language-agnostic, no object model in the way.</p>
<p>Two are different. Core Animation and Core Data are written in Objective-C, object-model frameworks built around a tree of objects rather than a bag of C functions: Core Animation with its layer tree, Core Data with its managed-object graph. They were designed object-first because the problems they solve are object-shaped.</p>
<p>Those two still do not have Swift-native replacements. SwiftUI does not replace Core Animation. It sits on top of it. Your <code>.animation()</code> modifier drives a <code>CAAnimation</code>. Your view hierarchy resolves to a layer tree and a render server that has been doing this job, more or less unchanged, since 2007. SwiftData does the same thing to Core Data. The friendly declarative surface is new. The foundation underneath is the same machine it always was.</p>
<p>Core Animation did not need to change, because it was right the first time. The layer tree, the split between the model tree and the presentation tree, the separate render process, the implicit transactions: all still there, all still doing the work. What SwiftUI replaced was not Core Animation. It was UIKit’s interface to Core Animation: the manual animation blocks, the animator APIs, the layout system that had accreted cruft for a decade. The part that aged badly was how we talked to the machine, not the machine.</p>
<h2>So what is the truth we can’t handle?</h2>
<p>It is not that we are too stupid for pointers. Plenty of us could learn the layer tree in a weekend.</p>
<p>The truth is quieter: the foundation is still there, you have just stopped being able to see it, and that is a debt, not a gift. It comes due on the specific day SwiftUI does something you cannot explain. A frame that drops. An animation that fights itself. A view that will not redraw when every rule you know says it should. On that day the abstraction has failed, and the only people who can fix it are the ones who know what is underneath: the layer tree, implicit versus explicit animation, transforms, compositing, the render server.</p>
<p>Apple did not give us Swift and SwiftUI because we are too soft for the real thing. They gave them to us because the people who built the real thing learned, over fifteen years, which parts you genuinely should not have to touch most days. That is not contempt. It is the opposite. It is mastery deciding the default.</p>
<p>A good enough abstraction does not insult you. It just makes you comfortable on the surface, and helpless the day the machinery underneath asks whether you were paying attention. The layer tree is a weekend’s study. Learn it before the day you need it, because the framework will not tell you, in advance, which kind of day today is.</p>]]></content:encoded>
</item>
<item>
<title>cupertino v1.2.0: lands the right Apple doc 9 times in 10</title>
<link>https://aleahim.com/blog/cupertino-v1-2-0-ironclad/</link>
<guid isPermaLink="true">https://aleahim.com/blog/cupertino-v1-2-0-ironclad/</guid>
<pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate>
<description>Search-quality release. AI coding assistants now land the right Apple documentation page on the first try 9 times out of 10, up from 5 out of 10 in v1.1.0. Cross-validated on three independent corpora, zero regressions. The architecture, and what it still gets wrong, in writing.</description>
<content:encoded><![CDATA[<h1>cupertino v1.2.0: lands the right Apple doc 9 times in 10</h1>
<p><em>Released 2026-05-21.</em></p>
<h2>What you actually get</h2>
<p>If you’ve ever asked an AI coding assistant about a Swift API and watched it confidently invent a method that doesn’t exist (or point you at a deprecated 2017 version), cupertino is what I built for that.</p>
<p>This release: AI assistants using cupertino now land the right Apple documentation page on the first try <strong>9 times out of 10</strong>. Was 5 out of 10 in v1.1.0.</p>
<p>A few concrete cases:</p>
<ul><li><p>Ask about <code>URLSession</code> async/await, get the modern API. Not the completion-handler page from 2017.</p></li><li><p>Ask “what replaced <code>NSURLConnection</code>”, go straight to <code>URLSession</code>.</p></li><li><p>Ask about <code>@Observable</code>, don’t get it fused with the older <code>ObservableObject</code>.</p></li><li><p>Compare Combine and AsyncSequence, get both pages, not a hallucinated synthesis.</p></li></ul>
<p>352,712 indexed Apple documentation pages, zero garbage rows on the merged corpus. Runs locally, no cloud, no subscription, free and open source.</p>
<p>Upgrade is one command:</p>
<pre><code class="language-bash">brew upgrade cupertino <span class="tok-operator">&amp;</span><span class="tok-operator">&amp;</span> cupertino setup</code></pre>
<p>The rest of this post is for the technically curious.</p>
<h2>The measurement</h2>
<p>Rank-1 accuracy on a 50-query canonical-lookup corpus (Swift stdlib types, Foundation, SwiftUI, UIKit, AppKit, Combine, framework roots) went from <strong>52% in v1.1.0</strong> to <strong>92% in v1.2.0</strong>. Cross-validated on two more corpora, zero regressions across 110 paired queries, statistically significant at McNemar p ≤ 1e-5 on the largest corpus.</p>
<table><thead><tr><th>Corpus</th><th style="text-align:right">v1.1.0</th><th style="text-align:right">v1.2.0</th></tr></thead><tbody><tr><td>Canonical lookup (50 queries)</td><td style="text-align:right">26/50 rank-1</td><td style="text-align:right">46/50 rank-1</td></tr><tr><td>Canonical lookup V2 (30 queries, no overlap with the first set)</td><td style="text-align:right">19/30 rank-1</td><td style="text-align:right">28/30 rank-1</td></tr><tr><td>Deprecation pairs (30 modern/legacy triples like <code>URLSession</code> vs <code>NSURLConnection</code>)</td><td style="text-align:right">27/30 prefers modern</td><td style="text-align:right">30/30 prefers modern</td></tr></tbody></table>
<p>Three independent samples, same direction, similar effect size. Not a fluke.</p>
<p>Methodology lives at <code>docs/design/search-quality-eval.md</code>; per-query rank movements live in <a href="https://codeberg.org/CupertinoHQ/cupertino/src/branch/main/docs/audits">docs/audits</a>. If a number is wrong in public, an audit markdown is wrong first.</p>
<h2>How it works</h2>
<p>cupertino is the retrieval layer of RAG, nothing else. The AI agent does the generation; cupertino just returns the right Apple page when one is asked for.</p>
<p>The substrate is SQLite FTS5 with field-weighted BM25, populated by an AST extraction pass at index time that pulls symbol signatures, generic constraints, platform availability, and deprecation markers into queryable columns. No embeddings, no vector database, no reranker.</p>
<p>Anthropic’s published RAG recipe (<a href="https://www.anthropic.com/news/contextual-retrieval">Contextual Retrieval</a>, September 2024) keeps a BM25 stage alongside the dense one as the non-optional floor. cupertino takes that floor and runs it without the rest, on a corpus class where that’s enough: identifier-heavy, terminology-precise, Apple API docs.</p>
<h2>What it still gets wrong</h2>
<table><thead><tr><th>Query class</th><th style="text-align:right">Rank-1</th><th>Status</th></tr></thead><tbody><tr><td>Prose / conceptual (“what’s the deal with…”)</td><td style="text-align:right">~27%</td><td>Open: candidate fix in design</td></tr><tr><td>Acronym / synonym (“KVO”, “GCD”, “CALayer”)</td><td style="text-align:right">~18%</td><td>Open: needs a new index pass</td></tr><tr><td>Querying by structural attribute (<code>actor type</code>, <code>initializer</code>, <code>Hashable conformance</code>)</td><td style="text-align:right">P@5 ~0.25</td><td>Open: search path doesn’t consult the symbol metadata table</td></tr></tbody></table>
<p>These were weak in v1.1.0 too. The next release is meant to move them. If it does, the live page will say so. If it doesn’t, the same page will say that, in the same row, without anyone editing copy.</p>
<p>This is not a feature roadmap. It is a list of things the tool does badly, published next to the things it does well.</p>
<h2>Upgrade</h2>
<pre><code class="language-bash">brew upgrade cupertino
cupertino setup</code></pre>
<p><code>cupertino setup</code> pulls the v1.2.0 database bundle from GitHub. If you had v1.1.0 installed locally, the migrator handles the schema bumps idempotently.</p>
<hr>
<p><em>Full release write-up: <a href="https://codeberg.org/CupertinoHQ/cupertino/src/branch/main/docs/release-writeup-v1.2.0.md">docs/release-writeup-v1.2.0.md</a>. Source for every measurement: <a href="https://codeberg.org/CupertinoHQ/cupertino/src/branch/main/docs/audits">docs/audits/</a>.</em></p>]]></content:encoded>
</item>
<item>
<title>Cupertino v1.1.0: my Apple docs index was 30% lies and I didn&apos;t know</title>
<link>https://aleahim.com/blog/cupertino-v1-1-0-poison-cleanup/</link>
<guid isPermaLink="true">https://aleahim.com/blog/cupertino-v1-1-0-poison-cleanup/</guid>
<pubDate>Thu, 14 May 2026 00:00:00 +0000</pubDate>
<description>A 3 a.m. SQL query found one row of HTTP poison in the search index. Pulling on that thread unraveled 13 ways Apple&apos;s CDN lies to crawlers, plus a release bug where the binary cheerfully downgraded every user to the previous bundle.</description>
<content:encoded><![CDATA[<h1>Cupertino v1.1.0: my Apple docs index was 30% lies and I didn’t know</h1>
<p><code>brew upgrade cupertino</code> this week and your local Apple docs database is 7,095 documents richer and zero rows are HTTP error pages pretending to be documentation. You will not notice. Searches feel the same. <code>cupertino doctor</code> was already green. The MCP server returns the same shape.</p>
<p>That gap, between “looks fine” and “actually fine”, is the whole post.</p>
<h2>TL;DR</h2>
<pre><code>brew update
brew upgrade cupertino
cupertino setup     # downloads cupertino-databases-v1.1.0.zip (685 MB)
cupertino doctor    # "Bundled version: 1.1.0", apple-docs: 284,518</code></pre>
<p>Done. The rest is how I found out the index was lying to me.</p>
<h2>The 3 a.m. query</h2>
<p>I was confident about v1.0.2. Audit clean. Bundle rebuilt. Counts verified. Shipped.</p>
<p>Then I ran one more query against the v1.0.2 <code>search.db</code>, the kind you only run when you can’t sleep:</p>
<pre><code class="language-sql"><span class="tok-keyword">SELECT</span> uri, title, abstract, source
<span class="tok-keyword">FROM</span> docs_metadata m <span class="tok-keyword">JOIN</span> docs_structured s <span class="tok-keyword">ON</span> s<span class="tok-operator">.</span>uri <span class="tok-operator">=</span> m<span class="tok-operator">.</span>uri
<span class="tok-keyword">WHERE</span> s<span class="tok-operator">.</span>title <span class="tok-keyword">IN</span> (<span class="tok-string">'Forbidden'</span>, <span class="tok-string">'403 Forbidden'</span>, <span class="tok-string">'404 Not Found'</span>,
                  <span class="tok-string">'Bad Gateway'</span>, <span class="tok-string">'502 Bad Gateway'</span>);</code></pre>
<p>One row came back.</p>
<pre><code>uri:      swift-org://docc_documentation
title:    Forbidden
abstract: You don't have permission to access this resource.
source:   swift-org</code></pre>
<p>That is not Apple’s DocC documentation. That is an HTTP 403 page indexed as canonical content for the DocC URL. Anyone querying cupertino for DocC would get a result with a correct URL, a plausible title, and a body of pure garbage. The worst kind of indexing bug: silent and authoritative.</p>
<p>The mistake was small and embarrassing. My poison scan walked <code>~/.cupertino/docs/</code>. The bundle is built from <strong>six</strong> sibling directories. I had been auditing one out of six.</p>
<pre><code>~/.cupertino/
  docs/             # 412k JSON files
  swift-evolution/  # 483 proposals
  swift-org/        # 196 pages    ← never scanned
  hig/              # 173 pages
  archive/          # 406 legacy guides
  packages/         # README files</code></pre>
<p>Sweeping the full tree turned up six poison files in <code>swift-org/</code>. Five of them had been shipping in every prior release. Nobody had noticed. But the six was the small problem. The big problem: my scanning was wrong, and I had no idea what else was hiding in 412,523 other files.</p>
<h2>The 13 ways Apple’s docs site poisons a crawler</h2>
<p>I rebuilt the audit as a matrix: every pattern, every source, every file. Each row below came from a real failure I had already hit. None of it is theoretical.</p>
<table><thead><tr><th>#</th><th>Pattern</th><th>What it actually is</th></tr></thead><tbody><tr><td>1</td><td>Title <code>403 Forbidden</code>, <code>502 Bad Gateway</code>, <code>404 Not Found</code>, <code>500</code>, <code>503</code>, <code>504</code>, <code>429</code></td><td>Apple’s CDN serves an HTTP error page at status 200, gets indexed as a doc page</td></tr><tr><td>2</td><td>Title bare phrase: <code>Forbidden</code>, <code>Bad Gateway</code></td><td>Same as 1 minus the status code</td></tr><tr><td>3</td><td>Body: <code>Please turn on JavaScript in your browser…</code></td><td>React SPA noscript fallback</td></tr><tr><td>4</td><td>Body: <code>[ Skip Navigation ](#app-main)# An unknown error occurred.</code></td><td>React app’s generic error sub-view rendered as the whole body</td></tr><tr><td>5</td><td>Body contains <code>openresty</code></td><td>Apple’s edge CDN error template</td></tr><tr><td>6</td><td>Body contains <code>&lt;center&gt;nginx</code></td><td>Different nginx error template</td></tr><tr><td>7</td><td>Body: <code>You don't have permission to access this resource</code></td><td>Forbidden as plain prose</td></tr><tr><td>8</td><td>Body: <code>cf-error-code</code> / <code>Cloudflare Ray ID</code></td><td>Cloudflare error page</td></tr><tr><td>9</td><td>Body: <code>Reference&amp;#32;&amp;#35;</code> / <code>Akamai</code> markers</td><td>Akamai edge error</td></tr><tr><td>10</td><td>Body: <code>&lt;Code&gt;NoSuchKey&lt;/Code&gt;</code> / <code>&lt;Code&gt;AccessDenied&lt;/Code&gt;</code></td><td>AWS S3 XML errors for missing assets</td></tr><tr><td>11</td><td>Zero-byte files</td><td>Truncated writes from killed crawls</td></tr><tr><td>12</td><td>Files under 200 bytes</td><td>Almost-empty responses</td></tr><tr><td>13</td><td><code>.json</code> files not starting with <code>{</code> or <code>[</code></td><td>Corrupt JSON, sometimes HTML in disguise</td></tr></tbody></table>
<p>Against every expectation, <code>docs/</code> scanned clean across all 13. Earlier passes had actually stuck. The poison was concentrated in the small directories I had been ignoring.</p>
<p>(Quick footnote, since it cost me an hour. Category 13 is a byte read on 412k files. My first pass shelled out <code>head -c 1</code> per file. Projected wall time: about five hours. Rewriting as a single Python <code>os.walk</code> that stats, opens, and reads one byte inside one process: 41 seconds. The per-file fork was the entire cost.)</p>
<h2>How Apple’s docs site actually poisons your index</h2>
<p>Three failure modes. In the order they fooled me.</p>
<p><strong>Mode 1: the CDN returns an HTML error page at status 200.</strong> Title is <code>403 Forbidden</code>. A five-line title check catches it. Done in PR #289 last release.</p>
<p><strong>Mode 2: the SPA shell.</strong> Apple’s docs site is a client-rendered React app. Fetch without JS and you get a <code>&lt;noscript&gt;</code> blurb that politely asks you to enable JavaScript. v1.0.2 had 1,327 of these. PR #291 filtered them at indexing time, but it was scoped to the apple-docs path, which is exactly why the <code>swift-org</code> <code>Forbidden</code> row strolled past.</p>
<p><strong>Mode 3: the one that ate my Tuesday.</strong> JavaScript runs. The React app boots. The React app’s data API returns 404 for the URL. The React app cheerfully renders its own 404 sub-view as the page body:</p>
<pre><code class="language-html"><span class="tok-operator">&lt;</span><span class="tok-keyword">h1</span><span class="tok-operator">&gt;</span>Apple Developer Documentation<span class="tok-operator">&lt;</span><span class="tok-operator">/</span><span class="tok-keyword">h1</span><span class="tok-operator">&gt;</span>
<span class="tok-operator">&lt;</span><span class="tok-keyword">a</span> <span class="tok-property">href</span><span class="tok-operator">=</span><span class="tok-string">"#app-main"</span><span class="tok-operator">&gt;</span>Skip Navigation<span class="tok-operator">&lt;</span><span class="tok-operator">/</span><span class="tok-keyword">a</span><span class="tok-operator">&gt;</span>
<span class="tok-operator">&lt;</span><span class="tok-keyword">h1</span><span class="tok-operator">&gt;</span>The page you're looking for can't be found.<span class="tok-operator">&lt;</span><span class="tok-operator">/</span><span class="tok-keyword">h1</span><span class="tok-operator">&gt;</span>
<span class="tok-operator">&lt;</span><span class="tok-keyword">input</span> <span class="tok-property">placeholder</span><span class="tok-operator">=</span><span class="tok-string">"Search developer.apple.com"</span><span class="tok-operator">&gt;</span></code></pre>
<p>Title is generic. Body is the app announcing its own failure. No error markers. Status 200. HTML perfectly clean. Every defense I had waved this through, because from the network’s point of view Apple successfully served me a page. The page just happens to be Apple’s React app politely informing the user that the page doesn’t exist.</p>
<h2>The fix that was missing</h2>
<p>PR #432 is small. A substring check on the rendered HTML, before the crawler writes anything:</p>
<pre><code class="language-swift"><span class="tok-keyword">public</span> <span class="tok-keyword">static</span> <span class="tok-keyword">func</span> <span class="tok-function">looksLikeJavaScriptFallback</span>(html<span class="tok-operator">:</span> <span class="tok-type">String</span>) <span class="tok-operator">-</span><span class="tok-operator">&gt;</span> <span class="tok-type">Bool</span> {
    <span class="tok-keyword">if</span> html<span class="tok-operator">.</span><span class="tok-function">contains</span>(<span class="tok-string">"The page you're looking for can't be found"</span>) { <span class="tok-keyword">return</span> <span class="tok-literal">true</span> }
    <span class="tok-keyword">if</span> html<span class="tok-operator">.</span><span class="tok-function">contains</span>(<span class="tok-string">"An unknown error occurred"</span>) { <span class="tok-keyword">return</span> <span class="tok-literal">true</span> }
    <span class="tok-keyword">return</span> <span class="tok-literal">false</span>
}</code></pre>
<p>Both phrases live exclusively inside Apple’s no-content sub-views. Real docs do not quote either sentence. The check runs after WebKit renders, so it sees the same bytes that would have hit disk. If it returns true, the crawler logs a skip and walks away.</p>
<p>Every skip writes one JSONL row to <code>&lt;output&gt;/.cupertino-rejected-urls.jsonl</code>:</p>
<pre><code class="language-json"><span class="tok-operator">{</span><span class="tok-property">"url"</span><span class="tok-operator">:</span><span class="tok-string">"https://developer.apple.com/documentation/.../fetch-apple"</span><span class="tok-operator">,</span>
 <span class="tok-property">"framework"</span><span class="tok-operator">:</span><span class="tok-string">"accountorganizationaldatasharing"</span><span class="tok-operator">,</span>
 <span class="tok-property">"reason"</span><span class="tok-operator">:</span><span class="tok-string">"js_fallback"</span><span class="tok-operator">,</span>
 <span class="tok-property">"timestamp"</span><span class="tok-operator">:</span><span class="tok-string">"2026-05-13T11:53:42Z"</span><span class="tok-operator">}</span></code></pre>
<p>This is the part I think generalizes. Once a poison file lands on disk, every downstream consumer has to re-discover that it’s poison. Catch it at write time and it is nobody’s problem ever again. The JSONL puts the visibility back: <code>jq</code> over it and you have a retry list, a recrawl set, or a bug-report folder.</p>
<p>Targeted recrawl of 51 known-poison URLs from v1.0.2: 48 rejection rows, all correctly tagged <code>js_fallback</code>, zero new files on disk. Pre-fix, the same set wrote 9 poison files in 90 seconds before I killed the run.</p>
<p>One false-positive risk worth naming. Two real Apple symbol pages quote “An unknown error occurred” in their Discussion section (<code>cfnetworkserviceerrorunknown</code>, <code>generalinternalretryableerror</code>). Both are already in the v1.1.0 bundle, but a future recrawl of those exact URLs would currently get rejected. Filed for v1.2: tighten the match so the phrase has to sit in a standalone position (h1, adjacent to Skip Navigation) rather than anywhere in the HTML.</p>
<h2>The reindex</h2>
<p><code>cupertino save --docs --clear</code> on the cleaned corpus:</p>
<pre><code>documents:           285,735       (84.7% of 351,249 source, rest deduped)
frameworks:          420
db size:             2.36 GB
defense trips:       0
errors:              0
warnings:            0
runtime:             9h 39m</code></pre>
<p>The line I care about is <code>defense trips: 0</code>. Across 351,249 indexer reads the indexer-side gate from PR #291 did not fire once. The test wasn’t whether the defense works. The test was whether anything was left for it to filter. Nothing was.</p>
<p>(The 65,514 gap between source files and indexed rows is real deduplication. Apple’s docs ship hash-suffixed variants of overloaded methods (<code>equatable_-3axv1_de895d6c.json</code>) that all point at the same logical URL.)</p>
<h2>The release saga, or: what <code>databaseVersion</code> actually does</h2>
<p>This is the part I’d flag to anyone shipping a similar tool.</p>
<p>v1.1.0 first shipped tagged correctly, binary rebuilt correctly, bundle uploaded to a parallel GitHub release. I ran <code>cupertino setup</code> to test the upgrade. It cheerfully downloaded the <strong>old</strong> v1.0.2 bundle.</p>
<p>The bug was one line in <code>Shared.Constants.swift</code>:</p>
<pre><code class="language-swift"><span class="tok-keyword">public</span> <span class="tok-keyword">static</span> <span class="tok-keyword">let</span> database<span class="tok-type">Version</span> <span class="tok-operator">=</span> <span class="tok-string">"1.0.2"</span></code></pre>
<p><code>cupertino setup</code> builds the download URL from this constant:</p>
<pre><code>{baseURL}/v{databaseVersion}/cupertino-databases-v{databaseVersion}.zip</code></pre>
<p>The release-tooling commit had explicitly not bumped it, with a confident note: “databaseVersion stays at 1.0.2 (no schema/content change).” Schema-wise that was true. Content-wise it was a confident lie. The corpus was the cleanest it had ever been, the bundle was genuinely new, and the binary was politely asking the CDN for last week’s poisoned one.</p>
<p>Fix: one-line change, fix branch, squash-merge, force-move the v1.1.0 tag to the new HEAD. <code>release.yml</code> rebuilds, codesigns, notarizes, and replaces the assets on tag push. Then a Homebrew bump for the new tarball SHA. About 25 minutes of clock time once I noticed. Approximately 25 minutes of dignity, also gone.</p>
<p><strong>The lesson:</strong> when the bundle changes, bump <code>databaseVersion</code>, even if the schema doesn’t. Otherwise your install path silently downgrades users to the previous bundle. No error. Doctor reports green. The binary asked for last week’s bundle, and last week’s bundle is exactly what it got.</p>
<h2>What I’d take with me</h2>
<p><strong>HTTP 200 doesn’t mean “valid content.”</strong> It means the CDN served some bytes. Documentation, error page, noscript fallback, React 404 sub-view: your problem to tell apart. Title checks miss two of three.</p>
<p><strong>Defense gates belong upstream.</strong> Catching poison at write time costs the same as catching it later, and prevents a class of bug instead of detecting one. Append an audit log so you keep the visibility.</p>
<p><strong>Bump <code>databaseVersion</code> when the bundle changes, even if the schema doesn’t.</strong> Then run <code>cupertino setup</code> end-to-end before the release is “shipped.” Trust the binary, not the release notes.</p>
<p>If you want to try it:</p>
<pre><code>brew tap cupertinohq/tap https://codeberg.org/CupertinoHQ/homebrew-tap.git
brew install cupertinohq/tap/cupertino    # or brew upgrade
cupertino setup
cupertino search "your favorite Apple symbol"</code></pre>
<p>Three defense PRs: #289, #291, #432. About 350 lines of Swift including tests. The cleanup itself was one long afternoon of careful greps.</p>]]></content:encoded>
</item>
<item>
<title>Why I built OpenAPIDoctor</title>
<link>https://aleahim.com/blog/why-i-built-openapi-doctor/</link>
<guid isPermaLink="true">https://aleahim.com/blog/why-i-built-openapi-doctor/</guid>
<pubDate>Thu, 14 May 2026 00:00:00 +0000</pubDate>
<description>OpenAPIKit&apos;s strict parser meets real-world specs. A typed diagnosis CLI with auto-repair, proven across 594 YAML files.</description>
<content:encoded><![CDATA[<h1>Why I built OpenAPIDoctor</h1>
<h2>“The given data was not valid YAML.”</h2>
<p>The first time I saw this error, the YAML was fine.</p>
<p>I had pasted ten kilobytes of perfectly formed OpenAPI into a generator, watched it crash, and squinted at a stack trace that very confidently told me my document was malformed. It wasn’t. The document was a well-indented, schema-valid spec that someone had hand-edited the week before. The actual problem was a single stray key on a <code>Tag</code> object, eleven levels down, which <code>OpenAPIKit</code> had quietly rejected and which <code>Yams</code> had then wrapped in a <code>DecodingError.dataCorrupted</code> carrying a debug message that had nothing to do with the actual cause. The real error lived in <code>Context.underlyingError</code>, three more layers in.</p>
<p>I lost an afternoon to that error. Then I lost a morning to it again, a week later, on a different spec. Then I built OpenAPIDoctor.</p>
<h2>What spec-first Swift gets you, and what it costs</h2>
<p>When you generate Swift code from an OpenAPI document, the document is no longer documentation that lags behind the code. It <em>is</em> the code. Apple’s <a href="https://github.com/apple/swift-openapi-generator"><code>swift-openapi-generator</code></a> reads the spec, runs it through <a href="https://github.com/mattpolzin/OpenAPIKit"><code>mattpolzin/OpenAPIKit</code></a>, and emits typed Swift clients and servers from what it finds. Spec parses cleanly, the generated code compiles. Spec has any structural issue OpenAPIKit doesn’t like, the generator either fails outright or emits code that won’t compile.</p>
<p>That contract is what makes spec-first Swift worth doing. It is also what makes the spec a load-bearing artifact that nobody can afford to hand-edit carelessly. Which, of course, everybody does.</p>
<p>OpenAPIKit is a strict parser by design. It does not silently round-trip unknown keys, because that would let bugs through into your generated client. The trouble is that real-world specs accumulate small violations the moment more than one person edits them, or the moment anyone copy-pastes a fragment from another tool. A short, honest taxonomy:</p>
<ul><li><p>A <code>Tag</code> object carrying a <code>slug:</code> field. Not part of the spec. Strict parser rejects it.</p></li><li><p>A <code>Parameter</code> with stray <code>email:</code> and <code>phone:</code> keys left behind by a half-finished refactor. Strict parser rejects them.</p></li><li><p><code>nullable: true</code> on a spec that has been migrated to OpenAPI 3.1, where the keyword is no longer valid. Strict parser rejects it.</p></li><li><p>A response body declaring <code>application/x-www-form-urlencoded</code>, which the Swift OpenAPI runtime cannot serve. Parser accepts it; the runtime explodes downstream and writes you a different kind of letter.</p></li></ul>
<p>Each violation is small. None is the sort of structural problem a code review catches. All of them surface as <code>InconsistencyError</code> from OpenAPIKit, which is the right thing for OpenAPIKit to do. The wrong thing is what tends to happen next: the caller gets a stack trace, fixes the first key by hand, runs the generator again, hits the next key, fixes that, runs again. On a single-file spec it is tedious. On a spec split across folders with shared schemas it is a half-day exercise that involves opening seventeen files and remembering which one you were in.</p>
<p>There are excellent OpenAPI linters in the JavaScript world. They are also not OpenAPIKit. If I lint with one tool and generate with another, I am postponing the failure, not preventing it. The validator had to be OpenAPIKit, end of debate.</p>
<h2>What I wanted</h2>
<p>The shape was small and specific:</p>
<ol><li><p>Validate using the exact parser the generator uses. No second source of truth for “valid.”</p></li><li><p>Surface every failure as a typed value, not a stack trace.</p></li><li><p>Auto-repair the mechanical violations, with an audit trail of what changed.</p></li><li><p>Stop on the violations that need human judgement, and say clearly which ones.</p></li><li><p>Handle multi-file specs cleanly. Everything serious lives in more than one file.</p></li><li><p>Exit with codes a shell pipeline can use.</p></li></ol>
<p>That list looks obvious in hindsight. It mostly is. The interesting part is what stands between you and it.</p>
<h2>What OpenAPIDoctor does</h2>
<p>OpenAPIDoctor is a Swift package and CLI built on <code>OpenAPIKit</code> and <code>Yams</code>. Three pieces, in increasing order of how much fun they were to write:</p>
<ul><li><p>A <strong>Validator</strong> that decodes the spec through OpenAPIKit and categorises every result.</p></li><li><p>A <strong>SpecLoader</strong> that handles multi-file specs.</p></li><li><p>A <strong>Repairer</strong> that runs validate, fix, revalidate in a loop until the spec is clean or it hits something it cannot touch.</p></li></ul>
<p>Every result comes back as a <code>DiagnosisKind</code>. Six cases. Only one is auto-repairable:</p>
<ul><li><p><code>.ok</code>: spec parses cleanly. Go home.</p></li><li><p><code>.vendorExtensionPrefix(codingPath, invalidKeys, subjectName)</code>: an extensible OpenAPI object carries keys that are not in the type’s documented set and do not start with <code>x-</code>. The diagnosis carries the exact coding path and the exact keys to strip. <code>isAutoRepairable == true</code>.</p></li><li><p><code>.inconsistency(codingPath, details, subjectName)</code>: every other <code>InconsistencyError</code> from OpenAPIKit. Unsupported <code>openapi:</code> version, malformed <code>$ref:</code> pointing at a component that does not exist. Needs a human.</p></li><li><p><code>.decodingError(codingPath, details)</code>: Foundation-level type mismatch or missing required field. No <code>title:</code> on <code>info:</code>. Wrong type for <code>version:</code>. Needs a human.</p></li><li><p><code>.fileError(details)</code>: the file is not there or cannot be read. Needs a different human.</p></li><li><p><code>.unknown(details)</code>: escape hatch. Nobody is happy about this case existing, but the alternative is lying.</p></li></ul>
<p>Having these as typed values changes how you write callers. You can pattern-match. You can ask <code>diagnosis.kind.isAutoRepairable</code> and route accordingly. From the CLI it looks like this:</p>
<pre><code class="language-bash"><span class="tok-property">$</span> openapi<span class="tok-operator">-</span>doctor my<span class="tok-operator">-</span>spec<span class="tok-operator">.</span>yaml
{<span class="tok-string">"status"</span><span class="tok-operator">:</span><span class="tok-string">"ok"</span>}</code></pre>
<pre><code class="language-bash"><span class="tok-property">$</span> openapi<span class="tok-operator">-</span>doctor stray<span class="tok-operator">-</span>tag<span class="tok-operator">-</span>keys<span class="tok-operator">.</span>yaml
{<span class="tok-string">"codingPath"</span><span class="tok-operator">:</span>[<span class="tok-string">"tags"</span>,<span class="tok-string">"Index 0"</span>],<span class="tok-string">"invalidKeys"</span><span class="tok-operator">:</span>[<span class="tok-string">"slug"</span>,<span class="tok-string">"timezone"</span>],<span class="tok-string">"kind"</span><span class="tok-operator">:</span><span class="tok-string">"vendor-extension-prefix"</span>,<span class="tok-string">"status"</span><span class="tok-operator">:</span><span class="tok-string">"inconsistency"</span>,<span class="tok-string">"subject"</span><span class="tok-operator">:</span><span class="tok-string">"Vendor Extension"</span>}</code></pre>
<p>One line of JSON on stdout, always. Exit code 0 for clean, 1 for “fixable, try <code>--fix</code>”, 2 for “you need to edit this, or the file is missing.” Nothing to parse, nothing to grep through.</p>
<h2>The seams of the abstraction</h2>
<p>If you have read this far, you might enjoy the parts that didn’t come out as cleanly as the API suggests.</p>
<p><strong>OpenAPIKit tells you which keys are invalid in English.</strong> The error message contains a sentence like <code>Invalid properties: [slug, timezone]</code>. There is no <code>[String]</code> accessor for those keys; they live inside the human-readable <code>details</code> string, sitting between two brackets, separated by commas. So OpenAPIDoctor parses them back out:</p>
<pre><code class="language-swift"><span class="tok-keyword">guard</span> <span class="tok-keyword">let</span> open<span class="tok-type">Range</span> <span class="tok-operator">=</span> details<span class="tok-operator">.</span><span class="tok-function">range</span>(of<span class="tok-operator">:</span> <span class="tok-string">"Invalid properties: ["</span>) <span class="tok-keyword">else</span> { <span class="tok-keyword">return</span> [] }
<span class="tok-keyword">let</span> after <span class="tok-operator">=</span> details[open<span class="tok-type">Range</span><span class="tok-operator">.</span>upper<span class="tok-type">Bound</span><span class="tok-operator">.</span><span class="tok-operator">.</span><span class="tok-operator">.</span>]
<span class="tok-keyword">guard</span> <span class="tok-keyword">let</span> close<span class="tok-type">Range</span> <span class="tok-operator">=</span> after<span class="tok-operator">.</span><span class="tok-function">range</span>(of<span class="tok-operator">:</span> <span class="tok-string">"]"</span>) <span class="tok-keyword">else</span> { <span class="tok-keyword">return</span> [] }
<span class="tok-keyword">return</span> after[<span class="tok-operator">.</span><span class="tok-operator">.</span><span class="tok-operator">&lt;</span>close<span class="tok-type">Range</span><span class="tok-operator">.</span>lower<span class="tok-type">Bound</span>]
    <span class="tok-operator">.</span><span class="tok-function">split</span>(separator<span class="tok-operator">:</span> <span class="tok-string">","</span>)
    <span class="tok-operator">.</span>map { $<span class="tok-number">0.</span><span class="tok-function">trimmingCharacters</span>(<span class="tok-keyword">in</span><span class="tok-operator">:</span> <span class="tok-operator">.</span>whitespaces) }
    <span class="tok-operator">.</span>filter { <span class="tok-operator">!</span>$<span class="tok-number">0.</span>is<span class="tok-type">Empty</span> }</code></pre>
<p>This is honest engineering, not clever engineering. If OpenAPIKit ever exposes the rejected keys as structured data, this disappears in one commit. Until then, the seam is here and visible.</p>
<p><strong>Same applies to the classification.</strong> A vendor-extension-prefix violation is identified by <code>details.contains("vendor extension property")</code>. A substring match against an error message. I am aware of how that looks. I am also aware that it has worked perfectly across 594 spec files and counting.</p>
<p><strong>OpenAPIKit reports array positions as strings.</strong> A coding path looks like <code>["tags", "Index 0", "slug"]</code>. The <code>"Index 0"</code> segment is a literal string, not an integer wrapped in <code>Any</code>. The walker that descends into the YAML to delete keys has to parse that string back into an <code>Int</code>:</p>
<pre><code class="language-swift"><span class="tok-keyword">static</span> <span class="tok-keyword">func</span> <span class="tok-function">parseIndex</span>(_ segment<span class="tok-operator">:</span> <span class="tok-type">String</span>) <span class="tok-operator">-</span><span class="tok-operator">&gt;</span> <span class="tok-type">Int</span><span class="tok-operator">?</span> {
    <span class="tok-keyword">guard</span> segment<span class="tok-operator">.</span><span class="tok-function">hasPrefix</span>(<span class="tok-string">"Index "</span>) <span class="tok-keyword">else</span> { <span class="tok-keyword">return</span> <span class="tok-literal">nil</span> }
    <span class="tok-keyword">return</span> <span class="tok-type">Int</span>(segment<span class="tok-operator">.</span><span class="tok-function">dropFirst</span>(<span class="tok-string">"Index "</span><span class="tok-operator">.</span>count))
}</code></pre>
<p>Small, dumb, real. If you have ever shipped a tool that runs against someone else’s API, you know that almost all of the work is small and dumb and real.</p>
<p><strong>The 3.0 / 3.1 sniff.</strong> OpenAPIKit splits its Document types: <code>OpenAPIKit.OpenAPI.Document</code> is 3.1, <code>OpenAPIKit30.OpenAPI.Document</code> is 3.0. If you point the 3.1 decoder at a 3.0 spec, you get the immortal error <code>"Failed to parse Document Version 3.0.x as one of OpenAPIKit's supported options."</code> So OpenAPIDoctor scans the raw YAML for the <code>openapi:</code> line, picks a decoder, and only parses once. Cheap, deterministic, no second source of truth for what version the spec is.</p>
<p><strong>Yams.dump doesn’t preserve key order.</strong> A comment in the Repairer is candid about it: <em>“preserves enough of the structure for our auto-repair use case (the spec is regenerated on every round anyway). Stable key ordering isn’t guaranteed.”</em> In the auto-repair flow this doesn’t matter, because the source of truth after a repair is the repaired YAML, and you read what was just written. In a workflow that diffs the result against the original, you’ll see ordering churn. That tradeoff is documented in the code, not hidden in a footnote.</p>
<h2>Repair, round by round</h2>
<p>The Repairer is my favourite part. It runs as a loop, capped at thirty rounds: validate, fix, revalidate, until either the spec is clean or it hits a diagnosis that is not auto-repairable. Each round removes exactly the keys OpenAPIKit reported, at exactly the coding path it reported them. No regex matches against the YAML, no guessing. A recursive walker descends to the target, deletes the listed keys from the dict at the leaf, and rebuilds the tree from the bottom up.</p>
<pre><code class="language-bash"><span class="tok-property">$</span> openapi<span class="tok-operator">-</span>doctor <span class="tok-operator">-</span><span class="tok-operator">-</span>fix multi<span class="tok-operator">-</span>stray<span class="tok-operator">.</span>yaml
<span class="tok-comment"># stderr: one JSON per round, streamed as it happens</span>
<span class="tok-comment"># {"removedKeys":["slug"],"round":1,...}</span>
<span class="tok-comment"># {"removedKeys":["timezone","region"],"round":2,...}</span>
<span class="tok-comment"># stdout: aggregate result</span>
{<span class="tok-string">"finalDiagnosis"</span><span class="tok-operator">:</span><span class="tok-string">"ok"</span>,<span class="tok-string">"roundsApplied"</span><span class="tok-operator">:</span><span class="tok-number">2</span>,<span class="tok-string">"status"</span><span class="tok-operator">:</span><span class="tok-string">"repaired"</span>,<span class="tok-string">"totalRemovedKeys"</span><span class="tok-operator">:</span><span class="tok-number">3</span>,<span class="tok-operator">.</span><span class="tok-operator">.</span><span class="tok-operator">.</span>}</code></pre>
<p>The 30-round cap is not arbitrary, but it is generous. The worst-shaped spec I have thrown at it cleared in five. The cap is there so that if the validator and the YAML ever disagree about shape and the loop starts looking like progress that isn’t, the tool stops with a real diagnosis instead of running forever.</p>
<p>For a destructive-free survey of how broken a spec is, there’s a dry-run mode that uses the same loop but discards the repaired YAML, keeping only the list of diagnoses found along the way:</p>
<pre><code class="language-bash"><span class="tok-property">$</span> openapi<span class="tok-operator">-</span>doctor <span class="tok-operator">-</span><span class="tok-operator">-</span>all my<span class="tok-operator">-</span>spec<span class="tok-operator">.</span>yaml <span class="tok-number">2</span><span class="tok-operator">&gt;</span><span class="tok-operator">/</span>tmp<span class="tok-operator">/</span>diagnoses<span class="tok-operator">.</span>jsonl
{<span class="tok-string">"diagnosesFound"</span><span class="tok-operator">:</span><span class="tok-number">3</span>,<span class="tok-string">"fixableDiagnoses"</span><span class="tok-operator">:</span><span class="tok-number">2</span>,<span class="tok-string">"status"</span><span class="tok-operator">:</span><span class="tok-string">"ok"</span>,<span class="tok-string">"terminalKind"</span><span class="tok-operator">:</span><span class="tok-string">"ok"</span>}</code></pre>
<p>Internally, <code>--all</code> and <code>--fix</code> share the strip implementation. The Validator’s <code>collectAll</code> reaches into <code>Repairer.stripKeys</code> directly. One source of truth for “how do you remove a vendor extension key from a YAML tree.” That is the kind of taste that doesn’t show up in the README, which is exactly why I am putting it here.</p>
<p><code>--output &lt;path&gt;</code> writes the repaired YAML to a side file and leaves the source untouched, useful for diffing before-and-after in CI. <code>--corpus &lt;dir&gt;</code> walks a directory of specs and reports per-file results with an aggregate summary, useful when you have inherited a folder of half-maintained specs and want a triage report before you touch anything.</p>
<h2>The 594-file corpus</h2>
<p>Multi-file specs are where the interesting bugs live. A serious API spans folders: a <code>finance</code> service that pulls <code>../core/parameters/id.yml</code>; an <code>analytics</code> service that pulls <code>../core/schemas/apiError.yml</code>; a <code>ledger</code> service that references both. <code>OpenAPIKit</code> expects one document. Without help, every external <code>$ref</code> would dangle.</p>
<p>The loader delegates to <a href="https://codeberg.org/Mihaela/Stitcher">Stitcher</a>, a separate library I published in November 2025 for exactly this problem. OpenAPIDoctor followed in May 2026 and took Stitcher as a dependency rather than re-implementing the resolver, which is the right division of labour: cross-file <code>$ref</code> resolution is a generic problem; OpenAPI validation is a specific one. Two libraries, two responsibilities, one composes the other.</p>
<p>To prove the two pieces actually agree about what “valid” means on the shape of spec I care about, the test suite drives an anonymised real-world corpus through the validator: <strong>594 YAML files across 22 services</strong>, of which <strong>146 contain at least one cross-folder <code>../</code> reference</strong>, all OpenAPI 3.0.3, the kind of layout you only get when humans have been editing for a year. The test asserts every entry point validates cleanly. When it does, I know Stitcher’s merged output and OpenAPIKit’s strict parser agree, in detail, across the whole tree, every commit.</p>
<p>You can flip Stitcher off with <code>--no-resolve-refs</code>, which loads the spec file as-is. Useful when the referenced files are not on disk, or when you want to isolate a problem to a single file and stop wondering if the resolver is the one lying to you.</p>
<h2>Where it fits</h2>
<p>OpenAPIDoctor is one stage in a longer code-generation pipeline I run. Specs come in. Validation happens. Generation happens. Swift compiles. There is nothing exotic about that flow except the requirement that every stage produces something the next stage can use, without surprises.</p>
<p>OpenAPIDoctor’s contract is the simplest in the chain. After it runs, the spec is either clean, or the caller knows exactly what is wrong with it, whether a tool can fix it, and where in the document the problem lives. That contract is what lets the rest of the pipeline be boring. Boring pipelines are the only kind that ship.</p>
<h2>Where to find it</h2>
<p><a href="https://codeberg.org/Mihaela/OpenAPIDoctor">codeberg.org/Mihaela/OpenAPIDoctor</a>, tagged at <code>1.0.0</code>. MIT licensed. Library for embedding in your own pipeline, CLI for the shell:</p>
<pre><code class="language-swift"><span class="tok-operator">.</span><span class="tok-function">package</span>(url<span class="tok-operator">:</span> <span class="tok-string">"https://codeberg.org/Mihaela/OpenAPIDoctor"</span>, from<span class="tok-operator">:</span> <span class="tok-string">"1.0.0"</span>),</code></pre>
<p>If you generate Swift from OpenAPI on anything bigger than a toy spec, you probably want this somewhere upstream of the generator. And if you ever see <em>“The given data was not valid YAML.”</em> in a stack trace, you now know two things: it is lying to you, and somebody already wrote the tool that tells you the truth.</p>]]></content:encoded>
</item>
<item>
<title>Cupertino v1.0.2: the duplicate that `LOWER(uri)` could not see</title>
<link>https://aleahim.com/blog/cupertino-v1-0-2/</link>
<guid isPermaLink="true">https://aleahim.com/blog/cupertino-v1-0-2/</guid>
<pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate>
<description>A third of the search index was the same Apple page indexed twice. The verification query that &quot;proved&quot; it wasn&apos;t could not have seen the bug. What v1.0.2 ships, and three takeaways from the audit.</description>
<content:encoded><![CDATA[<h1>Cupertino v1.0.2: the duplicate that <code>LOWER(uri)</code> could not see</h1>
<p>A few weeks ago I shipped cupertino v1.0.1 and closed the bug that was supposed to make it impossible for the same Apple page to appear twice in the local search index. The release notes carried a confident sentence: <em>“verified that the shipped v1.0.0 search.db has zero case-axis duplicate pairs.”</em> The query that verified it returned empty across 405,782 rows. Done.</p>
<p>Today I shipped v1.0.2, which removes 128,142 of those rows. They were duplicates. They had been duplicates the whole time. The verification query was structurally incapable of seeing them.</p>
<p>Here is what happened, what v1.0.2 contains, and the three things I took away from the audit.</p>
<h2>What v1.0.2 ships</h2>
<p>Concrete numbers first.</p>
<table><thead><tr><th></th><th style="text-align:right">v1.0.0 / v1.0.1 bundle</th><th style="text-align:right">v1.0.2 bundle</th><th style="text-align:right">delta</th></tr></thead><tbody><tr><td><code>docs_metadata</code> rows</td><td style="text-align:right">405,782</td><td style="text-align:right">277,640</td><td style="text-align:right">−128,142 (−31.6 %)</td></tr><tr><td>Frameworks</td><td style="text-align:right">422</td><td style="text-align:right">402</td><td style="text-align:right">−20</td></tr><tr><td><code>search.db</code> size</td><td style="text-align:right">~3.2 GB</td><td style="text-align:right">~2.4 GB</td><td style="text-align:right">−0.8 GB (−25 %)</td></tr><tr><td>Case-axis duplicate clusters</td><td style="text-align:right">61,257</td><td style="text-align:right">0</td><td style="text-align:right">gone</td></tr></tbody></table>
<p>So a third of the database was the same Apple page indexed twice, once under <code>documentation/Swift/...</code> and once under <code>documentation/swift/...</code>, with slightly different URI hashes so the rows did not look identical to a casual audit.</p>
<p>The fix is one canonicalization call inside <code>URLUtilities.filename(_:)</code>. The bundle is a full reindex of 404,729 JSON pages, 12 hours 36 minutes wall-clock on an M4 Max. The schema version bumps from 12 to 13 so old databases get rejected at open with a “rebuild required” message that points at <code>cupertino setup</code>, which downloads the new clean bundle in seconds.</p>
<p>Two other things ride along in this release:</p>
<ul><li><p><strong>Post-redirect URL canonicalization</strong> (#277), contributed by <a href="https://github.com/Vignesh-Thangamariappan">@Vignesh-Thangamariappan</a> in PR #278. When Apple issues a 301/302 redirect (e.g. <code>professional_video_applications</code> to <code>professional-video-applications</code>), the crawler now files the page under the post-redirect URL instead of the request URL. Five regression tests plus an integration test against a mock redirect server. Thank you, Vignesh. This was a subtle bug to find and a careful PR to author.</p></li><li><p><strong>HTML link augmentation for sparse-references pages</strong> (#203, PR #281). The crawler now unions HTML <code>&lt;a href&gt;</code> links with DocC’s JSON references on pages where the JSON link count is below a configurable threshold. Catches operator overloads, legacy numeric-ID symbols, REST sub-paths, and the handful of frameworks Apple serves only as HTML.</p></li></ul>
<p>Earlier groundwork on <code>#200</code> (case-axis dedup at index time) came from <a href="https://github.com/imwyvern">@imwyvern</a>, who supplied the crawler queue dedup, the dedup helper, and the URI alignment in PR #201. v1.0.2 builds on that work; the change here is finding the layer it missed.</p>
<h2>The bug</h2>
<p>Apple serves the same documentation page under multiple URL casings. The reference graph inside Apple’s DocC JSON is sometimes <code>documentation/Swift/withTaskGroup(of:returning:isolation:body:)</code> and sometimes the all-lowercase variant. Cupertino treats both as crawl targets. Pre-#283, the indexer would also treat them as separate pages, producing two rows in <code>docs_metadata</code>, two rows in <code>docs_fts</code>, two answers in the search results.</p>
<p>Wesley’s PR #201 in v1.0.1 fixed <code>URLUtilities.normalize(_:)</code> to lowercase the path, which was supposed to collapse these at queue time. It did, partially. There was a layer below the queue that #201 did not cover.</p>
<p><code>URLUtilities.filename(from:)</code> is the function that turns a URL into an on-disk filename and, by extension, into the URI key used in <code>docs_metadata</code>. For URLs containing parens or colons (so, any Swift method signature with named parameters), it appends an 8-hex disambiguator hash. That hash was computed from the <em>raw</em> URL string, before the function’s internal lowercasing. So the same Apple page, fetched from two case-variant URLs, produced two different hash suffixes:</p>
<pre><code>documentation/Swift/withTaskGroup(of:returning:isolation:body:)
  -&gt; documentation_swift_withtaskgroup_of_returning_isolation_body_b9f71892

documentation/swift/withtaskgroup(of:returning:isolation:body:)
  -&gt; documentation_swift_withtaskgroup_of_returning_isolation_body_969a5400</code></pre>
<p>Different filename, different URI, two rows in the database, two cards in your search results.</p>
<h2>The verification trap</h2>
<p>This is the part that bothers me.</p>
<p>The v1.0.1 closure of #200 was not careless. It included a verification query, run against the actual shipped DB, with the explicit goal of confirming the bug was gone:</p>
<pre><code class="language-sql"><span class="tok-keyword">SELECT</span> <span class="tok-function">LOWER</span>(uri), <span class="tok-function">COUNT</span>(<span class="tok-operator">*</span>)
  <span class="tok-keyword">FROM</span> docs_metadata
  <span class="tok-keyword">GROUP</span> <span class="tok-keyword">BY</span> <span class="tok-function">LOWER</span>(uri)
  <span class="tok-keyword">HAVING</span> <span class="tok-function">COUNT</span>(<span class="tok-operator">*</span>) <span class="tok-operator">&gt;</span> <span class="tok-number">1</span>;
<span class="tok-comment">-- (returned empty across 405,782 rows)</span></code></pre>
<p>Empty result, ship it. Felt rigorous. Was wrong.</p>
<p>The query is structurally incapable of finding the bug. The URIs in <code>docs_metadata</code> carry an 8-hex suffix that varies per source URL, so the case-variant pair <code>documentation_swift_withtaskgroup_..._b9f71892</code> and <code>documentation_swift_withtaskgroup_..._969a5400</code> lowercase to two <em>different</em> strings. <code>GROUP BY LOWER(uri)</code> cannot collapse them. It returns empty. It will always return empty no matter how broken the dedup is, because the column itself is wrong for the question.</p>
<p>The correct query is on <code>docs_structured.url</code>, which preserves the original Apple URL with its original casing:</p>
<pre><code class="language-sql"><span class="tok-keyword">SELECT</span> <span class="tok-string">'clusters'</span>, <span class="tok-function">COUNT</span>(<span class="tok-operator">*</span>) <span class="tok-keyword">FROM</span> (
  <span class="tok-keyword">SELECT</span> <span class="tok-function">LOWER</span>(url) <span class="tok-keyword">FROM</span> docs_structured
    <span class="tok-keyword">GROUP</span> <span class="tok-keyword">BY</span> <span class="tok-function">LOWER</span>(url) <span class="tok-keyword">HAVING</span> <span class="tok-function">COUNT</span>(<span class="tok-operator">*</span>) <span class="tok-operator">&gt;</span> <span class="tok-number">1</span>
);
<span class="tok-comment">-- clusters | 61257</span></code></pre>
<p>Two columns, two completely different stories. One verification you would write because the index key obviously <em>is</em> the URI. One verification you would write because that is the column a human would look at to spot the duplicate.</p>
<p>This generalizes. The verification query for a data correctness fix has to come from the column where the bug would <em>show up to a reader</em>, not the column the data is <em>keyed by</em>. They look interchangeable until you realize that the key has been transformed by code that may itself be where the bug lives. If you verify in the transformed space, you verify against the same transformation that may be broken.</p>
<p>A simpler version of the same lesson: ask yourself which column a customer would email you about, not which column the code uses as its primary key.</p>
<h2>What I learned</h2>
<p>Three takeaways, in order of how much they cost me.</p>
<h3>1. “Already fixed in vN” claims with a clean test are very compelling and very capable of being wrong.</h3>
<p>I do not think Wesley or I were reckless. The #200 fix was real and good. The verification query passed. The release notes were faithful to what the query said. Everyone behaved sensibly. The query was still the wrong query.</p>
<p>What I want to internalize: in any audit that asserts “we checked the data and it’s clean”, the question is not just “what did the query return”, but “if the bug were present, would this exact query show it”. For data bugs, that means: which column does the bug live in, and is that the column the verifier looked at.</p>
<p>For this PR, I added a docs entry that explicitly names the wrong query and the right query, side by side, as a permanent footnote. Anyone reading the v1.0.0 release notes in five years can follow the link to see why the empty result was misleading.</p>
<h3>2. The migration you built and then deleted is still good engineering.</h3>
<p>I spent a real amount of time on an in-place v12 to v13 migration. The plan was: walk <code>docs_metadata</code>, recompute each URI through the post-#283 helper, group by canonical URI, pick the survivor by latest <code>last_crawled</code>, delete losers, rename survivors across <code>docs_metadata</code> + <code>docs_structured</code> + <code>docs_fts</code> + <code>doc_symbols</code> + <code>doc_imports</code> + <code>doc_code_examples</code>, all in one transaction with FK off. 270 lines of code, nine unit tests, three integration tests against a v12 fixture DB. The unit tests passed. The integration test passed on a 10-row fixture in milliseconds.</p>
<p>Then I ran it against my real 405k-row search.db. First attempt: over an hour, never committed, journal at 314 MB, killed it. Second attempt with prepared-statement reuse: marginally faster, journal at 415 MB at the 1h mark, killed it. Third attempt with <code>PRAGMA journal_mode = MEMORY</code> plus <code>synchronous = OFF</code>: another hour, never observably finished.</p>
<p>Meanwhile, <code>cupertino setup</code> downloads the pre-built v1.0.2 bundle in about thirty seconds. That is the path. Always was. The architecture already had the answer; the migration was solving a problem that did not need to be solved.</p>
<p>I deleted the migration before the tag. All 270 lines, all twelve tests. The CHANGELOG notes the draft existed and was removed, because that fact matters for anyone wondering why the codebase has a v12-to-v13 throw block but no migration body to call. The bug fix lives entirely in <code>URLUtilities.filename(_:)</code>. The data fix lives entirely in the new bundle. The schema bump is a one-line <code>Int32</code> that forces the upgrade path.</p>
<p>What I learned: write the migration code if you have to think through the algorithm, but ship the path that the architecture wants you to ship. The investment was not wasted. It was the thing that forced me to fully understand what the dedup boundary actually looks like across all six tables that hold URIs.</p>
<h3>3. Reindexing 404,729 pages costs you an evening of wall-clock and removes one third of your data.</h3>
<p>The bundle reindex took 12 hours 36 minutes on an M4 Max. I watched a stale-build SwiftPM lock kill three attempts before the fourth one ran clean. I babysat the cupertino-rel binary signing through Apple’s notarization queue. I watched the search.db file grow from 425 MB to 2.41 GB in 27-second progress increments, all night.</p>
<p>The first time I ran <code>cupertino search "withTaskGroup"</code> against the new bundle it returned a single result, lowercase, canonical. The same query against the v1.0.1 bundle returns two: one capital S, one lowercase. That moment is worth twelve hours.</p>
<h2>What also changed in the cupertino skill</h2>
<p>The cupertino skill (the bundle of instructions that ships in <code>skills/cupertino/SKILL.md</code> for LLMs that integrate with cupertino) got a “Two rules” block at the top of its body. The previous version had query strategy + verification guidance scattered throughout. The new version makes the two things that matter the first thing the calling model sees:</p>
<ol><li><p>Any Apple-related question goes through <code>cupertino search</code> first. Do not reach for training-data memory of Apple APIs.</p></li><li><p>After drafting an answer, verify that the code exists on Apple <strong>and</strong> that the pattern is appropriate. Existence catches invented method names. Appropriateness catches deprecated symbols that cupertino indexes alongside current ones (e.g. <code>UIWebView</code> when <code>WKWebView</code> exists).</p></li></ol>
<p>The token cost of doing this is small (about 5 % overhead for cite-as-you-go, a few hundred tokens per verification re-search) and the failure mode it prevents is the most common LLM Apple-API hallucination class.</p>
<h2>Upgrade</h2>
<p>If you already have cupertino installed:</p>
<pre><code class="language-bash">brew update <span class="tok-operator">&amp;</span><span class="tok-operator">&amp;</span> brew upgrade cupertinohq<span class="tok-operator">/</span>tap<span class="tok-operator">/</span>cupertino
cupertino setup</code></pre>
<p>The first command pulls the new universal binary (signed and notarized via the GitHub Actions release workflow). The second downloads the new database bundle (669 MB compressed, 3.3 GB on disk). Your old v12 search.db will be rejected at open with a “rebuild required” message and replaced.</p>
<p>If you are installing for the first time:</p>
<pre><code class="language-bash">bash <span class="tok-operator">&lt;</span>(curl <span class="tok-operator">-</span>sSL https<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>codeberg<span class="tok-operator">.</span>org<span class="tok-operator">/</span>CupertinoHQ<span class="tok-operator">/</span>cupertino<span class="tok-operator">/</span>raw<span class="tok-operator">/</span>branch<span class="tok-operator">/</span>ma<span class="tok-keyword">in</span><span class="tok-operator">/</span>install<span class="tok-operator">.</span>sh)</code></pre>
<h2>Thanks</h2>
<p>To <a href="https://github.com/Vignesh-Thangamariappan">@Vignesh-Thangamariappan</a> for PR #278 (post-redirect canonicalization, #277). To <a href="https://github.com/imwyvern">@imwyvern</a> for the upstream #200 work that v1.0.2 builds on. To everyone who filed issues against the v1.0.0 corpus and made #283 surface in the first place.</p>
<p>Cupertino is free, open-source, local-first, and built by one person on her own time.</p>
<p><em>Built by <a href="https://aleahim.com">Mihaela</a>. I also work on <a href="https://codeweaver.info/">Codeweaver</a>.</em></p>]]></content:encoded>
</item>
<item>
<title>Cupertino v1.0.0 &quot;First Light&quot;</title>
<link>https://aleahim.com/blog/cupertino-first-light/</link>
<guid isPermaLink="true">https://aleahim.com/blog/cupertino-first-light/</guid>
<pubDate>Tue, 05 May 2026 00:00:00 +0000</pubDate>
<description>First release stable across crawl, index, rank, serve, and distribute. Search that finally returns the right answer, one bundle, MCP spec 2025-11-25.</description>
<content:encoded><![CDATA[<h1>Cupertino v1.0.0 “First Light”</h1>
<p><em>The first release I’d actually call stable. Apple’s documentation, search that doesn’t suck, and an MCP server that an AI agent can read from before it answers.</em></p>
<hr>
<p>If you’ve ever asked an LLM “what does Swift’s <code>Task</code> do?” and got back something about Mach kernel context switches, you’ve met the problem cupertino solves. Models hallucinate Apple APIs because their training data is stale, partial, and weighted toward whatever prose happened to mention a symbol — not toward Apple’s actual documentation. The fix isn’t a bigger model. The fix is making the documentation <strong>available to the model</strong> at inference time, ranked correctly, served over a protocol the model already speaks.</p>
<p>That’s cupertino. <strong>v1.0.0 “First Light”</strong> is the first release I’m willing to call stable across the whole pipeline: crawl, index, rank, serve, distribute. Before today every previous release had at least one of those broken. Today none of them are.</p>
<h2>The headline: search now returns the right answer</h2>
<p>Run this against an installed v1.0:</p>
<pre><code class="language-bash"><span class="tok-property">$</span> cupertino search Task <span class="tok-operator">-</span><span class="tok-operator">-</span>limit <span class="tok-number">3</span>
Searched<span class="tok-operator">:</span> packages, apple<span class="tok-operator">-</span>docs, swift<span class="tok-operator">-</span>evolution

[<span class="tok-number">1</span>] Task <span class="tok-operator">|</span> Apple Developer Documentation  •  source<span class="tok-operator">:</span> apple<span class="tok-operator">-</span>docs  •  score<span class="tok-operator">:</span> <span class="tok-number">0.0492</span>
    apple<span class="tok-operator">-</span>docs<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>swift<span class="tok-operator">/</span>documentation_swift_task
    A unit of asynchronous work<span class="tok-operator">.</span>

[<span class="tok-number">2</span>] task <span class="tok-operator">|</span> Apple Developer Documentation  •  source<span class="tok-operator">:</span> apple<span class="tok-operator">-</span>docs  •  score<span class="tok-operator">:</span> <span class="tok-number">0.0484</span>
    apple<span class="tok-operator">-</span>docs<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>kernel<span class="tok-operator">/</span>documentation_kernel_kernel_resource_sizes_data_t_<span class="tok-number">1586795</span><span class="tok-operator">-</span>task

[<span class="tok-number">3</span>] task  •  source<span class="tok-operator">:</span> apple<span class="tok-operator">-</span>docs  •  score<span class="tok-operator">:</span> <span class="tok-number">0.0476</span>
    apple<span class="tok-operator">-</span>docs<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>foundation<span class="tok-operator">/</span>documentation_foundation_urlprotocol_task</code></pre>
<p>That’s the canonical Swift <code>Task</code> struct at #1, where it belongs. The same binary against the same database before today’s changes returned a Mach kernel C function as the top result. Cupertino’s pre-1.0 ranker — and frankly every documentation search I’ve seen, including Apple’s own — was wrong on common type names. Type “Task”, get a kernel essay. Type “View”, get a DeviceManagement payload schema. Type “Result”, get a Vision associated type.</p>
<p>Multiple compounding bugs caused this. Fixing them was most of what this release is about.</p>
<h3>Reciprocal rank fusion that actually understands the query</h3>
<p>Cupertino’s <code>cupertino search</code> (no <code>--source</code> flag) fans out across every available corpus in parallel — apple-docs, the Apple Archive, HIG, swift-evolution, swift.org, the Swift Book, the curated packages corpus, the Apple sample-code corpus — and fuses the per-source rankings via <strong>reciprocal rank fusion</strong> (Cormack/Clarke/Büttcher, 2009). The math is <code>1 / (k + rank)</code> per source, summed across sources. RRF is the right shape for prose-vague queries where any source might hold the answer (“how do I cancel an async operation”). It is the <em>wrong</em> shape for symbol queries where the answer is unambiguously in apple-docs.</p>
<p>The pre-1.0 default was uniform RRF. Every source’s rank-1 contributed <code>1/61 ≈ 0.0164</code>. Three sources tied at 0.0164 on a query like <code>Task</code>, the alphabetic tiebreak buried apple-docs, and you got “Common Tasks in OS X” from the Apple Archive instead of the Swift <code>Task</code> struct.</p>
<p>v1.0 splits the routing decision by query shape. <strong>Symbol-shaped queries</strong> — single token, ASCII identifier, leading uppercase, the kind of thing a Swift developer types on muscle memory — only fan out across <code>apple-docs + swift-evolution + packages</code>. The four prose-shaped sources don’t even participate in the fusion. Plus an authority weight on RRF: apple-docs gets <code>3.0</code>, swift-evolution and packages <code>1.5</code>, swift.org and the Swift Book <code>1.0</code>, the Apple Archive and HIG <code>0.5</code>. apple-docs’s rank-1 fuses to <code>3.0/61 ≈ 0.0492</code> and beats peer rank-1’s <code>1.0/61 ≈ 0.0164</code> without alphabetic tiebreaks deciding anything.</p>
<p>The <code>Searched:</code> line in the output above is the routing decision made visible. It listed only <code>packages, apple-docs, swift-evolution</code> because <code>Task</code> looks like a symbol. Type <code>cupertino search "swiftui state management"</code> and you’ll see <code>Searched: hig, samples, apple-docs, swift-evolution, packages</code> — five sources, broader fan-out, because the query is prose.</p>
<h3>Inside apple-docs: BM25F, AST symbols, and a ladder of post-rank heuristics</h3>
<p>Per-source ranking inside apple-docs is <strong>field-weighted BM25 (BM25F</strong>, Robertson/Zaragoza/Taylor 2004) over an 8-column FTS5 index — <code>uri</code>, <code>source</code>, <code>framework</code>, <code>language</code>, <code>title</code>, <code>content</code>, <code>summary</code>, <code>symbols</code>. Title gets weight 10×, AST-derived symbols 5×, summary 3×, framework 2×, body and metadata 1×. The <code>symbols</code> column is new in v1.0: it’s populated by a Swift AST extractor that runs over both code blocks AND declaration lines on every page. So a query like <code>Observable</code> ranks the SwiftUI macro page above prose mentions of the word “observable” in unrelated articles.</p>
<p>On top of raw BM25F, there’s a ladder of post-rank heuristics that handle pathological cases the indexer alone can’t fix:</p>
<ul><li><p><strong>Title-suffix awareness.</strong> Apple writes <code>&lt;canonical type name&gt; | Apple Developer Documentation</code> only on the parent landing page of a type. Sub-symbols (properties, methods, nested types) get clean titles. The suffixed page’s raw BM25 is <em>worse</em> because the suffix dilutes title term-frequency over field length, so a 50× boost is applied to suffixed exact-title matches, while clean-titled siblings keep the existing 20×. Flips canonical-vs-sub-symbol order without touching BM25F.</p></li><li><p><strong>Exact-title peer tiebreak.</strong> <code>Result</code> matches three apple-docs pages exactly: Swift’s <code>Result</code> enum, Vision’s <code>VisionRequest.Result</code> associated type, and Installer JS’s runtime <code>Result</code>. All three get the same 50×, so BM25F decides — and BM25F has no opinion. v1.0 adds two orthogonal signals: URI simplicity (<code>documentation_FRAMEWORK_QUERY</code> is the framework’s top-level type page; anything deeper is a sub-symbol) and a small framework-authority map (<code>swift</code> 0.5, <code>swiftui</code> 0.7, <code>foundation</code> 0.7; <code>installer_js</code> and <code>webkitjs</code> mildly demoted). Fires only inside the exact-title branch — does not crowd out framework-specific symbol queries (<code>VisionRequest</code> still resolves to <code>vision/VisionRequest</code>).</p></li><li><p><strong>Force-include canonical type pages past fetchLimit.</strong> Some canonical pages get buried by raw BM25 length-normalization — Foundation <code>URL</code> lands at raw BM25 position 1017, Swift <code>Identifiable</code> at 2577, Foundation <code>Data</code> past 3000 — well past the 1000-row over-fetch the post-rank multipliers operate on. v1.0 hand-fetches <code>apple-docs://FRAMEWORK/documentation_FRAMEWORK_QUERY</code> directly by URI for the top-tier frameworks (swift, swiftui, foundation) and force-prepends them. O(1) per probe via <code>docs_metadata</code>’s primary key.</p></li></ul>
<p>The combined effect: against the v1.0 corpus (405,782 documents across 422 frameworks), <strong>34 of 35 canonical type queries land their canonical apple-docs page at fused #1</strong>. The holdout, <code>Stack</code>, doesn’t have a canonical Swift / SwiftUI / Foundation type — TVML’s Stack is the right answer, and that’s what comes back.</p>
<h3>The packages corpus gets the same treatment</h3>
<p>Same BM25F-buries-the-canonical-repo problem on <code>package_files_fts</code>. Type <code>vapor middleware</code> and pre-1.0 you got <code>apple/swift-openapi-generator</code> because that repo has long prose mentioning Vapor and middleware in the same article; the actual <code>vapor/vapor</code> repo’s README ranked second. Type <code>swift testing</code> and you got <code>pointfreeco/swift-dependencies</code> because that package’s testing article has more keyword density than the actual <code>swiftlang/swift-testing</code> repo.</p>
<p>The fix mirrors the apple-docs canonical force-include. When the query tokens — joined with dashes for multi-word queries, or as single tokens for single-word queries — match an indexed <code>repo</code> name exactly, force-fetch that repo’s top BM25 file and prepend it. Two priority tiers: dashed forms (“swift-testing”) beat single tokens (“swift”) so a multi-word query that has a dashed canonical doesn’t <em>also</em> pull in the broader-corpus repo via the bare token. <code>swift testing</code> resolves to <code>swiftlang/swift-testing</code>, not <code>swiftlang/swift</code>. <code>vapor middleware</code> resolves to <code>vapor/vapor</code> because no <code>vapor-middleware</code> repo exists; the single-token fallback finds <code>vapor</code>.</p>
<p>The probe is bounded — it only fires when there’s an indexed canonical match, so queries that genuinely don’t have a canonical repo (<code>alamofire request</code> against a corpus where Alamofire isn’t indexed; <code>actor isolation</code> which isn’t a repo name at all) fall through to plain BM25 with no overhead.</p>
<h3>A SQL gotcha that cost us ~3 seconds per query</h3>
<p>While testing the canonical force-include, I shipped it with the wrong SQL shape:</p>
<pre><code class="language-sql"><span class="tok-keyword">SELECT</span> <span class="tok-operator">.</span><span class="tok-operator">.</span><span class="tok-operator">.</span> <span class="tok-keyword">FROM</span> docs_fts f
<span class="tok-keyword">JOIN</span> docs_metadata m <span class="tok-keyword">ON</span> f<span class="tok-operator">.</span>uri <span class="tok-operator">=</span> m<span class="tok-operator">.</span>uri
<span class="tok-keyword">WHERE</span> f<span class="tok-operator">.</span>uri <span class="tok-operator">=</span> <span class="tok-operator">?</span>
<span class="tok-keyword">LIMIT</span> <span class="tok-number">1</span>;</code></pre>
<p><code>docs_fts</code> is an FTS5 virtual table. Its <code>uri</code> column is <em>not</em> a queryable index — FTS5 stores columns inside the inverted index, accessible only through <code>MATCH</code>. SQLite’s planner picked <code>SCAN VIRTUAL TABLE INDEX 0</code> and walked the entire FTS5 index for every probe. About <strong>3.2 seconds per probe</strong> on the 3.4 GB v1.0 search.db. Three probes per search call = 10 seconds of pure overhead before the actual query ran.</p>
<p>The fix is a one-line SQL rewrite — query <code>docs_metadata</code> (whose <code>uri</code> is <code>TEXT PRIMARY KEY</code>) directly, pull title and summary out of <code>json_data</code> via <code>json_extract($.title, $.abstract)</code>, never touch the FTS5 virtual table for this lookup. <strong>5 milliseconds per probe.</strong> Single-process search wall time on the 3.4 GB corpus dropped from ~18 seconds to ~4 seconds.</p>
<p>The same bug was lurking in the older <code>fetchFrameworkRoot</code> helper that’s been there since #81, untimed and presumably contributing to user-visible latency for months. Both are fixed in v1.0.</p>
<h3>Concurrent-search lock contention</h3>
<p>Every <code>Search.Index</code> initialization used to issue an unconditional <code>PRAGMA user_version = N</code> write — even when the version was already correct. SQLite is single-writer. Two parallel <code>cupertino search</code> invocations contended on the open-time write lock, and one would fail with <code>database is locked</code> because SQLite’s default <code>busy_timeout</code> is <code>0</code>.</p>
<p>Fixed two ways: read-then-write (skip the PRAGMA when the version already matches, so the steady-state path does zero writes), and <code>sqlite3_busy_timeout(db, 5000)</code> right after open so any future write contention degrades to a wait-then-succeed instead of an immediate failure. Cupertino’s MCP server can now handle concurrent requests against the same database without flaking.</p>
<h2>Distribution: one bundle, not two</h2>
<p>Pre-1.0 the deployment shape was: <code>search.db</code> and <code>samples.db</code> shipped on <code>mihaelamj/cupertino-docs</code>, while the curated <code>packages.db</code> was scoped for a separate <code>mihaelamj/cupertino-packages</code> companion repo. The split made sense when the packages corpus was a separate-cadence thing. It turned out it isn’t — the same crawl produces all three, on the same schedule, with the same release cycle. Two repos meant two release tags per cut, two sequential downloads on <code>cupertino setup</code>, soft-fail logic when one release lagged the other, and two URL constants in <code>Shared.Constants</code> to keep in sync.</p>
<p>v1.0 collapses everything to one bundle: <code>cupertino-databases-v1.0.0.zip</code> on <code>mihaelamj/cupertino-docs</code>, ~833 MB compressed, contains all three databases. <code>cupertino setup</code> does <strong>one</strong> download + extract + version stamp. The companion repo is deleted; deleting it was the last action before tagging.</p>
<p>The release tooling caught up too. <code>cupertino-rel databases</code> now bundles all three databases into the single zip and uploads to the docs repo. Hard-fails if <code>packages.db</code> is missing under <code>--base-dir</code> unless <code>--allow-missing-packages</code> is passed (lets a release runner publish a partial bundle in genuinely time-sensitive cases without making it the default).</p>
<h2>The schema bump (BREAKING)</h2>
<p>Schema went from 10 → 12 across two intermediate steps. v11 added <code>kind</code> and <code>symbols</code> columns to <code>docs_metadata</code>. v12 added the <code>symbols</code> column to <code>docs_fts</code>. FTS5 doesn’t support <code>ALTER TABLE ADD COLUMN</code>, so v12 is a hard break — existing v10/v11 databases throw on open with a clear error message:</p>
<pre><code>Database schema version 10 requires migration to version 12.
This is a breaking change that adds AST-derived symbols to the FTS index.
Please delete the database and run 'cupertino save' to rebuild:
rm ~/.cupertino/search.db &amp;&amp; cupertino save</code></pre>
<p>Most users won’t see this — they’ll just <code>cupertino setup</code> once and get the v1.0 bundle directly, with <code>setup</code> automatically backing up any pre-existing databases as <code>.backup-&lt;version&gt;-&lt;iso8601&gt;</code> siblings (<code>#249</code>) before extraction overwrites them. The rollback is one rename.</p>
<p>Users who built their own search.db with <code>cupertino save</code> against an older binary need to rebuild, but the docs corpus on disk is unchanged — the same <code>~/.cupertino/docs/</code> produces a v12 search.db without re-crawling.</p>
<h2>MCP protocol upgrade — 2025-11-25</h2>
<p>The Model Context Protocol moved to spec <code>2025-11-25</code>. Cupertino now negotiates that as its preferred version and retains backward-compat for <code>2025-06-18</code> and <code>2024-11-05</code> so older clients keep working through three negotiation hops. The new spec adds <code>Icon</code> to <code>Implementation</code>, so cupertino’s MCP <code>serve</code> advertises a 64×64 PNG via a <code>data:image/png;base64,...</code> URI (embedded as a Swift literal in the binary, no external bundle).</p>
<h2>Crawler hardening</h2>
<p>The crawler took a lot of fire across this release.</p>
<ul><li><p><strong>Per-URL JSON-then-WebView fallback.</strong> <code>cupertino fetch --type docs</code> does one pass through the queue, trying Apple’s JSON API first and falling back to WKWebView when a page has no JSON endpoint. Single-pass, full coverage. (The fallback was already there; the previous “two-pass” orchestration was running the same crawler twice and is now removed.)</p></li><li><p><strong>Auto-resume by default.</strong> If <code>metadata.json</code> has an active session matching the start URL, <code>cupertino fetch</code> picks it up. The previous <code>--resume</code> flag was a log-message switch and is gone.</p></li><li><p><strong>Crash-safe metadata writes.</strong> <code>JSONCoding.encode(_:to:)</code> writes with <code>.atomic</code> (temp + rename), so a kill mid-save can never leave <code>metadata.json</code> corrupt. Mid-save corruption was the one failure mode that could make a multi-day crawl unresumable.</p></li><li><p><strong>Default page cap raised 15,000 → 1,000,000.</strong> Effectively uncapped for full Apple-corpus crawls (~50–80k pages). The old 15k default would silently truncate at ~15–30% coverage.</p></li><li><p><strong>URL canonicalization (case axis).</strong> <code>URLUtilities.normalize</code> now lowercases the URL path. Apple’s docs server is case-insensitive, but the crawler used to treat <code>/documentation/Cinematic/CNAssetInfo</code> and <code>/documentation/cinematic/cnassetinfo</code> as different URLs. Queue inflated ~3× with case duplicates (62% of queue entries on the April 2026 crawl). Fragment + query stripping unchanged.</p></li><li><p><strong>Filename-length cap, queue dedup at enqueue time, autorelease pool around per-page work, full DocC <code>references</code> walk</strong> — the unglamorous fixes that let a multi-day crawl finish without RSS leaks, dropped pages, or queue explosion.</p></li></ul>
<p>A reproducible recrawl pipeline (<code>scripts/recrawl.sh</code>) wraps the whole thing in 10 phases with named markers so you can tail-follow the log and see “phase 5/10 starting” in real time.</p>
<h2>Architecture: the four-package CLI lift</h2>
<p>This is more for contributors than users, but worth flagging. Logic that powered four CLI commands — <code>setup</code>, <code>doctor</code>, <code>save</code>, <code>fetch</code> — moved out of <code>Sources/CLI/Commands/*</code> into four new SPM packages:</p>
<ul><li><p><strong><code>Distribution</code></strong> — the download + extract + version-stamp pipeline (<code>SetupService</code>, <code>ArtifactDownloader</code>, <code>ArtifactExtractor</code>, <code>InstalledVersion</code>).</p></li><li><p><strong><code>Diagnostics</code></strong> — pure-data probes for SQLite + filesystem corpus, used by <code>doctor</code>. Zero external deps.</p></li><li><p><strong><code>Indexer</code></strong> — write-side counterpart to <code>Search</code>. Three indexer services (<code>DocsService</code>, <code>PackagesService</code>, <code>SamplesService</code>) each emit per-stage events. Hosts the preflight pipeline for <code>cupertino save</code> + <code>doctor --save</code>.</p></li><li><p><strong><code>Ingest</code></strong> — package skeleton + <code>Session</code> helpers lifted from <code>FetchCommand</code>. The seven <code>&lt;Type&gt;Pipeline</code> services still live in CLI; lifting them is queued for v1.0.1.</p></li></ul>
<p>The point of the lift is that MCP tooling, future agent-shell adapters, and tests can now drive the pipelines without depending on <code>ArgumentParser</code>. CLI files become thin front-doors that parse flags and render progress.</p>
<h2>What “First Light” means</h2>
<p>The release name is from astronomy. First light is the first time a new telescope is pointed at the sky and an actual image comes back — not a calibration target, not a test pattern, but real data. Everything before is engineering. First light is when the instrument starts being a telescope.</p>
<p>That’s how this release feels. The previous twelve months were engineering — getting the crawler not to leak memory, getting the indexer to handle the field weights, getting the MCP server to negotiate protocols, getting the install to not strand users on stale databases. v1.0.0 is when those parts compose into something that does what it was built to do.</p>
<h2>Try it</h2>
<pre><code class="language-bash">brew tap cupertinohq<span class="tok-operator">/</span>tap https<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>codeberg<span class="tok-operator">.</span>org<span class="tok-operator">/</span>CupertinoHQ<span class="tok-operator">/</span>homebrew<span class="tok-operator">-</span>tap<span class="tok-operator">.</span>git
brew install cupertinohq<span class="tok-operator">/</span>tap<span class="tok-operator">/</span>cupertino
cupertino setup
cupertino search Task</code></pre>
<p><code>brew install</code> pulls a notarized universal binary (~13 MB). <code>cupertino setup</code> downloads the v1.0.0 database bundle (~833 MB compressed, ~5 GB extracted). <code>cupertino search Task</code> should land the canonical Swift <code>Task</code> struct as result #1.</p>
<p>If you want it as an MCP server for Claude / Cursor / VS Code Copilot / Zed / Windsurf / opencode / Codex, point your client config at <code>cupertino serve</code>. Setup guides for each are in the README.</p>
<h2>What’s next: v1.0.1</h2>
<p>A few items deferred from v1.0.0 that I want to land in a follow-up patch:</p>
<ul><li><p><strong>WKWebView re-crawl on the May 2026 corpus.</strong> The April 2026 corpus shipped with v1.0.0 because rebuilding the schema-12 search.db from on-disk docs doesn’t need a re-crawl. The next release will refresh the corpus content itself and pick up Apple-side documentation edits since April plus pages the JSON API doesn’t expose.</p></li><li><p><strong>Kind-tier ranking.</strong> Roughly 49% of apple-docs rows have <code>kind=unknown</code>. When the crawler-side metadata extraction improves, a <code>kind ∈ {enum, struct, class, protocol}</code> tier slots ahead of the framework-authority tiebreak in HEURISTIC 1. The exact-title peer cluster gets a third orthogonal signal and the few remaining edge cases (<code>Result</code>’s lowercase-property competition, <code>Identifiable</code>’s conformance-page domination) collapse cleanly.</p></li><li><p><strong><code>Ingest</code> sub-PRs 4b–4f.</strong> The seven <code>&lt;Type&gt;Pipeline</code> services still in CLI lift cleanly into the new package once they grow callback-based shapes. Tracked under #247.</p></li></ul>
<p>That’s it. Cupertino v1.0.0 “First Light” is out. Tag, binary, bundle, formula — all live as of today.</p>
<p>Apple’s documentation is now something an AI agent can actually read before it answers.</p>]]></content:encoded>
</item>
<item>
<title>Cupertino v0.10: Full Coverage and Community Contributions</title>
<link>https://aleahim.com/blog/cupertino-10-release/</link>
<guid isPermaLink="true">https://aleahim.com/blog/cupertino-10-release/</guid>
<pubDate>Fri, 13 Mar 2026 00:00:00 +0000</pubDate>
<description>307 frameworks, 302,424 docs, Agent Skills, Claude Code plugin, and the first community PRs.</description>
<content:encoded><![CDATA[<h1>Cupertino v0.10: Full Coverage and Community Contributions</h1>
<p><strong>TL;DR:</strong> All 307 Apple frameworks are now indexed. 302,424 documentation pages searchable. Community contributors added Agent Skill support and a Claude Code plugin. Homebrew resource bundle fix.</p>
<h2>The Completionist Achievement: Unlocked</h2>
<p>I wanted every Apple framework in the index. Not most of them. <em>All</em> of them.</p>
<p>Turns out, the crawler was only finding ~20 frameworks by following links from Apple’s developer homepage. That’s like exploring a library by only reading the “Staff Picks” shelf. The rest of Apple’s documentation was sitting there, patiently waiting to be discovered.</p>
<p>The fix: seed the crawler from Apple’s own <code>technologies.json</code> – their internal index of every framework. Feed that to the BFS crawler, and suddenly the whole library opens up.</p>
<p>But that was just the first of five bugs hiding in the crawl engine:</p>
<table><thead><tr><th>Bug</th><th>What Went Wrong</th><th>Fix</th></tr></thead><tbody><tr><td>Session resume ignored <code>startURL</code></td><td>Resumed any session, even for wrong URLs</td><td>Check URL match before resuming</td></tr><tr><td>BFS-only discovery</td><td>Homepage links only find ~20 frameworks</td><td>Seed from <code>technologies.json</code></td></tr><tr><td>Case-sensitive URL matching</td><td>Apple’s JSON returns mixed-case paths</td><td><code>shouldVisit()</code> now case-insensitive</td></tr><tr><td>0.5s request delay</td><td>Life is short</td><td>Reduced to 0.05s</td></tr><tr><td>Skip without enqueue</td><td>Content-unchanged pages never enqueued their links</td><td>Moved link enqueue before skip check</td></tr></tbody></table>
<p>That last one was sneaky. If a page’s content hadn’t changed since the last crawl, the crawler skipped it – but skipped <em>before</em> adding its child links to the queue. Entire subtrees silently disappeared.</p>
<p>The result: 13 previously missing frameworks crawled and indexed. CoreGraphics, CoreMedia, CoreVideo, CoreText, CoreAudio, dnssd, Hypervisor, IOBluetooth, HIDDriverKit, Matter, OpenGLES, USBDriverKit, and CoreAudioTypes.</p>
<p>Yes, even OpenGLES. Deprecated, but completionism has no room for judgment.</p>
<table><thead><tr><th>Framework</th><th style="text-align:right">Documents</th></tr></thead><tbody><tr><td>Kernel</td><td style="text-align:right">39,396</td></tr><tr><td>Matter</td><td style="text-align:right">24,320</td></tr><tr><td>Swift</td><td style="text-align:right">17,466</td></tr><tr><td>AppKit</td><td style="text-align:right">12,443</td></tr><tr><td>Foundation</td><td style="text-align:right">12,423</td></tr><tr><td>UIKit</td><td style="text-align:right">11,158</td></tr><tr><td>SwiftUI</td><td style="text-align:right">7,062</td></tr><tr><td>IOBluetooth</td><td style="text-align:right">3,895</td></tr><tr><td>CoreGraphics</td><td style="text-align:right">3,500+</td></tr><tr><td>CoreMedia</td><td style="text-align:right">2,800+</td></tr><tr><td>…</td><td style="text-align:right">…</td></tr><tr><td><strong>307 Frameworks</strong></td><td style="text-align:right"><strong>302,424</strong></td></tr></tbody></table>
<p>Your AI agent can now answer questions about Hypervisor’s vCPU mapping <em>and</em> IOBluetooth’s RFCOMM channels. You’re welcome.</p>
<h2>Community Contributions</h2>
<p>This release marks a milestone I’m genuinely excited about: the first external contributions to Cupertino.</p>
<p>Both add new ways to use Apple documentation without running an MCP server. Two contributors saw something they wanted, built it, and sent a PR. That’s the open source dream right there.</p>
<h3>Agent Skill Support (PR #167)</h3>
<p><a href="https://github.com/tijs">Tijs Teulings</a> added support for using Cupertino as a stateless <a href="https://agentskills.io">Agent Skill</a>. AI coding agents get direct access to Apple documentation via CLI commands with JSON output – no MCP server process needed.</p>
<p>Install with <a href="https://github.com/numman-ali/openskills">OpenSkills</a>:</p>
<pre><code class="language-bash">npx openskills install mihaelamj<span class="tok-operator">/</span>cupertino</code></pre>
<p>Or manually copy the skill definition to <code>.claude/skills/cupertino/</code>.</p>
<p>No daemon. No stdio pipe. Just <code>cupertino search "SwiftUI" --format json</code> and you’re done.</p>
<h3>Claude Code Plugin (PR #169)</h3>
<p><a href="https://github.com/gpambrozio">Gustavo Ambrozio</a> took Tijs’s skill and leveled it up into a proper Claude Code plugin with marketplace support.</p>
<pre><code class="language-bash">claude <span class="tok-operator">/</span>plug<span class="tok-keyword">in</span> marketplace add https<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>codeberg<span class="tok-operator">.</span>org<span class="tok-operator">/</span>CupertinoHQ<span class="tok-operator">/</span>cupertino<span class="tok-operator">.</span>git</code></pre>
<p>The plugin adds a manifest, marketplace metadata, and improved skill descriptions with better trigger phrases. One command to install, zero configuration after that.</p>
<p>Thank you Tijs and Gustavo. It’s a great feeling to see the project grow beyond a single-maintainer effort.</p>
<h2>The Homebrew Symlink Saga</h2>
<p>Here’s a fun one. After the v0.10 release, <code>brew install cupertino</code> worked fine… until you actually ran any command. Instant crash:</p>
<pre><code>Fatal error: could not load resource bundle:
from /opt/homebrew/bin/Cupertino_Resources.bundle</code></pre>
<p>The problem: Homebrew symlinks <em>files</em> from the Cellar to <code>/opt/homebrew/bin/</code>, but not <em>directories</em>. The resource bundle was sitting happily in the Cellar, but <code>Bundle.main.bundleURL</code> (which doesn’t resolve symlinks) was looking in <code>/opt/homebrew/bin/</code> where the symlink lives. Two ships passing in the night.</p>
<p>The fix was two-pronged:</p>
<ol><li><p><strong>Formula</strong>: Added a <code>post_install</code> hook that manually creates the symlink for the resource bundle directory</p></li><li><p><strong>Code</strong>: <code>CupertinoResources.bundle</code> now resolves symlinks via <code>executableURL.resolvingSymlinksInPath()</code> before falling back to <code>Bundle.module</code></p></li></ol>
<p>Belt and suspenders. If you had the crash, <code>brew update &amp;&amp; brew upgrade cupertinohq/tap/cupertino</code> fixes it.</p>
<h2>Search Improvements</h2>
<p>Framework search now includes synonym matching. Because nobody remembers if it’s “CoreGraphics” or “Core Graphics”:</p>
<table><thead><tr><th>You search for</th><th>Finds</th></tr></thead><tbody><tr><td>“Core Graphics”</td><td>coregraphics</td></tr><tr><td>“UIKit”</td><td>uikit</td></tr><tr><td>“SwiftUI”</td><td>swiftui</td></tr><tr><td>“Core Data”</td><td>coredata</td></tr></tbody></table>
<p>Case-insensitive, space-tolerant. Type it however you remember it.</p>
<h2>Install or Update</h2>
<pre><code class="language-bash"><span class="tok-comment"># New install</span>
brew tap cupertinohq<span class="tok-operator">/</span>tap https<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>codeberg<span class="tok-operator">.</span>org<span class="tok-operator">/</span>CupertinoHQ<span class="tok-operator">/</span>homebrew<span class="tok-operator">-</span>tap<span class="tok-operator">.</span>git
brew install cupertinohq<span class="tok-operator">/</span>tap<span class="tok-operator">/</span>cupertino

<span class="tok-comment"># Update existing</span>
brew update <span class="tok-operator">&amp;</span><span class="tok-operator">&amp;</span> brew upgrade cupertinohq<span class="tok-operator">/</span>tap<span class="tok-operator">/</span>cupertino

<span class="tok-comment"># Or one-line install</span>
bash <span class="tok-operator">&lt;</span>(curl <span class="tok-operator">-</span>sSL https<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>codeberg<span class="tok-operator">.</span>org<span class="tok-operator">/</span>CupertinoHQ<span class="tok-operator">/</span>cupertino<span class="tok-operator">/</span>raw<span class="tok-operator">/</span>branch<span class="tok-operator">/</span>ma<span class="tok-keyword">in</span><span class="tok-operator">/</span>install<span class="tok-operator">.</span>sh)</code></pre>
<p>Then:</p>
<pre><code class="language-bash">cupertino setup  <span class="tok-comment"># Download databases (~30 seconds)</span>
cupertino serve  <span class="tok-comment"># Start MCP server</span></code></pre>
<h2>Issues and PRs Closed</h2>
<ul><li><p>#159 - Missing framework documentation</p></li><li><p>#160 - Crawler improvements for full coverage</p></li><li><p>#167 - Agent Skill support (<a href="https://github.com/tijs">@tijs</a>)</p></li><li><p>#169 - Claude Code plugin (<a href="https://github.com/gpambrozio">@gpambrozio</a>)</p></li><li><p>#162 - CONTRIBUTING.md (<a href="https://github.com/William-Laverty">@William-Laverty</a>)</p></li><li><p>#133 - README link fix (<a href="https://github.com/lwdupont">@lwdupont</a>)</p></li><li><p>#172 - Crawler session resume and delay fixes</p></li></ul>
<h2>Links</h2>
<ul><li><a href="https://codeberg.org/CupertinoHQ/cupertino">Codeberg Repository</a></li><li><a href="https://codeberg.org/CupertinoHQ/cupertino/src/branch/main/docs">Documentation</a></li><li><a href="https://codeberg.org/CupertinoHQ/cupertino/src/tag/v0.10.0">Tag v0.10.0</a></li></ul>]]></content:encoded>
</item>
<item>
<title>iRelay: Text Your Mac, Run an AI Agent</title>
<link>https://aleahim.com/blog/irelay/</link>
<guid isPermaLink="true">https://aleahim.com/blog/irelay/</guid>
<pubDate>Fri, 13 Mar 2026 00:00:00 +0000</pubDate>
<description>A Swift daemon that turns iMessage into a remote terminal for Claude Code</description>
<content:encoded><![CDATA[<h1>iRelay: Text Your Mac, Run an AI Agent</h1>
<p>I created a GitHub repo from my phone while walking around the house. No terminal, no SSH, no laptop open. Just iMessage.</p>
<p><strong>iRelay</strong> is a Swift daemon that listens for iMessages on your Mac, spawns Claude Code in your project directory, and sends the result back as a text. You message your Mac like you’d message a person, except it writes code.</p>
<p><strong>Codeberg:</strong> <a href="https://codeberg.org/Mihaela/iRelay">codeberg.org/Mihaela/iRelay</a></p>
<h2>Demo</h2>
<figure class="td-embed" style="--td-embed-aspect-ratio: 315 / 560;">
<div class="td-embed-frame">
<iframe
  class="td-embed-iframe"
  src="https://www.youtube-nocookie.com/embed/ehOY7BnPOf0"
  title="Demo video"
  loading="lazy"
  allowfullscreen
  referrerpolicy="strict-origin-when-cross-origin"
  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
></iframe>
</div>
</figure>
<h2>How It Works</h2>
<p>The flow is simple:</p>
<pre><code>iPhone (iMessage) → Mac (iRelay daemon) → Claude Code → response → iMessage</code></pre>
<p>You send something like <code>irelayy fix the failing test</code> from your phone. Your Mac picks it up, runs Claude Code against your project, and texts you back with what it did.</p>
<p>That’s it. No web UI, no port forwarding, no VPN. Just iMessage as a transport layer.</p>
<h2>Why the Double Y</h2>
<p>I already run <a href="https://openclaw.ai/">OpenClaw</a> on the same Mac for iMessage automation. Two daemons listening to the same iMessage account would fight over every incoming message.</p>
<p>The fix: a prefix. iRelay claims any message starting with <code>irelayy</code> (double y). OpenClaw ignores those. Everything else goes to OpenClaw. Simple namespace partitioning over a shared channel.</p>
<h2>What I Actually Did With It</h2>
<p>First real test: I asked it to investigate my repos and suggest a business idea. It came back with MCPMe. Then I told it to create the repo, write the README, and push it. All from iMessage. The Mac did the work while I made coffee.</p>
<p>That’s the use case — you’re away from your desk but you want something done <em>now</em>. A quick fix, a repo setup, a refactor. Text it, walk away, get the result back on your phone.</p>
<h2>Architecture</h2>
<p>iRelay is designed around channels and providers:</p>
<ul><li><p><strong>Channels</strong> handle message transport — iMessage is the one that works today. The architecture supports Telegram, Slack, Discord, Signal, Matrix, IRC, and WebChat, but those are scaffolded, not wired up yet.</p></li><li><p><strong>Providers</strong> handle LLM execution — Claude Code is the primary one. OpenAI, Ollama, and Gemini slots exist for future use.</p></li></ul>
<p>Conversation history is preserved across messages, so context carries over. You can also save and restore sessions.</p>
<h2>What’s Next</h2>
<p>iRelay pairs well with <a href="https://codeberg.org/CupertinoHQ/cupertino">Cupertino</a> — when Claude Code has offline access to Apple’s documentation, the responses you get back over iMessage are significantly better. No hallucinated APIs, no deprecated patterns. I’ve been quietly working on a major Cupertino update that nearly doubles framework coverage, which will make this combination even stronger. More on that soon.</p>
<hr>
<p><em>Built in a day. Used from the couch.</em></p>]]></content:encoded>
</item>
<item>
<title>Cupertino v0.9: Multi-Agent Support and MCP Protocol Upgrade</title>
<link>https://aleahim.com/blog/cupertino-09-release/</link>
<guid isPermaLink="true">https://aleahim.com/blog/cupertino-09-release/</guid>
<pubDate>Sun, 25 Jan 2026 00:00:00 +0000</pubDate>
<description>Cupertino now works with OpenAI Codex, Cursor, VS Code Copilot, Zed, Windsurf, and opencode. Plus MCP protocol upgrade to 2025-06-18.</description>
<content:encoded><![CDATA[<h1>Cupertino v0.9: Multi-Agent Support and MCP Protocol Upgrade</h1>
<p><strong>TL;DR:</strong> Cupertino now has setup guides for 8 different AI tools. MCP protocol upgraded to 2025-06-18. Comprehensive documentation for all binaries. No more Node.js in the codebase.</p>
<p>This release focuses on reach. More AI tools can use Cupertino. Better documentation. Cleaner internals.</p>
<h2>Release Summary</h2>
<p>Since v0.8.0, Cupertino has had 5 releases:</p>
<table><thead><tr><th>Version</th><th>Date</th><th>Highlights</th></tr></thead><tbody><tr><td>0.8.1</td><td>2025-12-28</td><td>Installer color fix</td></tr><tr><td>0.8.2</td><td>2025-12-31</td><td>Download progress animation</td></tr><tr><td>0.8.3</td><td>2025-12-31</td><td>Swift-only tests, no Node.js</td></tr><tr><td>0.9.0</td><td>2025-12-31</td><td>MCP protocol 2025-06-18</td></tr><tr><td>0.9.1</td><td>2026-01-25</td><td>Multi-agent docs, binary docs</td></tr></tbody></table>
<h2>Multi-Agent Support (v0.9.1)</h2>
<p>The most requested feature: “How do I use Cupertino with X?”</p>
<p>Cupertino started as a Claude tool. But MCP is an open protocol. Any AI assistant that speaks MCP can use it.</p>
<p>v0.9.1 adds setup guides for 8 AI tools:</p>
<table><thead><tr><th>Tool</th><th>Config Location</th></tr></thead><tbody><tr><td>Claude Desktop</td><td><code>~/Library/Application Support/Claude/claude_desktop_config.json</code></td></tr><tr><td>Claude Code</td><td><code>claude mcp add cupertino</code></td></tr><tr><td>OpenAI Codex</td><td><code>codex mcp add cupertino</code> or <code>~/.codex/config.toml</code></td></tr><tr><td>Cursor</td><td><code>.cursor/mcp.json</code></td></tr><tr><td>VS Code (Copilot)</td><td><code>.vscode/mcp.json</code></td></tr><tr><td>Zed</td><td><code>settings.json</code></td></tr><tr><td>Windsurf</td><td><code>~/.codeium/windsurf/mcp_config.json</code></td></tr><tr><td>opencode</td><td><code>opencode.jsonc</code></td></tr></tbody></table>
<h3>Quick Setup</h3>
<p>For most tools, configuration is a JSON object:</p>
<pre><code class="language-json"><span class="tok-operator">{</span>
  <span class="tok-property">"mcpServers"</span><span class="tok-operator">:</span> <span class="tok-operator">{</span>
    <span class="tok-property">"cupertino"</span><span class="tok-operator">:</span> <span class="tok-operator">{</span>
      <span class="tok-property">"command"</span><span class="tok-operator">:</span> <span class="tok-string">"/opt/homebrew/bin/cupertino"</span><span class="tok-operator">,</span>
      <span class="tok-property">"args"</span><span class="tok-operator">:</span> <span class="tok-operator">[</span><span class="tok-string">"serve"</span><span class="tok-operator">]</span>
    <span class="tok-operator">}</span>
  <span class="tok-operator">}</span>
<span class="tok-operator">}</span></code></pre>
<p>The exact structure varies by tool. Full examples are in the <a href="https://codeberg.org/CupertinoHQ/cupertino/src/branch/main/docs/commands/serve/README.md">serve command documentation</a>.</p>
<h3>Dynamic Path Detection</h3>
<p>If you’re not sure where cupertino is installed:</p>
<pre><code class="language-bash">claude mcp add cupertino <span class="tok-operator">-</span><span class="tok-operator">-</span> <span class="tok-property">$</span>(which cupertino)
codex mcp add cupertino <span class="tok-operator">-</span><span class="tok-operator">-</span> <span class="tok-property">$</span>(which cupertino) serve</code></pre>
<p>The <code>$(which cupertino)</code> expands to the actual binary path. Works for Homebrew installs on both Apple Silicon (<code>/opt/homebrew/bin</code>) and Intel (<code>/usr/local/bin</code>).</p>
<h2>MCP Protocol Upgrade (v0.9.0)</h2>
<p>v0.9.0 upgrades the MCP protocol from <code>2024-11-05</code> to <code>2025-06-18</code>.</p>
<p>This matters because newer AI tools expect the newer protocol. Without this upgrade, Cupertino would fail to initialize with clients that only support <code>2025-06-18</code>.</p>
<p>The upgrade includes backward compatibility:</p>
<pre><code class="language-swift"><span class="tok-comment">// Server negotiates with client</span>
<span class="tok-keyword">public</span> <span class="tok-keyword">let</span> <span class="tok-type">MCPProtocolVersion</span> <span class="tok-operator">=</span> <span class="tok-string">"2025-06-18"</span>
<span class="tok-keyword">public</span> <span class="tok-keyword">let</span> <span class="tok-type">MCPProtocolVersionsSupported</span> <span class="tok-operator">=</span> [<span class="tok-string">"2025-06-18"</span>, <span class="tok-string">"2024-11-05"</span>]

<span class="tok-comment">// During initialization, find common version</span>
<span class="tok-keyword">let</span> negotiated<span class="tok-type">Version</span> <span class="tok-operator">=</span> client<span class="tok-type">Versions</span>
    <span class="tok-operator">.</span>first { <span class="tok-type">MCPProtocolVersionsSupported</span><span class="tok-operator">.</span><span class="tok-function">contains</span>($<span class="tok-number">0</span>) }</code></pre>
<p>If a client requests <code>2024-11-05</code>, Cupertino still works. If it requests <code>2025-06-18</code>, that works too. The <code>MCPClient</code> and <code>MockAIAgent</code> also support version fallback when connecting to servers.</p>
<p>Thanks to <a href="https://github.com/erikmackinnon">@erikmackinnon</a> for the contribution (#130).</p>
<h2>Binary Documentation (v0.9.1)</h2>
<p>Cupertino ships 4 executables. Until now, only <code>cupertino</code> had proper documentation.</p>
<p>v0.9.1 adds 48 documentation files covering all binaries:</p>
<h3>cupertino</h3>
<p>The main CLI with 12 commands. Already documented, but now with updated MCP client configuration examples.</p>
<h3>cupertino-tui</h3>
<p>The terminal UI for browsing and selecting packages:</p>
<pre><code class="language-bash">cupertino<span class="tok-operator">-</span>tui
cupertino<span class="tok-operator">-</span>tui <span class="tok-operator">-</span><span class="tok-operator">-</span>version</code></pre>
<p>5 views documented:</p>
<ul><li><p><strong>Dashboard</strong> - Overview and quick actions</p></li><li><p><strong>Packages</strong> - Browse and select Swift packages</p></li><li><p><strong>Archive</strong> - Apple Archive documentation selection</p></li><li><p><strong>Videos</strong> - WWDC video selection</p></li><li><p><strong>Settings</strong> - Configuration options</p></li></ul>
<h3>mock-ai-agent</h3>
<p>A testing tool that simulates MCP client behavior:</p>
<pre><code class="language-bash">mock<span class="tok-operator">-</span>ai<span class="tok-operator">-</span>agent search <span class="tok-string">"SwiftUI navigation"</span>
mock<span class="tok-operator">-</span>ai<span class="tok-operator">-</span>agent list<span class="tok-operator">-</span>tools
mock<span class="tok-operator">-</span>ai<span class="tok-operator">-</span>agent <span class="tok-operator">-</span><span class="tok-operator">-</span>server <span class="tok-operator">/</span>path<span class="tok-operator">/</span>to<span class="tok-operator">/</span>cupertino
mock<span class="tok-operator">-</span>ai<span class="tok-operator">-</span>agent <span class="tok-operator">-</span><span class="tok-operator">-</span>version  <span class="tok-comment"># New in v0.9.1</span></code></pre>
<p>Arguments documented:</p>
<ul><li><p><code>&lt;query&gt;</code> - Search query to execute</p></li><li><p><code>--server</code> - Path to MCP server binary</p></li></ul>
<p>Useful for testing MCP servers without a full AI client.</p>
<h3>cupertino-rel</h3>
<p>The release automation tool (maintainer-only):</p>
<pre><code class="language-bash">cupertino<span class="tok-operator">-</span>rel bump patch          <span class="tok-comment"># Bump version</span>
cupertino<span class="tok-operator">-</span>rel tag <span class="tok-operator">-</span><span class="tok-operator">-</span>version <span class="tok-number">0.9.1</span> <span class="tok-operator">-</span><span class="tok-operator">-</span>push  <span class="tok-comment"># Create and push tag</span>
cupertino<span class="tok-operator">-</span>rel homebrew <span class="tok-operator">-</span><span class="tok-operator">-</span>version <span class="tok-number">0.9.1</span>    <span class="tok-comment"># Update Homebrew formula</span>
cupertino<span class="tok-operator">-</span>rel databases <span class="tok-operator">-</span><span class="tok-operator">-</span>version <span class="tok-number">0.9.1</span>   <span class="tok-comment"># Upload database release</span>
cupertino<span class="tok-operator">-</span>rel docs<span class="tok-operator">-</span>update patch   <span class="tok-comment"># Documentation-only release</span>
cupertino<span class="tok-operator">-</span>rel full <span class="tok-number">0.10.0</span>         <span class="tok-comment"># Complete release workflow</span></code></pre>
<p>6 subcommands with all options documented:</p>
<ul><li><p><code>bump</code> - Update version in all files</p></li><li><p><code>tag</code> - Create and push git tags</p></li><li><p><code>homebrew</code> - Update Homebrew formula</p></li><li><p><code>databases</code> - Upload databases to cupertino-docs</p></li><li><p><code>docs-update</code> - Documentation-only releases</p></li><li><p><code>full</code> - Complete release workflow</p></li></ul>
<h3>Documentation Structure</h3>
<p>The documentation follows the same granular folder structure as the main CLI:</p>
<pre><code>docs/binaries/
├── README.md
├── cupertino-tui/
│   ├── README.md
│   ├── option (--)/
│   │   └── version.md
│   └── view/
│       ├── dashboard.md
│       ├── packages.md
│       ├── archive.md
│       ├── videos.md
│       └── settings.md
├── mock-ai-agent/
│   ├── README.md
│   ├── option (--)/
│   │   ├── server.md
│   │   └── version.md
│   └── argument (&lt;&gt;)/
│       └── query.md
└── cupertino-rel/
    ├── README.md
    ├── option (--)/
    │   └── version.md
    └── subcommand/
        ├── bump/
        ├── tag/
        ├── homebrew/
        ├── databases/
        ├── docs-update/
        └── full/</code></pre>
<h2>No More Node.js (v0.8.3)</h2>
<p>v0.8.3 removed the last Node.js dependency from the codebase.</p>
<p>The MCP integration tests previously used npm packages to simulate client behavior. Now they use <code>cupertino serve</code> directly:</p>
<pre><code class="language-swift"><span class="tok-attribute">@Test</span>(<span class="tok-string">"Initialize handshake with cupertino server"</span>)
<span class="tok-keyword">func</span> <span class="tok-function">cupertinoServerInitialize</span>() <span class="tok-keyword">async</span> <span class="tok-keyword">throws</span> {
    <span class="tok-keyword">let</span> process <span class="tok-operator">=</span> <span class="tok-type">Process</span>()
    process<span class="tok-operator">.</span>executable<span class="tok-type">URL</span> <span class="tok-operator">=</span> <span class="tok-type">URL</span>(file<span class="tok-type">URLWithPath</span><span class="tok-operator">:</span> <span class="tok-string">".build/debug/cupertino"</span>)
    process<span class="tok-operator">.</span>arguments <span class="tok-operator">=</span> [<span class="tok-string">"serve"</span>]

    <span class="tok-keyword">let</span> stdin<span class="tok-type">Pipe</span> <span class="tok-operator">=</span> <span class="tok-type">Pipe</span>()
    <span class="tok-keyword">let</span> stdout<span class="tok-type">Pipe</span> <span class="tok-operator">=</span> <span class="tok-type">Pipe</span>()
    process<span class="tok-operator">.</span>standard<span class="tok-type">Input</span> <span class="tok-operator">=</span> stdin<span class="tok-type">Pipe</span>
    process<span class="tok-operator">.</span>standard<span class="tok-type">Output</span> <span class="tok-operator">=</span> stdout<span class="tok-type">Pipe</span>

    <span class="tok-keyword">try</span> process<span class="tok-operator">.</span><span class="tok-function">run</span>()

    <span class="tok-comment">// Send initialize request (compact JSON + newline)</span>
    <span class="tok-keyword">let</span> init<span class="tok-type">Request</span> <span class="tok-operator">=</span> <span class="tok-string">"""
    {"jsonrpc":"2.0","id":1,"method":"initialize","params":{...}}\n
    """</span>
    stdin<span class="tok-type">Pipe</span><span class="tok-operator">.</span>file<span class="tok-type">HandleForWriting</span><span class="tok-operator">.</span><span class="tok-function">write</span>(<span class="tok-type">Data</span>(init<span class="tok-type">Request</span><span class="tok-operator">.</span>utf<span class="tok-number">8</span>))

    <span class="tok-comment">// Parse and verify response</span>
    <span class="tok-keyword">let</span> response <span class="tok-operator">=</span> <span class="tok-keyword">try</span> <span class="tok-type">JSONDecoder</span>()<span class="tok-operator">.</span><span class="tok-function">decode</span>(<span class="tok-type">JSONRPCResponse</span><span class="tok-operator">.</span><span class="tok-keyword">self</span>, from<span class="tok-operator">:</span> response<span class="tok-type">Data</span>)
    #<span class="tok-function">expect</span>(response<span class="tok-operator">.</span>server<span class="tok-type">Info</span><span class="tok-operator">.</span>name <span class="tok-operator">=</span><span class="tok-operator">=</span> <span class="tok-string">"cupertino"</span>)
}</code></pre>
<p>The tests verify:</p>
<ul><li><p>MCP initialize handshake</p></li><li><p><code>tools/list</code> responses</p></li><li><p>Protocol version negotiation</p></li><li><p>Server info and tool registration</p></li></ul>
<p>Swift-only tests. No npm. No node_modules. No package-lock.json.</p>
<p>This is now policy in AGENTS.md: <strong>no Node.js or npm in the Cupertino codebase</strong>.</p>
<h2>Setup Progress Animation (v0.8.2)</h2>
<p>Database downloads are ~400MB. Previously, <code>cupertino setup</code> showed nothing while downloading.</p>
<p>Now you get real-time feedback:</p>
<p><strong>Download progress with bar:</strong></p>
<pre><code>⠹ [████████████░░░░░░░░░░░░░░░░░░] 42% (168MB/400MB)</code></pre>
<p><strong>Extraction spinner:</strong></p>
<pre><code>⠧ Extracting databases...</code></pre>
<p>Implementation details:</p>
<ul><li><p><code>DownloadProgressDelegate</code> implements <code>URLSessionDownloadDelegate</code> for real-time progress</p></li><li><p><code>ExtractionSpinner</code> shows animated feedback during unzip</p></li><li><p>Extended download timeout to 10 minutes for large database files</p></li><li><p>Uses ANSI escape codes for in-place updates</p></li></ul>
<p>Small things that make the tool feel responsive.</p>
<h2>Installer ANSI Fix (v0.8.1)</h2>
<p>The one-line installer had broken ANSI colors:</p>
<p><strong>Before:</strong></p>
<pre><code>\033[32m✓ Installation complete\033[0m</code></pre>
<p><strong>After:</strong></p>
<pre><code>✓ Installation complete  (in green)</code></pre>
<p>Two <code>echo</code> statements were missing the <code>-e</code> flag for escape sequence interpretation. Fixed in #124.</p>
<h2>Database Compatibility</h2>
<p>This release uses the same database schema as v0.8.2. No migration needed.</p>
<p>When you run <code>cupertino setup</code>, it downloads databases from the v0.8.2 release. The CLI version (0.9.1) and database version (0.8.2) are now decoupled.</p>
<table><thead><tr><th>Version Type</th><th>Current</th><th>Purpose</th></tr></thead><tbody><tr><td>CLI version</td><td>0.9.1</td><td>Binary release</td></tr><tr><td>Database version</td><td>0.8.2</td><td>Pre-built database release</td></tr><tr><td>Schema version</td><td>9</td><td>SQLite schema</td></tr></tbody></table>
<p>This means:</p>
<ul><li><p>CLI updates don’t require database re-downloads</p></li><li><p>Database updates don’t require CLI updates</p></li><li><p>Schema changes are tracked separately</p></li></ul>
<h2>Install or Update</h2>
<pre><code class="language-bash"><span class="tok-comment"># New install</span>
brew tap cupertinohq<span class="tok-operator">/</span>tap https<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>codeberg<span class="tok-operator">.</span>org<span class="tok-operator">/</span>CupertinoHQ<span class="tok-operator">/</span>homebrew<span class="tok-operator">-</span>tap<span class="tok-operator">.</span>git
brew install cupertinohq<span class="tok-operator">/</span>tap<span class="tok-operator">/</span>cupertino

<span class="tok-comment"># Update existing</span>
brew update <span class="tok-operator">&amp;</span><span class="tok-operator">&amp;</span> brew upgrade cupertinohq<span class="tok-operator">/</span>tap<span class="tok-operator">/</span>cupertino

<span class="tok-comment"># Or one-line install</span>
bash <span class="tok-operator">&lt;</span>(curl <span class="tok-operator">-</span>sSL https<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>codeberg<span class="tok-operator">.</span>org<span class="tok-operator">/</span>CupertinoHQ<span class="tok-operator">/</span>cupertino<span class="tok-operator">/</span>raw<span class="tok-operator">/</span>branch<span class="tok-operator">/</span>ma<span class="tok-keyword">in</span><span class="tok-operator">/</span>install<span class="tok-operator">.</span>sh)</code></pre>
<p>Then:</p>
<pre><code class="language-bash">cupertino setup  <span class="tok-comment"># Download databases (~30 seconds)</span>
cupertino serve  <span class="tok-comment"># Start MCP server</span></code></pre>
<p>Configure your AI tool using the guides above, and you’re ready.</p>
<h2>Issues Closed</h2>
<ul><li><p>#124 - Installer ANSI escape sequences</p></li><li><p>#96 - Setup progress animation</p></li><li><p>#131 - Remove Node.js dependency from MCP integration tests</p></li><li><p>#130 - MCP protocol mismatch (2025-06-18 support)</p></li><li><p>#134 - Better documentation for use with different agents</p></li><li><p>#137 - Documentation improvements: MCP client configs and binary documentation</p></li></ul>
<h2>Links</h2>
<ul><li><a href="https://codeberg.org/CupertinoHQ/cupertino">Codeberg Repository</a></li><li><a href="https://codeberg.org/CupertinoHQ/cupertino/src/branch/main/docs">Documentation</a></li><li><a href="https://codeberg.org/CupertinoHQ/cupertino/src/tag/v0.9.1">Tag v0.9.1</a></li></ul>]]></content:encoded>
</item>
<item>
<title>Cupertino v0.8.0: AST Indexing and a Major Architecture Refactor</title>
<link>https://aleahim.com/blog/cupertino-08-release/</link>
<guid isPermaLink="true">https://aleahim.com/blog/cupertino-08-release/</guid>
<pubDate>Sat, 20 Dec 2025 00:00:00 +0000</pubDate>
<description>SwiftSyntax enables semantic code search. A deep refactoring of the codebase delivers cleaner results and better AI capabilities.</description>
<content:encoded><![CDATA[<h1>Cupertino v0.8.0: AST Indexing and a Major Architecture Refactor</h1>
<p><strong>TL;DR:</strong> SwiftSyntax-based AST indexing. Major codebase refactoring. Cleaner search results. Test suite grew from 93 to 698 tests.</p>
<p>This release is about foundations. Less visible features, more architectural work that makes everything else possible.</p>
<h2>The Big Feature: AST Indexing</h2>
<p>This is issue #81, and it’s been brewing for a while. Sample code search was purely keyword-based - you search “async”, you get files containing “async”. Comments, strings, documentation, actual code - all treated equally.</p>
<p>Now Cupertino extracts structured symbol data from Swift source:</p>
<pre><code class="language-swift"><span class="tok-comment">// SwiftSourceExtractor analyzes this:</span>
<span class="tok-attribute">@MainActor</span>
<span class="tok-keyword">class</span> <span class="tok-type">NetworkManager</span><span class="tok-operator">:</span> <span class="tok-type">ObservableObject</span> {
    <span class="tok-attribute">@Published</span> <span class="tok-keyword">var</span> is<span class="tok-type">Loading</span> <span class="tok-operator">=</span> <span class="tok-literal">false</span>

    <span class="tok-keyword">func</span> <span class="tok-function">fetch</span>() <span class="tok-keyword">async</span> <span class="tok-keyword">throws</span> <span class="tok-operator">-</span><span class="tok-operator">&gt;</span> <span class="tok-type">Data</span> { <span class="tok-operator">.</span><span class="tok-operator">.</span><span class="tok-operator">.</span> }
}</code></pre>
<p>And extracts:</p>
<table><thead><tr><th>Symbol</th><th>Kind</th><th>Attributes</th></tr></thead><tbody><tr><td><code>NetworkManager</code></td><td>class</td><td><code>@MainActor</code></td></tr><tr><td><code>isLoading</code></td><td>property</td><td><code>@Published</code></td></tr><tr><td><code>fetch()</code></td><td>function</td><td>async, throws</td></tr></tbody></table>
<h3>What This Enables</h3>
<p><strong>Better ranking, not magic.</strong> When you search “@Observable”, plain text search finds every file mentioning “Observable” - comments, strings, documentation, actual declarations. All ranked equally.</p>
<p>With AST indexing, Cupertino knows which results contain actual <code>@Observable</code> class declarations. These get boosted in ranking. The real symbol usages surface first.</p>
<pre><code class="language-bash"><span class="tok-property">$</span> cupertino search <span class="tok-string">"@Observable"</span> <span class="tok-operator">-</span><span class="tok-operator">-</span>source samples <span class="tok-operator">-</span><span class="tok-operator">-</span>format markdown</code></pre>
<pre><code class="language-markdown">## <span class="tok-type">Projects</span> (<span class="tok-number">5</span> found)

### <span class="tok-number">1.</span> <span class="tok-type">Migrating</span> from the <span class="tok-type">Observable</span> <span class="tok-type">Object</span> protocol to the <span class="tok-type">Observable</span> macro
<span class="tok-operator">-</span> <span class="tok-operator">*</span><span class="tok-operator">*</span><span class="tok-type">Frameworks</span><span class="tok-operator">:</span><span class="tok-operator">*</span><span class="tok-operator">*</span> swiftui
<span class="tok-operator">-</span> <span class="tok-operator">*</span><span class="tok-operator">*</span><span class="tok-type">Files</span><span class="tok-operator">:</span><span class="tok-operator">*</span><span class="tok-operator">*</span> <span class="tok-number">14</span>

## <span class="tok-type">Matching</span> <span class="tok-type">Files</span>

### <span class="tok-type">Library</span><span class="tok-operator">.</span>swift
<span class="tok-operator">&gt;</span> <span class="tok-attribute">@Observable</span> final <span class="tok-keyword">class</span> <span class="tok-type">Library</span> {
<span class="tok-operator">&gt;</span>     <span class="tok-keyword">var</span> books<span class="tok-operator">:</span> [<span class="tok-type">Book</span>] <span class="tok-operator">=</span> [<span class="tok-type">Book</span>(), <span class="tok-type">Book</span>(), <span class="tok-type">Book</span>()]
<span class="tok-operator">&gt;</span> }

### <span class="tok-type">Book</span><span class="tok-operator">.</span>swift
<span class="tok-operator">&gt;</span> <span class="tok-attribute">@Observable</span> final <span class="tok-keyword">class</span> <span class="tok-type">Book</span><span class="tok-operator">:</span> <span class="tok-type">Identifiable</span> {
<span class="tok-operator">&gt;</span>     <span class="tok-keyword">var</span> title <span class="tok-operator">=</span> <span class="tok-string">"Sample Book Title"</span>
<span class="tok-operator">&gt;</span>     <span class="tok-keyword">let</span> id <span class="tok-operator">=</span> <span class="tok-type">UUID</span>()
<span class="tok-operator">&gt;</span> }</code></pre>
<p>The search still matches text, but files with actual <code>@Observable</code> declarations rank higher than files that just mention the word in comments.</p>
<p><strong>Dedicated semantic tools.</strong> The symbol tables power these MCP tools:</p>
<ul><li><p><code>search_symbols</code> - query by symbol type and attributes</p></li><li><p><code>search_property_wrappers</code> - find <code>@Published</code>, <code>@State</code>, <code>@Binding</code> usage</p></li><li><p><code>search_conformances</code> - find protocol implementations</p></li><li><p><code>search_concurrency</code> - find async/await patterns</p></li></ul>
<p>These query extracted symbol data directly, not text. AI assistants can ask “find all async functions in SwiftUI samples” and get structured results.</p>
<h3>How It Works</h3>
<p>The new <code>ASTIndexer</code> package uses SwiftSyntax to parse Swift files and extract:</p>
<ul><li><p><strong>Symbols</strong>: Classes, structs, enums, actors, protocols, functions</p></li><li><p><strong>Attributes</strong>: <code>@MainActor</code>, <code>@Published</code>, <code>@Observable</code>, etc.</p></li><li><p><strong>Conformances</strong>: Protocol adoptions</p></li><li><p><strong>Modifiers</strong>: async, throws, static, public</p></li></ul>
<p>SwiftSyntax is Apple’s official Swift parser - the same one the compiler uses. No regex hacks, no fragile pattern matching.</p>
<p>The tradeoff? Build times. SwiftSyntax is a beast. First release build takes 10-15 minutes now. But the capability is worth it.</p>
<h2>The Refactoring Story</h2>
<p>This release involved substantial architectural changes that touch almost every part of the codebase. The goal: make search results cleaner and the system more capable.</p>
<h3>Unified Search Service</h3>
<p>Previously, each search source (docs, HIG, samples, videos) had its own formatting logic scattered across the codebase. Now there’s a unified <code>SearchService</code> that handles all sources consistently:</p>
<ul><li><p>Single entry point for all search types</p></li><li><p>Consistent result formatting across sources</p></li><li><p>Shared footer generation with tips and guidance</p></li><li><p>Hierarchical numbering that works across all result types</p></li></ul>
<h3>Package Architecture Cleanup</h3>
<p>The package structure got significant attention:</p>
<ul><li><p>Consolidated duplicate functionality across modules</p></li><li><p>Clearer boundaries between <code>Services</code>, <code>Core</code>, and domain packages</p></li><li><p>Better separation of CLI concerns from library logic</p></li><li><p>Removed dead code paths and unused types</p></li></ul>
<h3>Result Formatter Protocol</h3>
<p>A new <code>ResultFormatter</code> protocol standardizes how search results become output:</p>
<pre><code class="language-swift"><span class="tok-keyword">protocol</span> <span class="tok-type">ResultFormatter</span> {
    <span class="tok-keyword">associatedtype</span> <span class="tok-type">Input</span>
    <span class="tok-keyword">func</span> <span class="tok-function">format</span>(_ result<span class="tok-operator">:</span> <span class="tok-type">Input</span>) <span class="tok-operator">-</span><span class="tok-operator">&gt;</span> <span class="tok-type">String</span>
}</code></pre>
<p>This enabled reusable formatters for different output contexts (CLI text, MCP markdown, JSON) while keeping the core search logic unchanged.</p>
<p>The refactoring wasn’t glamorous work, but it paid off immediately in cleaner search results and faster feature development.</p>
<h2>Smarter Search Output</h2>
<p>Here’s a subtle change that matters more than it sounds: hierarchical result numbering.</p>
<p>Before:</p>
<pre><code>## Apple Documentation
### UIButton
### UIView
### UIControl

## Sample Code
### ButtonStyles
### CustomControls</code></pre>
<p>After:</p>
<pre><code>## 1. Apple Documentation (20)
### 1.1 UIButton
### 1.2 UIView
### 1.3 UIControl

## 2. Sample Code (5)
### 2.1 ButtonStyles
### 2.2 CustomControls</code></pre>
<p>Why does this matter? When Claude is navigating search results, it can now reference specific items: “Looking at result 1.3…” or “Sample 2.1 shows this pattern…”</p>
<p>The count in headers (<code>20</code>, <code>5</code>) also helps the AI understand result distribution. If one source has 50 results and another has 2, that’s useful context.</p>
<p>Small formatting changes, big impact on AI reasoning.</p>
<h2>Display Bugs: Death by a Thousand Cuts</h2>
<p>Sometimes bugs accumulate in strange ways. I noticed search results had weird formatting artifacts:</p>
<ul><li><p><code>Tabbars|AppleDeveloperDocumentation###</code> - trailing garbage</p></li><li><p><code>Tab  bars</code> - double spaces</p></li><li><p><code>Goingfull screen</code> - missing space in HIG titles</p></li><li><p><code>Framework# SwiftUI</code> - inline markdown headers</p></li></ul>
<p>Each individually minor. Together, they made results look unprofessional and confused AI parsing.</p>
<p>The fix was a string extension that cleans display text:</p>
<pre><code class="language-swift"><span class="tok-keyword">var</span> cleaned<span class="tok-type">ForDisplay</span><span class="tok-operator">:</span> <span class="tok-type">String</span> {
    <span class="tok-keyword">var</span> result <span class="tok-operator">=</span> <span class="tok-keyword">self</span>

    <span class="tok-comment">// Remove trailing ###</span>
    <span class="tok-keyword">while</span> result<span class="tok-operator">.</span><span class="tok-function">hasSuffix</span>(<span class="tok-string">"###"</span>) { <span class="tok-operator">.</span><span class="tok-operator">.</span><span class="tok-operator">.</span> }

    <span class="tok-comment">// Remove |AppleDeveloperDocumentation</span>
    result <span class="tok-operator">=</span> result<span class="tok-operator">.</span><span class="tok-function">replacingOccurrences</span>(of<span class="tok-operator">:</span> <span class="tok-string">"|AppleDeveloperDocumentation"</span>, with<span class="tok-operator">:</span> <span class="tok-string">""</span>)

    <span class="tok-comment">// Fix "Goingfull" -&gt; "Going full"</span>
    result <span class="tok-operator">=</span> result<span class="tok-operator">.</span>adding<span class="tok-type">SpacesToCamelCase</span>

    <span class="tok-comment">// Collapse double spaces</span>
    <span class="tok-keyword">while</span> result<span class="tok-operator">.</span><span class="tok-function">contains</span>(<span class="tok-string">"  "</span>) { <span class="tok-operator">.</span><span class="tok-operator">.</span><span class="tok-operator">.</span> }

    <span class="tok-keyword">return</span> result
}</code></pre>
<p>Now there are 34 unit tests just for string formatting. Because every edge case that slipped through was a papercut in every search result.</p>
<h2>The Test Suite Story</h2>
<table><thead><tr><th>Metric</th><th>v0.7.0</th><th>v0.8.0</th></tr></thead><tbody><tr><td>Tests</td><td>93</td><td>698</td></tr><tr><td>Suites</td><td>7</td><td>73</td></tr><tr><td>Duration</td><td>~5 min</td><td>~35 sec</td></tr></tbody></table>
<p>That’s not a typo. We went from 93 tests to 698.</p>
<p>Where did they come from?</p>
<ol><li><p><strong>AST Indexer tests</strong> - SwiftSyntax extraction needs thorough testing</p></li><li><p><strong>String formatter tests</strong> - All those display edge cases</p></li><li><p><strong>Service layer tests</strong> - Unified search service coverage</p></li><li><p><strong>Symbol database tests</strong> - Integration tests for code analysis</p></li></ol>
<p>The duration dropped despite 7x more tests because I fixed a race condition in the <code>PriorityPackagesCatalog</code> tests. The old code used <code>defer { Task { await ... } }</code> which created detached async tasks that didn’t complete before the next test ran. State leaked between tests, causing random failures.</p>
<p>The fix was embarrassingly simple: don’t use <code>defer</code> with async cleanup. Just await at the end of the test.</p>
<h2>Doctor Command Gets Smarter</h2>
<p>The <code>cupertino doctor</code> command now diagnoses package-related issues:</p>
<pre><code class="language-bash"><span class="tok-property">$</span> cupertino doctor

Cupertino v<span class="tok-number">0.8.0</span> <span class="tok-operator">-</span> Health Check

Databases<span class="tok-operator">:</span>
  ✅ search<span class="tok-operator">.</span><span class="tok-function">db</span> (<span class="tok-number">2.3</span>GB, <span class="tok-number">302</span>,<span class="tok-number">424</span> docs)
  ✅ sample<span class="tok-operator">.</span><span class="tok-function">db</span> (<span class="tok-number">156</span>MB, <span class="tok-number">606</span> projects)

Package Status<span class="tok-operator">:</span>
  ✅ User selections<span class="tok-operator">:</span> <span class="tok-operator">~</span><span class="tok-operator">/</span><span class="tok-operator">.</span>cupertino<span class="tok-operator">/</span>selected<span class="tok-operator">-</span>packages<span class="tok-operator">.</span><span class="tok-function">json</span> (<span class="tok-number">12</span> packages)
  ✅ Downloaded READMEs<span class="tok-operator">:</span> <span class="tok-number">12</span><span class="tok-operator">/</span><span class="tok-number">12</span>
  ⚠️  Orphaned READMEs<span class="tok-operator">:</span> <span class="tok-number">3</span> (packages no longer selected)

Priority Breakdown<span class="tok-operator">:</span>
  📦 Apple Official<span class="tok-operator">:</span> <span class="tok-number">31</span> packages
  📦 Ecosystem<span class="tok-operator">:</span> <span class="tok-number">5</span> packages

MCP Tools<span class="tok-operator">:</span> <span class="tok-number">8</span> registered
Resources<span class="tok-operator">:</span> <span class="tok-number">3</span> providers active</code></pre>
<p>The “orphaned READMEs” warning catches when you’ve unselected packages but their downloaded docs remain. Helpful for debugging unexpected search results.</p>
<h2>What’s Next</h2>
<p>The semantic tools (<code>search_symbols</code>, <code>search_conformances</code>, etc.) are built. Next steps:</p>
<ol><li><p><strong>Index all sample code</strong> - Run AST extraction across all 606 projects to populate symbol tables</p></li><li><p><strong>Cross-referencing</strong> - Link extracted symbols to their documentation pages</p></li><li><p><strong>Symbol coverage</strong> - Expand extraction to catch more edge cases</p></li></ol>
<p>The infrastructure is in place. Now to fill the database and refine the queries.</p>
<h2>Install or Update</h2>
<pre><code class="language-bash"><span class="tok-comment"># New install</span>
brew tap cupertinohq<span class="tok-operator">/</span>tap https<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>codeberg<span class="tok-operator">.</span>org<span class="tok-operator">/</span>CupertinoHQ<span class="tok-operator">/</span>homebrew<span class="tok-operator">-</span>tap<span class="tok-operator">.</span>git
brew install cupertinohq<span class="tok-operator">/</span>tap<span class="tok-operator">/</span>cupertino

<span class="tok-comment"># Update existing</span>
brew upgrade cupertino

<span class="tok-comment"># Or one-line install</span>
bash <span class="tok-operator">&lt;</span>(curl <span class="tok-operator">-</span>sSL https<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>codeberg<span class="tok-operator">.</span>org<span class="tok-operator">/</span>CupertinoHQ<span class="tok-operator">/</span>cupertino<span class="tok-operator">/</span>raw<span class="tok-operator">/</span>branch<span class="tok-operator">/</span>ma<span class="tok-keyword">in</span><span class="tok-operator">/</span>install<span class="tok-operator">.</span>sh)</code></pre>
<p>Then:</p>
<pre><code class="language-bash">cupertino setup  <span class="tok-comment"># Download databases (~30 seconds)</span>
cupertino serve  <span class="tok-comment"># Start MCP server</span></code></pre>
<p>Try the improved search:</p>
<pre><code class="language-bash"><span class="tok-comment"># See hierarchical numbering</span>
cupertino search <span class="tok-string">"SwiftUI navigation"</span> <span class="tok-operator">-</span><span class="tok-operator">-</span>source all

<span class="tok-comment"># Clean HIG results</span>
cupertino search <span class="tok-string">"tab bar"</span> <span class="tok-operator">-</span><span class="tok-operator">-</span>source hig

<span class="tok-comment"># Check your installation health</span>
cupertino doctor</code></pre>
<hr>
<p><em>Cupertino is an Apple Documentation MCP Server. 302,424 pages of Apple documentation, searchable by AI. Source at <a href="https://codeberg.org/CupertinoHQ/cupertino">codeberg.org/CupertinoHQ/cupertino</a>.</em></p>]]></content:encoded>
</item>
<item>
<title>Cupertino v0.7.0: 302K Docs, OS Version Filtering, and Teaching AI to Dig Deeper</title>
<link>https://aleahim.com/blog/cupertino-07-release/</link>
<guid isPermaLink="true">https://aleahim.com/blog/cupertino-07-release/</guid>
<pubDate>Mon, 15 Dec 2025 00:00:00 +0000</pubDate>
<description>The complete Apple documentation crawl is done. Now you can filter by iOS 17+ and Claude finally knows where to look for answers.</description>
<content:encoded><![CDATA[<h1>Cupertino v0.7.0: 302K Docs, OS Version Filtering, and Teaching AI to Dig Deeper</h1>
<p><strong>TL;DR:</strong> Finally crawled all of Apple’s docs. Added OS version filtering. Made AI agents smarter about finding the right documentation.</p>
<p>Four days after v0.5.0, and I’ve shipped two more releases. At this point I’m not sure if this is momentum or obsession. Let’s call it momentum.</p>
<h2>The Numbers</h2>
<table><thead><tr><th>Metric</th><th>v0.5.0</th><th>v0.7.0</th><th>Change</th></tr></thead><tbody><tr><td>Total Documents</td><td>234,331</td><td>302,424</td><td>+29%</td></tr><tr><td>Frameworks</td><td>287</td><td>307</td><td>+20</td></tr><tr><td>Database Size</td><td>~1.9GB</td><td>~2.3GB</td><td>+21%</td></tr></tbody></table>
<p>Three hundred thousand pages. That’s every framework, every class, every method Apple has documented. The crawl ran for almost two weeks total across multiple sessions.</p>
<h3>Top Frameworks by Document Count</h3>
<table><thead><tr><th>Framework</th><th style="text-align:right">Documents</th></tr></thead><tbody><tr><td>Kernel</td><td style="text-align:right">39,396</td></tr><tr><td>Matter</td><td style="text-align:right">24,320</td></tr><tr><td>Swift</td><td style="text-align:right">17,466</td></tr><tr><td>AppKit</td><td style="text-align:right">12,443</td></tr><tr><td>Foundation</td><td style="text-align:right">12,423</td></tr><tr><td>UIKit</td><td style="text-align:right">11,158</td></tr><tr><td>Accelerate</td><td style="text-align:right">9,114</td></tr><tr><td>SwiftUI</td><td style="text-align:right">7,062</td></tr></tbody></table>
<p>Kernel jumped from 25,000 to nearly 40,000 pages. That’s IOKit, BSD interfaces, Mach APIs, driver development - the low-level stuff that’s hardest to find and most valuable when you need it. Matter has 24,000+ docs for smart home development. Accelerate and SIMD documentation is fully indexed now - performance optimization guides that AI models probably didn’t see much of during training.</p>
<h2>The Feature I’ve Been Waiting For: OS Version Filtering</h2>
<p>This was issue #99, and it’s been on my mind since day one. Apple’s documentation includes version information - which iOS/macOS version introduced each API. We now extract this.</p>
<pre><code class="language-bash">cupertino search <span class="tok-string">"async"</span> <span class="tok-operator">-</span><span class="tok-operator">-</span>m<span class="tok-keyword">in</span><span class="tok-operator">-</span>ios <span class="tok-number">16</span></code></pre>
<p>Or in Claude:</p>
<blockquote><p>“Search for SwiftUI navigation APIs available in iOS 17+”</p></blockquote>
<p>The AI agent can now filter results by platform version. No more suggestions for deprecated iOS 12 APIs when you’re building for iOS 17.</p>
<h3>How It Works</h3>
<p>Different documentation sources need different strategies:</p>
<table><thead><tr><th>Source</th><th>Strategy</th></tr></thead><tbody><tr><td>apple-docs</td><td>Apple’s JSON API + fallbacks</td></tr><tr><td>sample-code</td><td>Derived from framework</td></tr><tr><td>apple-archive</td><td>Derived from framework</td></tr><tr><td>swift-evolution</td><td>Swift version mapping</td></tr><tr><td>swift-book/hig</td><td>Universal (all platforms)</td></tr></tbody></table>
<p>For Apple’s modern docs, there’s a hidden JSON endpoint at <code>/tutorials/data/documentation/</code> that returns structured metadata including availability. For older stuff, we derive from the framework - if it’s UIKit, we know the minimum versions.</p>
<p>The cool part? You can filter by <em>any</em> platform:</p>
<ul><li><p><code>--min-ios 16</code></p></li><li><p><code>--min-macos 14</code></p></li><li><p><code>--min-tvos 17</code></p></li><li><p><code>--min-watchos 10</code></p></li><li><p><code>--min-visionos 1</code></p></li></ul>
<p>visionOS filtering. For when you’re building spatial computing apps and need APIs that actually exist on the platform.</p>
<h2>Teaching AI Where to Look</h2>
<p>Here’s something I discovered while using Cupertino with Claude: the AI doesn’t know what it doesn’t know.</p>
<p>When Claude searches for “CALayer animation”, it gets API reference - classes, methods, properties. Good stuff. But the <em>conceptual</em> documentation - the “why” behind Core Animation - lives in Apple Archive. And Claude had no idea.</p>
<h3>The Problem</h3>
<p>I had added archive support in v0.2.3. Users could search with <code>--source apple-archive</code>. But AI agents don’t read release notes. They read tool descriptions once, then forget. Every search was missing the deep conceptual guides.</p>
<h3>The Solution: Teasers</h3>
<p>Now every search result includes hints from alternate sources:</p>
<pre><code class="language-markdown"># <span class="tok-type">Search</span> <span class="tok-type">Results</span> <span class="tok-keyword">for</span> <span class="tok-string">"CALayer animation"</span>

## <span class="tok-type">Results</span> from apple<span class="tok-operator">-</span><span class="tok-function">docs</span> (<span class="tok-number">5</span> found)

<span class="tok-number">1.</span> <span class="tok-operator">*</span><span class="tok-operator">*</span><span class="tok-type">CALayer</span><span class="tok-operator">*</span><span class="tok-operator">*</span> <span class="tok-operator">-</span> <span class="tok-type">The</span> basic object <span class="tok-keyword">for</span> managing and presenting drawable content<span class="tok-operator">.</span><span class="tok-operator">.</span><span class="tok-operator">.</span>
<span class="tok-number">2.</span> <span class="tok-operator">*</span><span class="tok-operator">*</span><span class="tok-type">CABasicAnimation</span><span class="tok-operator">*</span><span class="tok-operator">*</span> <span class="tok-operator">-</span> <span class="tok-type">An</span> animation that applies a single keyframe<span class="tok-operator">.</span><span class="tok-operator">.</span><span class="tok-operator">.</span>

<span class="tok-operator">-</span><span class="tok-operator">-</span><span class="tok-operator">-</span>

<span class="tok-operator">*</span><span class="tok-operator">*</span><span class="tok-type">Other</span> sources to explore<span class="tok-operator">:</span><span class="tok-operator">*</span><span class="tok-operator">*</span>

<span class="tok-operator">-</span> <span class="tok-operator">*</span><span class="tok-operator">*</span>apple<span class="tok-operator">-</span>archive<span class="tok-operator">*</span><span class="tok-operator">*</span><span class="tok-operator">:</span> <span class="tok-number">3</span> <span class="tok-function">results</span> (<span class="tok-type">Core</span> <span class="tok-type">Animation</span> <span class="tok-type">Programming</span> <span class="tok-type">Guide</span>, <span class="tok-type">Animation</span> <span class="tok-type">Types</span><span class="tok-operator">.</span><span class="tok-operator">.</span><span class="tok-operator">.</span>)
<span class="tok-operator">-</span> <span class="tok-operator">*</span><span class="tok-operator">*</span>samples<span class="tok-operator">*</span><span class="tok-operator">*</span><span class="tok-operator">:</span> <span class="tok-number">2</span> <span class="tok-function">results</span> (<span class="tok-type">LayerPlayer</span>, <span class="tok-type">AnimationExplorer</span>)
<span class="tok-operator">-</span> <span class="tok-operator">*</span><span class="tok-operator">*</span>swift<span class="tok-operator">-</span>evolution<span class="tok-operator">*</span><span class="tok-operator">*</span><span class="tok-operator">:</span> <span class="tok-number">1</span> <span class="tok-function">result</span> (<span class="tok-type">SE</span><span class="tok-number">-0421</span><span class="tok-operator">:</span> <span class="tok-type">Generalize</span> async sequence<span class="tok-operator">.</span><span class="tok-operator">.</span><span class="tok-operator">.</span>)

<span class="tok-type">Use</span> `source` parameter to search<span class="tok-operator">:</span> apple<span class="tok-operator">-</span>archive, samples, hig, swift<span class="tok-operator">-</span>evolution<span class="tok-operator">.</span><span class="tok-operator">.</span><span class="tok-operator">.</span></code></pre>
<p>Every search response is now a teaching moment. Claude sees there are archive guides available. It sees sample code exists. It learns <em>in context</em> how to dig deeper.</p>
<h2>Human Interface Guidelines: Design Docs for AI</h2>
<p>Here’s something I didn’t expect to matter as much as it does: HIG support.</p>
<p>Apple’s Human Interface Guidelines aren’t just for designers. When Claude is helping you build a button, a navigation pattern, or a settings screen, it should know Apple’s design recommendations - not just the API.</p>
<pre><code class="language-bash">cupertino search <span class="tok-string">"buttons"</span> <span class="tok-operator">-</span><span class="tok-operator">-</span>source hig</code></pre>
<p>Now when you ask “what’s the recommended button style for destructive actions?”, Claude can actually look it up instead of guessing. Platform-specific guidance for iOS, macOS, watchOS, visionOS - all searchable.</p>
<p>The HIG docs are marked as “universal” for availability - they apply across all OS versions. Design principles don’t deprecate like APIs do.</p>
<h2>One Tool to Rule Them All</h2>
<p>Speaking of simplification - I consolidated the search tools.</p>
<p>Before v0.7.0:</p>
<ul><li><p><code>search_docs</code> - Apple documentation</p></li><li><p><code>search_hig</code> - Human Interface Guidelines</p></li><li><p><code>search_samples</code> - Sample code</p></li><li><p>…and more</p></li></ul>
<p>Now:</p>
<ul><li><p><code>search</code> - Everything, with a <code>source</code> parameter</p></li></ul>
<pre><code class="language-bash"><span class="tok-comment"># CLI</span>
cupertino search <span class="tok-string">"buttons"</span> <span class="tok-operator">-</span><span class="tok-operator">-</span>source hig
cupertino search <span class="tok-string">"networking"</span> <span class="tok-operator">-</span><span class="tok-operator">-</span>source samples
cupertino search <span class="tok-string">"actors"</span> <span class="tok-operator">-</span><span class="tok-operator">-</span>source swift<span class="tok-operator">-</span>evolution
cupertino search <span class="tok-string">"everything"</span> <span class="tok-operator">-</span><span class="tok-operator">-</span>source all</code></pre>
<p>One tool, eight sources, cleaner mental model. The <code>source</code> parameter accepts:</p>
<ul><li><p><code>apple-docs</code> (default) - API reference</p></li><li><p><code>samples</code> - Working code examples</p></li><li><p><code>hig</code> - Design guidelines</p></li><li><p><code>apple-archive</code> - Legacy conceptual guides</p></li><li><p><code>swift-evolution</code> - Language proposals</p></li><li><p><code>swift-org</code> - Swift.org documentation</p></li><li><p><code>swift-book</code> - The Swift Programming Language</p></li><li><p><code>packages</code> - Swift package docs</p></li><li><p><code>all</code> (searches everything)</p></li></ul>
<p>The <code>all</code> option is surprisingly useful. When you’re not sure where something lives, just search everywhere.</p>
<h2>What I Learned</h2>
<p>Building for AI is different than building for humans.</p>
<p>Humans read documentation. They explore. They remember “oh yeah, there’s an archive section I should check.” AI agents don’t work that way. They have tool descriptions and search results. That’s it.</p>
<p>The best way to teach an AI agent? Put the lesson in every response it sees. Not in documentation it reads once. Not in tool descriptions it might forget. In the actual output, every single time.</p>
<p>That’s why teasers matter. That’s why source-aware messaging matters. Every search result is documentation.</p>
<h2>The Road Ahead</h2>
<p>With 302,424 pages indexed and version filtering working, the foundation is solid. What’s next:</p>
<ol><li><p><strong>Semantic code search</strong> - Search sample code by what it <em>does</em>, not just keywords</p></li><li><p><strong>Framework relationships</strong> - Search UIKit, get relevant Foundation results too</p></li><li><p><strong>Smarter ranking</strong> - Weight results by your target platform</p></li></ol>
<p>The crawl is complete. The curation continues.</p>
<h2>Install or Update</h2>
<pre><code class="language-bash"><span class="tok-comment"># New install</span>
brew tap cupertinohq<span class="tok-operator">/</span>tap https<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>codeberg<span class="tok-operator">.</span>org<span class="tok-operator">/</span>CupertinoHQ<span class="tok-operator">/</span>homebrew<span class="tok-operator">-</span>tap<span class="tok-operator">.</span>git
brew install cupertinohq<span class="tok-operator">/</span>tap<span class="tok-operator">/</span>cupertino

<span class="tok-comment"># Update existing</span>
brew upgrade cupertino

<span class="tok-comment"># Or one-line install</span>
bash <span class="tok-operator">&lt;</span>(curl <span class="tok-operator">-</span>sSL https<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>codeberg<span class="tok-operator">.</span>org<span class="tok-operator">/</span>CupertinoHQ<span class="tok-operator">/</span>cupertino<span class="tok-operator">/</span>raw<span class="tok-operator">/</span>branch<span class="tok-operator">/</span>ma<span class="tok-keyword">in</span><span class="tok-operator">/</span>install<span class="tok-operator">.</span>sh)</code></pre>
<p>Then:</p>
<pre><code class="language-bash">cupertino setup  <span class="tok-comment"># Download 2.3GB database (~30 seconds)</span>
cupertino serve  <span class="tok-comment"># Start MCP server</span></code></pre>
<p>Try the new features:</p>
<pre><code class="language-bash"><span class="tok-comment"># Filter by OS version</span>
cupertino search <span class="tok-string">"SwiftUI navigation"</span> <span class="tok-operator">-</span><span class="tok-operator">-</span>m<span class="tok-keyword">in</span><span class="tok-operator">-</span>ios <span class="tok-number">17</span>

<span class="tok-comment"># Search design guidelines</span>
cupertino search <span class="tok-string">"buttons destructive"</span> <span class="tok-operator">-</span><span class="tok-operator">-</span>source hig

<span class="tok-comment"># Search legacy conceptual guides</span>
cupertino search <span class="tok-string">"Core Animation"</span> <span class="tok-operator">-</span><span class="tok-operator">-</span>source apple<span class="tok-operator">-</span>archive

<span class="tok-comment"># Search everything at once</span>
cupertino search <span class="tok-string">"async await"</span> <span class="tok-operator">-</span><span class="tok-operator">-</span>source all</code></pre>
<hr>
<p><em>Cupertino is an Apple Documentation MCP Server. 302,424 pages of Apple documentation, searchable by AI. Source at <a href="https://codeberg.org/CupertinoHQ/cupertino">codeberg.org/CupertinoHQ/cupertino</a>.</em></p>]]></content:encoded>
</item>
<item>
<title>Cupertino v0.5.0: 234K Docs, 287 Frameworks, and the Roadmap Ahead</title>
<link>https://aleahim.com/blog/cupertino-05-release/</link>
<guid isPermaLink="true">https://aleahim.com/blog/cupertino-05-release/</guid>
<pubDate>Thu, 11 Dec 2025 00:00:00 +0000</pubDate>
<description>The biggest documentation update yet - nearly double the docs, plus what&apos;s coming next for the Apple documentation MCP server.</description>
<content:encoded><![CDATA[<h1>Cupertino v0.5.0: 234K Docs, 287 Frameworks, and the Roadmap Ahead</h1>
<p><strong>TL;DR:</strong> Documentation database nearly doubled. New release tooling. A roadmap for what’s next.</p>
<p>Two days after v0.4.0, we’re shipping v0.5.0. Why so fast? Because I finally finished a 5-day deep crawl of Apple’s documentation.</p>
<h2>The Numbers</h2>
<table><thead><tr><th>Metric</th><th>v0.4.0</th><th>v0.5.0</th><th>Change</th></tr></thead><tbody><tr><td>Total Documents</td><td>138,000</td><td>234,331</td><td>+70%</td></tr><tr><td>Frameworks</td><td>263</td><td>287</td><td>+24</td></tr><tr><td>Database Size</td><td>~1.2GB</td><td>~1.9GB</td><td>+58%</td></tr><tr><td>Crawl Time</td><td>20-24 hours</td><td>32-48 hours</td><td>Deeper</td></tr></tbody></table>
<p>The crawl ran for 5 days because I wanted <em>everything</em>. Every framework, every page, every deprecated API. The result: nearly 100,000 more documents than before.</p>
<h3>Top Frameworks by Document Count</h3>
<ol><li><p><strong>Kernel</strong> - 24,747 docs</p></li><li><p><strong>Matter</strong> - 22,013 docs</p></li><li><p><strong>Swift</strong> - 17,466 docs</p></li><li><p><strong>Foundation</strong> - 14,892 docs</p></li><li><p><strong>UIKit</strong> - 12,341 docs</p></li></ol>
<p>Kernel documentation alone has almost 25,000 pages. That’s IOKit, BSD interfaces, Mach APIs - the stuff that’s usually impossible to find.</p>
<h2>What This Means for AI</h2>
<p>When Claude searches Cupertino now, it has access to:</p>
<ul><li><p><strong>Low-level APIs</strong> - Kernel programming, IOKit drivers, Mach ports</p></li><li><p><strong>Hardware interfaces</strong> - Matter protocol (24k+ docs for smart home development)</p></li><li><p><strong>Complete Swift coverage</strong> - Every proposal, every standard library type</p></li><li><p><strong>Obscure frameworks</strong> - The ones you forget exist until you need them</p></li></ul>
<p>This is deterministic access to Apple’s documentation. No hallucination, no guessing from training data. Just the actual docs.</p>
<h2>Why I Built Release Automation</h2>
<p>I remember the release oopsie from v0.4.0 like it was just a few days ago. Wait, it <em>was</em> just a few days ago. 😅 I tagged before committing the version bump, had to delete tags, rebuild everything, update SHA256 checksums twice…</p>
<p>Almost happened again with v0.5.0. Caught it in time, but the close call was enough.</p>
<p>So I finally wrote the automation I’d been putting off. Not because users need it - they don’t. But because <em>I</em> was losing hours to release mechanics instead of building features.</p>
<p>The release process now touches 4 repositories (main repo, cupertino-docs for databases, Homebrew tap, and the blog). Miss one step, wrong order, typo in a version number - and you’re starting over.</p>
<p>Now it’s one command. I run it, go make coffee, come back to a published release. That’s the dream, anyway. We’ll see how v0.6.0 goes.</p>
<h2>The Roadmap</h2>
<p>The current roadmap lives with the <a href="https://codeberg.org/CupertinoHQ/cupertino">Cupertino repository</a>. Here’s what’s coming next, in dependency order:</p>
<h3>Phase 1: Complete the Crawl (#88)</h3>
<p>The crawl is extensive but not complete. Some frameworks have deep hierarchies that weren’t fully traversed. The goal is 100% coverage of Apple’s public documentation.</p>
<p><strong>Status:</strong> In progress. Currently at depth level 8. No idea when it finishes - some frameworks go deep.</p>
<h3>Phase 2: Prune the Database (#87)</h3>
<p>Once we have everything, we need to clean it up. The database has:</p>
<ul><li><p>Duplicate entries (same doc indexed multiple times)</p></li><li><p>Misclassified documents (wrong framework attribution)</p></li><li><p>Orphaned entries from URL changes</p></li></ul>
<p>This is blocked by #88 - no point pruning until the crawl is complete.</p>
<h3>Phase 3: API Version Tracking (#99)</h3>
<p>This is the big one. Apple’s documentation includes version information - which iOS/macOS version introduced each API. We can extract this.</p>
<p>Imagine searching with:</p>
<pre><code class="language-bash">cupertino search <span class="tok-string">"async"</span> <span class="tok-operator">-</span><span class="tok-operator">-</span>m<span class="tok-keyword">in</span><span class="tok-operator">-</span>os ios<span class="tok-number">16</span></code></pre>
<p>Or having Claude automatically filter out deprecated APIs when you’re targeting iOS 17+.</p>
<p>I’ve been investigating Apple’s <code>/tutorials/data/documentation/</code> endpoint. It returns JSON with version metadata for most frameworks. Some older frameworks (Kernel, IOKit) don’t have it, but the coverage is good.</p>
<p><strong>Status:</strong> Research complete. Implementation pending.</p>
<h3>Phase 4: Version-Filtered Search (#100)</h3>
<p>Once we have version data in the database, we can expose it as a search filter. Both CLI and MCP tool would support filtering by minimum OS version.</p>
<p><strong>Blocked by:</strong> #99 (need the data first)</p>
<h3>Phase 5: Semantic Code Search (#81)</h3>
<p>The end goal: search sample code by what it <em>does</em>, not just by keyword.</p>
<pre><code class="language-bash">cupertino search <span class="tok-string">"async image loading with caching"</span></code></pre>
<p>This requires:</p>
<ol><li><p>Complete documentation (#88)</p></li><li><p>Clean database (#87)</p></li><li><p>SwiftSyntax AST indexing of sample code</p></li></ol>
<p>It’s ambitious, but the foundation is being laid with each release.</p>
<h2>Install or Update</h2>
<pre><code class="language-bash"><span class="tok-comment"># New install</span>
brew tap cupertinohq<span class="tok-operator">/</span>tap https<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>codeberg<span class="tok-operator">.</span>org<span class="tok-operator">/</span>CupertinoHQ<span class="tok-operator">/</span>homebrew<span class="tok-operator">-</span>tap<span class="tok-operator">.</span>git
brew install cupertinohq<span class="tok-operator">/</span>tap<span class="tok-operator">/</span>cupertino

<span class="tok-comment"># Update</span>
brew upgrade cupertino

<span class="tok-comment"># Or instant setup</span>
bash <span class="tok-operator">&lt;</span>(curl <span class="tok-operator">-</span>sSL https<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>codeberg<span class="tok-operator">.</span>org<span class="tok-operator">/</span>CupertinoHQ<span class="tok-operator">/</span>cupertino<span class="tok-operator">/</span>raw<span class="tok-operator">/</span>branch<span class="tok-operator">/</span>ma<span class="tok-keyword">in</span><span class="tok-operator">/</span>install<span class="tok-operator">.</span>sh)</code></pre>
<p>After installing, databases download automatically:</p>
<pre><code class="language-bash">cupertino setup</code></pre>
<p>Takes about 30 seconds to download the pre-built 1.9GB database.</p>
<h2>The Vision: Human-Curated Developer Docs</h2>
<p>Here’s what I’ve learned: having <em>all</em> the docs isn’t enough. We need them <em>in context</em>.</p>
<p>When you’re building an app targeting iOS 15+, you don’t want to see iOS 12 APIs. When you’re learning Core Animation, you want the conceptual guides from Apple Archive, not just the API reference. When you’re debugging a Matter integration, you need the 22,000 Matter docs filtered to what’s actually relevant.</p>
<p>Raw documentation is noise. Curated documentation is signal.</p>
<p>That’s where Cupertino is heading. Not just “here’s 234,331 docs” but “here’s what you actually need for your specific context.” Version filtering is coming - ask for iOS 16+ APIs only. Source awareness is coming - archive guides surfaced alongside modern reference. Framework relationships are coming - search UIKit and get relevant Foundation results too.</p>
<p>The crawl is the foundation. The curation is the value.</p>
<p>234,331 documents today. Context-aware, version-filtered, human-curated search tomorrow.</p>
<hr>
<p><em>Cupertino is an Apple Documentation MCP Server. Install with <code>brew tap cupertinohq/tap https://codeberg.org/CupertinoHQ/homebrew-tap.git brew install cupertinohq/tap/cupertino</code>. Source at <a href="https://codeberg.org/CupertinoHQ/cupertino">codeberg.org/CupertinoHQ/cupertino</a>.</em></p>]]></content:encoded>
</item>
<item>
<title>Cupertino v0.4.0: HIG Support, Framework Aliases, and Release Engineering Lessons</title>
<link>https://aleahim.com/blog/cupertino-04-release/</link>
<guid isPermaLink="true">https://aleahim.com/blog/cupertino-04-release/</guid>
<pubDate>Tue, 09 Dec 2025 00:00:00 +0000</pubDate>
<description>New features for Apple documentation search, plus a cautionary tale about release processes and why order matters when tagging.</description>
<content:encoded><![CDATA[<h1>Cupertino v0.4.0: HIG Support, Framework Aliases, and Release Engineering Lessons</h1>
<p><strong>TL;DR:</strong> Human Interface Guidelines support, smarter framework search, and a hard lesson about release engineering.</p>
<p>Why jump from 0.3.4 to 0.4.0? Five issues closed: HIG support (#69), framework aliases (#91, #92), and Swift.org fixes (#93, #94). Enough new functionality to warrant a minor version bump.</p>
<h2>What’s New</h2>
<h3>Human Interface Guidelines Support</h3>
<p>Cupertino can now crawl and index Apple’s Human Interface Guidelines:</p>
<pre><code class="language-bash">cupertino fetch <span class="tok-operator">-</span><span class="tok-operator">-</span>type hig</code></pre>
<p>There’s also a new <code>search_hig</code> MCP tool that lets AI agents search design guidelines with platform and category filters. When you ask Claude “what are Apple’s recommendations for buttons?”, it can actually look it up instead of guessing from training data.</p>
<p>Everything available via MCP tools is also available via command line:</p>
<pre><code class="language-bash">cupertino search <span class="tok-string">"buttons"</span> <span class="tok-operator">-</span><span class="tok-operator">-</span>source hig</code></pre>
<p>This is something people might not realize: you can query Cupertino the same way AI does. Every MCP tool has a CLI equivalent. Want to see exactly what Claude sees when it searches? Run the same query yourself. Useful for testing, debugging, or when you just want quick answers without a conversation.</p>
<h3>Framework Aliases</h3>
<p>Before, searching for “Core Animation” might not find results indexed under “QuartzCore” (the actual import name). Now there are 249 framework aliases in the database:</p>
<ul><li><p><code>QuartzCore</code> ↔ <code>CoreAnimation</code> ↔ <code>Core Animation</code></p></li><li><p><code>CoreGraphics</code> ↔ <code>Quartz2D</code> ↔ <code>Quartz 2D</code></p></li></ul>
<p>Search works regardless of which name variant you use.</p>
<h3>Swift.org Fixes</h3>
<p>The Swift.org crawler was broken. The base URL had changed from <code>docs.swift.org</code> to <code>www.swift.org/documentation/</code>, and the indexer was looking for <code>.md</code> files when the crawler was saving <code>.json</code> files. Fixed.</p>
<h2>Why This Matters</h2>
<p>Here’s a real example from today. I asked Claude about Apple’s Foundation Models framework - the new on-device ML APIs. Claude’s response:</p>
<blockquote><p>“The MCP server pulls the complete Foundation Models sample code documentation - straight from Apple’s current docs. No hallucinated API names!”</p></blockquote>
<p>More examples of documentation you can search:</p>
<ul><li><p><strong>FoundationModels</strong> - Apple’s new on-device ML framework</p></li><li><p><strong>Writing Tools</strong> - System-wide AI writing assistance</p></li><li><p><strong>Genmoji</strong> - Custom emoji generation</p></li><li><p><strong>Image Playground</strong> - On-device image generation</p></li></ul>
<p>This is also why Cupertino needs a human touch - it can’t be fully automated. Someone has to notice when Apple adds new frameworks, when URLs change (like Swift.org did), when documentation structures shift. The crawler is automated, but the awareness isn’t.</p>
<p>That’s the whole point: deterministic, up-to-date docs instead of training data guesses.</p>
<h3>Token Efficiency</h3>
<p>A side benefit: Cupertino is more token-efficient than you’d expect.</p>
<p>Yes, tool results use tokens - documentation goes into context. But:</p>
<ul><li><p><strong>Accurate = fewer turns</strong> - No hallucination means no correction loop</p></li><li><p><strong>Curated content</strong> - Returns exactly what’s needed, not web search noise</p></li><li><p><strong>Local search is free</strong> - No API cost for the search operation itself</p></li><li><p><strong>Truncated summaries</strong> - Results are summarized; full doc only when you call <code>read_document</code></p></li></ul>
<p>So not zero tokens, but more <em>efficient</em> token use. And no external API costs for search.</p>
<h2>The Archive Discovery Problem</h2>
<p>Here’s something interesting I discovered while testing. When Claude searched for “CALayer animation”, it got API reference pages - classes, methods, properties. Claude wanted to dig deeper:</p>
<blockquote><p>“Let me try a more targeted search - something like the layer tree architecture or model/presentation layers that would be covered in the Core Animation Programming Guide.”</p></blockquote>
<p>Then I suggested searching with <code>source: "apple-archive"</code>. Claude’s reaction:</p>
<blockquote><p>“Now THAT’s the difference! When searching with source: apple-archive, we get the Core Animation Programming Guide and the Animation Types and Timing Programming Guide - the real meat.”</p></blockquote>
<p>The conceptual deep-dives that explain <em>why</em> things work the way they do.</p>
<p>Modern Apple docs = API reference (the <em>what</em>)
Archive docs = Conceptual guides (the <em>why</em>)</p>
<p>The problem? AI agents don’t know to search the archive. It’s an opt-in parameter buried in the tool description.</p>
<h3>The Solution: Auto-Surface Archive Teasers</h3>
<p>I’m planning to modify search results to automatically hint at archive content:</p>
<pre><code class="language-markdown"># <span class="tok-type">Search</span> <span class="tok-type">Results</span> <span class="tok-keyword">for</span> <span class="tok-string">"CALayer animation"</span>

## <span class="tok-type">Modern</span> <span class="tok-type">Documentation</span>
[<span class="tok-number">1</span>] <span class="tok-type">CALayer</span> <span class="tok-operator">-</span> <span class="tok-type">API</span> reference<span class="tok-operator">.</span><span class="tok-operator">.</span><span class="tok-operator">.</span>
[<span class="tok-number">2</span>] <span class="tok-type">CABasicAnimation</span> <span class="tok-operator">-</span> <span class="tok-type">API</span> reference<span class="tok-operator">.</span><span class="tok-operator">.</span><span class="tok-operator">.</span>

<span class="tok-operator">-</span><span class="tok-operator">-</span><span class="tok-operator">-</span>

## 📚 <span class="tok-type">Related</span> <span class="tok-type">Archive</span> <span class="tok-type">Guides</span>

<span class="tok-operator">*</span><span class="tok-operator">*</span><span class="tok-type">Core</span> <span class="tok-type">Animation</span> <span class="tok-type">Programming</span> <span class="tok-type">Guide</span><span class="tok-operator">*</span><span class="tok-operator">*</span> <span class="tok-operator">-</span> <span class="tok-type">Layer</span> geometry, animation timing<span class="tok-operator">.</span><span class="tok-operator">.</span><span class="tok-operator">.</span>

<span class="tok-operator">&gt;</span> <span class="tok-keyword">For</span> full archive content, search with `source<span class="tok-operator">:</span> <span class="tok-string">"apple-archive"</span>`</code></pre>
<p>This way:</p>
<ul><li><p>Modern docs still come first</p></li><li><p>AI agents always see archive exists</p></li><li><p>They learn how to get more, in context</p></li></ul>
<p>Only show the archive section if there are relevant results. Same principle could apply to sample code, Swift packages, or any other source.</p>
<h2>The Release Process Disaster</h2>
<p>Here’s where it gets interesting. I had all the code ready, all the tests passing, and I was ready to ship.</p>
<p>The process seemed simple:</p>
<ol><li><p>Bump version in <code>Constants.swift</code></p></li><li><p>Update <code>README.md</code> and <code>CHANGELOG.md</code></p></li><li><p>Create git tag</p></li><li><p>Push tag (triggers GitHub Actions build)</p></li><li><p>Upload databases to <code>cupertino-docs</code></p></li><li><p>Update Homebrew formula</p></li></ol>
<p>What could go wrong?</p>
<h3>The Tag Timing Problem</h3>
<p>I merged my feature branch, created the tag, pushed it… and then realized I hadn’t committed the version bump to <code>main</code> yet. The tag pointed to a commit where <code>Constants.swift</code> still said <code>0.3.5</code>.</p>
<p>GitHub Actions dutifully built a beautiful, signed, notarized universal binary… that reported version <code>0.3.5</code>.</p>
<pre><code class="language-bash"><span class="tok-property">$</span> cupertino <span class="tok-operator">-</span><span class="tok-operator">-</span>version
<span class="tok-number">0.3.5</span></code></pre>
<p>When users ran <code>cupertino setup</code>, it tried to download databases from <code>v0.3.5</code> instead of <code>v0.4.0</code>. Everything was broken.</p>
<h3>The Fix</h3>
<p>I had to:</p>
<ol><li><p>Delete the tag on GitHub</p></li><li><p>Delete the local tag</p></li><li><p>Make sure the version bump commit was pushed to <code>main</code></p></li><li><p>Verify the built binary reports correct version <strong>before</strong> tagging</p></li><li><p>Recreate the tag</p></li><li><p>Wait for GitHub Actions to rebuild</p></li><li><p>Update the Homebrew formula with the new SHA256</p></li></ol>
<h3>The SHA256 Dance</h3>
<p>When I first updated the Homebrew formula, I grabbed the SHA256 from the old (broken) binary. After rebuilding, the checksum changed. Users got:</p>
<pre><code>Error: Formula reports different checksum: cf035352...
SHA-256 checksum of downloaded file: 5c5cf7ab...</code></pre>
<p>Another round of updating the tap.</p>
<h2>The 11-Step Release Process</h2>
<p>After today’s adventures, here’s what the release process actually looks like:</p>
<ol><li><p>Update version in <code>Constants.swift</code>, <code>README.md</code>, <code>CHANGELOG.md</code></p></li><li><p>Commit and push to main</p></li><li><p>Build locally and verify <code>--version</code> matches</p></li><li><p>Create and push tag</p></li><li><p>Wait for GitHub Actions (~5 min)</p></li><li><p>Create GitHub release with notes</p></li><li><p>Build locally and install</p></li><li><p>Upload databases with <code>cupertino release</code></p></li><li><p>Get new SHA256 from release</p></li><li><p>Update Homebrew tap formula</p></li><li><p>Verify on fresh machine</p></li></ol>
<p>That’s 11 steps across 4 repositories. Miss one, and you’re rebuilding everything.</p>
<h2>Time for Automation?</h2>
<p>I’m seriously considering writing a release script. Swift or Bash?</p>
<p>The pragmatic choice is Bash - it’s just orchestrating CLI commands. But I’m a Swift purist. The <code>cupertino release</code> command already handles database uploads with proper GitHub API integration. Extending it to handle the full workflow feels right.</p>
<p>Something like <code>cupertino release --full</code> that:</p>
<ol><li><p>Checks for uncommitted changes</p></li><li><p>Verifies version consistency</p></li><li><p>Builds and validates <code>--version</code></p></li><li><p>Creates and pushes the tag</p></li><li><p>Waits for GitHub Actions</p></li><li><p>Uploads databases</p></li><li><p>Updates the Homebrew tap</p></li></ol>
<p>One command. No mistakes. Written in Swift.</p>
<h2>Lessons Learned</h2>
<ol><li><p><strong>Order matters.</strong> Commit the version bump before creating the tag.</p></li><li><p><strong>Verify before you ship.</strong> Build locally and check <code>--version</code> before tagging.</p></li><li><p><strong>Document your release process.</strong> I now have a detailed <code>DEPLOYMENT.md</code> with warnings.</p></li><li><p><strong>Automate what hurts.</strong> If you make the same mistake twice, write a script.</p></li></ol>
<h2>What’s Next</h2>
<ul><li><p><strong>Enhanced search results</strong> - Auto-surface archive, samples, packages in every search (high priority)</p></li><li><p><strong>Release automation</strong> - <code>cupertino release --full</code> in Swift</p></li><li><p><strong>Fix setup animations</strong> - They’re broken</p></li><li><p><strong>Keep crawling</strong> - Fresh docs matter</p></li></ul>
<p>This release taught me a lot. Not just about release engineering, but about how AI agents actually use documentation. The archive discovery problem was a surprise - valuable content hidden behind an opt-in flag that agents don’t know to use.</p>
<p>Building tools for AI is different. You’re not just building for humans who read documentation. You’re building for agents that learn from tool descriptions and in-context hints.</p>
<p>Key discovery: Claude reads tool descriptions once, but reads search results every time. If you want agents to use a feature, put it in the output - not buried in documentation they’ll forget. Every search result is a teaching moment.</p>
<p>Next up: implementing enhanced search results. Once that ships, every search will auto-surface relevant content from archives, samples, and packages - with the exact commands to dig deeper. No more hidden treasure.</p>
<hr>
<p><em>Cupertino is an Apple Documentation MCP Server. Install with <code>brew tap cupertinohq/tap https://codeberg.org/CupertinoHQ/homebrew-tap.git brew install cupertinohq/tap/cupertino</code>. Check it out at <a href="https://codeberg.org/CupertinoHQ/cupertino">codeberg.org/CupertinoHQ/cupertino</a>.</em></p>]]></content:encoded>
</item>
<item>
<title>Cupertino v0.3.4: One-Line Install &amp; 150K+ Apple Docs</title>
<link>https://aleahim.com/blog/cupertino-one-line-install/</link>
<guid isPermaLink="true">https://aleahim.com/blog/cupertino-one-line-install/</guid>
<pubDate>Sat, 06 Dec 2025 00:00:00 +0000</pubDate>
<description>Install Cupertino with a single command - signed, notarized, and ready in seconds. Now with 150,000+ Apple documentation pages.</description>
<content:encoded><![CDATA[<h1>Cupertino v0.3.4: One-Line Install &amp; 150K+ Apple Docs</h1>
<p><strong>TL;DR:</strong> One command installs everything. Homebrew is also available.</p>
<pre><code class="language-bash">bash <span class="tok-operator">&lt;</span>(curl <span class="tok-operator">-</span>sSL https<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>codeberg<span class="tok-operator">.</span>org<span class="tok-operator">/</span>CupertinoHQ<span class="tok-operator">/</span>cupertino<span class="tok-operator">/</span>raw<span class="tok-operator">/</span>branch<span class="tok-operator">/</span>ma<span class="tok-keyword">in</span><span class="tok-operator">/</span>install<span class="tok-operator">.</span>sh)</code></pre>
<h2>What Changed</h2>
<p>In v0.3.0, I introduced <code>cupertino setup</code> to download pre-built databases. But you still had to clone the repo and build from source first. That’s two steps too many.</p>
<p>Now it’s one line:</p>
<pre><code class="language-bash">bash <span class="tok-operator">&lt;</span>(curl <span class="tok-operator">-</span>sSL https<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>codeberg<span class="tok-operator">.</span>org<span class="tok-operator">/</span>CupertinoHQ<span class="tok-operator">/</span>cupertino<span class="tok-operator">/</span>raw<span class="tok-operator">/</span>branch<span class="tok-operator">/</span>ma<span class="tok-keyword">in</span><span class="tok-operator">/</span>install<span class="tok-operator">.</span>sh)</code></pre>
<p>This:</p>
<ol><li><p>Downloads a pre-built universal binary (arm64 + x86_64)</p></li><li><p>Installs to <code>/usr/local/bin</code></p></li><li><p>Downloads documentation databases (~230 MB)</p></li></ol>
<p>Total time: under a minute.</p>
<h2>Signed and Notarized</h2>
<p>Getting a command-line tool to “just work” on macOS is harder than it sounds. Without proper signing, users see scary “unidentified developer” warnings. Without notarization, Gatekeeper blocks execution entirely.</p>
<p>The release workflow now handles this automatically:</p>
<ol><li><p><strong>GitHub Actions</strong> builds a universal binary (arm64 + x86_64) on every release tag</p></li><li><p><strong>Developer ID signing</strong> with my Apple Developer certificate</p></li><li><p><strong>Apple notarization</strong> submits to Apple’s servers for malware scanning</p></li><li><p><strong>Preserved signatures</strong> using <code>ditto</code> instead of <code>cp</code> to maintain code signing through the install process</p></li></ol>
<p>The result: download, install, run. No security dialogs. No right-click workarounds. No “are you sure?” prompts.</p>
<p>This took several iterations to get right—the signing must happen in CI, notarization requires waiting for Apple’s servers, and even copying the file wrong can strip the signature. But now it’s automated and works every time.</p>
<h2>Homebrew Support</h2>
<p>Prefer Homebrew? Now available:</p>
<pre><code class="language-bash">brew tap cupertinohq<span class="tok-operator">/</span>tap https<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>codeberg<span class="tok-operator">.</span>org<span class="tok-operator">/</span>CupertinoHQ<span class="tok-operator">/</span>homebrew<span class="tok-operator">-</span>tap<span class="tok-operator">.</span>git
brew install cupertinohq<span class="tok-operator">/</span>tap<span class="tok-operator">/</span>cupertino
cupertino setup</code></pre>
<p>Three lines instead of one, but integrates with your existing Homebrew workflow.</p>
<h2>150,000+ Apple Documentation Pages</h2>
<p>The database has grown significantly:</p>
<table><thead><tr><th>Version</th><th>Documentation Pages</th><th>Sample Projects</th></tr></thead><tbody><tr><td>v0.2.x</td><td>~21,000</td><td>606</td></tr><tr><td>v0.3.0</td><td>~138,000</td><td>606</td></tr><tr><td>v0.3.4</td><td>~150,000+</td><td>606</td></tr></tbody></table>
<p>More coverage means better search results for your AI assistant.</p>
<h2>Demo Video</h2>
<p>Watch the one-line install and Claude Desktop in action:</p>
<p><a href="https://youtu.be/B-mRdainTMA"><img src="https://img.youtube.com/vi/B-mRdainTMA/0.jpg" alt="Demo Video"></a></p>
<h2>Three Install Methods</h2>
<h3>1. One-Line Install (Recommended)</h3>
<pre><code class="language-bash">bash <span class="tok-operator">&lt;</span>(curl <span class="tok-operator">-</span>sSL https<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>codeberg<span class="tok-operator">.</span>org<span class="tok-operator">/</span>CupertinoHQ<span class="tok-operator">/</span>cupertino<span class="tok-operator">/</span>raw<span class="tok-operator">/</span>branch<span class="tok-operator">/</span>ma<span class="tok-keyword">in</span><span class="tok-operator">/</span>install<span class="tok-operator">.</span>sh)</code></pre>
<p>Downloads binary and databases. Ready in under a minute.</p>
<h3>2. Homebrew</h3>
<pre><code class="language-bash">brew tap cupertinohq<span class="tok-operator">/</span>tap https<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>codeberg<span class="tok-operator">.</span>org<span class="tok-operator">/</span>CupertinoHQ<span class="tok-operator">/</span>homebrew<span class="tok-operator">-</span>tap<span class="tok-operator">.</span>git
brew install cupertinohq<span class="tok-operator">/</span>tap<span class="tok-operator">/</span>cupertino
cupertino setup</code></pre>
<p>Integrates with Homebrew for updates via <code>brew upgrade cupertino</code>.</p>
<h3>3. Build from Source</h3>
<pre><code class="language-bash">git clone https<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>codeberg<span class="tok-operator">.</span>org<span class="tok-operator">/</span>CupertinoHQ<span class="tok-operator">/</span>cupertino<span class="tok-operator">.</span>git
cd cupertino <span class="tok-operator">&amp;</span><span class="tok-operator">&amp;</span> make build <span class="tok-operator">&amp;</span><span class="tok-operator">&amp;</span> su<span class="tok-keyword">do</span> make install
cupertino setup</code></pre>
<p>For those who want to inspect or modify the code.</p>
<h2>What’s Inside</h2>
<p>After installation, you have:</p>
<ul><li><p><code>/usr/local/bin/cupertino</code> - The MCP server binary</p></li><li><p><code>~/.cupertino/search.db</code> - 150K+ documentation pages</p></li><li><p><code>~/.cupertino/samples.db</code> - 606 sample projects, 18K+ source files</p></li></ul>
<p>Start the server and configure Claude Desktop:</p>
<pre><code class="language-bash">cupertino serve</code></pre>
<pre><code class="language-json"><span class="tok-operator">{</span>
  <span class="tok-property">"mcpServers"</span><span class="tok-operator">:</span> <span class="tok-operator">{</span>
    <span class="tok-property">"cupertino"</span><span class="tok-operator">:</span> <span class="tok-operator">{</span>
      <span class="tok-property">"command"</span><span class="tok-operator">:</span> <span class="tok-string">"/usr/local/bin/cupertino"</span>
    <span class="tok-operator">}</span>
  <span class="tok-operator">}</span>
<span class="tok-operator">}</span></code></pre>
<h2>Registry Submissions</h2>
<p>I’ve submitted Cupertino to the major MCP server registries:</p>
<ul><li><p><a href="https://mcpservers.org">mcpservers.org</a></p></li><li><p><a href="https://pulsemcp.com">PulseMCP</a></p></li><li><p><a href="https://github.com/punkpeye/awesome-mcp-servers">awesome-mcp-servers</a></p></li></ul>
<p>These directories help developers discover MCP servers—getting listed would bring more visibility to the Apple developer community.</p>
<h2>Links</h2>
<ul><li><p><strong>Codeberg:</strong> <a href="https://codeberg.org/CupertinoHQ/cupertino">codeberg.org/CupertinoHQ/cupertino</a></p></li><li><p><strong>Homebrew Tap:</strong> <a href="https://codeberg.org/CupertinoHQ/homebrew-tap">codeberg.org/CupertinoHQ/homebrew-tap</a></p></li><li><p><strong>Demo Video:</strong> <a href="https://youtu.be/B-mRdainTMA">YouTube</a></p></li></ul>
<hr>
<p><em>The best install experience is no install experience. One line is close enough.</em></p>]]></content:encoded>
</item>
<item>
<title>Cupertino v0.3.0: From 20 Hours to 30 Seconds</title>
<link>https://aleahim.com/blog/cupertino-instant-setup/</link>
<guid isPermaLink="true">https://aleahim.com/blog/cupertino-instant-setup/</guid>
<pubDate>Fri, 05 Dec 2025 00:00:00 +0000</pubDate>
<description>The new setup command downloads pre-built databases instantly - no more crawling Apple&apos;s documentation for hours</description>
<content:encoded><![CDATA[<h1>Cupertino v0.3.0: From 20 Hours to 30 Seconds</h1>
<p><strong>TL;DR:</strong> <code>cupertino setup</code> now downloads pre-built databases in ~30 seconds. No more 20-hour crawls.</p>
<h2>The Problem With v0.2.x</h2>
<p>When I released Cupertino, the setup process looked like this:</p>
<pre><code class="language-bash">cupertino fetch <span class="tok-operator">-</span><span class="tok-operator">-</span>type docs <span class="tok-operator">-</span><span class="tok-operator">-</span>max<span class="tok-operator">-</span>pages <span class="tok-number">15000</span>  <span class="tok-comment"># ~20-24 hours</span>
cupertino fetch <span class="tok-operator">-</span><span class="tok-operator">-</span>type evolution               <span class="tok-comment"># ~5 minutes</span>
cupertino save                                 <span class="tok-comment"># ~5 minutes</span></code></pre>
<p>Twenty hours. To respect Apple’s servers, the crawler waits 0.5 seconds between requests. 21,000 pages × 0.5s = a very long initial setup.</p>
<p>Users loved the tool but hated the onboarding. Fair.</p>
<h2>The Solution: Pre-built Databases</h2>
<p>Instead of everyone crawling Apple’s documentation independently, I now publish pre-built databases on GitHub Releases. The new setup flow:</p>
<pre><code class="language-bash">cupertino setup   <span class="tok-comment"># ~30 seconds</span>
cupertino serve   <span class="tok-comment"># Start MCP server</span></code></pre>
<p>That’s it. Two commands. Under a minute.</p>
<h2>How It Works</h2>
<h3>For Users</h3>
<p>The <code>setup</code> command downloads a zip file from GitHub Releases containing:</p>
<ul><li><p><code>search.db</code> (~1.1 GB) - 138,000+ documentation pages</p></li><li><p><code>samples.db</code> (~183 MB) - 606 sample projects, 18,000+ source files</p></li></ul>
<pre><code class="language-bash"><span class="tok-property">$</span> cupertino setup

📦 Cupertino Setup

⬇️  Downloading Databases<span class="tok-operator">.</span><span class="tok-operator">.</span><span class="tok-operator">.</span>
   ⠹ [████████████████░░░░░░░░░░░░░░]  <span class="tok-number">53</span><span class="tok-operator">%</span> (<span class="tok-number">121.0</span> MB<span class="tok-operator">/</span><span class="tok-number">228.1</span> MB)
   ✓ <span class="tok-function">Databases</span> (<span class="tok-number">228.1</span> MB)
📂 Extracting databases<span class="tok-operator">.</span><span class="tok-operator">.</span><span class="tok-operator">.</span>
   ✓ Extracted

✅ Setup complete<span class="tok-operator">!</span>
   Documentation<span class="tok-operator">:</span> <span class="tok-operator">/</span>Users<span class="tok-operator">/</span>you<span class="tok-operator">/</span><span class="tok-operator">.</span>cupertino<span class="tok-operator">/</span>search<span class="tok-operator">.</span>db
   Sample code<span class="tok-operator">:</span>   <span class="tok-operator">/</span>Users<span class="tok-operator">/</span>you<span class="tok-operator">/</span><span class="tok-operator">.</span>cupertino<span class="tok-operator">/</span>samples<span class="tok-operator">.</span>db

💡 Start the server with<span class="tok-operator">:</span> cupertino serve</code></pre>
<p>If databases already exist, it skips the download:</p>
<pre><code class="language-bash"><span class="tok-property">$</span> cupertino setup

📦 Cupertino Setup

✅ Databases already exist
   Documentation<span class="tok-operator">:</span> <span class="tok-operator">/</span>Users<span class="tok-operator">/</span>you<span class="tok-operator">/</span><span class="tok-operator">.</span>cupertino<span class="tok-operator">/</span>search<span class="tok-operator">.</span>db
   Sample code<span class="tok-operator">:</span>   <span class="tok-operator">/</span>Users<span class="tok-operator">/</span>you<span class="tok-operator">/</span><span class="tok-operator">.</span>cupertino<span class="tok-operator">/</span>samples<span class="tok-operator">.</span>db

💡 Use <span class="tok-operator">-</span><span class="tok-operator">-</span>force to overwrite with latest version
💡 Start the server with<span class="tok-operator">:</span> cupertino serve</code></pre>
<h3>For Me (Maintainer)</h3>
<p>A new <code>release</code> command automates publishing:</p>
<pre><code class="language-bash"><span class="tok-property">$</span> cupertino release

📦 Cupertino Release v<span class="tok-number">0.3.0</span>

📊 Database sizes<span class="tok-operator">:</span>
   search<span class="tok-operator">.</span>db<span class="tok-operator">:</span>  <span class="tok-number">1.2</span> GB
   samples<span class="tok-operator">.</span>db<span class="tok-operator">:</span> <span class="tok-number">192.2</span> MB

📁 Creating cupertino<span class="tok-operator">-</span>databases<span class="tok-operator">-</span>v<span class="tok-number">0.3.0.</span>zip<span class="tok-operator">.</span><span class="tok-operator">.</span><span class="tok-operator">.</span>
   ✓ <span class="tok-function">Created</span> (<span class="tok-number">228.3</span> MB)

🔐 Calculating SHA<span class="tok-number">256...</span>
   <span class="tok-number">17</span>dac<span class="tok-number">4</span>b<span class="tok-number">84</span>adaa<span class="tok-number">04</span>b<span class="tok-number">5</span>f<span class="tok-number">976</span>a<span class="tok-number">7</span>d<span class="tok-number">1</span>b<span class="tok-number">9126630545</span>f<span class="tok-number">0101</span>fe<span class="tok-number">84</span>ca<span class="tok-number">5423163</span>da<span class="tok-number">886386</span>a<span class="tok-number">6</span>

🚀 Creating release v<span class="tok-number">0.3.0...</span>
   ✓ Release created

⬆️  Uploading cupertino<span class="tok-operator">-</span>databases<span class="tok-operator">-</span>v<span class="tok-number">0.3.0.</span>zip<span class="tok-operator">.</span><span class="tok-operator">.</span><span class="tok-operator">.</span>
   ✓ Upload complete

✅ Release v<span class="tok-number">0.3.0</span> published<span class="tok-operator">!</span>
   https<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>codeberg<span class="tok-operator">.</span>org<span class="tok-operator">/</span>CupertinoHQ<span class="tok-operator">/</span>cupertino<span class="tok-operator">/</span>src<span class="tok-operator">/</span>branch<span class="tok-operator">/</span>ma<span class="tok-keyword">in</span><span class="tok-operator">/</span>docs<span class="tok-operator">/</span>artifacts</code></pre>
<p>When I refresh the documentation (re-crawl Apple’s docs), I bump the version and run <code>cupertino release</code>. Users get the update with <code>cupertino setup --force</code>.</p>
<h2>Version Parity</h2>
<p>The CLI version matches the database release tag:</p>
<table><thead><tr><th>CLI Version</th><th>Release Tag</th><th>Database</th></tr></thead><tbody><tr><td>0.3.0</td><td>v0.3.0</td><td>cupertino-databases-v0.3.0.zip</td></tr><tr><td>0.4.0</td><td>v0.4.0</td><td>cupertino-databases-v0.4.0.zip</td></tr></tbody></table>
<p>This ensures schema compatibility. If I change the database structure in v0.4.0, users with CLI v0.4.0 will download v0.4.0 databases.</p>
<h2>Three Ways to Set Up Cupertino</h2>
<p>Now you have options:</p>
<h3>1. Instant Setup (Recommended)</h3>
<pre><code class="language-bash">cupertino setup    <span class="tok-comment"># ~30 seconds</span>
cupertino serve</code></pre>
<p>Download pre-built databases. Fastest option.</p>
<h3>2. Build from GitHub</h3>
<pre><code class="language-bash">cupertino save <span class="tok-operator">-</span><span class="tok-operator">-</span>remote    <span class="tok-comment"># ~45 minutes</span>
cupertino serve</code></pre>
<p>Stream documentation from GitHub and build locally. Useful if you want to verify the build process or can’t download the 228MB zip.</p>
<h3>3. Full Crawl (Advanced)</h3>
<pre><code class="language-bash">cupertino fetch <span class="tok-operator">-</span><span class="tok-operator">-</span>type docs <span class="tok-operator">-</span><span class="tok-operator">-</span>max<span class="tok-operator">-</span>pages <span class="tok-number">15000</span>    <span class="tok-comment"># ~20-24 hours</span>
cupertino fetch <span class="tok-operator">-</span><span class="tok-operator">-</span>type evolution
cupertino save
cupertino serve</code></pre>
<p>Crawl Apple’s documentation yourself. Only needed if you want to modify the crawler or need documentation not in the pre-built database.</p>
<h2>Compression Matters</h2>
<p>The uncompressed databases total ~1.3 GB. The zip file is 228 MB - about 1/6 the size. SQLite databases compress extremely well because they contain repetitive text data.</p>
<p>This makes the download fast and keeps GitHub Release storage reasonable.</p>
<h2>Database Updates</h2>
<p>I’m actively crawling Apple’s documentation and updating the databases several times a week. Run <code>cupertino setup --force</code> to get the latest version.</p>
<p>The current public material lives with the main Cupertino repository:</p>
<ul><li><p><strong><a href="https://codeberg.org/CupertinoHQ/cupertino/src/branch/main/docs/artifacts">Database artifacts</a></strong> - Pre-crawled documentation and database material</p></li><li><p><strong><a href="https://codeberg.org/CupertinoHQ/cupertino/src/branch/main/docs/sources/samples">Sample source definitions</a></strong> - Apple sample code source metadata</p></li></ul>
<h2>What’s Next</h2>
<ul><li><p><strong>Automatic updates</strong> - Check for new database versions on startup</p></li><li><p><strong>Delta updates</strong> - Download only changed documents instead of full database</p></li><li><p><strong>More documentation sources</strong> - WWDC transcripts, Apple Tech Notes</p></li></ul>
<h2>Try It</h2>
<pre><code class="language-bash"><span class="tok-comment"># Install</span>
git clone https<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>codeberg<span class="tok-operator">.</span>org<span class="tok-operator">/</span>CupertinoHQ<span class="tok-operator">/</span>cupertino<span class="tok-operator">.</span>git
cd cupertino <span class="tok-operator">&amp;</span><span class="tok-operator">&amp;</span> make build <span class="tok-operator">&amp;</span><span class="tok-operator">&amp;</span> su<span class="tok-keyword">do</span> make install

<span class="tok-comment"># Setup (the new way)</span>
cupertino setup
cupertino serve</code></pre>
<p>Configure Claude Desktop and you’re done. Full Apple documentation access in under a minute.</p>
<h2>Links</h2>
<ul><li><p><strong>Codeberg:</strong> <a href="https://codeberg.org/CupertinoHQ/cupertino">codeberg.org/CupertinoHQ/cupertino</a></p></li><li><p><strong>Tag:</strong> <a href="https://codeberg.org/CupertinoHQ/cupertino/src/tag/v0.3.0">v0.3.0</a></p></li><li><p><strong>Databases:</strong> <a href="https://codeberg.org/CupertinoHQ/cupertino/src/branch/main/docs/artifacts">Cupertino artifacts</a></p></li></ul>
<hr>
<p><em>The best feature is the one that removes friction. Twenty hours of crawling was friction.</em></p>]]></content:encoded>
</item>
<item>
<title>Cupertino: 27 Issues Down, Just Getting Started</title>
<link>https://aleahim.com/blog/cupertino-ecosystem/</link>
<guid isPermaLink="true">https://aleahim.com/blog/cupertino-ecosystem/</guid>
<pubDate>Wed, 03 Dec 2025 00:00:00 +0000</pubDate>
<description>From MCP server to a complete Apple documentation ecosystem with pre-crawled docs and 606 sample projects</description>
<content:encoded><![CDATA[<p>I’m currently working on two macOS apps, and I rely heavily on Claude Code to help me ship faster. But here’s the thing—I kept hitting the same wall: hallucinated APIs, outdated patterns, confident suggestions for methods that don’t exist.</p>
<p>I’m not alone. Developers everywhere report the same issues: AI suggesting <code>@ObservableObject</code> and <code>@Published</code> when Apple now recommends <code>@Observable</code>. Xcode 26’s AI integration <a href="https://www.fline.dev/why-im-not-using-xcode-26s-ai-chat-integration-and-what-could-change-my-mind/">hallucinating 100% of the time</a> when asked about new 2025 frameworks. One developer described fixing AI-generated Swift code as <a href="https://medium.com/@agnislav/one-more-chatgpt-vs-claude-comparison-for-macos-development-8cae53ec7c3e">“whack-a-mole”</a>—fix one obsolete API, another pops up.</p>
<p>That frustration is why I built Cupertino.</p>
<p>In my <a href="https://aleahim.com/blog/cupertino/">previous post</a>, I introduced it—an MCP server that gives Claude offline access to 22,000+ Apple documentation pages. That was version 0.1.x. Since then, things have evolved rapidly: nine releases in 72 hours, two new companion repositories, and a complete ecosystem for AI-assisted Apple development.</p>
<p>I need this tool. I use it every day. And I’m building it in public because I know I’m not the only one frustrated with AI that hallucinates Apple APIs.</p>
<h2>What Changed: v0.1.6 → v0.2.3</h2>
<p>The original Cupertino solved the core problem—searching Apple docs locally. But real-world usage revealed gaps: ranking wasn’t smart enough, storage was bloated, and legacy documentation was missing. Here’s what shipped:</p>
<h3>Intelligent Ranking (v0.2.1)</h3>
<p>The initial BM25 search treated all documents equally. A query for “View” might return some random extension before <code>SwiftUI.View</code> itself. Eight ranking heuristics now prioritize:</p>
<ul><li><p><strong>Core types over extensions</strong> - <code>View</code> ranks above <code>View+Accessibility</code></p></li><li><p><strong>URL depth analysis</strong> - <code>/documentation/swiftui/view</code> beats <code>/documentation/swiftui/view/some/nested/thing</code></p></li><li><p><strong>Title pattern detection</strong> - Exact matches surface first</p></li><li><p><strong>Modern over deprecated</strong> - Current APIs rank higher</p></li></ul>
<p>The result: sub-100ms queries that actually return what you’re looking for.</p>
<h3>Storage Cleanup (v0.1.8, v0.2.0)</h3>
<p>The initial crawl produced ~27GB of data. Most of it was cruft: <code>.git</code> folders from sample code downloads, <code>.DS_Store</code> files, Xcode user data. A new <code>cleanup</code> command reduced storage to 2-3GB—a 90% reduction.</p>
<p>Version 0.2.0 fixed a critical bug where cleanup was accidentally deleting source code instead of just metadata. Now 99.8% of sample ZIPs retain intact source.</p>
<h3>Language Filtering (v0.1.9)</h3>
<p>Not everyone wants Objective-C results. The CLI and MCP tools now accept a <code>language</code> parameter:</p>
<pre><code class="language-bash">cupertino search <span class="tok-string">"NSObject"</span> <span class="tok-operator">-</span><span class="tok-operator">-</span>language swift</code></pre>
<p>Claude can filter too—just ask for “Swift-only results.”</p>
<h3>Apple Archive Support (v0.2.3)</h3>
<p>Apple’s modern documentation is great, but some topics only exist in the legacy archive: Core Animation Programming Guide, Quartz 2D Programming Guide, Core Text Programming Guide. These are still the authoritative sources for low-level graphics work.</p>
<p>Cupertino now crawls <code>developer.apple.com/library/archive/</code> and integrates results with smart ranking that prioritizes modern docs while keeping legacy content searchable.</p>
<h2>The Ecosystem: Three Repositories</h2>
<p>Cupertino grew from one repo to three:</p>
<h3><a href="https://codeberg.org/CupertinoHQ/cupertino">cupertino</a> — The MCP Server</h3>
<p>The main Swift package. Crawls, indexes, and serves documentation via MCP protocol. Install it, point Claude at it, done.</p>
<pre><code class="language-bash"><span class="tok-comment"># Quick setup with Claude Code</span>
claude mcp add cupertino <span class="tok-operator">-</span><span class="tok-operator">-</span>scope user <span class="tok-operator">-</span><span class="tok-operator">-</span> <span class="tok-operator">/</span>usr<span class="tok-operator">/</span><span class="tok-keyword">local</span><span class="tok-operator">/</span>b<span class="tok-keyword">in</span><span class="tok-operator">/</span>cupertino</code></pre>
<h3><a href="https://codeberg.org/CupertinoHQ/cupertino/src/branch/main/docs/artifacts">Cupertino database artifacts</a> — Pre-Crawled Documentation</h3>
<p>Don’t want to crawl everything yourself? Install Cupertino and populate the local databases:</p>
<pre><code class="language-bash">cupertino setup</code></pre>
<p>Currently includes:</p>
<table><thead><tr><th>Source</th><th>Content</th><th>Status</th></tr></thead><tbody><tr><td><code>swift-evolution/</code></td><td>All Swift proposals (~400)</td><td>Ready</td></tr><tr><td><code>swift-org/</code></td><td>Swift.org language docs</td><td>Ready</td></tr><tr><td><code>packages/</code></td><td>Swift Package Index metadata</td><td>Ready</td></tr><tr><td><code>docs/</code></td><td>Apple Developer Documentation (13K+ pages)</td><td>WIP - requires manual crawl</td></tr><tr><td><code>archive/</code></td><td>Legacy Apple guides</td><td>WIP - requires manual crawl</td></tr></tbody></table>
<p>Swift Evolution, Swift.org, and package docs are pre-indexed and ready. Apple Developer Documentation and Archive still require running <code>cupertino fetch</code> yourself due to size—but having the infrastructure in place is a start. The tooling exists; full pre-crawled distribution is coming.</p>
<h3><a href="https://codeberg.org/CupertinoHQ/cupertino/src/branch/main/docs/sources/samples">Cupertino sample sources</a> — Apple Samples</h3>
<p>This is the fun one. Every official Apple sample code project, cleaned and ready to build:</p>
<ul><li><p><strong>100+ frameworks covered</strong>: SwiftUI, ARKit, RealityKit, Metal, CoreML, Vision, AVFoundation, HealthKit, HomeKit, and more</p></li><li><p><strong>606 projects</strong>: From “Hello World” to complex GPU shaders</p></li><li><p><strong>Build-ready</strong>: No <code>.git</code> folders, no xcuserdata, no cruft</p></li><li><p><strong>MIT Licensed</strong>: Use them however you want</p></li></ul>
<p>When you ask Claude “show me how Apple implements ARKit face tracking,” it can now search actual Apple sample code—not hallucinate an approximation.</p>
<p>Example projects include:</p>
<ul><li><p><code>arkit-*</code> — Augmented reality implementations</p></li><li><p><code>swiftui-*</code> — Modern UI patterns</p></li><li><p><code>metal-*</code> — GPU programming examples</p></li><li><p><code>coreml-*</code> — Machine learning integrations</p></li><li><p><code>avfoundation-*</code> — Video and audio capture</p></li></ul>
<h2>The 72-Hour Sprint</h2>
<p>Nine releases shipped in three days. Here’s the changelog:</p>
<table><thead><tr><th>Version</th><th>Key Changes</th></tr></thead><tbody><tr><td>v0.1.6</td><td>JSON-first crawling, WKWebView memory fixes</td></tr><tr><td>v0.1.7</td><td>Swift Book content retrieval fixes</td></tr><tr><td>v0.1.8</td><td>Cleanup command (27GB → 2-3GB)</td></tr><tr><td>v0.1.9</td><td>Language filtering (Swift/ObjC)</td></tr><tr><td>v0.2.0</td><td>Critical fix: source code retention</td></tr><tr><td>v0.2.1</td><td>Eight ranking heuristics</td></tr><tr><td>v0.2.2</td><td>URL depth analysis, resume fixes</td></tr><tr><td>v0.2.3</td><td>Apple Archive legacy docs</td></tr></tbody></table>
<p>Each release addressed real issues discovered while using Cupertino with Claude Code. Fast iteration, real-world testing.</p>
<h2>Using the Full Ecosystem</h2>
<p>Here’s the quickest path to AI-assisted Apple development with accurate documentation:</p>
<pre><code class="language-bash"><span class="tok-comment"># 1. Build the MCP server</span>
git clone https<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>codeberg<span class="tok-operator">.</span>org<span class="tok-operator">/</span>CupertinoHQ<span class="tok-operator">/</span>cupertino
cd cupertino
swift build <span class="tok-operator">-</span>c release
su<span class="tok-keyword">do</span> cp <span class="tok-operator">.</span>build<span class="tok-operator">/</span>release<span class="tok-operator">/</span>cupertino <span class="tok-operator">/</span>usr<span class="tok-operator">/</span><span class="tok-keyword">local</span><span class="tok-operator">/</span>b<span class="tok-keyword">in</span><span class="tok-operator">/</span>

<span class="tok-comment"># 2. Download the public database bundle</span>
cupertino setup

<span class="tok-comment"># 3. Configure Claude Code</span>
claude mcp add cupertino <span class="tok-operator">-</span><span class="tok-operator">-</span>scope user <span class="tok-operator">-</span><span class="tok-operator">-</span> <span class="tok-operator">/</span>usr<span class="tok-operator">/</span><span class="tok-keyword">local</span><span class="tok-operator">/</span>b<span class="tok-keyword">in</span><span class="tok-operator">/</span>cupertino</code></pre>
<p>Now when you ask Claude about Apple APIs, it searches 22,000+ pages of real documentation. No hallucinations. No deprecated patterns. Just accurate, current information.</p>
<h2>What’s Next</h2>
<p>The ranking heuristics work well but aren’t perfect. Planned improvements:</p>
<ul><li><p><strong><code>make install</code></strong> — One command to install everything: the MCP server, pre-crawled docs, and sample source material</p></li><li><p><strong>Semantic search</strong> — Embeddings-based similarity for conceptual queries</p></li><li><p><strong>Version awareness</strong> — Filter by iOS/macOS version</p></li><li><p><strong>Cross-reference linking</strong> — “See also” connections between related docs</p></li><li><p><strong>Sample code search</strong> — Full-text search across all 606 projects</p></li></ul>
<h2>Join Me</h2>
<p>If you’re an Apple developer tired of AI hallucinations, give Cupertino a try. If you find bugs or have ideas, open an issue—27 closed so far, plenty more to go.</p>
<p>This isn’t a polished product yet. It’s a tool I’m building because I need it, and I’m sharing it because you might need it too.</p>
<h2>Repositories</h2>
<ul><li><p><a href="https://codeberg.org/CupertinoHQ/cupertino">cupertino</a> — MCP Server (Swift)</p></li><li><p><a href="https://codeberg.org/CupertinoHQ/cupertino/src/branch/main/docs/artifacts">Cupertino artifacts</a> — Pre-crawled documentation</p></li><li><p><a href="https://codeberg.org/CupertinoHQ/cupertino/src/branch/main/docs/sources/samples">Cupertino sample sources</a> — Apple sample source metadata</p></li></ul>
<p>No more AI hallucinations about Apple APIs. Just real documentation that compiles.</p>]]></content:encoded>
</item>
<item>
<title>Cupertino: Offline Apple Documentation for AI Agents</title>
<link>https://aleahim.com/blog/cupertino/</link>
<guid isPermaLink="true">https://aleahim.com/blog/cupertino/</guid>
<pubDate>Fri, 28 Nov 2025 00:00:00 +0000</pubDate>
<description>An MCP server that gives Claude Desktop offline access to 22,000+ Apple documentation pages</description>
<content:encoded><![CDATA[<h1>Cupertino: Offline Apple Documentation for AI Agents</h1>
<p><strong>TL;DR:</strong> I built an MCP server that gives Claude Desktop offline access to 22,000+ Apple documentation pages across 261 frameworks. No more hallucinated APIs.</p>
<h2>The Problem</h2>
<p>If you’ve used Claude or ChatGPT for Swift/iOS development, you’ve seen this:</p>
<blockquote><p>“Use the <code>NavigationView</code> with <code>.navigationBarTitle()</code> modifier…”</p></blockquote>
<p>Except <code>NavigationView</code> is deprecated. It’s <code>NavigationStack</code> now. And the modifier is <code>.navigationTitle()</code>.</p>
<p>AI models hallucinate APIs. They mix up iOS 14 patterns with iOS 17. They invent methods that don’t exist. And when you’re deep in a coding session, these small errors cost you hours.</p>
<h2>The Solution: Cupertino</h2>
<p>Cupertino is an MCP (Model Context Protocol) server that:</p>
<ol><li><p><strong>Crawls</strong> Apple Developer documentation, Swift Evolution proposals, and Swift.org</p></li><li><p><strong>Indexes</strong> everything into a local SQLite FTS5 database with BM25 ranking</p></li><li><p><strong>Serves</strong> documentation to Claude Desktop via MCP</p></li></ol>
<p>When Claude needs to answer a SwiftUI question, it searches your local documentation instead of relying on training data.</p>
<h2>What You Get</h2>
<p><strong>22,044 documents across 223 frameworks:</strong></p>
<p><img src="https://aleahim.com/images/blog/cupertino/frameworks.png" alt="Available Frameworks">
<em>The documentation index contains 22,044 documents across 223 frameworks, including all major Apple platforms.</em></p>
<table><thead><tr><th>Framework</th><th style="text-align:right">Documents</th></tr></thead><tbody><tr><td>SwiftUI</td><td style="text-align:right">5,853</td></tr><tr><td>Swift</td><td style="text-align:right">2,814</td></tr><tr><td>UIKit</td><td style="text-align:right">1,906</td></tr><tr><td>AppKit</td><td style="text-align:right">1,316</td></tr><tr><td>Foundation</td><td style="text-align:right">1,219</td></tr><tr><td>Swift.org</td><td style="text-align:right">501</td></tr><tr><td>Swift Evolution</td><td style="text-align:right">429</td></tr><tr><td>RealityKit</td><td style="text-align:right">423</td></tr><tr><td>…</td><td style="text-align:right">…</td></tr></tbody></table>
<p><strong>Two MCP tools:</strong></p>
<ul><li><p><code>search_docs</code> - Full-text search with BM25 ranking</p></li><li><p><code>list_frameworks</code> - Discover available documentation</p></li></ul>
<p><strong>Example search result:</strong></p>
<pre><code># Search Results for "SwiftUI View"

## 1. View | Apple Developer Documentation
- **Framework:** swiftui
- **URI:** apple-docs://swiftui/documentation_swiftui_view
- **Score:** 1.82
- **Words:** 2,682

Protocol# View
A type that represents part of your app's user interface...</code></pre>
<p><img src="https://aleahim.com/images/blog/cupertino/mcp-tools.png" alt="Claude Desktop using Cupertino MCP tools">
<em>Claude Desktop using Cupertino’s MCP tools to search documentation and list available frameworks</em></p>
<h2>Installation</h2>
<h3>1. Build from source</h3>
<pre><code class="language-bash">git clone https<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>codeberg<span class="tok-operator">.</span>org<span class="tok-operator">/</span>CupertinoHQ<span class="tok-operator">/</span>cupertino<span class="tok-operator">.</span>git
cd cupertino
make build
su<span class="tok-keyword">do</span> make install</code></pre>
<h3>2. Fetch documentation</h3>
<pre><code class="language-bash"><span class="tok-comment"># Quick start: Swift Evolution only (~5 minutes)</span>
cupertino fetch <span class="tok-operator">-</span><span class="tok-operator">-</span>type evolution
cupertino save

<span class="tok-comment"># Full documentation (~20-24 hours, one-time)</span>
cupertino fetch <span class="tok-operator">-</span><span class="tok-operator">-</span>type docs <span class="tok-operator">-</span><span class="tok-operator">-</span>max<span class="tok-operator">-</span>pages <span class="tok-number">15000</span>
cupertino fetch <span class="tok-operator">-</span><span class="tok-operator">-</span>type evolution
cupertino save</code></pre>
<p><strong>Expected output after indexing:</strong></p>
<pre><code>✅ Search index built successfully!
   Total documents: 22044
   Frameworks: 261
   Database: /Users/mm/.cupertino/search.db
   Size: 163.6 MB

💡 Tip: Start the MCP server with 'cupertino serve' to enable search</code></pre>
<p>Why 20+ hours? The crawler respects Apple’s servers with a 0.5s delay between requests. 21,000 pages × 0.5s = many hours. But it’s a one-time operation.</p>
<h3>3. Configure Claude Desktop</h3>
<p>Edit <code>~/Library/Application Support/Claude/claude_desktop_config.json</code>:</p>
<pre><code class="language-json"><span class="tok-operator">{</span>
  <span class="tok-property">"mcpServers"</span><span class="tok-operator">:</span> <span class="tok-operator">{</span>
    <span class="tok-property">"cupertino"</span><span class="tok-operator">:</span> <span class="tok-operator">{</span>
      <span class="tok-property">"command"</span><span class="tok-operator">:</span> <span class="tok-string">"/usr/local/bin/cupertino"</span>
    <span class="tok-operator">}</span>
  <span class="tok-operator">}</span>
<span class="tok-operator">}</span></code></pre>
<p>Restart Claude Desktop. Done.</p>
<h3>4. Verify it works</h3>
<pre><code class="language-bash">cupertino doctor</code></pre>
<pre><code>✅ MCP Server
   ✓ Server can initialize
   ✓ Transport: stdio
   ✓ Protocol version: 2024-11-05

📚 Documentation Directories
   ✓ Apple docs: ~/.cupertino/docs (21,114 files)
   ✓ Swift Evolution: ~/.cupertino/swift-evolution (429 proposals)

🔍 Search Index
   ✓ Database: ~/.cupertino/search.db
   ✓ Size: 156.0 MB
   ✓ Frameworks: 261

✅ All checks passed - MCP server ready</code></pre>
<h2>How It Works</h2>
<pre><code>1. Fetch:  cupertino fetch --type docs
   → WKWebView renders JavaScript-heavy pages
   → HTML converted to Markdown
   → Saved to ~/.cupertino/docs/

2. Save:   cupertino save
   → Markdown files indexed into SQLite FTS5
   → BM25 ranking for relevance scoring
   → ~160MB database for full index

3. Serve:  cupertino serve
   → MCP server starts on stdio
   → Claude Desktop connects via JSON-RPC
   → Search queries hit local database</code></pre>
<h2>Architecture</h2>
<p>Built with Swift 6.2 and strict concurrency:</p>
<pre><code>Packages/
├── MCP/                 # Model Context Protocol implementation
├── Search/              # SQLite FTS5 search engine
├── Core/                # Crawlers (WKWebView, GitHub API)
├── CLI/                 # Command-line interface
└── Resources/           # Bundled catalogs (9,699 packages, 606 samples)</code></pre>
<p>Key decisions:</p>
<ul><li><p><strong>Swift 6.2</strong> with 100% strict concurrency checking</p></li><li><p><strong>Actor isolation</strong> for thread-safe state management</p></li><li><p><strong>SQLite FTS5</strong> with BM25 for fast, relevant search</p></li><li><p><strong>WKWebView</strong> to render Apple’s JavaScript-heavy documentation</p></li></ul>
<h2>Room for Improvement</h2>
<p>Honest feedback from testing: the search works, but it’s not perfect.</p>
<p><strong>Current limitation:</strong> BM25 treats all documents equally. A search for “SwiftUI” returns <code>mouseEntered(with:)</code> alongside <code>View</code> protocol because both contain “SwiftUI” with similar frequency.</p>
<p><strong>Planned improvements:</strong></p>
<ol><li><p><strong>Boost by word count</strong> - Comprehensive docs (2000+ words) rank higher than stub pages (61 words)</p></li><li><p><strong>Boost by path depth</strong> - <code>/swiftui/view</code> ranks higher than <code>/swiftui/nshostingview/mouseentered</code></p></li><li><p><strong>Document type weighting</strong> - Overview pages rank higher than individual method docs</p></li><li><p><strong>Curated importance flags</strong> - Mark foundational docs for priority ranking</p></li></ol>
<p>The search is still useful for targeted queries like “SwiftUI NavigationStack” or “SE-0001”. Broad queries need work.</p>
<h2>Why I Built This</h2>
<p>I was tired of:</p>
<ul><li><p>Claude inventing deprecated APIs</p></li><li><p>Switching to Safari to verify every code suggestion</p></li><li><p>Losing flow state to documentation lookups</p></li></ul>
<p>Now Claude searches my local 160MB documentation database. Same query, same results, every time. Offline. Fast.</p>
<h2>Links</h2>
<ul><li><p><strong>Codeberg:</strong> <a href="https://codeberg.org/CupertinoHQ/cupertino">codeberg.org/CupertinoHQ/cupertino</a></p></li><li><p><strong>Requirements:</strong> macOS 15+, Swift 6.2+, Xcode 16+</p></li><li><p><strong>License:</strong> MIT</p></li></ul>
<hr>
<p><em>Built with Swift, frustration, and too many hours debugging WKWebView concurrency issues.</em></p>]]></content:encoded>
</item>
<item>
<title>Building a Concurrency-Safe Bearer Token Middleware for Swift OpenAPI Clients</title>
<link>https://aleahim.com/blog/token-middleware/</link>
<guid isPermaLink="true">https://aleahim.com/blog/token-middleware/</guid>
<pubDate>Sat, 08 Nov 2025 00:00:00 +0000</pubDate>
<description>Elegant, Thread-Safe Authentication for OpenAPI Runtime</description>
<content:encoded><![CDATA[<p>Authentication is one of those things that every API client needs, yet implementing it cleanly and safely can be surprisingly tricky—especially in Swift’s modern concurrency world. Today, I’m sharing a lightweight middleware solution that handles Bearer token authentication for OpenAPI-generated Swift clients in a thread-safe, composable way.</p>
<h2>The Problem</h2>
<p>When working with OpenAPI Runtime in Swift, you often need to:</p>
<ul><li><p>Inject an <code>Authorization: Bearer &lt;token&gt;</code> header into API requests</p></li><li><p>Update tokens dynamically (after login, token refresh, etc.)</p></li><li><p>Skip authentication for certain endpoints (like login or public routes)</p></li><li><p>Ensure thread-safety in concurrent environments</p></li></ul>
<p>While this sounds straightforward, getting it right with Swift 6’s strict concurrency checking requires careful design. You need proper actor isolation, Sendable conformance, and a clean API that doesn’t fight the type system.</p>
<h2>The Solution: BearerTokenAuthenticationMiddleware</h2>
<p>I built <code>BearerTokenAuthenticationMiddleware</code>—a minimal, zero-dependency package that solves these problems elegantly. Here’s what makes it special:</p>
<h3>1. Actor-Isolated Token Storage</h3>
<p>The heart of the middleware is a private <code>TokenStorage</code> actor that manages the authentication token:</p>
<pre><code class="language-swift"><span class="tok-keyword">private</span> <span class="tok-keyword">actor</span> <span class="tok-type">TokenStorage</span> {
    <span class="tok-keyword">var</span> token<span class="tok-operator">:</span> <span class="tok-type">String</span><span class="tok-operator">?</span>

    <span class="tok-keyword">func</span> <span class="tok-function">getToken</span>() <span class="tok-operator">-</span><span class="tok-operator">&gt;</span> <span class="tok-type">String</span><span class="tok-operator">?</span> {
        token
    }

    <span class="tok-keyword">func</span> <span class="tok-function">setToken</span>(_ new<span class="tok-type">Token</span><span class="tok-operator">:</span> <span class="tok-type">String</span><span class="tok-operator">?</span>) {
        token <span class="tok-operator">=</span> new<span class="tok-type">Token</span>
    }
}</code></pre>
<p>This ensures that token reads and writes are <strong>never concurrent</strong>, eliminating race conditions completely.</p>
<h3>2. Clean, Composable API</h3>
<p>Setting up the middleware is straightforward:</p>
<pre><code class="language-swift"><span class="tok-keyword">import</span> <span class="tok-type">OpenAPIRuntime</span>
<span class="tok-keyword">import</span> <span class="tok-type">BearerTokenAuthMiddleware</span>

<span class="tok-keyword">let</span> auth<span class="tok-type">Middleware</span> <span class="tok-operator">=</span> <span class="tok-type">BearerTokenAuthenticationMiddleware</span>(
    initial<span class="tok-type">Token</span><span class="tok-operator">:</span> <span class="tok-string">"my-secret-token"</span>
)

<span class="tok-keyword">let</span> client <span class="tok-operator">=</span> <span class="tok-type">Client</span>(
    server<span class="tok-type">URL</span><span class="tok-operator">:</span> <span class="tok-type">URL</span>(string<span class="tok-operator">:</span> <span class="tok-string">"https://api.example.com"</span>)<span class="tok-operator">!</span>,
    transport<span class="tok-operator">:</span> <span class="tok-type">AsyncHTTPClientTransport</span>(),
    middlewares<span class="tok-operator">:</span> [auth<span class="tok-type">Middleware</span>]
)</code></pre>
<p>Every request now automatically includes <code>Authorization: Bearer my-secret-token</code>.</p>
<h2>Wrapping Up</h2>
<p>Building authentication middleware might seem like boilerplate, but doing it right—especially with modern Swift concurrency—requires thoughtful design. <code>BearerTokenAuthenticationMiddleware</code> provides:</p>
<ul><li><p>Thread-safe token management via actors</p></li><li><p>Selective authentication with operation-level control</p></li><li><p>Dynamic token updates</p></li><li><p>Zero dependencies</p></li><li><p>Clean composition with other middlewares</p></li></ul>
<p><strong>Repository</strong>: <a href="https://codeberg.org/Mihaela/BearerTokenAuthMiddleware">codeberg.org/Mihaela/BearerTokenAuthMiddleware</a></p>]]></content:encoded>
</item>
<item>
<title>From ExtremePackaging to OpenAPI Integration</title>
<link>https://aleahim.com/blog/extreme-packaging-open-a-p-i/</link>
<guid isPermaLink="true">https://aleahim.com/blog/extreme-packaging-open-a-p-i/</guid>
<pubDate>Tue, 23 Sep 2025 00:00:00 +0000</pubDate>
<description>Building a Full OpenAPI Workflow on Top of the Extreme Packaging Architecture</description>
<content:encoded><![CDATA[<p>When I first published <a href="https://codeberg.org/Mihaela/ExtremePackaging">ExtremePackaging</a>, the goal was simple — to prove that <strong>highly modular Swift architectures</strong> can scale across platforms while keeping build times fast, dependencies isolated, and the mental model crystal clear.</p>
<p>But then came the next step: <em>Could this same architecture host a complete OpenAPI workflow — with generated clients, servers, and full middleware integration — without losing its elegance?</em></p>
<p>That’s how the new reference implementation, <strong>swift-openapi-extremepackaging-example</strong>, was born.</p>
<hr>
<h2>🧩 The Foundation — ExtremePackaging</h2>
<p>The original ExtremePackaging repository introduced a clean, layered structure:</p>
<ul><li><p><strong>Shared packages</strong> for models and protocols</p></li><li><p><strong>Independent feature modules</strong> for UI, data, and networking</p></li><li><p><strong>No cross-package leaks</strong></p></li><li><p><strong>Unified Xcode workspace</strong> that felt like a monolith but built like microservices</p></li></ul>
<p>The guiding philosophy: <em>Each feature should be an island, communicating only through well-defined contracts.</em></p>
<p>That foundation made it ideal for integrating OpenAPI-generated code — which naturally fits into modular boundaries like <code>SharedApiModels</code>, <code>ApiClient</code>, and <code>ApiServer</code>.</p>
<hr>
<h2>🚀 Evolving Toward OpenAPI</h2>
<p>The OpenAPI version added an entire new layer of automation and functionality — transforming a static architecture into a <strong>living, self-describing API ecosystem</strong>.</p>
<h3>1. OpenAPI Schema &amp; Code Generation</h3>
<p>At the heart of the project is the <strong>OpenAPI specification (<code>openapi.yaml</code>)</strong> — defining endpoints, models, and responses for a DummyJSON-compatible API.</p>
<p>Newly introduced elements:</p>
<ul><li><p>🧱 Full schema definitions for <code>Users</code>, <code>Posts</code>, <code>Products</code>, <code>Todos</code>, and <code>Carts</code></p></li><li><p>🧩 Error schemas (404, validation, authentication)</p></li><li><p>⚙️ Integration with <strong>Swift OpenAPI Generator</strong></p></li><li><p>🔁 Automatic generation of clients, models, and server stubs</p></li><li><p>📦 Separation of generated code into <code>SharedApiModels</code></p></li></ul>
<p>This turned the architecture into a self-contained API ecosystem — one that can <strong>generate, serve, and consume</strong> its own endpoints.</p>
<hr>
<h3>2. API Server Implementation</h3>
<p>A complete <strong>Vapor-based local server</strong> (<code>ApiServer</code>) was added to simulate real-world backend behavior:</p>
<ul><li><p>17 endpoints fully implemented from the OpenAPI spec</p></li><li><p>Realistic mock data mirroring DummyJSON</p></li><li><p>Pagination and validation logic</p></li><li><p>Centralized error responses</p></li><li><p>🖥️ Prefixed logging for easy tracing in the console</p></li></ul>
<p>The server runs locally at <code>http://localhost:8080</code>, serving as both a mock backend and a test harness for the generated client.</p>
<hr>
<h3>3. Enhanced API Client Architecture</h3>
<p>The client evolved from a simple abstraction into a <strong>fully concurrent, actor-based networking layer</strong>.</p>
<h4>Highlights</h4>
<ul><li><p><code>ApiClient</code> actor manages shared state safely across async contexts</p></li><li><p>Middleware chain introduced: <strong>Logging → Authentication → Correction</strong></p></li><li><p>Runtime environment switching between <code>.production</code>, <code>.local</code>, and <code>.mock</code></p></li><li><p>Shared singleton <code>ApiClientState</code> stores token, settings, and preferences</p></li></ul>
<p>Example flow:</p>
<pre><code class="language-swift"><span class="tok-keyword">try</span> <span class="tok-keyword">await</span> <span class="tok-type">ApiClient</span><span class="tok-operator">.</span><span class="tok-function">initializeShared</span>(environment<span class="tok-operator">:</span> <span class="tok-operator">.</span>production)
<span class="tok-keyword">let</span> client <span class="tok-operator">=</span> <span class="tok-type">ApiClient</span><span class="tok-operator">.</span>shared<span class="tok-operator">!</span>
<span class="tok-keyword">let</span> auth <span class="tok-operator">=</span> <span class="tok-keyword">try</span> <span class="tok-keyword">await</span> client<span class="tok-operator">.</span><span class="tok-function">login</span>(username<span class="tok-operator">:</span> <span class="tok-string">"emilys"</span>, password<span class="tok-operator">:</span> <span class="tok-string">"emilyspass"</span>)
<span class="tok-keyword">await</span> <span class="tok-type">ApiClient</span><span class="tok-operator">.</span><span class="tok-function">setToken</span>(auth<span class="tok-operator">.</span>access<span class="tok-type">Token</span>)
<span class="tok-keyword">let</span> users <span class="tok-operator">=</span> <span class="tok-keyword">try</span> <span class="tok-keyword">await</span> client<span class="tok-operator">.</span><span class="tok-function">getUsers</span>(limit<span class="tok-operator">:</span> <span class="tok-number">10</span>)</code></pre>
<p>A clear separation between environment configuration and runtime state ensures deterministic, thread-safe behavior.</p>
<hr>
<h3>4. Middleware Integration</h3>
<p>The client leverages two reusable middlewares from sibling packages:</p>
<ul><li><p><strong><a href="https://codeberg.org/Mihaela/OpenAPILoggingMiddleware">OpenAPILoggingMiddleware</a></strong>
Provides structured, console + JSON logging with full request/response capture.</p></li><li><p><strong><a href="https://codeberg.org/Mihaela/BearerTokenAuthMiddleware">BearerTokenAuthMiddleware</a></strong>
Manages JWT token injection with a concurrency-safe actor and public operation rules.</p></li></ul>
<p>Together, they demonstrate the power of <strong>middleware chaining</strong> in OpenAPI Runtime — clean, modular extensions without inheritance or global state.</p>
<hr>
<h3>5. YAMLMerger — The Key to Structured API Specs</h3>
<p>The project uses <strong><a href="https://codeberg.org/Mihaela/YamlMerger">YamlMerger</a></strong> — a Swift package that merges multiple YAML files into a single combined OpenAPI specification.
If your project doesn’t already include an <code>openapi.yaml</code>, YamlMerger ensures you have one — and helps you maintain a <strong>structured, predictable folder layout</strong> under <code>Tests/</code> or <code>Sources/SharedApiModels/schemas/</code>.</p>
<h4>🧠 Why It Must Be Copied into the Project</h4>
<p>YamlMerger cannot simply be added as a SwiftPM dependency for build-time merging because of <strong>SPM’s read-only resolution model</strong>:</p>
<ol><li><p>Swift Package Manager stores dependencies in a <strong>cached, read-only</strong> location (<code>.build/checkouts/</code>).</p></li><li><p>The OpenAPI generator, however, needs <strong>write access</strong> to output the merged <code>openapi.yaml</code> file directly into your source tree.</p></li><li><p>SPM build scripts are not allowed to write to source folders outside their sandboxed build directory.</p></li></ol>
<p>✅ <strong>Solution:</strong> Copy the YamlMerger executable directly into your project (e.g. <code>Tools/YamlMerger/</code>) and call it from a pre-build script or CI pipeline.
This guarantees write permissions and makes the tool available to everyone checking out the repo.</p>
<h4>🧩 What It Does</h4>
<p>YamlMerger scans subdirectories (01 → 08) and merges YAML fragments in deterministic order:</p>
<ol><li><p>Folders are processed numerically.</p></li><li><p><code>__*.yaml</code> files merge first within each folder.</p></li><li><p>Remaining files merge alphabetically.</p></li><li><p>The final output is a complete OpenAPI spec, suitable for Swift OpenAPI Generator.</p></li></ol>
<h4>🧱 Example Schema Layout</h4>
<pre><code>Schema/
├── 01_Info/
├── 02_Servers/
├── 03_Tags/
├── 04_Paths/
├── 05_Webhooks/
├── 06_Components/
├── 07_Security/
└── 08_ExternalDocs/</code></pre>
<p>Each folder corresponds to a section of the OpenAPI spec, allowing multiple developers to work on different endpoints, schemas, or components without conflicts.</p>
<h4>⚙️ Typical Workflow</h4>
<pre><code class="language-bash"><span class="tok-comment"># Merge schemas before build</span>
<span class="tok-operator">.</span><span class="tok-operator">/</span>Tools<span class="tok-operator">/</span>YamlMerger merge   <span class="tok-operator">-</span><span class="tok-operator">-</span>input Sources<span class="tok-operator">/</span>SharedApiModels<span class="tok-operator">/</span>schemas<span class="tok-operator">/</span>   <span class="tok-operator">-</span><span class="tok-operator">-</span>output Sources<span class="tok-operator">/</span>SharedApiModels<span class="tok-operator">/</span>openapi<span class="tok-operator">.</span>yaml</code></pre>
<p>You can run this manually, in a pre-build phase, or as part of CI/CD automation.</p>
<h4>💡 Pro Tip</h4>
<p>If your project starts <strong>without</strong> an <code>openapi.yaml</code>, placing schema fragments in structured folders under <code>Tests/</code> ensures your API structure remains organized — even before full code generation.
YamlMerger gives your tests (and your teammates) a <strong>shared, visual map</strong> of your API’s evolving shape.</p>
<hr>
<h3>6. Test Coverage Expansion</h3>
<p>Two new test suites validate both local and production APIs:</p>
<ul><li><p>🧪 <code>ApiClientLocalTests.swift</code> — 25 tests targeting the local Vapor server</p></li><li><p>🌐 <code>ApiClientProductionTests.swift</code> — 29 integration tests against DummyJSON API</p></li></ul>
<p>Tests cover:</p>
<ul><li><p>Authentication and token persistence</p></li><li><p>Pagination behavior</p></li><li><p>Error responses and invalid IDs</p></li><li><p>Concurrent request handling</p></li></ul>
<p>Together they form a <strong>54-test safety net</strong> proving both architecture and OpenAPI compliance.</p>
<hr>
<h2>🧠 Architecture Snapshot</h2>
<pre><code>Packages/
├── Sources/
│   ├── ApiClient/
│   ├── ApiServer/
│   └── SharedApiModels/
└── Tests/
    └── ApiClientTests/</code></pre>
<p>Each target is self-contained — just like in the original ExtremePackaging — but now with full OpenAPI integration, client/server symmetry, and end-to-end testability.</p>
<hr>
<h2>⚡ Key Improvements Over ExtremePackaging</h2>
<table><thead><tr><th>Area</th><th>Before</th><th>After</th></tr></thead><tbody><tr><td>API Definition</td><td>Manual protocol layer</td><td>Generated OpenAPI spec</td></tr><tr><td>Networking</td><td>Custom client</td><td>Actor-based client w/ middlewares</td></tr><tr><td>Server</td><td>None</td><td>Vapor mock server (17 endpoints)</td></tr><tr><td>Authentication</td><td>Static token</td><td>BearerTokenAuthMiddleware</td></tr><tr><td>Logging</td><td>Simple print logs</td><td>Structured OpenAPILoggingMiddleware</td></tr><tr><td>Testing</td><td>Minimal unit tests</td><td>Full integration tests (54 total)</td></tr><tr><td>Schema Management</td><td>Handwritten</td><td>Modular YAML + YamlMerger</td></tr><tr><td>Tooling</td><td>Swift only</td><td>Swift + OpenAPI toolchain</td></tr></tbody></table>
<hr>
<h2>🧭 Lessons Learned</h2>
<ol><li><p><strong>OpenAPI fits perfectly into modular Swift architectures</strong> — generated code belongs in its own layer, and SwiftPM makes that separation effortless.</p></li><li><p><strong>Actors are the future of shared state</strong> — simple, safe, and transparent.</p></li><li><p><strong>Middleware &gt; Managers</strong> — function composition scales better than class hierarchies.</p></li><li><p><strong>Automation beats documentation</strong> — with OpenAPI, the spec <em>is</em> the documentation.</p></li></ol>
<hr>
<h2>💬 Closing Thoughts</h2>
<p>This evolution of ExtremePackaging into a full OpenAPI reference app is more than a demo — it’s a <strong>blueprint for modular API-driven development in Swift</strong>.</p>
<p>From YAML schemas to live servers and typed clients, everything now exists in one unified, testable ecosystem — powered by Apple’s official OpenAPI tools and guided by the ExtremePackaging philosophy.</p>
<p>👉 Explore the project: <a href="https://codeberg.org/Mihaela/swift-openapi-extremepackaging-example">swift-openapi-extremepackaging-example</a></p>
<hr>
<p><em>“Architecture should scale not by adding layers, but by removing assumptions.”</em></p>]]></content:encoded>
</item>
<item>
<title>Introducing OpenAPILoggingMiddleware</title>
<link>https://aleahim.com/blog/logging-middleware/</link>
<guid isPermaLink="true">https://aleahim.com/blog/logging-middleware/</guid>
<pubDate>Tue, 16 Sep 2025 00:00:00 +0000</pubDate>
<description>Elegant, Structured Request Logging for OpenAPI-Driven Swift Servers</description>
<content:encoded><![CDATA[<h2>Introduction</h2>
<p>In the world of <strong>OpenAPI-driven Swift servers</strong>, visibility is everything.
You can’t fix what you can’t see — and you can’t optimize what you don’t understand.</p>
<p>When I started building modular, OpenAPI-first architectures in Swift, one thing stood out: <strong>logging was either overwhelming or absent</strong>.
Either you got a raw dump of everything (headers, binary data, and stack traces), or you got nothing at all. Neither is particularly helpful when debugging or tracing an issue across microservices.</p>
<p>Frameworks like <strong>OpenAPIRuntime</strong> and <strong>OpenAPIVapor</strong> provide a strong foundation, but they deliberately avoid opinions about logging. They leave that up to you.
And that’s where most developers end up adding scattered <code>print()</code> statements or ad-hoc <code>logger.info()</code> calls inside handlers just to see what’s going on.</p>
<p>That’s not clean architecture. It’s noise.</p>
<p>So I built <strong>OpenAPILoggingMiddleware</strong>, a small, composable middleware that gives you <em>just enough visibility</em> — not too much, not too little — when working with OpenAPI-based servers.</p>
<hr>
<h2>The Problem: Logging in a Generated World</h2>
<p>OpenAPI code generation has revolutionized how backend systems are written. You define your contracts, generate both server and client, and everything should “just work.”
But once requests start flowing, visibility often becomes a blind spot.</p>
<p>If you’ve ever tried debugging an OpenAPI-generated Swift server, you might recognize these problems:</p>
<ul><li><p>Middleware logs every byte of the body, even binary data.</p></li><li><p>Multi-part and SSE requests produce unreadable noise.</p></li><li><p>Response logs are buried under system output.</p></li><li><p>There’s no consistent way to see request duration or endpoint performance.</p></li><li><p>You can’t easily toggle verbosity levels or redact sensitive data.</p></li></ul>
<p>You end up either drowning in log output or adding <code>print</code> calls just to survive the debugging process.</p>
<p>This middleware solves that.</p>
<hr>
<h2>The Solution: A Lightweight Logging Layer</h2>
<p><code>OpenAPILoggingMiddleware</code> sits quietly in the pipeline — between your generated routes and the transport layer.
It listens to every incoming request and outgoing response, measures duration, and prints a <strong>structured summary</strong> of what happened.</p>
<p>At its core, the idea is simple:</p>
<blockquote><p>Log exactly what you’d want to see if you were reading someone else’s code.</p></blockquote>
<p>Here’s how it looks in your Vapor or OpenAPI setup:</p>
<pre><code class="language-swift"><span class="tok-keyword">import</span> <span class="tok-type">OpenAPILoggingMiddleware</span>

app<span class="tok-operator">.</span>middleware<span class="tok-operator">.</span><span class="tok-function">use</span>(<span class="tok-type">OpenAPILoggingMiddleware</span>(level<span class="tok-operator">:</span> <span class="tok-operator">.</span>info))</code></pre>
<p>That one line transforms your debugging experience.</p>
<hr>
<h2>Under the Hood</h2>
<p>The middleware implements the <code>OpenAPIMiddleware</code> protocol, which lets it intercept the lifecycle of each OpenAPI operation — before the request is handled and after the response is sent.</p>
<p>Here’s a conceptual overview:</p>
<pre><code class="language-swift"><span class="tok-keyword">public</span> <span class="tok-keyword">struct</span> <span class="tok-type">OpenAPILoggingMiddleware</span><span class="tok-operator">:</span> <span class="tok-type">OpenAPIMiddleware</span> {
    <span class="tok-keyword">public</span> <span class="tok-keyword">func</span> <span class="tok-function">intercept</span>(
        _ request<span class="tok-operator">:</span> <span class="tok-type">Request</span>,
        operation<span class="tok-type">ID</span><span class="tok-operator">:</span> <span class="tok-type">String</span>,
        next<span class="tok-operator">:</span> (<span class="tok-type">Request</span>) <span class="tok-keyword">async</span> <span class="tok-keyword">throws</span> <span class="tok-operator">-</span><span class="tok-operator">&gt;</span> <span class="tok-type">Response</span>
    ) <span class="tok-keyword">async</span> <span class="tok-keyword">throws</span> <span class="tok-operator">-</span><span class="tok-operator">&gt;</span> <span class="tok-type">Response</span> {
        <span class="tok-keyword">let</span> start <span class="tok-operator">=</span> <span class="tok-type">Date</span>()
        <span class="tok-keyword">let</span> response <span class="tok-operator">=</span> <span class="tok-keyword">try</span> <span class="tok-keyword">await</span> <span class="tok-function">next</span>(request)
        <span class="tok-keyword">let</span> duration <span class="tok-operator">=</span> <span class="tok-type">Date</span>()<span class="tok-operator">.</span><span class="tok-function">timeIntervalSince</span>(start) <span class="tok-operator">*</span> <span class="tok-number">1000</span>

        <span class="tok-function">print</span>(<span class="tok-string">"[\(request.method)] \(request.url.path) -&gt; \(response.status.code) [\(Int(duration)) ms]"</span>)
        <span class="tok-keyword">return</span> response
    }
}</code></pre>
<p>Of course, the real implementation handles errors, structured formatting, and configurable verbosity, but the essence is the same:
it’s a <em>transparent layer of introspection</em> that doesn’t alter your data flow.</p>
<hr>
<h2>Logging Philosophy</h2>
<p>There’s a fine balance between too much and too little information.</p>
<p>Traditional logging tools either <strong>dump everything</strong> (which nobody reads) or <strong>show too little</strong> (which leaves you guessing).
<code>OpenAPILoggingMiddleware</code> follows a <em>minimalist</em> principle inspired by Apple’s own frameworks — log what matters, hide what doesn’t.</p>
<p>It’s designed around three key ideas:</p>
<ol><li><p><strong>Contextual logging</strong> — every entry includes method, path, status, and duration.</p></li><li><p><strong>Progressive verbosity</strong> — higher log levels show headers and bodies.</p></li><li><p><strong>Human readability first</strong> — logs are meant to be scanned, not parsed by machines.</p></li></ol>
<p>Here’s an example of what a single request looks like in <code>.info</code> mode:</p>
<pre><code class="language-text">[<span class="tok-type">POST</span>] <span class="tok-operator">/</span>mocks<span class="tok-operator">/</span>messages<span class="tok-operator">/</span>agent<span class="tok-operator">-</span><span class="tok-function">responses</span> (<span class="tok-number">200</span> <span class="tok-type">OK</span>)
<span class="tok-type">Duration</span><span class="tok-operator">:</span> <span class="tok-number">123</span> ms
<span class="tok-type">Body</span><span class="tok-operator">:</span> {<span class="tok-string">"message"</span><span class="tok-operator">:</span><span class="tok-string">"You shall not pass!"</span>}</code></pre>
<p>And at <code>.debug</code> level, you get more detail:</p>
<pre><code class="language-text">[<span class="tok-type">POST</span>] <span class="tok-operator">/</span>mocks<span class="tok-operator">/</span>messages<span class="tok-operator">/</span>agent<span class="tok-operator">-</span>responses
<span class="tok-type">Headers</span><span class="tok-operator">:</span>
  <span class="tok-type">Content</span><span class="tok-operator">-</span><span class="tok-type">Type</span><span class="tok-operator">:</span> application<span class="tok-operator">/</span>json
  <span class="tok-type">Authorization</span><span class="tok-operator">:</span> <span class="tok-type">Bearer</span> <span class="tok-type">Gandalf</span><span class="tok-operator">-</span><span class="tok-type">Was</span><span class="tok-operator">-</span><span class="tok-type">Here</span><span class="tok-number">-1</span>
<span class="tok-type">Duration</span><span class="tok-operator">:</span> <span class="tok-number">123</span> ms
<span class="tok-type">Body</span><span class="tok-operator">:</span> {<span class="tok-string">"message"</span><span class="tok-operator">:</span><span class="tok-string">"You shall not pass!"</span>}</code></pre>
<p>Readable. Structured. Useful.</p>
<hr>
<h2>Why Not Just Use Vapor’s Logger?</h2>
<p>That’s a valid question.</p>
<p>Vapor already includes a powerful logging system based on SwiftLog, so why add another layer?
Because Vapor’s logger is <strong>request-agnostic</strong> — it’s not aware of OpenAPI operations, generated endpoints, or typed models.</p>
<p><code>OpenAPILoggingMiddleware</code> is <strong>contract-aware</strong>.
It sits in the OpenAPI pipeline, so it can access the operation ID, typed body, and structured response — all without touching your route handlers. That distinction is crucial for OpenAPI-based architectures, where most of your server logic is generated.</p>
<hr>
<h2>Integration Examples</h2>
<p>Adding it to a Vapor-based OpenAPI server is trivial:</p>
<pre><code class="language-swift"><span class="tok-keyword">import</span> <span class="tok-type">Vapor</span>
<span class="tok-keyword">import</span> <span class="tok-type">OpenAPIRuntime</span>
<span class="tok-keyword">import</span> <span class="tok-type">OpenAPIVapor</span>
<span class="tok-keyword">import</span> <span class="tok-type">OpenAPILoggingMiddleware</span>

<span class="tok-keyword">let</span> app <span class="tok-operator">=</span> <span class="tok-keyword">try</span> <span class="tok-type">Application</span>(<span class="tok-operator">.</span><span class="tok-function">detect</span>())

<span class="tok-keyword">let</span> server <span class="tok-operator">=</span> <span class="tok-keyword">try</span><span class="tok-operator">!</span> <span class="tok-type">MyOpenAPIServer</span>(app<span class="tok-operator">:</span> app)

app<span class="tok-operator">.</span>middleware<span class="tok-operator">.</span><span class="tok-function">use</span>(<span class="tok-type">OpenAPILoggingMiddleware</span>(level<span class="tok-operator">:</span> <span class="tok-operator">.</span>info))
<span class="tok-keyword">try</span> app<span class="tok-operator">.</span><span class="tok-function">run</span>()</code></pre>
<p>If you’re using <code>OpenAPIRuntime</code> directly (without Vapor), it’s equally simple:</p>
<pre><code class="language-swift"><span class="tok-keyword">var</span> server <span class="tok-operator">=</span> <span class="tok-keyword">try</span> <span class="tok-type">OpenAPIServer</span>()
server<span class="tok-operator">.</span>middleware<span class="tok-operator">.</span><span class="tok-function">append</span>(<span class="tok-type">OpenAPILoggingMiddleware</span>(level<span class="tok-operator">:</span> <span class="tok-operator">.</span>debug))
<span class="tok-keyword">try</span> server<span class="tok-operator">.</span><span class="tok-function">start</span>()</code></pre>
<p>No configuration files. No dependencies. It just works.</p>
<hr>
<h2>Extensibility and Customization</h2>
<p>Logging needs vary between environments.
In development, you might want to see every detail. In production, you want concise summaries. The middleware supports multiple log levels, allowing you to tailor verbosity to your needs:</p>
<ul><li><p><code>.error</code> — log only failed requests.</p></li><li><p><code>.info</code> — log all requests with method, path, duration, and status.</p></li><li><p><code>.debug</code> — include headers and bodies.</p></li></ul>
<p>In future releases, I plan to add <strong>filters</strong> and <strong>formatters</strong> — so you could, for instance, redact specific headers (<code>Authorization</code>, <code>Cookie</code>) or export logs in JSON format for ingestion into a centralized system.</p>
<p>Example:</p>
<pre><code class="language-swift"><span class="tok-keyword">let</span> middleware <span class="tok-operator">=</span> <span class="tok-type">OpenAPILoggingMiddleware</span>(
    level<span class="tok-operator">:</span> <span class="tok-operator">.</span>debug,
    redact<span class="tok-type">Headers</span><span class="tok-operator">:</span> [<span class="tok-string">"Authorization"</span>, <span class="tok-string">"Cookie"</span>]
)</code></pre>
<hr>
<h2>Design Details: Why Simplicity Wins</h2>
<p>Many logging libraries start small and end up as frameworks. They introduce dependency graphs, adapters, output sinks, configuration DSLs — and complexity sneaks in through the back door.</p>
<p>I deliberately avoided that path.</p>
<p><code>OpenAPILoggingMiddleware</code> does one thing: <strong>it shows what your OpenAPI server is doing</strong>.
Nothing else.</p>
<p>No JSON serialization frameworks. No dependency injection. No external configuration.
Just clean Swift and a single dependency on <code>OpenAPIRuntime</code>.</p>
<p>The codebase is small enough to read in one sitting — and that’s intentional. In my opinion, <strong>you should be able to understand every line of code that runs in your server’s core path</strong>.</p>
<hr>
<h2>Performance Considerations</h2>
<p>Logging always comes at a cost, but the impact here is minimal.
The middleware measures request duration using <code>Date().timeIntervalSince(start)</code>, which is negligible compared to network latency or I/O.</p>
<p>You can safely keep it enabled even in staging or pre-production environments.
In production, switching to <code>.error</code> mode will keep your logs clean while still providing visibility into failures.</p>
<hr>
<h2>Testing and Debugging</h2>
<p>The middleware is fully testable using Vapor’s <code>Application</code> test harness or plain <code>XCTest</code> with mock requests.
Here’s a simple test that validates duration and structure:</p>
<pre><code class="language-swift"><span class="tok-keyword">func</span> <span class="tok-function">testMiddlewareLogsRequestAndResponse</span>() <span class="tok-keyword">async</span> <span class="tok-keyword">throws</span> {
    <span class="tok-keyword">let</span> app <span class="tok-operator">=</span> <span class="tok-type">Application</span>(<span class="tok-operator">.</span>testing)
    app<span class="tok-operator">.</span>middleware<span class="tok-operator">.</span><span class="tok-function">use</span>(<span class="tok-type">OpenAPILoggingMiddleware</span>(level<span class="tok-operator">:</span> <span class="tok-operator">.</span>info))
    <span class="tok-keyword">try</span> <span class="tok-keyword">await</span> app<span class="tok-operator">.</span><span class="tok-function">test</span>(<span class="tok-operator">.</span><span class="tok-type">POST</span>, <span class="tok-string">"/ping"</span>, after<span class="tok-type">Response</span><span class="tok-operator">:</span> { res <span class="tok-keyword">in</span>
        <span class="tok-type">XCTAssertEqual</span>(res<span class="tok-operator">.</span>status, <span class="tok-operator">.</span>ok)
    })
}</code></pre>
<p>You can also inject your own logger conforming to <code>LogHandler</code> if you prefer structured output rather than printing to stdout.</p>
<hr>
<h2>Future Work</h2>
<p>The roadmap includes:</p>
<ol><li><p><strong>JSON Formatter</strong> — for structured logs in production environments.</p></li><li><p><strong>Redaction Rules</strong> — for headers and sensitive body fields.</p></li><li><p><strong>Metrics Hooks</strong> — integration with Swift Metrics or Prometheus.</p></li><li><p><strong>Pluggable Output Destinations</strong> — allowing streaming to files or external monitoring systems.</p></li><li><p><strong>Async Logging</strong> — offloading logging I/O to background tasks for ultra-high performance scenarios.</p></li></ol>
<p>Each of these will follow the same guiding principles: clarity, composability, and minimalism.</p>
<hr>
<h2>Philosophy: Clean Visibility</h2>
<p>Logging is not just a developer tool — it’s part of the interface between humans and systems.
A well-designed logging layer doesn’t scream for attention; it quietly reveals how the system behaves.</p>
<p>When I was designing this package, I thought about the elegance of Apple’s own frameworks — the way their APIs feel inevitable, obvious in hindsight. That’s what I aim for here: <strong>a logging middleware that feels invisible until you need it, and indispensable once you use it.</strong></p>
<hr>
<h2>Example Output in Context</h2>
<p>Here’s a real-world example of multiple concurrent requests hitting the same endpoint:</p>
<pre><code class="language-text">[<span class="tok-type">GET</span>] <span class="tok-operator">/</span>user<span class="tok-operator">/</span><span class="tok-function">profile</span> (<span class="tok-number">200</span> <span class="tok-type">OK</span>)
<span class="tok-type">Duration</span><span class="tok-operator">:</span> <span class="tok-number">87</span> ms

[<span class="tok-type">POST</span>] <span class="tok-operator">/</span>mocks<span class="tok-operator">/</span>messages<span class="tok-operator">/</span>agent<span class="tok-operator">-</span><span class="tok-function">responses</span> (<span class="tok-number">200</span> <span class="tok-type">OK</span>)
<span class="tok-type">Duration</span><span class="tok-operator">:</span> <span class="tok-number">121</span> ms
<span class="tok-type">Body</span><span class="tok-operator">:</span> {<span class="tok-string">"message"</span><span class="tok-operator">:</span><span class="tok-string">"You shall not pass!"</span>}

[<span class="tok-type">PATCH</span>] <span class="tok-operator">/</span>user<span class="tok-operator">/</span><span class="tok-function">preferences</span> (<span class="tok-number">204</span> <span class="tok-type">No</span> <span class="tok-type">Content</span>)
<span class="tok-type">Duration</span><span class="tok-operator">:</span> <span class="tok-number">96</span> ms</code></pre>
<p>Notice how easy it is to see patterns — which endpoints are slow, which ones are frequent, which ones failed.
That’s the essence of <strong>observability</strong> — not more data, but <em>useful</em> data.</p>
<hr>
<h2>Real-World Use</h2>
<p>I use <code>OpenAPILoggingMiddleware</code> in all my OpenAPI projects — from small prototypes to complex SSE (Server-Sent Events) systems. It’s particularly valuable when debugging <strong>streamed responses</strong> or <strong>multipart form uploads</strong>, where conventional logs become unreadable.</p>
<p>Because it’s a simple <code>OpenAPIMiddleware</code>, it works with any generated server conforming to the OpenAPI ecosystem — including custom transports and pure SwiftNIO backends.</p>
<hr>
<h2>Closing Thoughts</h2>
<p>This middleware is a reminder that sometimes the simplest tools make the biggest difference.
It’s easy to underestimate the power of <strong>well-designed visibility</strong> — until you remove it and start guessing again.</p>
<p>Whether you’re debugging mock routes, profiling API latency, or simply curious about what your server is doing, <code>OpenAPILoggingMiddleware</code> gives you that quiet transparency every developer deserves.</p>
<p>👉 <strong>Codeberg:</strong> <a href="https://codeberg.org/Mihaela/OpenAPILoggingMiddleware">Mihaela/OpenAPILoggingMiddleware</a></p>
<hr>
<p><em>“Clean code should be composable, testable, and visible when it runs.”</em></p>]]></content:encoded>
</item>
<item>
<title>ExtremePackaging</title>
<link>https://aleahim.com/blog/extreme-packaging/</link>
<guid isPermaLink="true">https://aleahim.com/blog/extreme-packaging/</guid>
<pubDate>Sat, 13 Sep 2025 00:00:00 +0000</pubDate>
<description>Example of Extreme Packaging in Swift</description>
<content:encoded><![CDATA[<p>A Modular Architecture Methodology for Swift Projects
By Mihaela Mihaljevic</p>
<h2>Introduction</h2>
<p><strong>Extreme Packaging</strong> is a methodology for structuring Swift projects with <em>maximal modularity and minimal responsibility per module.</em>
Each module represents a single, isolated unit of logic — small enough to reason about, easy to test, and replaceable without side effects.</p>
<p>The core idea is <strong>separation</strong>:
modules depend on stable interfaces, not on each other’s implementations. This enables scalable architectures that remain flexible as projects evolve, while keeping build times short and dependencies explicit.</p>
<p><strong>Table Of Contents:</strong></p>
<ul><li><p><a href="https://aleahim.com/blog/extreme-packaging/#part-1--project-setup">Part 1 — Project Setup</a></p></li><li><p><a href="https://aleahim.com/blog/extreme-packaging/#part-2--tooling">Part 2 — Tooling</a></p></li></ul>
<h3>Goals</h3>
<ul><li><p>Promote clean boundaries between domains and features</p></li><li><p>Enable independent development and testing per package</p></li><li><p>Simplify refactoring and dependency management</p></li><li><p>Keep compilation fast and the architecture transparent</p></li></ul>
<h3>Process</h3>
<p>The repository is organized into <strong>stages</strong>, each representing a self-contained checkpoint in the project’s evolution.
When moving between stages — especially when reverting to earlier ones — always reset and clean your workspace to ensure it matches the intended state.</p>
<p>Here’s the example for <strong>stage 01</strong>:</p>
<pre><code class="language-bash"><span class="tok-comment"># Ensure you're on the branch you want</span>
git checkout stage<span class="tok-operator">/</span><span class="tok-number">01</span><span class="tok-operator">-</span>init<span class="tok-operator">-</span>packages

<span class="tok-comment"># Fetch the latest version</span>
git fetch orig<span class="tok-keyword">in</span>

<span class="tok-comment"># Reset your branch to the remote version</span>
git reset <span class="tok-operator">-</span><span class="tok-operator">-</span>hard stage<span class="tok-operator">/</span><span class="tok-number">01</span><span class="tok-operator">-</span>init<span class="tok-operator">-</span>packages

<span class="tok-comment"># Delete untracked files and directories</span>
git clean <span class="tok-operator">-</span>fdx</code></pre>
<h3>Philosophy</h3>
<p>In most projects, modularization is an afterthought — introduced when the codebase becomes too large to manage.
<strong>Extreme Packaging</strong> inverts that approach: modularization is the starting point.
By treating each package as an autonomous component from day one, you gain clarity, resilience, and a foundation that naturally scales with complexity.</p>
<h2>Part 1 — Project Setup</h2>
<p>The first step in <strong>Extreme Packaging</strong> is establishing a clear and reproducible project structure that can evolve gradually through defined stages.
Each stage in the repository builds upon the previous one — from the initial package setup, to workspace creation, and finally to adding the app target that ties everything together.</p>
<h3>Overview</h3>
<p>The project begins as a <strong>Swift Package Manager–based structure</strong>, designed for modularity from the start.
At its core is a single <code>Packages</code> directory that houses all functional modules (such as <code>AppFeature</code>, <code>SharedModels</code>, and later others).
Every addition to the project — whether a new feature, UI layer, or platform target — is layered on top of this foundation in small, trackable increments.</p>
<p>Here is the link to the project used:
<a href="https://codeberg.org/Mihaela/ExtremePackaging/src/branch/stage/01-init-packages">https://codeberg.org/Mihaela/ExtremePackaging</a></p>
<h3>1.1 Stage 01 — Initialize Packages</h3>
<h4>Purpose</h4>
<p>This stage ensures a working, self-contained Swift package that compiles and passes initial linting checks.</p>
<h4>Parts</h4>
<p>At <strong>stage/01-init-packages</strong>, the repository contains:</p>
<ul><li><p>A minimal <strong>package structure</strong> with <code>Sources</code> and <code>Tests</code></p></li><li><p><strong>Core configuration file</strong>s:</p>
<ul><li><p><code>.swiftlint.yml</code> for linting rules</p></li><li><p><code>.swiftformat</code> for consistent formatting</p></li><li><p><code>.gitignore</code>, <code>LICENSE</code>, and <code>README.md</code></p></li></ul></li><li><p><strong>Two base modules</strong>:</p>
<ul><li><p><code>AppFeature</code> — serves as the entry feature for the app</p></li><li><p><code>SharedModels</code> — holds simple model definitions</p></li></ul></li></ul>
<h4>Folder Structure snapshot</h4>
<p>Example folder structure:
![](/images/xpack/Screenshot 2025-03-31 at 17.07.23.png)</p>
<p>Inside the  folder:</p>
<pre><code class="language-bash">
➜  ExtremePackaging git<span class="tok-operator">:</span>(ma<span class="tok-keyword">in</span>) ✗ ls <span class="tok-operator">-</span>all
 <span class="tok-operator">.</span>
 <span class="tok-operator">.</span><span class="tok-operator">.</span>
 <span class="tok-operator">.</span>git
 <span class="tok-operator">.</span>gitignore
 <span class="tok-operator">.</span>swiftformat
 <span class="tok-operator">.</span>swiftlint<span class="tok-operator">.</span>yml
 Apps
 LICENSE
 Packages
 README<span class="tok-operator">.</span>md</code></pre>
<p>Inside the <code>Packages</code> folder:</p>
<pre><code class="language-bash">➜  ExtremePackaging git<span class="tok-operator">:</span>(ma<span class="tok-keyword">in</span>) cd Packages
➜  Packages git<span class="tok-operator">:</span>(ma<span class="tok-keyword">in</span>) ls <span class="tok-operator">-</span>all
 <span class="tok-operator">.</span>
 <span class="tok-operator">.</span><span class="tok-operator">.</span>
 Package<span class="tok-operator">.</span>swift
 Sources
 Tests</code></pre>
<p>Inside the <code>Apps</code> folder:</p>
<pre><code class="language-bash"><span class="tok-operator">.</span>
<span class="tok-operator">.</span><span class="tok-operator">.</span>
<span class="tok-operator">.</span>gitkeep</code></pre>
<h4>Basic Package Code:</h4>
<p>I start with package structure like this:</p>
<pre><code class="language-swift"><span class="tok-comment">// swift-tools-version: 6.0</span>

<span class="tok-keyword">import</span> <span class="tok-type">PackageDescription</span>

<span class="tok-keyword">let</span> package <span class="tok-operator">=</span> <span class="tok-type">Package</span>(
    name<span class="tok-operator">:</span> <span class="tok-string">"Main"</span>,
    platforms<span class="tok-operator">:</span> [
        <span class="tok-operator">.</span><span class="tok-function">iOS</span>(<span class="tok-operator">.</span>v<span class="tok-number">17</span>),
        <span class="tok-operator">.</span><span class="tok-function">macOS</span>(<span class="tok-operator">.</span>v<span class="tok-number">14</span>),
    ],
    products<span class="tok-operator">:</span> [
        <span class="tok-operator">.</span><span class="tok-function">singleTargetLibrary</span>(<span class="tok-string">"AppFeature"</span>),
    ],
    dependencies<span class="tok-operator">:</span> [
        <span class="tok-operator">.</span><span class="tok-function">package</span>(url<span class="tok-operator">:</span> <span class="tok-string">"https://github.com/realm/SwiftLint"</span>, exact<span class="tok-operator">:</span> <span class="tok-string">"0.52.3"</span>),
    ],
    targets<span class="tok-operator">:</span> [
        <span class="tok-operator">.</span><span class="tok-function">target</span>(
            name<span class="tok-operator">:</span> <span class="tok-string">"AppFeature"</span>,
            dependencies<span class="tok-operator">:</span> [
                <span class="tok-string">"SharedModels"</span>,
            ]
        ),
        <span class="tok-operator">.</span><span class="tok-function">testTarget</span>(
            name<span class="tok-operator">:</span> <span class="tok-string">"AppFeatureTests"</span>,
            dependencies<span class="tok-operator">:</span> [
                <span class="tok-string">"AppFeature"</span>
            ]
        ),
        <span class="tok-operator">.</span><span class="tok-function">target</span>(
            name<span class="tok-operator">:</span> <span class="tok-string">"SharedModels"</span>
        )
    ]
)

<span class="tok-comment">// Inject base plugins into each target</span>
package<span class="tok-operator">.</span>targets <span class="tok-operator">=</span> package<span class="tok-operator">.</span>targets<span class="tok-operator">.</span>map { target <span class="tok-keyword">in</span>
    <span class="tok-keyword">var</span> plugins <span class="tok-operator">=</span> target<span class="tok-operator">.</span>plugins <span class="tok-operator">?</span><span class="tok-operator">?</span> []
    plugins<span class="tok-operator">.</span><span class="tok-function">append</span>(<span class="tok-operator">.</span><span class="tok-function">plugin</span>(name<span class="tok-operator">:</span> <span class="tok-string">"SwiftLintPlugin"</span>, package<span class="tok-operator">:</span> <span class="tok-string">"SwiftLint"</span>))
    target<span class="tok-operator">.</span>plugins <span class="tok-operator">=</span> plugins
    <span class="tok-keyword">return</span> target
}

<span class="tok-keyword">extension</span> <span class="tok-type">Product</span> {
    <span class="tok-keyword">static</span> <span class="tok-keyword">func</span> <span class="tok-function">singleTargetLibrary</span>(_ name<span class="tok-operator">:</span> <span class="tok-type">String</span>) <span class="tok-operator">-</span><span class="tok-operator">&gt;</span> <span class="tok-type">Product</span> {
        <span class="tok-operator">.</span><span class="tok-function">library</span>(name<span class="tok-operator">:</span> name, targets<span class="tok-operator">:</span> [name])
    }
}</code></pre>
<h4>Dummy Files</h4>
<h5>AppView</h5>
<pre><code class="language-swift"><span class="tok-keyword">import</span> <span class="tok-type">SharedModels</span>
<span class="tok-keyword">import</span> <span class="tok-type">SharedViews</span>
<span class="tok-keyword">import</span> <span class="tok-type">SwiftUI</span>

<span class="tok-keyword">public</span> <span class="tok-keyword">struct</span> <span class="tok-type">AppView</span><span class="tok-operator">:</span> <span class="tok-type">View</span> {
    <span class="tok-keyword">public</span> <span class="tok-keyword">var</span> body<span class="tok-operator">:</span> some <span class="tok-type">View</span> {
        <span class="tok-type">VStack</span> {
            <span class="tok-type">Text</span>(<span class="tok-string">"Extreme Packaging!"</span>)
                <span class="tok-operator">.</span><span class="tok-function">font</span>(<span class="tok-operator">.</span>title)
                <span class="tok-operator">.</span><span class="tok-function">fontWeight</span>(<span class="tok-operator">.</span>bold)
                <span class="tok-operator">.</span><span class="tok-function">multilineTextAlignment</span>(<span class="tok-operator">.</span>center)
                <span class="tok-operator">.</span><span class="tok-function">padding</span>()
        }
    }
}</code></pre>
<h5>DummyModel</h5>
<pre><code class="language-swift"><span class="tok-keyword">import</span> <span class="tok-type">Foundation</span>

<span class="tok-keyword">public</span> <span class="tok-keyword">struct</span> <span class="tok-type">DummyModel</span><span class="tok-operator">:</span> <span class="tok-type">Identifiable</span> {
    <span class="tok-keyword">public</span> <span class="tok-keyword">var</span> id<span class="tok-operator">:</span> <span class="tok-type">UUID</span> <span class="tok-operator">=</span> <span class="tok-operator">.</span><span class="tok-keyword">init</span>()
    <span class="tok-keyword">public</span> <span class="tok-keyword">var</span> title<span class="tok-operator">:</span> <span class="tok-type">String</span>
    <span class="tok-keyword">public</span> <span class="tok-keyword">init</span>(
        id<span class="tok-operator">:</span> <span class="tok-type">UUID</span> <span class="tok-operator">=</span> <span class="tok-operator">.</span><span class="tok-keyword">init</span>(),
        title<span class="tok-operator">:</span> <span class="tok-type">String</span>
    ) {
        <span class="tok-keyword">self</span><span class="tok-operator">.</span>id <span class="tok-operator">=</span> id
        <span class="tok-keyword">self</span><span class="tok-operator">.</span>title <span class="tok-operator">=</span> title
    }
}</code></pre>
<h4>Stage 01-init-packages</h4>
<p>Here’s the code for <strong>stage 01</strong>:</p>
<pre><code class="language-bash">
<span class="tok-comment"># Clone repo if needed</span>
git clone https<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>codeberg<span class="tok-operator">.</span>org<span class="tok-operator">/</span>Mihaela<span class="tok-operator">/</span>ExtremePackaging<span class="tok-operator">.</span>git

<span class="tok-comment"># Ensure you're on the branch you want</span>
git checkout stage<span class="tok-operator">/</span><span class="tok-number">01</span><span class="tok-operator">-</span>init<span class="tok-operator">-</span>packages

<span class="tok-comment"># Fetch the latest version</span>
git fetch orig<span class="tok-keyword">in</span>

<span class="tok-comment"># Reset your branch to the remote version</span>
git reset <span class="tok-operator">-</span><span class="tok-operator">-</span>hard stage<span class="tok-operator">/</span><span class="tok-number">01</span><span class="tok-operator">-</span>init<span class="tok-operator">-</span>packages

<span class="tok-comment"># Delete untracked files and directories</span>
git clean <span class="tok-operator">-</span>fdx</code></pre>
<p>To see how it looks in <strong>Xcode</strong> we need to switch to subfolder <code>packages</code>, since we haven’t created the workspace yet.</p>
<pre><code class="language-bash">➜  ExtremePackaging git<span class="tok-operator">:</span>(stage<span class="tok-operator">/</span><span class="tok-number">01</span><span class="tok-operator">-</span>init<span class="tok-operator">-</span>packages) ✗ cd Packages
➜  Packages git<span class="tok-operator">:</span>(stage<span class="tok-operator">/</span><span class="tok-number">01</span><span class="tok-operator">-</span>init<span class="tok-operator">-</span>packages) ✗ xed <span class="tok-operator">.</span></code></pre>
<h3>1.2 Stage 02 — Add Workspace</h3>
<h4>Purpose</h4>
<p>In <strong>stage/02-workspace-added</strong>, an Xcode <strong>workspace</strong> named <code>Main.xcworkspace</code> is introduced at the project root.
It includes the <code>Packages</code> folder, allowing smooth integration of multiple modules while keeping them decoupled.
This step establishes the foundation for a multi-target environment.</p>
<h4>Workspace creation steps</h4>
<p>Create a new workspace (I name it <code>Main</code>), in the root of our folder.</p>
<p><img src="https://aleahim.com/images/xpack/workspace.png" alt=""></p>
<p>Add folder <code>Packages</code> to the workspace.
It will add our package structure to the project:</p>
<p>![](/images/xpack/Screenshot 2025-03-31 at 18.33.55.png)</p>
<h4>Stage 02-add—to-workspace</h4>
<p>Here’s the code for <strong>stage 02</strong>:</p>
<pre><code class="language-bash">
<span class="tok-comment"># Clone repo if needed</span>
git clone https<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>codeberg<span class="tok-operator">.</span>org<span class="tok-operator">/</span>Mihaela<span class="tok-operator">/</span>ExtremePackaging<span class="tok-operator">.</span>git

<span class="tok-comment"># Ensure you're on the branch you want</span>
git checkout stage<span class="tok-operator">/</span><span class="tok-number">02</span><span class="tok-operator">-</span>add—to<span class="tok-operator">-</span>workspace

<span class="tok-comment"># Fetch the latest version</span>
git fetch orig<span class="tok-keyword">in</span>

<span class="tok-comment"># Reset your branch to the remote version</span>
git reset <span class="tok-operator">-</span><span class="tok-operator">-</span>hard stage<span class="tok-operator">/</span><span class="tok-number">02</span><span class="tok-operator">-</span>add—to<span class="tok-operator">-</span>workspace

<span class="tok-comment"># Delete untracked files and directories</span>
git clean <span class="tok-operator">-</span>fdx</code></pre>
<p>Now we have the <strong>workspace</strong>, so we just need to open <strong>Xcode</strong>:</p>
<pre><code class="language-bash">➜  Packages git<span class="tok-operator">:</span>(stage<span class="tok-operator">/</span><span class="tok-number">02</span><span class="tok-operator">-</span>add—to<span class="tok-operator">-</span>workspace) ✗ xed <span class="tok-operator">.</span>
➜  Packages git<span class="tok-operator">:</span>(stage<span class="tok-operator">/</span><span class="tok-number">02</span><span class="tok-operator">-</span>add—to<span class="tok-operator">-</span>workspace) ✗</code></pre>
<h3>1.3 Stage 03 — Add iOS and macOS App Targets</h3>
<h4>Platform targets creation</h4>
<p><strong>Add new iOS and a new macOS project</strong>
Repeat this for each target:</p>
<p>Create a new project in Xcode:</p>
<p><img src="https://aleahim.com/images/xpack/macos1.jpg" alt=""></p>
<p><img src="https://aleahim.com/images/xpack/macos2.jpg" alt=""></p>
<p>Add each to <code>Apps</code> folder:</p>
<p><img src="https://aleahim.com/images/xpack/macosFolder.jpg" alt=""></p>
<h4>Adding them to workspace</h4>
<p>We will be adding each app target to the workspace, below <em>Packages</em>.
Open the current repository folder and drag both new projects (iOS and macOS) into the workspace sidebar.</p>
<p>Each target remains isolated within its own folder, but both share the same logic through the <em>AppFeature</em> module.
This ensures that all common code — views, models, and reducers — stays within shared packages, while each platform target keeps its own configuration files.</p>
<p>Open the current repo folder:
<img src="https://aleahim.com/images/xpack/show_in_finder.jpg" alt=""></p>
<p><img src="https://aleahim.com/images/xpack/folders_on_disk.jpg" alt=""></p>
<p>Add <strong>macOS</strong> target:</p>
<p><img src="https://aleahim.com/images/xpack/add_macos.jpg" alt=""></p>
<p>Choose “Reference files in place”</p>
<p><img src="https://aleahim.com/images/xpack/reference_files.jpg" alt=""></p>
<p>This is how it looks when added:</p>
<p><img src="https://aleahim.com/images/xpack/add_macOS_done.jpg" alt=""></p>
<p>Add <strong>iOS</strong> target:</p>
<p><img src="https://aleahim.com/images/xpack/add_iOS.jpg" alt=""></p>
<p>This is how it looks when added:</p>
<p><img src="https://aleahim.com/images/xpack/add_iOS_done.jpg" alt=""></p>
<h4>Explanation: Why separate targets</h4>
<p>Keeping iOS and macOS projects distinct allows:</p>
<ul><li><p>Independent platform configuration (e.g. Info.plist, app icons, signing settings)</p></li><li><p>Platform-specific features when needed (e.g. menu commands on macOS, touch gestures on iOS)</p></li><li><p>Consistent architecture and shared logic through modular Swift packages</p></li></ul>
<p>This structure aligns perfectly with the <strong>Extreme Packaging</strong> philosophy — shared foundation, platform-specific shells.</p>
<h4>Configuration steps</h4>
<ul><li><p>Delete automatically added <code>ContentView.swift</code></p></li><li><p>Add <code>AppFeature</code> package to the our target:</p></li></ul>
<p><img src="https://aleahim.com/images/xpack/AppTarget05.png" alt=""></p>
<p>Now our project looks like this:
<img src="https://aleahim.com/images/xpack/AppTarget06.png" alt=""></p>
<p>And the main app starts with the fully testable <code>AppFeature</code></p>
<p><img src="https://aleahim.com/images/xpack/AppTarget07.png" alt=""></p>
<pre><code class="language-swift"><span class="tok-keyword">import</span> <span class="tok-type">SwiftUI</span>
<span class="tok-keyword">import</span> <span class="tok-type">AppFeature</span>

<span class="tok-attribute">@main</span>
<span class="tok-keyword">struct</span> <span class="tok-type">SwiftUIAppApp</span><span class="tok-operator">:</span> <span class="tok-type">App</span> {
    <span class="tok-keyword">var</span> body<span class="tok-operator">:</span> some <span class="tok-type">Scene</span> {
        <span class="tok-type">WindowGroup</span> {
            <span class="tok-type">AppView</span>()
        }
    }
}</code></pre>
<h4>Final structure</h4>
<p>At this point, the project has evolved into a fully cross-platform modular setup.</p>
<p>The screenshot below illustrates the final structure after completing <strong>Stage 03 (iOS &amp; macOS targets added)</strong>:</p>
<p><img src="https://aleahim.com/images/xpack/final_struc.jpg" alt=""></p>
<ul><li><p>The <strong>Packages</strong> directory defines the shared Swift Package with <code>AppFeature</code> and <code>SharedModels</code> modules.</p></li><li><p>Both <strong>iOS</strong> and <strong>macOS</strong> apps live inside the <code>Apps</code> folder, each with its own assets, entry point, and platform-specific configuration files.</p></li><li><p>The <strong><code>Package.swift</code></strong> file defines a single modular product, with SwiftLint integrated as a plugin and dependencies kept cleanly separated.</p></li><li><p>This structure enables both platforms to share logic and UI built with <strong>SwiftUI</strong>, while still allowing native customization per platform.</p></li></ul>
<p><strong>Result:</strong>
A clean, modular workspace where shared code resides in packages, and each platform acts only as a thin presentation shell — the essence of <em>Extreme Packaging</em>.</p>
<h4>Stage 03-apps-added</h4>
<p>In <strong>stage/03-apps-added</strong>, we create two separate app targets — one for <strong>iOS</strong> and one for <strong>macOS</strong> — both sharing the same SwiftUI core logic.</p>
<p>Each app lives inside the <code>Apps</code> folder and imports the <code>AppFeature</code> module, demonstrating how the same feature package can power multiple platforms with no duplicated code.</p>
<p>After adding the targets:</p>
<ul><li><p>Delete the automatically generated <code>ContentView.swift</code></p></li><li><p>Add the <code>AppFeature</code> package dependency to both targets</p></li><li><p>Verify that both apps build successfully</p></li></ul>
<p>This stage concludes with a fully functional cross-platform setup where both iOS and macOS share a unified architecture driven by modular packages.</p>
<p>Here’s the code for <strong>stage 03</strong>:</p>
<pre><code class="language-bash">
<span class="tok-comment"># Clone repo if needed</span>
git clone https<span class="tok-operator">:</span><span class="tok-operator">/</span><span class="tok-operator">/</span>codeberg<span class="tok-operator">.</span>org<span class="tok-operator">/</span>Mihaela<span class="tok-operator">/</span>ExtremePackaging<span class="tok-operator">.</span>git

<span class="tok-comment"># Ensure you're on the branch you want</span>
git checkout stage<span class="tok-operator">/</span><span class="tok-number">03</span><span class="tok-operator">-</span>apps<span class="tok-operator">-</span>added

<span class="tok-comment"># Fetch the latest version</span>
git fetch orig<span class="tok-keyword">in</span>

<span class="tok-comment"># Reset your branch to the remote version</span>
git reset <span class="tok-operator">-</span><span class="tok-operator">-</span>hard stage<span class="tok-operator">/</span><span class="tok-number">03</span><span class="tok-operator">-</span>apps<span class="tok-operator">-</span>added

<span class="tok-comment"># Delete untracked files and directories</span>
git clean <span class="tok-operator">-</span>fdx</code></pre>
<h3>Summary</h3>
<table><thead><tr><th style="text-align:left">Stage</th><th style="text-align:left">Description</th><th style="text-align:left">Key Additions</th></tr></thead><tbody><tr><td style="text-align:left"><strong>01</strong></td><td style="text-align:left">Initial package setup</td><td style="text-align:left"><code>Package.swift</code>, SwiftLint &amp; SwiftFormat configs, dummy modules (<code>AppFeature</code>, <code>SharedModels</code>)</td></tr><tr><td style="text-align:left"><strong>02</strong></td><td style="text-align:left">Workspace creation</td><td style="text-align:left"><code>Main.xcworkspace</code>, integrated <code>Packages</code> folder for modular management</td></tr><tr><td style="text-align:left"><strong>03</strong></td><td style="text-align:left">iOS &amp; macOS app targets added</td><td style="text-align:left">Separate iOS and macOS apps in <code>Apps/</code>, both using <code>AppFeature</code> for shared SwiftUI logic</td></tr></tbody></table>
<p>Each stage corresponds to a dedicated branch in the repository, allowing you to switch between checkpoints and observe the project’s evolution step by step.
This structure provides a transparent history of how a modular Swift project grows from a single package into a fully multi-platform architecture.</p>
<h2>Part 2 — Tooling</h2>
<ul><li><p>Every project includes <strong>SwiftLint</strong> and <strong>SwiftFormat</strong> for enforcing consistent code style and quality</p></li><li><p>Each stage of the repository introduces incremental improvements, from initializing packages to adding app targets and integrations</p></li><li><p>These are applied from the very first stage but can be customized at any point.</p></li></ul>
<h3>GitIgnore</h3>
<pre><code class="language-bash"><span class="tok-comment"># Xcode</span>
<span class="tok-comment">#</span>
<span class="tok-comment"># gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore &amp; Swift.gitignore</span>

<span class="tok-comment">## User settings</span>
xcuserdata<span class="tok-operator">/</span>

<span class="tok-comment">## Obj-C/Swift specific</span>
<span class="tok-operator">*</span><span class="tok-operator">.</span>hmap

<span class="tok-comment">## App packaging</span>
<span class="tok-operator">*</span><span class="tok-operator">.</span>ipa
<span class="tok-operator">*</span><span class="tok-operator">.</span>dSYM<span class="tok-operator">.</span>zip
<span class="tok-operator">*</span><span class="tok-operator">.</span>dSYM

<span class="tok-comment">## Playgrounds</span>
timeline<span class="tok-operator">.</span>xctimeline
playground<span class="tok-operator">.</span>xcworkspace

<span class="tok-comment"># Swift Package Manager</span>
<span class="tok-operator">.</span>build<span class="tok-operator">/</span>
<span class="tok-operator">.</span>swiftpm<span class="tok-operator">/</span>
Package<span class="tok-operator">.</span>resolved

<span class="tok-comment"># Xcode workspace &amp; projects</span>
<span class="tok-operator">*</span><span class="tok-operator">.</span>xcworkspace<span class="tok-operator">/</span>xcshareddata<span class="tok-operator">/</span>WorkspaceSettings<span class="tok-operator">.</span>xcsettings
<span class="tok-operator">*</span><span class="tok-operator">.</span>xcworkspace<span class="tok-operator">/</span>xcuserdata<span class="tok-operator">/</span>
DerivedData<span class="tok-operator">/</span>
<span class="tok-operator">*</span><span class="tok-operator">.</span>xcuserstate
<span class="tok-operator">*</span><span class="tok-operator">.</span>xcscmblueprint
<span class="tok-operator">*</span><span class="tok-operator">.</span>xccheckout

<span class="tok-comment"># macOS system files</span>
<span class="tok-operator">.</span>DS_Store

<span class="tok-comment"># Carthage</span>
Carthage<span class="tok-operator">/</span>Build<span class="tok-operator">/</span>

<span class="tok-comment"># fastlane</span>
fastlane<span class="tok-operator">/</span>report<span class="tok-operator">.</span>xml
fastlane<span class="tok-operator">/</span>Preview<span class="tok-operator">.</span>html
fastlane<span class="tok-operator">/</span>screenshots<span class="tok-operator">/</span><span class="tok-operator">*</span><span class="tok-operator">*</span><span class="tok-operator">/</span><span class="tok-operator">*</span><span class="tok-operator">.</span>png
fastlane<span class="tok-operator">/</span>test_output

<span class="tok-comment"># Custom folders used in ExtremePackaging</span>
Stage<span class="tok-operator">/</span>
Tasks<span class="tok-operator">/</span><span class="tok-operator">.</span>build<span class="tok-operator">/</span></code></pre>
<h3>SwiftFormat Config</h3>
<p>This is my <code>.swiftformat</code></p>
<pre><code class="language-yaml"><span class="tok-comment"># General Options</span>
--swiftversion <span class="tok-number">5.7</span>

<span class="tok-comment"># File Options</span>
--exclude Stage<span class="tok-operator">,</span>Tasks/.build<span class="tok-operator">,</span>ThirdParty<span class="tok-operator">,</span>**/SwiftGen/*<span class="tok-operator">,</span>**/Sourcery/*<span class="tok-operator">,</span>Frameworks/swift-composable-architecture<span class="tok-operator">,</span>**.generated.swift

<span class="tok-comment"># Format Options</span>

--allman <span class="tok-literal">false</span>
--binarygrouping none
--closingparen balanced
--commas always
--conflictmarkers reject
--decimalgrouping none
--elseposition same-line
--exponentcase lowercase
--exponentgrouping disabled
--fractiongrouping disabled
--fragment <span class="tok-literal">false</span>
--header ignore
--hexgrouping none
--hexliteralcase lowercase
--ifdef <span class="tok-literal">no</span>-indent
--importgrouping alphabetized
--indent <span class="tok-number">4</span>
--indentcase <span class="tok-literal">false</span>
<span class="tok-comment"># make sure this matches .swiftlint</span>
--maxwidth <span class="tok-number">180</span>
--nospaceoperators ..&lt;<span class="tok-operator">,</span>...
--octalgrouping none
--operatorfunc <span class="tok-literal">no</span>-space
--selfrequired
--stripunusedargs closure-only
--trailingclosures
--wraparguments before-first
--wrapcollections before-first
--wrapparameters before-first

<span class="tok-comment"># Rules</span>
--disable hoistAwait
--disable hoistPatternLet
--disable hoistTry
--disable wrapMultilineStatementBraces
--disable extensionAccessControl</code></pre>
<h3>SwiftLint Config</h3>
<pre><code class="language-yaml"><span class="tok-property">disabled_rules</span><span class="tok-operator">:</span> <span class="tok-comment"># rule identifiers to exclude from running</span>
  - opening_brace
  - operator_whitespace
  - orphaned_doc_comment

<span class="tok-property">opt_in_rules</span><span class="tok-operator">:</span>
  - empty_count
  - force_unwrapping
  - shorthand_optional_binding
  - weak_delegate

<span class="tok-property">excluded</span><span class="tok-operator">:</span>
  - <span class="tok-string">"*.generated"</span>

<span class="tok-property">custom_rules</span><span class="tok-operator">:</span>
  <span class="tok-comment"># check's for Combine's .assign(to: xxx, on: self) ref-cycle</span>
  <span class="tok-property">combine_assign_to_self</span><span class="tok-operator">:</span>
    <span class="tok-property">included</span><span class="tok-operator">:</span> <span class="tok-string">".*\\.swift"</span>
    <span class="tok-property">name</span><span class="tok-operator">:</span> <span class="tok-string">"`assign` to self"</span>
    <span class="tok-property">regex</span><span class="tok-operator">:</span> <span class="tok-string">'\.assign\(to: [^,]*, on: self\)'</span>
    <span class="tok-property">message</span><span class="tok-operator">:</span> <span class="tok-string">"For assigning on self, use assignNoRetain(to: ..., on: self)."</span>
    <span class="tok-property">severity</span><span class="tok-operator">:</span> error
  <span class="tok-property">duplicate_remove_duplicates</span><span class="tok-operator">:</span>
    <span class="tok-property">included</span><span class="tok-operator">:</span> <span class="tok-string">".*\\.swift"</span>
    <span class="tok-property">name</span><span class="tok-operator">:</span> <span class="tok-string">"Duplicate `removeDuplicates()`"</span>
    <span class="tok-property">message</span><span class="tok-operator">:</span> <span class="tok-string">"ViewStore's publisher already does `removeDuplicates()`"</span>
    <span class="tok-property">regex</span><span class="tok-operator">:</span> <span class="tok-string">'publisher\.[^(|{|,]*removeDuplicates\(\)'</span>
    <span class="tok-property">severity</span><span class="tok-operator">:</span> error
  <span class="tok-property">dont_scale_to_zero</span><span class="tok-operator">:</span>
    <span class="tok-property">included</span><span class="tok-operator">:</span> <span class="tok-string">".*\\.swift"</span>
    <span class="tok-property">name</span><span class="tok-operator">:</span> <span class="tok-string">"Don't scale down to 0."</span>
    <span class="tok-property">regex</span><span class="tok-operator">:</span> <span class="tok-string">"\\.scaleEffect\\([^\\)]*(\\ 0\\ [^\\)]*\\)|0.0(\\ |\\))|\\ 0(\\)|,))"</span>
    <span class="tok-property">message</span><span class="tok-operator">:</span> <span class="tok-string">"Please make sure to pass a number not equal zero, so transformations don't throw warnings like `ignoring singular matrix`."</span>
    <span class="tok-property">severity</span><span class="tok-operator">:</span> error
  <span class="tok-property">use_data_constructor_over_string_member</span><span class="tok-operator">:</span>
    <span class="tok-property">included</span><span class="tok-operator">:</span> <span class="tok-string">".*\\.swift"</span>
    <span class="tok-property">name</span><span class="tok-operator">:</span> <span class="tok-string">"Do not use String.data(using: .utf8)"</span>
    <span class="tok-property">regex</span><span class="tok-operator">:</span> <span class="tok-string">"\\.?data\\(using: \\.utf8\\)"</span>
    <span class="tok-property">message</span><span class="tok-operator">:</span> <span class="tok-string">"Please use Data(string.utf8) instead of String.data(using: .utf8) because the Data constructor is non-optional and Strings are guaranteed to be encodable as .utf8"</span>
    <span class="tok-property">severity</span><span class="tok-operator">:</span> error
  <span class="tok-property">tca_explicit_generics_reducer</span><span class="tok-operator">:</span>
    <span class="tok-property">included</span><span class="tok-operator">:</span> <span class="tok-string">".*\\.swift"</span>
    <span class="tok-property">name</span><span class="tok-operator">:</span> <span class="tok-string">"Explicit Generics for Reducer"</span>
    <span class="tok-property">regex</span><span class="tok-operator">:</span> <span class="tok-string">'Reduce\s+\{'</span>
    <span class="tok-property">message</span><span class="tok-operator">:</span> <span class="tok-string">"Use explicit generics in ReducerBuilder (Reduce&lt;State, Action&gt;) for successful autocompletion."</span>
    <span class="tok-property">severity</span><span class="tok-operator">:</span> error
  <span class="tok-property">tca_scope_unused_closure_parameter</span><span class="tok-operator">:</span>
    <span class="tok-property">name</span><span class="tok-operator">:</span> <span class="tok-string">"TCA Scope Unused Closure Parameter"</span>
    <span class="tok-property">regex</span><span class="tok-operator">:</span> <span class="tok-string">'\.scope\(\s*state\s*:\s*\{\s*\_'</span>
    <span class="tok-property">message</span><span class="tok-operator">:</span> <span class="tok-string">"Explicitly use closure parameter when scoping store (ensures the right state is being mutated)"</span>
    <span class="tok-property">severity</span><span class="tok-operator">:</span> error
  <span class="tok-property">tca_use_observe_viewstore_api</span><span class="tok-operator">:</span>
    <span class="tok-property">name</span><span class="tok-operator">:</span> <span class="tok-string">"TCA ViewStore observe API"</span>
    <span class="tok-property">regex</span><span class="tok-operator">:</span> <span class="tok-string">'ViewStore\(store\.scope'</span>
    <span class="tok-property">message</span><span class="tok-operator">:</span> <span class="tok-string">"Use modern observe: api instead of store.scope"</span>
    <span class="tok-property">severity</span><span class="tok-operator">:</span> error

<span class="tok-property">trailing_comma</span><span class="tok-operator">:</span>
    <span class="tok-property">mandatory_comma</span><span class="tok-operator">:</span> <span class="tok-literal">true</span>

<span class="tok-property">cyclomatic_complexity</span><span class="tok-operator">:</span>
  <span class="tok-property">ignores_case_statements</span><span class="tok-operator">:</span> <span class="tok-literal">true</span>
  <span class="tok-property">warning</span><span class="tok-operator">:</span> <span class="tok-number">20</span>

<span class="tok-property">file_length</span><span class="tok-operator">:</span>
  <span class="tok-property">warning</span><span class="tok-operator">:</span> <span class="tok-number">1000</span>
  <span class="tok-property">error</span><span class="tok-operator">:</span> <span class="tok-number">1000</span>

<span class="tok-property">identifier_name</span><span class="tok-operator">:</span>
  <span class="tok-property">severity</span><span class="tok-operator">:</span> warning
  <span class="tok-property">allowed_symbols</span><span class="tok-operator">:</span> <span class="tok-string">"_"</span>
  <span class="tok-property">min_length</span><span class="tok-operator">:</span> <span class="tok-number">2</span>
  <span class="tok-property">max_length</span><span class="tok-operator">:</span>
    <span class="tok-property">warning</span><span class="tok-operator">:</span> <span class="tok-number">90</span>
    <span class="tok-property">error</span><span class="tok-operator">:</span> <span class="tok-number">90</span>
  <span class="tok-property">excluded</span><span class="tok-operator">:</span>
    - iO
    - id
    - vc
    - x
    - y
    - i
    - pi
    - d

<span class="tok-property">legacy_constant</span><span class="tok-operator">:</span> error
<span class="tok-property">legacy_constructor</span><span class="tok-operator">:</span> error

<span class="tok-property">line_length</span><span class="tok-operator">:</span>
  <span class="tok-property">warning</span><span class="tok-operator">:</span> <span class="tok-number">180</span>
  <span class="tok-property">error</span><span class="tok-operator">:</span> <span class="tok-number">180</span>
  <span class="tok-property">ignores_comments</span><span class="tok-operator">:</span> <span class="tok-literal">true</span>
  <span class="tok-property">ignores_urls</span><span class="tok-operator">:</span> <span class="tok-literal">true</span>

<span class="tok-property">nesting</span><span class="tok-operator">:</span>
  <span class="tok-property">type_level</span><span class="tok-operator">:</span>
    <span class="tok-property">warning</span><span class="tok-operator">:</span> <span class="tok-number">3</span>
    <span class="tok-property">error</span><span class="tok-operator">:</span> <span class="tok-number">3</span>
  <span class="tok-property">function_level</span><span class="tok-operator">:</span>
    <span class="tok-property">warning</span><span class="tok-operator">:</span> <span class="tok-number">5</span>
    <span class="tok-property">error</span><span class="tok-operator">:</span> <span class="tok-number">5</span>

<span class="tok-property">function_parameter_count</span><span class="tok-operator">:</span>
  <span class="tok-property">warning</span><span class="tok-operator">:</span> <span class="tok-number">5</span>

<span class="tok-property">force_cast</span><span class="tok-operator">:</span> warning
<span class="tok-property">force_unwrapping</span><span class="tok-operator">:</span> warning

<span class="tok-property">type_body_length</span><span class="tok-operator">:</span>
  - <span class="tok-number">300</span> <span class="tok-comment"># warning</span>
  - <span class="tok-number">300</span> <span class="tok-comment"># error</span>

<span class="tok-property">large_tuple</span><span class="tok-operator">:</span>
  - <span class="tok-number">3</span>  <span class="tok-comment"># warning</span>
  - <span class="tok-number">10</span> <span class="tok-comment"># error%</span></code></pre>]]></content:encoded>
</item>
<item>
<title>CVBuilder: A Swift Package for Professional CV Management</title>
<link>https://aleahim.com/blog/c-v-builder/</link>
<guid isPermaLink="true">https://aleahim.com/blog/c-v-builder/</guid>
<pubDate>Sun, 13 Apr 2025 00:00:00 +0000</pubDate>
<description>A Swift package for building and rendering CVs in multiple formats</description>
<content:encoded><![CDATA[<p>CVBuilder is a powerful Swift package that I developed to solve a common problem: managing and rendering professional CVs in multiple formats. Whether you need to generate a Markdown CV for GitHub, an HTML version for your website, or a plain text version for job applications, CVBuilder has you covered.</p>
<h2>Why CVBuilder?</h2>
<p>In today’s digital world, professionals often need to maintain their CVs in multiple formats and keep them in sync. Traditional word processors make this process tedious and error-prone. CVBuilder solves this by:</p>
<ul><li><p>Providing a strongly-typed data model for CVs</p></li><li><p>Supporting multiple output formats (Markdown, HTML, plain text)</p></li><li><p>Making it easy to maintain a single source of truth</p></li><li><p>Enabling programmatic CV generation and updates</p></li></ul>
<h2>Core Features</h2>
<h3>1. Strongly Typed Data Model</h3>
<p>CVBuilder provides a comprehensive data model that covers all aspects of a professional CV:</p>
<pre><code class="language-swift"><span class="tok-type">CV</span>
├── <span class="tok-type">ContactInfo</span>
├── <span class="tok-type">Education</span>
├── <span class="tok-type">WorkExperience</span>
│   ├── <span class="tok-type">Company</span>
│   ├── <span class="tok-type">Role</span>
│   └── <span class="tok-type">ProjectExperience</span>
│       ├── <span class="tok-type">Project</span>
│       └── <span class="tok-type">Tech</span>
└── <span class="tok-type">Period</span></code></pre>
<p>Each component is strongly typed, ensuring data integrity and making it impossible to create invalid CVs.</p>
<h3>2. Multiple Renderers</h3>
<p>CVBuilder supports three different renderers out of the box:</p>
<ol><li><p><strong>MarkdownCVRenderer</strong>: Generates clean, well-formatted Markdown</p></li><li><p><strong>StringCVRenderer</strong>: Produces plain text output</p></li><li><p><strong>IgniteRenderer</strong>: Creates beautiful HTML using the Ignite static site generator</p></li></ol>
<h3>3. Modular Architecture</h3>
<p>The package is split into two main components:</p>
<ul><li><p><code>CVBuilder</code>: Core types and basic rendering</p></li><li><p><code>CVBuilderIgnite</code>: Optional HTML rendering support</p></li></ul>
<p>This separation ensures that projects that don’t need HTML rendering don’t have to pull in unnecessary dependencies.</p>
<h2>Getting Started</h2>
<h3>Installation</h3>
<p>Add CVBuilder to your project using Swift Package Manager:</p>
<pre><code class="language-swift"><span class="tok-operator">.</span><span class="tok-function">package</span>(url<span class="tok-operator">:</span> <span class="tok-string">"https://codeberg.org/Mihaela/cvbuilder.git"</span>, branch<span class="tok-operator">:</span> <span class="tok-string">"main"</span>)</code></pre>
<p>Then add the desired product:</p>
<pre><code class="language-swift"><span class="tok-operator">.</span><span class="tok-function">product</span>(name<span class="tok-operator">:</span> <span class="tok-string">"CVBuilder"</span>, package<span class="tok-operator">:</span> <span class="tok-string">"cvbuilder"</span>),
<span class="tok-operator">.</span><span class="tok-function">product</span>(name<span class="tok-operator">:</span> <span class="tok-string">"CVBuilderIgnite"</span>, package<span class="tok-operator">:</span> <span class="tok-string">"cvbuilder"</span>) <span class="tok-comment">// if using Ignite</span></code></pre>
<h3>Creating a CV</h3>
<p>Here’s a simple example of creating a CV:</p>
<pre><code class="language-swift"><span class="tok-keyword">import</span> <span class="tok-type">CVBuilder</span>

<span class="tok-comment">// Create contact info</span>
<span class="tok-keyword">let</span> contact<span class="tok-type">Info</span> <span class="tok-operator">=</span> <span class="tok-type">ContactInfo</span>(
    email<span class="tok-operator">:</span> <span class="tok-string">"jane.doe@example.com"</span>,
    phone<span class="tok-operator">:</span> <span class="tok-string">"+1 (555) 123-4567"</span>,
    linked<span class="tok-keyword">In</span><span class="tok-operator">:</span> <span class="tok-type">URL</span>(string<span class="tok-operator">:</span> <span class="tok-string">"https://linkedin.com/in/janedoe"</span>),
    github<span class="tok-operator">:</span> <span class="tok-type">URL</span>(string<span class="tok-operator">:</span> <span class="tok-string">"https://github.com/janedoe"</span>),
    website<span class="tok-operator">:</span> <span class="tok-type">URL</span>(string<span class="tok-operator">:</span> <span class="tok-string">"https://janedoe.dev"</span>),
    location<span class="tok-operator">:</span> <span class="tok-string">"San Francisco, CA"</span>
)

<span class="tok-comment">// Create education</span>
<span class="tok-keyword">let</span> education <span class="tok-operator">=</span> <span class="tok-type">Education</span>(
    institution<span class="tok-operator">:</span> <span class="tok-string">"Stanford University"</span>,
    degree<span class="tok-operator">:</span> <span class="tok-string">"B.S."</span>,
    field<span class="tok-operator">:</span> <span class="tok-string">"Computer Science"</span>,
    period<span class="tok-operator">:</span> <span class="tok-type">Period</span>(
        start<span class="tok-operator">:</span> <span class="tok-operator">.</span><span class="tok-keyword">init</span>(month<span class="tok-operator">:</span> <span class="tok-number">9</span>, year<span class="tok-operator">:</span> <span class="tok-number">2010</span>),
        end<span class="tok-operator">:</span> <span class="tok-operator">.</span><span class="tok-keyword">init</span>(month<span class="tok-operator">:</span> <span class="tok-number">6</span>, year<span class="tok-operator">:</span> <span class="tok-number">2014</span>)
    )
)

<span class="tok-comment">// Create the CV</span>
<span class="tok-keyword">let</span> cv <span class="tok-operator">=</span> <span class="tok-type">CV</span><span class="tok-operator">.</span><span class="tok-function">create</span>(
    name<span class="tok-operator">:</span> <span class="tok-string">"Jane Doe"</span>,
    title<span class="tok-operator">:</span> <span class="tok-string">"Senior Mobile Developer"</span>,
    summary<span class="tok-operator">:</span> <span class="tok-string">"Passionate mobile developer with 5+ years experience..."</span>,
    contact<span class="tok-type">Info</span><span class="tok-operator">:</span> contact<span class="tok-type">Info</span>,
    education<span class="tok-operator">:</span> [education],
    projects<span class="tok-operator">:</span> [project<span class="tok-number">1</span>, project<span class="tok-number">2</span>]
)</code></pre>
<h3>Generating Output</h3>
<p>Once you have your CV data model, generating different formats is straightforward:</p>
<pre><code class="language-swift"><span class="tok-comment">// Generate Markdown</span>
<span class="tok-keyword">let</span> markdown <span class="tok-operator">=</span> <span class="tok-type">MarkdownCVRenderer</span>(cv<span class="tok-operator">:</span> cv)<span class="tok-operator">.</span><span class="tok-function">render</span>()

<span class="tok-comment">// Generate plain text</span>
<span class="tok-keyword">let</span> plain<span class="tok-type">Text</span> <span class="tok-operator">=</span> <span class="tok-type">StringCVRenderer</span>(cv<span class="tok-operator">:</span> cv)<span class="tok-operator">.</span><span class="tok-function">render</span>()

<span class="tok-comment">// Generate HTML (requires CVBuilderIgnite)</span>
<span class="tok-keyword">let</span> html <span class="tok-operator">=</span> <span class="tok-type">IgniteRenderer</span>(cv<span class="tok-operator">:</span> cv)<span class="tok-operator">.</span><span class="tok-function">render</span>()</code></pre>
<h2>Advanced Features</h2>
<h3>Project Builder Pattern</h3>
<p>CVBuilder includes a builder pattern for creating project experiences:</p>
<pre><code class="language-swift"><span class="tok-keyword">let</span> project <span class="tok-operator">=</span> <span class="tok-keyword">try</span><span class="tok-operator">!</span> <span class="tok-type">Project</span><span class="tok-operator">.</span><span class="tok-type">Builder</span>()
    <span class="tok-operator">.</span><span class="tok-function">withName</span>(<span class="tok-string">"iOS App Redesign"</span>)
    <span class="tok-operator">.</span><span class="tok-function">withCompany</span>(apple<span class="tok-type">Company</span>)
    <span class="tok-operator">.</span><span class="tok-function">withRole</span>(senior<span class="tok-type">IOS</span>)
    <span class="tok-operator">.</span><span class="tok-function">withPeriod</span>(start<span class="tok-operator">:</span> (month<span class="tok-operator">:</span> <span class="tok-number">3</span>, year<span class="tok-operator">:</span> <span class="tok-number">2020</span>), end<span class="tok-operator">:</span> (month<span class="tok-operator">:</span> <span class="tok-number">9</span>, year<span class="tok-operator">:</span> <span class="tok-number">2021</span>))
    <span class="tok-operator">.</span><span class="tok-function">addDescription</span>(<span class="tok-string">"Led a team of 5 developers..."</span>)
    <span class="tok-operator">.</span><span class="tok-function">withTechs</span>([swift, swift<span class="tok-type">UI</span>])
    <span class="tok-operator">.</span><span class="tok-function">build</span>()</code></pre>
<h3>Tech Stack Management</h3>
<p>The package includes a <code>Tech</code> type for managing technical skills:</p>
<pre><code class="language-swift"><span class="tok-keyword">let</span> swift <span class="tok-operator">=</span> <span class="tok-type">Tech</span>(name<span class="tok-operator">:</span> <span class="tok-string">"Swift"</span>, category<span class="tok-operator">:</span> <span class="tok-operator">.</span>language)
<span class="tok-keyword">let</span> swift<span class="tok-type">UI</span> <span class="tok-operator">=</span> <span class="tok-type">Tech</span>(name<span class="tok-operator">:</span> <span class="tok-string">"SwiftUI"</span>, category<span class="tok-operator">:</span> <span class="tok-operator">.</span>framework)
<span class="tok-keyword">let</span> rest<span class="tok-type">API</span> <span class="tok-operator">=</span> <span class="tok-type">Tech</span>(name<span class="tok-operator">:</span> <span class="tok-string">"REST API"</span>, category<span class="tok-operator">:</span> <span class="tok-operator">.</span>concept)</code></pre>
<h2>How I Use It on This Webpage</h2>
<p>My blog is built using <a href="https://github.com/twostraws/Ignite">Ignite</a>, a static site generator for Swift. I’ve integrated CVBuilder into my website to maintain and display my CV in a clean, professional format.</p>
<h3>Package Dependencies</h3>
<p>I include CVBuilder in my website’s package file:</p>
<pre><code class="language-swift"><span class="tok-keyword">let</span> package <span class="tok-operator">=</span> <span class="tok-type">Package</span>(
    name<span class="tok-operator">:</span> <span class="tok-string">"IgniteStarter"</span>,
    platforms<span class="tok-operator">:</span> [<span class="tok-operator">.</span><span class="tok-function">macOS</span>(<span class="tok-operator">.</span>v<span class="tok-number">13</span>)],
    dependencies<span class="tok-operator">:</span> [
        <span class="tok-operator">.</span><span class="tok-function">package</span>(url<span class="tok-operator">:</span> <span class="tok-string">"https://github.com/twostraws/Ignite.git"</span>, branch<span class="tok-operator">:</span> <span class="tok-string">"main"</span>),
        <span class="tok-operator">.</span><span class="tok-function">package</span>(url<span class="tok-operator">:</span> <span class="tok-string">"https://codeberg.org/Mihaela/cvbuilder.git"</span>, branch<span class="tok-operator">:</span> <span class="tok-string">"main"</span>)
    ],
    targets<span class="tok-operator">:</span> [
        <span class="tok-operator">.</span><span class="tok-function">executableTarget</span>(
            name<span class="tok-operator">:</span> <span class="tok-string">"IgniteStarter"</span>,
            dependencies<span class="tok-operator">:</span> [
                <span class="tok-operator">.</span><span class="tok-function">product</span>(name<span class="tok-operator">:</span> <span class="tok-string">"Ignite"</span>, package<span class="tok-operator">:</span> <span class="tok-string">"Ignite"</span>),
                <span class="tok-operator">.</span><span class="tok-function">product</span>(name<span class="tok-operator">:</span> <span class="tok-string">"CVBuilder"</span>, package<span class="tok-operator">:</span> <span class="tok-string">"cvbuilder"</span>),
                <span class="tok-operator">.</span><span class="tok-function">product</span>(name<span class="tok-operator">:</span> <span class="tok-string">"CVBuilderIgnite"</span>, package<span class="tok-operator">:</span> <span class="tok-string">"cvbuilder"</span>)
            ]
        ),
    ]
)</code></pre>
<h3>Personal Information</h3>
<p>I maintain my personal information in a dedicated Swift file (<code>CV+Mihaela.swift</code>):</p>
<pre><code class="language-swift"><span class="tok-keyword">public</span> <span class="tok-keyword">extension</span> <span class="tok-type">CV</span> {
    <span class="tok-keyword">static</span> <span class="tok-keyword">func</span> <span class="tok-function">createForMihaela</span>() <span class="tok-operator">-</span><span class="tok-operator">&gt;</span> <span class="tok-type">CV</span> {
        <span class="tok-keyword">let</span> contact<span class="tok-type">Info</span> <span class="tok-operator">=</span> <span class="tok-type">ContactInfo</span>(
            email<span class="tok-operator">:</span> <span class="tok-string">"me@me.com"</span>,
            phone<span class="tok-operator">:</span> <span class="tok-string">"+12233445566"</span>,
            linked<span class="tok-keyword">In</span><span class="tok-operator">:</span> <span class="tok-type">URL</span>(string<span class="tok-operator">:</span> <span class="tok-string">"https://www.linkedin.com/in/mylinkedin"</span>),
            github<span class="tok-operator">:</span> <span class="tok-type">URL</span>(string<span class="tok-operator">:</span> <span class="tok-string">"https://github.com/mygithub"</span>),
            website<span class="tok-operator">:</span> <span class="tok-type">URL</span>(string<span class="tok-operator">:</span> <span class="tok-string">"https://mywebsite.com"</span>),
            location<span class="tok-operator">:</span> <span class="tok-string">"City, Country"</span>
        )

        <span class="tok-keyword">let</span> education <span class="tok-operator">=</span> <span class="tok-type">Education</span>(<span class="tok-operator">.</span><span class="tok-operator">.</span><span class="tok-operator">.</span>)

        <span class="tok-comment">// ... rest of the implementation</span>
    }
}</code></pre>
<p><img src="https://aleahim.com/images/cvbuilder/cv01.png" alt=""></p>
<h3>Project History</h3>
<p>My professional experience is organized in a separate file (<code>CV+Projects.swift</code>), which contains detailed information about all my projects:</p>
<pre><code class="language-swift"><span class="tok-keyword">public</span> <span class="tok-keyword">extension</span> <span class="tok-type">CV</span> {
    <span class="tok-keyword">static</span> <span class="tok-keyword">func</span> <span class="tok-function">createMihaelasProjects</span>() <span class="tok-operator">-</span><span class="tok-operator">&gt;</span> [<span class="tok-type">Project</span>] {
        <span class="tok-keyword">var</span> result <span class="tok-operator">=</span> [<span class="tok-type">Project</span>]()

        <span class="tok-comment">// Create companies</span>
        <span class="tok-keyword">let</span> undabot <span class="tok-operator">=</span> <span class="tok-type">Company</span>(name<span class="tok-operator">:</span> <span class="tok-string">"Undabot"</span>)
        <span class="tok-keyword">let</span> token <span class="tok-operator">=</span> <span class="tok-type">Company</span>(name<span class="tok-operator">:</span> <span class="tok-string">"Token"</span>)
        <span class="tok-comment">// ... more companies</span>

        <span class="tok-comment">// Create tech skills</span>
        <span class="tok-keyword">let</span> swift <span class="tok-operator">=</span> <span class="tok-type">Tech</span>(name<span class="tok-operator">:</span> <span class="tok-string">"Swift"</span>, category<span class="tok-operator">:</span> <span class="tok-operator">.</span>language)
        <span class="tok-keyword">let</span> ui<span class="tok-type">Kit</span> <span class="tok-operator">=</span> <span class="tok-type">Tech</span>(name<span class="tok-operator">:</span> <span class="tok-string">"UIKit"</span>, category<span class="tok-operator">:</span> <span class="tok-operator">.</span>framework)
        <span class="tok-comment">// ... more tech skills</span>

        <span class="tok-comment">// Create projects using the builder pattern</span>
        <span class="tok-keyword">let</span> project<span class="tok-number">1</span> <span class="tok-operator">=</span> <span class="tok-keyword">try</span><span class="tok-operator">!</span> <span class="tok-type">Project</span><span class="tok-operator">.</span><span class="tok-type">Builder</span>()
            <span class="tok-operator">.</span><span class="tok-function">withName</span>(<span class="tok-string">"SomeProject"</span>)
            <span class="tok-operator">.</span><span class="tok-function">withCompany</span>(<span class="tok-type">Company</span> name)
            <span class="tok-operator">.</span><span class="tok-function">withRole</span>(junior<span class="tok-type">IOS</span>)
            <span class="tok-operator">.</span><span class="tok-function">withPeriod</span>(start<span class="tok-operator">:</span> (month<span class="tok-operator">:</span> <span class="tok-number">9</span>, year<span class="tok-operator">:</span> <span class="tok-number">20</span><span class="tok-type">XX</span>), end<span class="tok-operator">:</span> (month<span class="tok-operator">:</span> <span class="tok-number">12</span>, year<span class="tok-operator">:</span> <span class="tok-number">20</span><span class="tok-type">XX</span>))
            <span class="tok-operator">.</span><span class="tok-function">addDescription</span>(<span class="tok-string">"iOS (iPad) book application about...)
            .withTechs([swift, uiKit])
            .build()

        // ... more projects

        return result
    }
}</span></code></pre>
<p>This modular approach allows me to:</p>
<ol><li><p>Keep my CV data in a strongly-typed format</p></li><li><p>Easily update my experience and skills</p></li><li><p>Generate both HTML and Markdown versions of my CV</p></li><li><p>Maintain consistency across different platforms</p></li></ol>
<p>The CV is automatically generated when the website is built, ensuring that my professional information is always up-to-date and consistently formatted.</p>
<h3>Markdown Generation</h3>
<p>I also generate a Markdown version of my CV that I can use to create a fresh PDF whenever needed. This is done using a simple function:</p>
<pre><code class="language-swift"><span class="tok-keyword">func</span> <span class="tok-function">generateMihaelasCVMarkdownInContentFolder</span>() {
    <span class="tok-keyword">let</span> cv <span class="tok-operator">=</span> <span class="tok-type">CV</span><span class="tok-operator">.</span><span class="tok-function">createForMihaela</span>()
    <span class="tok-keyword">let</span> markdown <span class="tok-operator">=</span> <span class="tok-type">MarkdownCVRenderer</span>()<span class="tok-operator">.</span><span class="tok-function">render</span>(cv<span class="tok-operator">:</span> cv)

    <span class="tok-keyword">let</span> file<span class="tok-type">URL</span> <span class="tok-operator">=</span> <span class="tok-type">URL</span>(file<span class="tok-type">URLWithPath</span><span class="tok-operator">:</span> <span class="tok-string">"Assets/mihaela-cv.md"</span>)

    <span class="tok-keyword">do</span> {
        <span class="tok-keyword">try</span> markdown<span class="tok-operator">.</span><span class="tok-function">write</span>(to<span class="tok-operator">:</span> file<span class="tok-type">URL</span>, atomically<span class="tok-operator">:</span> <span class="tok-literal">true</span>, encoding<span class="tok-operator">:</span> <span class="tok-operator">.</span>utf<span class="tok-number">8</span>)
        <span class="tok-function">print</span>(<span class="tok-string">"✅ Written to \(fileURL.path)"</span>)
    } <span class="tok-keyword">catch</span> {
        <span class="tok-function">print</span>(<span class="tok-string">"❌ Failed to write Mihaela's CV: \(error.localizedDescription)"</span>)
    }
}</code></pre>
<p>This function:</p>
<ol><li><p>Creates my CV using the <code>createForMihaela()</code> function</p></li><li><p>Renders it to Markdown using <code>MarkdownCVRenderer</code></p></li><li><p>Saves the output to <code>Assets/mihaela-cv.md</code></p></li><li><p>Provides feedback about the success or failure of the operation</p></li></ol>
<p>I can then use this Markdown file to generate a fresh PDF whenever I need to update my CV for job applications or other purposes.</p>
<h2>Future Plans</h2>
<p>CVBuilder is actively maintained and has several planned improvements:</p>
<ol><li><p>Command-line interface for CV generation</p></li><li><p>Additional renderers (PDF, LaTeX)</p></li><li><p>Import/export functionality for common formats</p></li><li><p>Template system for customizing output</p></li></ol>
<h2>Conclusion</h2>
<p>CVBuilder provides a robust solution for managing professional CVs in Swift. Its type-safe design, multiple renderers, and modular architecture make it an excellent choice for developers who want to maintain their CVs programmatically.</p>
<p>Whether you’re building a personal website, managing multiple CV versions, or creating a CV management system, CVBuilder can help streamline your workflow and ensure consistency across all your professional documents.</p>
<h2>Resources</h2>
<ul><li><a href="https://codeberg.org/Mihaela/cvbuilder">Codeberg Repository</a></li><li><a href="https://codeberg.org/Mihaela/cvbuilder/src/branch/main/SampleCV.md">Sample CV</a></li><li><a href="https://codeberg.org/Mihaela/cvbuilder#readme">Documentation</a></li></ul>]]></content:encoded>
</item>
<item>
<title>Bringing Advanced SwiftUI Animations to macOS</title>
<link>https://aleahim.com/blog/swift-u-i-lab-advanced-animations/</link>
<guid isPermaLink="true">https://aleahim.com/blog/swift-u-i-lab-advanced-animations/</guid>
<pubDate>Sun, 13 Feb 2022 00:00:00 +0000</pubDate>
<description>A port of SwiftUILab&apos;s Advanced Animations that also supports macOS</description>
<content:encoded><![CDATA[<p>When I first discovered <a href="https://swiftui-lab.com/swiftui-animations-part1/">SwiftUILab’s Advanced Animations</a>, I was impressed by the beautiful animations and transitions it demonstrated. However, I noticed that these examples were primarily focused on iOS, leaving macOS developers without a clear path to implement similar effects. This inspired me to create <a href="https://codeberg.org/Mihaela/SwiftUILab_AdvancedAnimations">SwiftUILab_AdvancedAnimations</a>, a port of these animations that works seamlessly on both iOS and macOS.</p>
<h2>The Challenge</h2>
<p>SwiftUI’s animation system is powerful but can behave differently across platforms. While iOS and macOS share the same SwiftUI framework, there are subtle differences in how animations are handled, especially when dealing with:</p>
<ul><li><p>Gesture recognizers</p></li><li><p>View transitions</p></li><li><p>Animation timing</p></li><li><p>Platform-specific UI elements</p></li></ul>
<p>My goal was to create a unified codebase that would work flawlessly on both platforms while maintaining the original animations’ elegance and performance.</p>
<h2>Project Structure</h2>
<p>I organized the project to make it easy to understand and extend:</p>
<pre><code>SwiftUILab_AA/
├── Examples/
│   ├── Animation1/
│   ├── Animation2/
│   └── ...
├── Shared/
│   ├── Views/
│   ├── Models/
│   └── Extensions/
└── App/
    ├── iOS/
    └── macOS/</code></pre>
<p>Each animation example has its own folder, making it easy to:</p>
<ul><li><p>Study individual animations in isolation</p></li><li><p>Copy and paste specific animations into other projects</p></li><li><p>Modify and experiment with different parameters</p></li></ul>
<h2>Key Features</h2>
<h3>1. Cross-Platform Compatibility</h3>
<p>The project uses SwiftUI’s platform-agnostic features while handling platform-specific requirements:</p>
<pre><code class="language-swift">#<span class="tok-keyword">if</span> <span class="tok-function">os</span>(i<span class="tok-type">OS</span>)
    <span class="tok-comment">// iOS-specific code</span>
#else<span class="tok-keyword">if</span> <span class="tok-function">os</span>(mac<span class="tok-type">OS</span>)
    <span class="tok-comment">// macOS-specific code</span>
#end<span class="tok-keyword">if</span></code></pre>
<h3>2. Reusable Components</h3>
<p>I created a set of reusable components that work on both platforms:</p>
<pre><code class="language-swift"><span class="tok-keyword">struct</span> <span class="tok-type">AnimatedButton</span><span class="tok-operator">:</span> <span class="tok-type">View</span> {
    <span class="tok-attribute">@State</span> <span class="tok-keyword">private</span> <span class="tok-keyword">var</span> is<span class="tok-type">Pressed</span> <span class="tok-operator">=</span> <span class="tok-literal">false</span>

    <span class="tok-keyword">var</span> body<span class="tok-operator">:</span> some <span class="tok-type">View</span> {
        <span class="tok-type">Button</span>(action<span class="tok-operator">:</span> {}) {
            <span class="tok-type">Text</span>(<span class="tok-string">"Animate"</span>)
                <span class="tok-operator">.</span><span class="tok-function">scaleEffect</span>(is<span class="tok-type">Pressed</span> <span class="tok-operator">?</span> <span class="tok-number">0.95</span> <span class="tok-operator">:</span> <span class="tok-number">1.0</span>)
                <span class="tok-operator">.</span><span class="tok-function">animation</span>(<span class="tok-operator">.</span><span class="tok-function">spring</span>(), value<span class="tok-operator">:</span> is<span class="tok-type">Pressed</span>)
        }
        <span class="tok-operator">.</span><span class="tok-function">buttonStyle</span>(<span class="tok-type">PlainButtonStyle</span>())
        <span class="tok-operator">.</span>on<span class="tok-type">Hover</span> { hovering <span class="tok-keyword">in</span>
            is<span class="tok-type">Pressed</span> <span class="tok-operator">=</span> hovering
        }
    }
}</code></pre>
<h3>3. Gesture Handling</h3>
<p>The project includes platform-appropriate gesture handling:</p>
<pre><code class="language-swift"><span class="tok-keyword">struct</span> <span class="tok-type">DragGestureView</span><span class="tok-operator">:</span> <span class="tok-type">View</span> {
    <span class="tok-attribute">@State</span> <span class="tok-keyword">private</span> <span class="tok-keyword">var</span> offset <span class="tok-operator">=</span> <span class="tok-type">CGSize</span><span class="tok-operator">.</span>zero

    <span class="tok-keyword">var</span> body<span class="tok-operator">:</span> some <span class="tok-type">View</span> {
        <span class="tok-type">Rectangle</span>()
            <span class="tok-operator">.</span><span class="tok-function">fill</span>(<span class="tok-type">Color</span><span class="tok-operator">.</span>blue)
            <span class="tok-operator">.</span><span class="tok-function">frame</span>(width<span class="tok-operator">:</span> <span class="tok-number">100</span>, height<span class="tok-operator">:</span> <span class="tok-number">100</span>)
            <span class="tok-operator">.</span><span class="tok-function">offset</span>(offset)
            <span class="tok-operator">.</span><span class="tok-function">gesture</span>(
                <span class="tok-type">DragGesture</span>()
                    <span class="tok-operator">.</span>on<span class="tok-type">Changed</span> { value <span class="tok-keyword">in</span>
                        offset <span class="tok-operator">=</span> value<span class="tok-operator">.</span>translation
                    }
                    <span class="tok-operator">.</span>on<span class="tok-type">Ended</span> { _ <span class="tok-keyword">in</span>
                        <span class="tok-function">withAnimation</span>(<span class="tok-operator">.</span><span class="tok-function">spring</span>()) {
                            offset <span class="tok-operator">=</span> <span class="tok-operator">.</span>zero
                        }
                    }
            )
    }
}</code></pre>
<h2>Example Animations</h2>
<h3>1. Morphing Shapes</h3>
<p>One of the most impressive animations is the shape morphing effect:</p>
<pre><code class="language-swift"><span class="tok-keyword">struct</span> <span class="tok-type">MorphingShape</span><span class="tok-operator">:</span> <span class="tok-type">View</span> {
    <span class="tok-attribute">@State</span> <span class="tok-keyword">private</span> <span class="tok-keyword">var</span> is<span class="tok-type">Morphed</span> <span class="tok-operator">=</span> <span class="tok-literal">false</span>

    <span class="tok-keyword">var</span> body<span class="tok-operator">:</span> some <span class="tok-type">View</span> {
        <span class="tok-type">Circle</span>()
            <span class="tok-operator">.</span><span class="tok-function">fill</span>(<span class="tok-type">Color</span><span class="tok-operator">.</span>blue)
            <span class="tok-operator">.</span><span class="tok-function">frame</span>(width<span class="tok-operator">:</span> <span class="tok-number">100</span>, height<span class="tok-operator">:</span> <span class="tok-number">100</span>)
            <span class="tok-operator">.</span><span class="tok-function">scaleEffect</span>(is<span class="tok-type">Morphed</span> <span class="tok-operator">?</span> <span class="tok-number">1.5</span> <span class="tok-operator">:</span> <span class="tok-number">1.0</span>)
            <span class="tok-operator">.</span><span class="tok-function">animation</span>(<span class="tok-operator">.</span><span class="tok-function">spring</span>(response<span class="tok-operator">:</span> <span class="tok-number">0.6</span>, damping<span class="tok-type">Fraction</span><span class="tok-operator">:</span> <span class="tok-number">0.6</span>), value<span class="tok-operator">:</span> is<span class="tok-type">Morphed</span>)
            <span class="tok-operator">.</span>on<span class="tok-type">TapGesture</span> {
                is<span class="tok-type">Morphed</span><span class="tok-operator">.</span><span class="tok-function">toggle</span>()
            }
    }
}</code></pre>
<h3>2. Custom Transitions</h3>
<p>The project includes several custom transitions that work on both platforms:</p>
<pre><code class="language-swift"><span class="tok-keyword">extension</span> <span class="tok-type">AnyTransition</span> {
    <span class="tok-keyword">static</span> <span class="tok-keyword">var</span> custom<span class="tok-type">Scale</span><span class="tok-operator">:</span> <span class="tok-type">AnyTransition</span> {
        <span class="tok-operator">.</span><span class="tok-function">scale</span>(scale<span class="tok-operator">:</span> <span class="tok-number">0.1</span>)
        <span class="tok-operator">.</span><span class="tok-function">combined</span>(with<span class="tok-operator">:</span> <span class="tok-operator">.</span>opacity)
    }
}</code></pre>
<h2>Implementation Details</h2>
<h3>1. View Extensions</h3>
<p>I created several extensions to make animations more reusable:</p>
<pre><code class="language-swift"><span class="tok-keyword">extension</span> <span class="tok-type">View</span> {
    <span class="tok-keyword">func</span> <span class="tok-function">animateOnHover</span>() <span class="tok-operator">-</span><span class="tok-operator">&gt;</span> some <span class="tok-type">View</span> {
        <span class="tok-keyword">self</span><span class="tok-operator">.</span><span class="tok-function">modifier</span>(<span class="tok-type">HoverAnimationModifier</span>())
    }
}</code></pre>
<h3>2. Animation Timing</h3>
<p>Careful attention was paid to animation timing to ensure smooth performance:</p>
<pre><code class="language-swift"><span class="tok-keyword">struct</span> <span class="tok-type">TimingAnimation</span><span class="tok-operator">:</span> <span class="tok-type">View</span> {
    <span class="tok-attribute">@State</span> <span class="tok-keyword">private</span> <span class="tok-keyword">var</span> is<span class="tok-type">Animating</span> <span class="tok-operator">=</span> <span class="tok-literal">false</span>

    <span class="tok-keyword">var</span> body<span class="tok-operator">:</span> some <span class="tok-type">View</span> {
        <span class="tok-type">Circle</span>()
            <span class="tok-operator">.</span><span class="tok-function">fill</span>(<span class="tok-type">Color</span><span class="tok-operator">.</span>red)
            <span class="tok-operator">.</span><span class="tok-function">frame</span>(width<span class="tok-operator">:</span> <span class="tok-number">50</span>, height<span class="tok-operator">:</span> <span class="tok-number">50</span>)
            <span class="tok-operator">.</span><span class="tok-function">offset</span>(y<span class="tok-operator">:</span> is<span class="tok-type">Animating</span> <span class="tok-operator">?</span> <span class="tok-number">-100</span> <span class="tok-operator">:</span> <span class="tok-number">0</span>)
            <span class="tok-operator">.</span><span class="tok-function">animation</span>(
                <span class="tok-operator">.</span><span class="tok-function">timingCurve</span>(<span class="tok-number">0.68</span>, <span class="tok-number">-0.6</span>, <span class="tok-number">0.32</span>, <span class="tok-number">1.6</span>, duration<span class="tok-operator">:</span> <span class="tok-number">1</span>),
                value<span class="tok-operator">:</span> is<span class="tok-type">Animating</span>
            )
            <span class="tok-operator">.</span>on<span class="tok-type">Appear</span> {
                is<span class="tok-type">Animating</span> <span class="tok-operator">=</span> <span class="tok-literal">true</span>
            }
    }
}</code></pre>
<h2>Benefits for Developers</h2>
<ol><li><p><strong>Cross-Platform Learning</strong>: Developers can learn SwiftUI animations that work on both iOS and macOS</p></li><li><p><strong>Ready-to-Use Examples</strong>: Each animation is self-contained and can be easily copied into other projects</p></li><li><p><strong>Performance Optimized</strong>: Animations are optimized for smooth performance on both platforms</p></li><li><p><strong>Educational Resource</strong>: The project serves as a comprehensive guide to SwiftUI animations</p></li></ol>
<h2>Future Improvements</h2>
<p>The project is actively maintained with several planned enhancements:</p>
<ol><li><p>Adding more complex animation examples</p></li><li><p>Implementing accessibility features</p></li><li><p>Creating a documentation site with interactive examples</p></li><li><p>Adding support for visionOS</p></li></ol>
<h2>Conclusion</h2>
<p>SwiftUILab_AdvancedAnimations bridges the gap between iOS and macOS animation development in SwiftUI. By providing a unified codebase and clear examples, it helps developers create beautiful, performant animations that work across Apple’s platforms.</p>
<p>Whether you’re building for iOS, macOS, or both, this project provides valuable insights into SwiftUI’s animation capabilities and best practices for cross-platform development.</p>
<h2>Resources</h2>
<ul><li><a href="https://codeberg.org/Mihaela/SwiftUILab_AdvancedAnimations">Codeberg Repository</a></li><li><a href="https://swiftui-lab.com/swiftui-animations-part1/">Original SwiftUILab Article</a></li><li><a href="https://developer.apple.com/documentation/swiftui">SwiftUI Documentation</a></li></ul>]]></content:encoded>
</item>
<item>
<title>Core Animation Layers forming a 3D cube</title>
<link>https://aleahim.com/blog/core-animation-3d-cube/</link>
<guid isPermaLink="true">https://aleahim.com/blog/core-animation-3d-cube/</guid>
<pubDate>Sat, 06 Mar 2021 00:00:00 +0000</pubDate>
<description>Core Animation in 2.5D</description>
<content:encoded><![CDATA[<p>I’ve been researching <code>Core Animation</code> framework for the past few months.
I’ve started with several books on the subject, but I found watching related WWDC videos most rewarding. The presenters put the content in a relevant context which  makes it easier to apprehend and learn from it.</p>
<h2>1. Introduction</h2>
<p>One WWDC session particularly intrigued me: “2011–421 Core Animation Essentials”. They presented their demo named: “Layers in Perspective”, and it showed six layers, forming a flattened cube:
<img src="https://aleahim.com/images/cacube/02_cube_5sides.png" alt=""></p>
<p>The sixth layer is hiding behind the layer number 5. It has a lower <code>zPosition</code> then the layer above it.</p>
<p><img src="https://aleahim.com/images/cacube/03_cube_6sides.png" alt=""></p>
<p>They have also demonstrated opening and closing the cube formation.</p>
<p><img src="https://aleahim.com/images/cacube/04_cube_filmstrip.png" alt=""></p>
<p>So, I have decided to demonstrate that.</p>
<p>Here’s a link to a Codeberg repo with the source code:
<a href="https://codeberg.org/Mihaela/CubeIn3DWithCoreAnimation">Core Animation 3D Cube</a></p>
<p>Here’s the animation:</p>
<p><img src="https://aleahim.com/images/cacube/05_CoreAnimation_3D_Cube.gif" alt=""></p>
<p>You can also see it on <a href="https://www.youtube.com/watch?v=exIGbi36_bk">You Tube</a></p>
<p>Layers in <code>Core Animation</code> live in <code>3D</code> geometry. But a layer is a <code>2D</code> construct, so <code>Core Animation</code> coordinate space is called a <code>2.5D</code> geometry.</p>
<p>To illustrate that just see what happens when you mess up your transformations.</p>
<p><img src="https://aleahim.com/images/cacube/06_Intersecting_Layers.png" alt=""></p>
<p>Layers are <code>2D</code> objects, they don’t understand the third dimension.
They are like playing cards in space., and there is no <code>depth buffer</code> available.</p>
<p>And also, intersecting layers should be avoided because in the image above, Core Animation needs to do a lot of work.
So just to draw the <code>red</code> layer intersecting only the <code>blue</code> layer, Core Animation needs to</p>
<ul><li><p>cut the <code>red</code> layer into two pieces</p></li><li><p>render back part of the <code>red</code> layer</p></li><li><p>then render the full <code>blue</code> layer</p></li><li><p>then render the front part of the <code>red</code> layer again
And all that work is for just intersection, and here we have multiple.</p></li></ul>
<h2>2. Building the Cube in 3D</h2>
<h3>2.1. Setting up the multi-platform project</h3>
<p>I wanted the project to fun on the <code>macOS</code> , <code>iOS</code> and <code>iPadOS</code>, so I used <a href="https://codeberg.org/Mihaela/allapples">AllApples</a> Swift package.</p>
<p>After removing the storyboards and pimping up the <code>AppDelegate</code> and <code>main.swift</code> for the <code>macOS</code> version, and <code>SceneDelegate</code> for the mobile versions, I was ready to start.</p>
<h4>2.1.1. <code>main.swift</code> for the macOS</h4>
<pre><code class="language-swift"><span class="tok-keyword">import</span> <span class="tok-type">Cocoa</span>
<span class="tok-keyword">let</span> delegate <span class="tok-operator">=</span> <span class="tok-type">AppDelegate</span>()
<span class="tok-type">NSApplication</span><span class="tok-operator">.</span>shared<span class="tok-operator">.</span>delegate <span class="tok-operator">=</span> delegate
_ <span class="tok-operator">=</span> <span class="tok-type">NSApplicationMain</span>(<span class="tok-type">CommandLine</span><span class="tok-operator">.</span>argc, <span class="tok-type">CommandLine</span><span class="tok-operator">.</span>unsafe<span class="tok-type">Argv</span>)</code></pre>
<h4>2.1.2. AppDelegate for the <code>macOS</code></h4>
<pre><code class="language-swift"><span class="tok-keyword">import</span> <span class="tok-type">Cocoa</span>
<span class="tok-keyword">import</span> <span class="tok-type">AllApples</span>

<span class="tok-keyword">class</span> <span class="tok-type">AppDelegate</span><span class="tok-operator">:</span> <span class="tok-type">NSObject</span>, <span class="tok-type">NSApplicationDelegate</span> {
  <span class="tok-keyword">private</span> <span class="tok-keyword">var</span> window<span class="tok-operator">:</span> <span class="tok-type">NSWindow</span><span class="tok-operator">?</span>
  <span class="tok-keyword">func</span> <span class="tok-function">applicationDidFinishLaunching</span>(_ a<span class="tok-type">Notification</span><span class="tok-operator">:</span> <span class="tok-type">Notification</span>) {
    window <span class="tok-operator">=</span> <span class="tok-type">AppSceneDelegate</span><span class="tok-operator">.</span><span class="tok-function">makeWindow_Mac</span>(the<span class="tok-type">VC</span><span class="tok-operator">:</span> <span class="tok-type">CommonViewController</span>())
  }
}</code></pre>
<h4>2.1.3. SceneDelegate for the <code>iOS</code> and <code>iPadOS</code></h4>
<pre><code class="language-swift"><span class="tok-keyword">import</span> <span class="tok-type">UIKit</span>
<span class="tok-keyword">import</span> <span class="tok-type">AllApples</span>

<span class="tok-keyword">class</span> <span class="tok-type">SceneDelegate</span><span class="tok-operator">:</span> <span class="tok-type">UIResponder</span>, <span class="tok-type">UIWindowSceneDelegate</span> {
  <span class="tok-keyword">var</span> window<span class="tok-operator">:</span> <span class="tok-type">UIWindow</span><span class="tok-operator">?</span>
  <span class="tok-keyword">func</span> <span class="tok-function">scene</span>(_ scene<span class="tok-operator">:</span> <span class="tok-type">UIScene</span>, will<span class="tok-type">ConnectTo</span> session<span class="tok-operator">:</span> <span class="tok-type">UISceneSession</span>, options connection<span class="tok-type">Options</span><span class="tok-operator">:</span> <span class="tok-type">UIScene</span><span class="tok-operator">.</span><span class="tok-type">ConnectionOptions</span>) {
    <span class="tok-keyword">guard</span> <span class="tok-keyword">let</span> a<span class="tok-type">Scene</span> <span class="tok-operator">=</span> (scene <span class="tok-keyword">as</span><span class="tok-operator">?</span> <span class="tok-type">UIWindowScene</span>) <span class="tok-keyword">else</span> { <span class="tok-keyword">return</span> }
    window <span class="tok-operator">=</span> <span class="tok-type">AppSceneDelegate</span><span class="tok-operator">.</span><span class="tok-function">makeWindow_iOS</span>(the<span class="tok-type">Scene</span><span class="tok-operator">:</span> a<span class="tok-type">Scene</span>, the<span class="tok-type">VC</span><span class="tok-operator">:</span> <span class="tok-type">CommonViewController</span>())
  }
}</code></pre>
<h3>2.2. Building the Basic Blocks</h3>
<p>The first I did is make a <code>PlainLayerView </code>.
It is an <code>AView</code> descendant, which means that it is <code>typedef-ed</code> to be either a <code>UIView</code> or a <code>NSView</code>.</p>
<p>It is an intermediary object just to set up a thing on the <code>macOS</code> as well (doesn’t work yet).</p>
<p>I then created <code>CustomLayerView</code> to have nice sides for the cube, with <code>CATextLayer</code> as the number of the cube side, and a nice rounded border, so that we can peek into our cube while rotating.</p>
<p>Here’s the image of all six layers drawn by using a <code>CustomLayerView</code></p>
<p><img src="https://aleahim.com/images/cacube/07_original_abandoned_cube_layout.png" alt=""></p>
<p>This layout was abandoned because I couldn’t make the transformation of <code>purple</code> view to work when transforming in <code>3D</code>.</p>
<p><img src="https://aleahim.com/images/cacube/08_side4_broken.png" alt=""></p>
<p>The solution is to add an additional <code>CATransformLayer</code> to the <code>green</code> layer, and mount the <code>purple</code> layer onto it. As explained in this blog post by Oliver Drobnik <a href="https://www.cocoanetics.com/2012/08/cubed-coreanimation-conundrum/"> Cubed CoreAnimation Conundrum.</a></p>
<p>But I didn’t want to lose the linearity of the solution, and I used the approach demonstrated in the mentioned WWDC session: “2011–421 Core Animation Essentials”</p>
<p>They used the <code>zOrder</code> property of a layer, and so I put <code>purple</code> layer on top of the <code>red</code> layer to achieve that.</p>
<pre><code class="language-swift">    <span class="tok-keyword">if</span> number <span class="tok-operator">=</span><span class="tok-operator">=</span> <span class="tok-number">4</span> {
      view<span class="tok-operator">.</span>layer<span class="tok-operator">.</span>z<span class="tok-type">Position</span> <span class="tok-operator">=</span> <span class="tok-number">1</span>
    }</code></pre>
<p>As you can see in the image below, the <code>purple</code> layer is in front of the <code>red</code> layer, which is obvious when we rotate the flattened cube.</p>
<p><img src="https://aleahim.com/images/cacube/09_layers_zPosition.png" alt=""></p>
<h3>2.3. Turning the Transform On and Off</h3>
<p>I did take the approach that Over Drobnik did in his article: <a href="https://www.cocoanetics.com/2012/08/cubed-coreanimation-conundrum/"> Cubed CoreAnimation Conundrum </a>, and used it like this:</p>
<pre><code class="language-swift">	side<span class="tok-number">4.</span>layer<span class="tok-operator">?</span><span class="tok-operator">.</span>z<span class="tok-type">Position</span> <span class="tok-operator">=</span> on <span class="tok-operator">?</span> <span class="tok-type">CACube3DView</span><span class="tok-operator">.</span>side<span class="tok-type">Width</span> <span class="tok-operator">:</span> <span class="tok-number">1</span>
    side<span class="tok-number">1.</span>layer<span class="tok-operator">?</span><span class="tok-operator">.</span>transform <span class="tok-operator">=</span> on <span class="tok-operator">?</span> <span class="tok-type">CATransform3D</span><span class="tok-operator">.</span><span class="tok-function">transformFor3DCubeSide</span>(<span class="tok-number">1</span>, z<span class="tok-type">Width</span><span class="tok-operator">:</span> <span class="tok-type">CACube3DView</span><span class="tok-operator">.</span>side<span class="tok-type">Width</span>)  <span class="tok-operator">:</span> <span class="tok-type">CATransform3DIdentity</span>
    side<span class="tok-number">2.</span>layer<span class="tok-operator">?</span><span class="tok-operator">.</span>transform <span class="tok-operator">=</span> on <span class="tok-operator">?</span> <span class="tok-type">CATransform3D</span><span class="tok-operator">.</span><span class="tok-function">transformFor3DCubeSide</span>(<span class="tok-number">2</span>, z<span class="tok-type">Width</span><span class="tok-operator">:</span> <span class="tok-type">CACube3DView</span><span class="tok-operator">.</span>side<span class="tok-type">Width</span>)  <span class="tok-operator">:</span> <span class="tok-type">CATransform3DIdentity</span>
    side<span class="tok-number">3.</span>layer<span class="tok-operator">?</span><span class="tok-operator">.</span>transform <span class="tok-operator">=</span> on <span class="tok-operator">?</span> <span class="tok-type">CATransform3D</span><span class="tok-operator">.</span><span class="tok-function">transformFor3DCubeSide</span>(<span class="tok-number">3</span>, z<span class="tok-type">Width</span><span class="tok-operator">:</span> <span class="tok-type">CACube3DView</span><span class="tok-operator">.</span>side<span class="tok-type">Width</span>)  <span class="tok-operator">:</span> <span class="tok-type">CATransform3DIdentity</span>
    side<span class="tok-number">4.</span>layer<span class="tok-operator">?</span><span class="tok-operator">.</span>transform <span class="tok-operator">=</span> on <span class="tok-operator">?</span> <span class="tok-type">CATransform3D</span><span class="tok-operator">.</span><span class="tok-function">transformFor3DCubeSide</span>(<span class="tok-number">4</span>, z<span class="tok-type">Width</span><span class="tok-operator">:</span> <span class="tok-type">CACube3DView</span><span class="tok-operator">.</span>side<span class="tok-type">Width</span>)  <span class="tok-operator">:</span> <span class="tok-type">CATransform3DIdentity</span>
    side<span class="tok-number">5.</span>layer<span class="tok-operator">?</span><span class="tok-operator">.</span>transform <span class="tok-operator">=</span> on <span class="tok-operator">?</span> <span class="tok-type">CATransform3D</span><span class="tok-operator">.</span><span class="tok-function">transformFor3DCubeSide</span>(<span class="tok-number">5</span>, z<span class="tok-type">Width</span><span class="tok-operator">:</span> <span class="tok-type">CACube3DView</span><span class="tok-operator">.</span>side<span class="tok-type">Width</span>)  <span class="tok-operator">:</span> <span class="tok-type">CATransform3DIdentity</span>
    side<span class="tok-number">6.</span>layer<span class="tok-operator">?</span><span class="tok-operator">.</span>transform <span class="tok-operator">=</span> on <span class="tok-operator">?</span> <span class="tok-type">CATransform3D</span><span class="tok-operator">.</span><span class="tok-function">transformFor3DCubeSide</span>(<span class="tok-number">6</span>, z<span class="tok-type">Width</span><span class="tok-operator">:</span> <span class="tok-type">CACube3DView</span><span class="tok-operator">.</span>side<span class="tok-type">Width</span>)  <span class="tok-operator">:</span> <span class="tok-type">CATransform3DIdentity</span></code></pre>
<h3>2.4. Transforming the Layers to Form a Cube</h3>
<p>I didn’t use his transform code, since he used that additional <code>CATransformLayer</code>, so it wouldn’t work. So, here’s a small extension on the <code>CATransform3D</code></p>
<pre><code class="language-swift"><span class="tok-keyword">public</span> <span class="tok-keyword">extension</span> <span class="tok-type">CATransform3D</span> {
  <span class="tok-keyword">static</span> <span class="tok-keyword">func</span> <span class="tok-function">transformFor3DCubeSide</span>(_ number<span class="tok-operator">:</span> <span class="tok-type">Int</span>, z<span class="tok-type">Width</span><span class="tok-operator">:</span> <span class="tok-type">CGFloat</span>) <span class="tok-operator">-</span><span class="tok-operator">&gt;</span> <span class="tok-type">CATransform3D</span> {

    <span class="tok-keyword">let</span> half<span class="tok-type">Pi</span> <span class="tok-operator">=</span> <span class="tok-type">CGFloat</span>(<span class="tok-type">Double</span><span class="tok-operator">.</span>pi) <span class="tok-operator">/</span> <span class="tok-number">2.0</span>
    <span class="tok-keyword">var</span> trans <span class="tok-operator">=</span> <span class="tok-type">CATransform3DIdentity</span>
    <span class="tok-keyword">switch</span> number {
      <span class="tok-keyword">case</span> <span class="tok-number">1</span><span class="tok-operator">:</span>
        trans <span class="tok-operator">=</span> <span class="tok-type">CATransform3DMakeRotation</span>(half<span class="tok-type">Pi</span>, <span class="tok-number">0</span>, <span class="tok-number">1</span>, <span class="tok-number">0</span>)
        <span class="tok-keyword">break</span>
      <span class="tok-keyword">case</span> <span class="tok-number">2</span><span class="tok-operator">:</span>
        trans <span class="tok-operator">=</span> <span class="tok-type">CATransform3DIdentity</span>
        <span class="tok-keyword">break</span>
      <span class="tok-keyword">case</span> <span class="tok-number">3</span><span class="tok-operator">:</span>
        trans <span class="tok-operator">=</span> <span class="tok-type">CATransform3DMakeRotation</span>(<span class="tok-operator">-</span>half<span class="tok-type">Pi</span>, <span class="tok-number">0</span>, <span class="tok-number">1</span>, <span class="tok-number">0</span>)
        <span class="tok-keyword">break</span>
      <span class="tok-keyword">case</span> <span class="tok-number">4</span><span class="tok-operator">:</span>
        trans <span class="tok-operator">=</span> <span class="tok-type">CATransform3DMakeTranslation</span>(<span class="tok-number">0</span>, <span class="tok-number">0</span>, z<span class="tok-type">Width</span>)
        <span class="tok-keyword">break</span>
      <span class="tok-keyword">case</span> <span class="tok-number">5</span><span class="tok-operator">:</span>
        trans <span class="tok-operator">=</span> <span class="tok-type">CATransform3DMakeRotation</span>(<span class="tok-operator">-</span>half<span class="tok-type">Pi</span>, <span class="tok-number">1</span>, <span class="tok-number">0</span>, <span class="tok-number">0</span>)
        <span class="tok-keyword">break</span>
      <span class="tok-keyword">case</span> <span class="tok-number">6</span><span class="tok-operator">:</span>
        trans <span class="tok-operator">=</span> <span class="tok-type">CATransform3DMakeRotation</span>(half<span class="tok-type">Pi</span>, <span class="tok-number">1</span>, <span class="tok-number">0</span>, <span class="tok-number">0</span>)
        <span class="tok-keyword">break</span>
      <span class="tok-keyword">default</span><span class="tok-operator">:</span>
        <span class="tok-keyword">break</span>
    }
    <span class="tok-keyword">return</span> trans
  }
}</code></pre>
<p>I actually used the approach form that WWDC session, and also used <code>anchorPoint</code> properties of the <code>CALayer</code>.</p>
<h3>2.5. Positioning Cube Sides</h3>
<p>Here’s a little extension on <code>CGPoint</code> that returns a tuple of our cube side positions and anchor points:</p>
<pre><code class="language-swift"><span class="tok-keyword">public</span> <span class="tok-keyword">extension</span> <span class="tok-type">CGPoint</span> {
  <span class="tok-keyword">static</span> <span class="tok-keyword">func</span> <span class="tok-function">anchorPointAndPositionForCubeSideLayer</span>(number<span class="tok-operator">:</span> <span class="tok-type">Int</span>, side<span class="tok-type">Size</span><span class="tok-operator">:</span> <span class="tok-type">CGFloat</span>) <span class="tok-operator">-</span><span class="tok-operator">&gt;</span> (anchor<span class="tok-type">Point</span><span class="tok-operator">:</span> <span class="tok-type">CGPoint</span>, position<span class="tok-operator">:</span> <span class="tok-type">CGPoint</span>) {
    <span class="tok-keyword">var</span> result<span class="tok-type">AnchorPoint</span> <span class="tok-operator">=</span> <span class="tok-type">CGPoint</span>(x<span class="tok-operator">:</span><span class="tok-number">0.5</span>, y<span class="tok-operator">:</span><span class="tok-number">0.5</span>)
    <span class="tok-keyword">var</span> result<span class="tok-type">Position</span> <span class="tok-operator">=</span> <span class="tok-type">CGPoint</span>(x<span class="tok-operator">:</span><span class="tok-number">0.0</span>, y<span class="tok-operator">:</span><span class="tok-number">0.0</span>)
    <span class="tok-keyword">let</span> half<span class="tok-type">SideSize</span><span class="tok-operator">:</span> <span class="tok-type">CGFloat</span> <span class="tok-operator">=</span> side<span class="tok-type">Size</span> <span class="tok-operator">/</span> <span class="tok-number">2.0</span>
    <span class="tok-keyword">switch</span> number {
      <span class="tok-keyword">case</span> <span class="tok-number">1</span><span class="tok-operator">:</span>
        result<span class="tok-type">AnchorPoint</span> <span class="tok-operator">=</span> <span class="tok-type">CGPoint</span>(x<span class="tok-operator">:</span><span class="tok-number">1.0</span>, y<span class="tok-operator">:</span><span class="tok-number">0.5</span>)
        result<span class="tok-type">Position</span> <span class="tok-operator">=</span> <span class="tok-type">CGPoint</span>(x<span class="tok-operator">:</span><span class="tok-operator">-</span>half<span class="tok-type">SideSize</span>, y<span class="tok-operator">:</span><span class="tok-number">0.0</span>)
        <span class="tok-keyword">break</span>
      <span class="tok-keyword">case</span> <span class="tok-number">2</span><span class="tok-operator">:</span>
        result<span class="tok-type">AnchorPoint</span> <span class="tok-operator">=</span> <span class="tok-type">CGPoint</span>(x<span class="tok-operator">:</span><span class="tok-number">0.5</span>, y<span class="tok-operator">:</span><span class="tok-number">0.5</span>)
        result<span class="tok-type">Position</span> <span class="tok-operator">=</span> <span class="tok-type">CGPoint</span>(x<span class="tok-operator">:</span><span class="tok-number">0.0</span>, y<span class="tok-operator">:</span><span class="tok-number">0.0</span>)
        <span class="tok-keyword">break</span>
      <span class="tok-keyword">case</span> <span class="tok-number">3</span><span class="tok-operator">:</span>
        result<span class="tok-type">AnchorPoint</span> <span class="tok-operator">=</span> <span class="tok-type">CGPoint</span>(x<span class="tok-operator">:</span><span class="tok-number">0.0</span>, y<span class="tok-operator">:</span><span class="tok-number">0.5</span>)
        result<span class="tok-type">Position</span> <span class="tok-operator">=</span> <span class="tok-type">CGPoint</span>(x<span class="tok-operator">:</span>half<span class="tok-type">SideSize</span>, y<span class="tok-operator">:</span><span class="tok-number">0.0</span>)
        <span class="tok-keyword">break</span>
      <span class="tok-keyword">case</span> <span class="tok-number">4</span><span class="tok-operator">:</span>
        result<span class="tok-type">AnchorPoint</span> <span class="tok-operator">=</span> <span class="tok-type">CGPoint</span>(x<span class="tok-operator">:</span><span class="tok-number">0.5</span>, y<span class="tok-operator">:</span><span class="tok-number">0.5</span>)
        result<span class="tok-type">Position</span> <span class="tok-operator">=</span> <span class="tok-type">CGPoint</span>(x<span class="tok-operator">:</span><span class="tok-number">0.0</span>, y<span class="tok-operator">:</span><span class="tok-number">0.0</span>)
        <span class="tok-keyword">break</span>
      <span class="tok-keyword">case</span> <span class="tok-number">5</span><span class="tok-operator">:</span>
        result<span class="tok-type">AnchorPoint</span> <span class="tok-operator">=</span> <span class="tok-type">CGPoint</span>(x<span class="tok-operator">:</span><span class="tok-number">0.5</span>, y<span class="tok-operator">:</span><span class="tok-number">1.0</span>)
        result<span class="tok-type">Position</span> <span class="tok-operator">=</span> <span class="tok-type">CGPoint</span>(x<span class="tok-operator">:</span><span class="tok-number">0.0</span>, y<span class="tok-operator">:</span><span class="tok-operator">-</span>half<span class="tok-type">SideSize</span>)
        <span class="tok-keyword">break</span>
      <span class="tok-keyword">case</span> <span class="tok-number">6</span><span class="tok-operator">:</span>
        result<span class="tok-type">AnchorPoint</span> <span class="tok-operator">=</span> <span class="tok-type">CGPoint</span>(x<span class="tok-operator">:</span><span class="tok-number">0.5</span>, y<span class="tok-operator">:</span><span class="tok-number">0.0</span>)
        result<span class="tok-type">Position</span> <span class="tok-operator">=</span> <span class="tok-type">CGPoint</span>(x<span class="tok-operator">:</span><span class="tok-number">0.0</span>, y<span class="tok-operator">:</span>half<span class="tok-type">SideSize</span>)
        <span class="tok-keyword">break</span>
      <span class="tok-keyword">default</span><span class="tok-operator">:</span>
        <span class="tok-keyword">break</span>
    }
    <span class="tok-keyword">return</span> (anchor<span class="tok-type">Point</span><span class="tok-operator">:</span> result<span class="tok-type">AnchorPoint</span>, position<span class="tok-operator">:</span> result<span class="tok-type">Position</span>)
  }
}</code></pre>
<p>In the image below I have marked where the <code>anchor points</code> are for each layer:</p>
<p><img src="https://aleahim.com/images/cacube/10_anchor_points.png" alt=""></p>
<p>The only fallacy in the image above, is that the <code>purple</code> layer is actually above our <code>red layer</code>, but I wanted to show where those anchor points are.
So, the actual image looks like this, but we now don’t see the <code>red</code> layer.
<img src="https://aleahim.com/images/cacube/11_anchor_points.png" alt=""></p>
<p>An Anchor point is a center of rotation. It determines how will the layer rotate.
Imagine holding a playing card with two fingers. Then try to spin the card. The anchor point of that card is where you are holding it with fingers.</p>
<h2>3. Responding to Gestures</h2>
<p>I made a small <code>GestureRecognizerView</code> to be able to respond to gestures and move, rotate and scale our layers.</p>
<p>It hooks-up:</p>
<ul><li><p><code>NSPanGestureRecognizer</code> and <code>UIPanGestureRecognizer</code></p></li><li><p><code>func rotate(with event: NSEvent)</code> and <code>UIRotationGestureRecognizer</code></p></li><li><p><code>NSClickGestureRecognizer</code> and <code>UITapGestureRecognizer</code></p></li><li><p><code>func magnify(with event: NSEvent)</code> and <code>UIPinchGestureRecognizer</code></p></li></ul>
<p>It then exposes all those events to the developer to use:</p>
<pre><code class="language-swift"><span class="tok-keyword">public</span> <span class="tok-keyword">extension</span> <span class="tok-type">GestureRecognizerView</span> {
  <span class="tok-attribute">@objc</span> <span class="tok-keyword">func</span> <span class="tok-function">rotationChanged</span>(degrees<span class="tok-operator">:</span> <span class="tok-type">Float</span>) {}
  <span class="tok-attribute">@objc</span> <span class="tok-keyword">func</span> <span class="tok-function">rotationChanged</span>(radians<span class="tok-operator">:</span> <span class="tok-type">Float</span>) {}
  <span class="tok-attribute">@objc</span> <span class="tok-keyword">func</span> <span class="tok-function">displacementChanged</span>(displacement<span class="tok-operator">:</span> <span class="tok-type">CGPoint</span>) {}
  <span class="tok-attribute">@objc</span> <span class="tok-keyword">func</span> <span class="tok-function">scaleChanged</span>(scale<span class="tok-operator">:</span> <span class="tok-type">CGFloat</span>) {}
  <span class="tok-attribute">@objc</span> <span class="tok-keyword">func</span> <span class="tok-function">tapHappened</span>() {}
}</code></pre>
<h2>4. Building a Layer to hold a Cube</h2>
<p><code>CACube3DView</code> will hold the six layers than (can) make a cube.
In order for <code>Core Animation</code> to render the transformed views in perspective, there is a property <code>sublayerTransform</code>.</p>
<p>You either use that property of your parent layer, or add another layer class to your layer hierarchy: <code>CATransformLayer</code>. I opted to use that.</p>
<pre><code class="language-swift"><span class="tok-keyword">private</span>(set) <span class="tok-keyword">public</span> lazy <span class="tok-keyword">var</span> transformed<span class="tok-type">Layer</span><span class="tok-operator">:</span> <span class="tok-type">CALayer</span> <span class="tok-operator">=</span> {
    <span class="tok-keyword">let</span> l <span class="tok-operator">=</span> <span class="tok-type">CATransformLayer</span>()
    l<span class="tok-operator">.</span>name <span class="tok-operator">=</span> <span class="tok-string">"Transform Layer"</span>
    #<span class="tok-keyword">if</span> <span class="tok-function">os</span>(<span class="tok-type">OSX</span>)
    l<span class="tok-operator">.</span>is<span class="tok-type">GeometryFlipped</span> <span class="tok-operator">=</span> <span class="tok-literal">true</span>
    #end<span class="tok-keyword">if</span>
    <span class="tok-keyword">return</span> l
}()</code></pre>
<p>When adding sublayers, I add them to this <code>transformedLayer</code> property, and not my view’s layer.</p>
<pre><code class="language-swift"><span class="tok-keyword">func</span> <span class="tok-function">addSideSubview</span>(_ subview<span class="tok-operator">:</span> <span class="tok-type">AView</span>) {
    <span class="tok-function">addSubview</span>(subview)

    #<span class="tok-keyword">if</span> <span class="tok-function">os</span>(i<span class="tok-type">OS</span>) <span class="tok-operator">|</span><span class="tok-operator">|</span> <span class="tok-function">os</span>(tv<span class="tok-type">OS</span>)
    transformed<span class="tok-type">Layer</span><span class="tok-operator">.</span><span class="tok-function">addSublayer</span>(subview<span class="tok-operator">.</span>layer)
    #end<span class="tok-keyword">if</span>

    #<span class="tok-keyword">if</span> <span class="tok-function">os</span>(<span class="tok-type">OSX</span>)
    <span class="tok-keyword">if</span> <span class="tok-keyword">let</span> a<span class="tok-type">Layer</span> <span class="tok-operator">=</span> subview<span class="tok-operator">.</span>layer {
      transformed<span class="tok-type">Layer</span><span class="tok-operator">.</span><span class="tok-function">addSublayer</span>(a<span class="tok-type">Layer</span>)
    } <span class="tok-keyword">else</span> {
      <span class="tok-function">fatalError</span>(<span class="tok-string">"`subview.layer` == `nil`"</span>)
    }
    #end<span class="tok-keyword">if</span>
}</code></pre>
<h2>5. Perspective &amp; Rotation</h2>
<p>When the app first runs it shows in perspective.
I made a little extension:</p>
<pre><code class="language-swift"><span class="tok-keyword">public</span> <span class="tok-keyword">extension</span> <span class="tok-type">CATransform3D</span> {
  <span class="tok-keyword">static</span> <span class="tok-keyword">func</span> <span class="tok-function">somePerspectiveTransform</span>() <span class="tok-operator">-</span><span class="tok-operator">&gt;</span> <span class="tok-type">CATransform3D</span> {
    <span class="tok-keyword">var</span> perspective <span class="tok-operator">=</span> <span class="tok-type">CATransform3DIdentity</span>;
    perspective<span class="tok-operator">.</span>m<span class="tok-number">34</span> <span class="tok-operator">=</span> <span class="tok-number">-1.0</span> <span class="tok-operator">/</span> <span class="tok-number">500.0</span>;
    perspective <span class="tok-operator">=</span> <span class="tok-type">CATransform3DRotate</span>(perspective, <span class="tok-type">CGFloat</span>(<span class="tok-type">Double</span><span class="tok-operator">.</span>pi) <span class="tok-operator">/</span> <span class="tok-number">8</span>, <span class="tok-number">1</span>, <span class="tok-number">0</span>, <span class="tok-number">0</span>);
    perspective <span class="tok-operator">=</span> <span class="tok-type">CATransform3DRotate</span>(perspective, <span class="tok-type">CGFloat</span>(<span class="tok-type">Double</span><span class="tok-operator">.</span>pi) <span class="tok-operator">/</span> <span class="tok-number">8</span>, <span class="tok-number">0</span>, <span class="tok-number">1</span>, <span class="tok-number">0</span>);
    perspective <span class="tok-operator">=</span> <span class="tok-type">CATransform3DScale</span>(perspective, <span class="tok-number">0.7</span>, <span class="tok-number">0.7</span>, <span class="tok-number">0.7</span>)
    <span class="tok-keyword">return</span> perspective
  }
}</code></pre>
<p>The part: <code>perspective.m34 = -1.0 / 500.0;</code> sets the perspective.
The <code>.34</code> field of a matrix shows the amount of perspective distortion applied.
The amount <code>500</code> is often used in examples. If it were smaller, the layers would seem very close and distorted, like a fish-eye effect.</p>
<p>This is the initial transform, but we want to be able to move and rotate our cube (flattened or not) with our fingers.</p>
<p>Here’s the code:</p>
<pre><code class="language-swift"><span class="tok-keyword">public</span> <span class="tok-keyword">extension</span> <span class="tok-type">CATransform3D</span> {

  <span class="tok-keyword">func</span> <span class="tok-function">rotationFromDisplacement</span>(_ displacement<span class="tok-operator">:</span> <span class="tok-type">CGPoint</span>, side<span class="tok-type">Width</span><span class="tok-operator">:</span> <span class="tok-type">CGFloat</span>, is<span class="tok-number">3</span><span class="tok-type">D</span><span class="tok-operator">:</span> <span class="tok-type">Bool</span>) <span class="tok-operator">-</span><span class="tok-operator">&gt;</span> <span class="tok-type">CATransform3D</span> {

    <span class="tok-keyword">var</span> current<span class="tok-type">Transform</span> <span class="tok-operator">=</span> <span class="tok-keyword">self</span>

    <span class="tok-keyword">let</span> total<span class="tok-type">Rotation</span><span class="tok-operator">:</span> <span class="tok-type">CGFloat</span> <span class="tok-operator">=</span> <span class="tok-function">sqrt</span>(displacement<span class="tok-operator">.</span>x <span class="tok-operator">*</span> displacement<span class="tok-operator">.</span>x <span class="tok-operator">+</span> displacement<span class="tok-operator">.</span>y <span class="tok-operator">*</span> displacement<span class="tok-operator">.</span>y)
    <span class="tok-keyword">let</span> angle<span class="tok-operator">:</span> <span class="tok-type">CGFloat</span> <span class="tok-operator">=</span> total<span class="tok-type">Rotation</span> <span class="tok-operator">*</span> <span class="tok-operator">.</span>pi <span class="tok-operator">/</span> <span class="tok-number">180.0</span>

    <span class="tok-keyword">let</span> x<span class="tok-type">RotationFactor</span> <span class="tok-operator">=</span> displacement<span class="tok-operator">.</span>x <span class="tok-operator">/</span> angle
    <span class="tok-keyword">let</span> y<span class="tok-type">RotationFactor</span> <span class="tok-operator">=</span> displacement<span class="tok-operator">.</span>y <span class="tok-operator">/</span> angle

    <span class="tok-keyword">if</span> is<span class="tok-number">3</span><span class="tok-type">D</span> {
      current<span class="tok-type">Transform</span> <span class="tok-operator">=</span> <span class="tok-type">CATransform3DTranslate</span>(current<span class="tok-type">Transform</span>, <span class="tok-number">0</span>, <span class="tok-number">0</span>, side<span class="tok-type">Width</span> <span class="tok-operator">/</span> <span class="tok-number">2.0</span>)
    }

    <span class="tok-keyword">var</span> rotational<span class="tok-type">Transform</span> <span class="tok-operator">=</span> <span class="tok-type">CATransform3DRotate</span>(current<span class="tok-type">Transform</span>, angle,
                                                  (x<span class="tok-type">RotationFactor</span> <span class="tok-operator">*</span> current<span class="tok-type">Transform</span><span class="tok-operator">.</span>m<span class="tok-number">12</span> <span class="tok-operator">-</span> y<span class="tok-type">RotationFactor</span> <span class="tok-operator">*</span> current<span class="tok-type">Transform</span><span class="tok-operator">.</span>m<span class="tok-number">11</span>),
                                                  (x<span class="tok-type">RotationFactor</span> <span class="tok-operator">*</span> current<span class="tok-type">Transform</span><span class="tok-operator">.</span>m<span class="tok-number">22</span> <span class="tok-operator">-</span> y<span class="tok-type">RotationFactor</span> <span class="tok-operator">*</span> current<span class="tok-type">Transform</span><span class="tok-operator">.</span>m<span class="tok-number">21</span>),
                                                  (x<span class="tok-type">RotationFactor</span> <span class="tok-operator">*</span> current<span class="tok-type">Transform</span><span class="tok-operator">.</span>m<span class="tok-number">32</span> <span class="tok-operator">-</span> y<span class="tok-type">RotationFactor</span> <span class="tok-operator">*</span> current<span class="tok-type">Transform</span><span class="tok-operator">.</span>m<span class="tok-number">31</span>))

    <span class="tok-keyword">if</span> (is<span class="tok-number">3</span><span class="tok-type">D</span>) {
      rotational<span class="tok-type">Transform</span> <span class="tok-operator">=</span> <span class="tok-type">CATransform3DTranslate</span>(rotational<span class="tok-type">Transform</span>, <span class="tok-number">0</span>, <span class="tok-number">0</span>, <span class="tok-operator">-</span>side<span class="tok-type">Width</span> <span class="tok-operator">/</span> <span class="tok-number">2.0</span>);
    }

    <span class="tok-keyword">return</span> rotational<span class="tok-type">Transform</span>
  }
}</code></pre>
<p>We call it from our pan-gesture methods</p>
<pre><code class="language-swift">  override <span class="tok-keyword">func</span> <span class="tok-function">displacementChanged</span>(displacement<span class="tok-operator">:</span> <span class="tok-type">CGPoint</span>) {
    <span class="tok-keyword">guard</span> <span class="tok-operator">!</span>(displacement<span class="tok-operator">.</span>x <span class="tok-operator">=</span><span class="tok-operator">=</span> <span class="tok-number">0</span> <span class="tok-operator">&amp;</span><span class="tok-operator">&amp;</span> displacement<span class="tok-operator">.</span>y <span class="tok-operator">=</span><span class="tok-operator">=</span> <span class="tok-number">0</span>) <span class="tok-keyword">else</span> { <span class="tok-keyword">return</span> }

    <span class="tok-keyword">let</span> rotation<span class="tok-type">Transform</span> <span class="tok-operator">=</span> transformed<span class="tok-type">Layer</span><span class="tok-operator">.</span>sublayer<span class="tok-type">Transform</span><span class="tok-operator">.</span><span class="tok-function">rotationFromDisplacement</span>(displacement, side<span class="tok-type">Width</span><span class="tok-operator">:</span> <span class="tok-type">CACube3DView</span><span class="tok-operator">.</span>side<span class="tok-type">Width</span>, is<span class="tok-number">3</span><span class="tok-type">D</span><span class="tok-operator">:</span> is<span class="tok-type">On</span>)
    transformed<span class="tok-type">Layer</span><span class="tok-operator">.</span>sublayer<span class="tok-type">Transform</span> <span class="tok-operator">=</span> rotation<span class="tok-type">Transform</span>
  }</code></pre>
<p>We hooked up the pan-gestures prior:</p>
<pre><code class="language-swift">#<span class="tok-keyword">if</span> <span class="tok-function">os</span>(<span class="tok-type">OSX</span>)
<span class="tok-keyword">private</span> <span class="tok-keyword">func</span> <span class="tok-function">setupPanGestureRecognizer</span>() {
    <span class="tok-keyword">let</span> pan<span class="tok-type">GR</span> <span class="tok-operator">=</span> <span class="tok-type">NSPanGestureRecognizer</span>(target<span class="tok-operator">:</span> <span class="tok-keyword">self</span>, action<span class="tok-operator">:</span> #<span class="tok-function">selector</span>(<span class="tok-function">handlePanGesture</span>(_<span class="tok-operator">:</span>)))
    <span class="tok-function">addGestureRecognizer</span>(pan<span class="tok-type">GR</span>)
}
<span class="tok-attribute">@objc</span> <span class="tok-keyword">func</span> <span class="tok-function">handlePanGesture</span>(_ gesture<span class="tok-type">Recognizer</span><span class="tok-operator">:</span> <span class="tok-type">NSPanGestureRecognizer</span>) {
    <span class="tok-keyword">let</span> displacement<span class="tok-operator">:</span> <span class="tok-type">CGPoint</span> <span class="tok-operator">=</span> gesture<span class="tok-type">Recognizer</span><span class="tok-operator">.</span><span class="tok-function">translation</span>(<span class="tok-keyword">in</span><span class="tok-operator">:</span> <span class="tok-keyword">self</span>)
    <span class="tok-function">handlePan</span>(displacement<span class="tok-operator">:</span> displacement, changed<span class="tok-operator">:</span> gesture<span class="tok-type">Recognizer</span><span class="tok-operator">.</span>state <span class="tok-operator">=</span><span class="tok-operator">=</span> <span class="tok-operator">.</span>changed)
}
#end<span class="tok-keyword">if</span>

#<span class="tok-keyword">if</span> <span class="tok-function">os</span>(i<span class="tok-type">OS</span>) <span class="tok-operator">|</span><span class="tok-operator">|</span> <span class="tok-function">os</span>(tv<span class="tok-type">OS</span>)
<span class="tok-keyword">private</span> <span class="tok-keyword">func</span> <span class="tok-function">setupPanGestureRecognizer</span>() {
    <span class="tok-keyword">let</span> pan<span class="tok-type">GR</span> <span class="tok-operator">=</span> <span class="tok-type">UIPanGestureRecognizer</span>(target<span class="tok-operator">:</span> <span class="tok-keyword">self</span>, action<span class="tok-operator">:</span> #<span class="tok-function">selector</span>(<span class="tok-function">handlePanGesture</span>(_<span class="tok-operator">:</span>)))
    <span class="tok-function">addGestureRecognizer</span>(pan<span class="tok-type">GR</span>)
}

<span class="tok-attribute">@objc</span> <span class="tok-keyword">func</span> <span class="tok-function">handlePanGesture</span>(_ gesture<span class="tok-type">Recognizer</span><span class="tok-operator">:</span> <span class="tok-type">UIPanGestureRecognizer</span>) {
    <span class="tok-keyword">let</span> displacement<span class="tok-operator">:</span> <span class="tok-type">CGPoint</span> <span class="tok-operator">=</span> gesture<span class="tok-type">Recognizer</span><span class="tok-operator">.</span><span class="tok-function">translation</span>(<span class="tok-keyword">in</span><span class="tok-operator">:</span> <span class="tok-keyword">self</span>)

    <span class="tok-function">handlePan</span>(displacement<span class="tok-operator">:</span> displacement, changed<span class="tok-operator">:</span> gesture<span class="tok-type">Recognizer</span><span class="tok-operator">.</span>state <span class="tok-operator">=</span><span class="tok-operator">=</span> <span class="tok-operator">.</span>changed)

    <span class="tok-keyword">if</span> gesture<span class="tok-type">Recognizer</span><span class="tok-operator">.</span>state <span class="tok-operator">=</span><span class="tok-operator">=</span> <span class="tok-operator">.</span>changed {
      gesture<span class="tok-type">Recognizer</span><span class="tok-operator">.</span><span class="tok-function">setTranslation</span>(<span class="tok-operator">.</span>zero, <span class="tok-keyword">in</span><span class="tok-operator">:</span> <span class="tok-keyword">self</span>)
    }
}
#end<span class="tok-keyword">if</span></code></pre>
<p>We can also add a simple rotation transform for the rotation events/ gestures.</p>
<pre><code class="language-swift">  override <span class="tok-keyword">func</span> <span class="tok-function">rotationChanged</span>(radians<span class="tok-operator">:</span> <span class="tok-type">Float</span>) {
    <span class="tok-keyword">let</span> transform <span class="tok-operator">=</span> transformed<span class="tok-type">Layer</span><span class="tok-operator">.</span>sublayer<span class="tok-type">Transform</span>
    <span class="tok-keyword">let</span> rot <span class="tok-operator">=</span> <span class="tok-type">CATransform3DRotate</span>(transform, <span class="tok-type">CGFloat</span>(radians), <span class="tok-number">0</span>, <span class="tok-number">1</span>, <span class="tok-number">0</span>)
    transformed<span class="tok-type">Layer</span><span class="tok-operator">.</span>sublayer<span class="tok-type">Transform</span> <span class="tok-operator">=</span> rot
  }</code></pre>
<p>And scale, in all three axes:</p>
<pre><code class="language-swift">  override <span class="tok-keyword">func</span> <span class="tok-function">scaleChanged</span>(scale<span class="tok-operator">:</span> <span class="tok-type">CGFloat</span>) {
    <span class="tok-keyword">let</span> scale<span class="tok-type">Transform</span> <span class="tok-operator">=</span> <span class="tok-type">CATransform3DScale</span>(transformed<span class="tok-type">Layer</span><span class="tok-operator">.</span>sublayer<span class="tok-type">Transform</span>, scale, scale, scale)
    transformed<span class="tok-type">Layer</span><span class="tok-operator">.</span>sublayer<span class="tok-type">Transform</span> <span class="tok-operator">=</span> scale<span class="tok-type">Transform</span>
  }</code></pre>
<p>The <code>tap</code> turns on and off our 3D transform</p>
<pre><code class="language-swift">  override <span class="tok-keyword">func</span> <span class="tok-function">tapHappened</span>() {
    <span class="tok-function">set3DCube</span>(on<span class="tok-operator">:</span> is<span class="tok-type">On</span>)
  }</code></pre>
<p>Here’s the 3D cube code</p>
<pre><code class="language-swift">  <span class="tok-keyword">func</span> <span class="tok-function">set3DCube</span>(on<span class="tok-operator">:</span> <span class="tok-type">Bool</span>) {
    side<span class="tok-number">4.</span>layer<span class="tok-operator">.</span>z<span class="tok-type">Position</span> <span class="tok-operator">=</span> on <span class="tok-operator">?</span> <span class="tok-type">CACube3DView</span><span class="tok-operator">.</span>side<span class="tok-type">Width</span> <span class="tok-operator">:</span> <span class="tok-number">1</span>
    side<span class="tok-number">1.</span>layer<span class="tok-operator">.</span>transform <span class="tok-operator">=</span> on <span class="tok-operator">?</span> <span class="tok-type">CATransform3D</span><span class="tok-operator">.</span><span class="tok-function">transformFor3DCubeSide</span>(<span class="tok-number">1</span>, z<span class="tok-type">Width</span><span class="tok-operator">:</span> <span class="tok-type">CACube3DView</span><span class="tok-operator">.</span>side<span class="tok-type">Width</span>) <span class="tok-operator">:</span> <span class="tok-type">CATransform3DIdentity</span>
    side<span class="tok-number">2.</span>layer<span class="tok-operator">.</span>transform <span class="tok-operator">=</span> on <span class="tok-operator">?</span> <span class="tok-type">CATransform3D</span><span class="tok-operator">.</span><span class="tok-function">transformFor3DCubeSide</span>(<span class="tok-number">2</span>, z<span class="tok-type">Width</span><span class="tok-operator">:</span> <span class="tok-type">CACube3DView</span><span class="tok-operator">.</span>side<span class="tok-type">Width</span>) <span class="tok-operator">:</span> <span class="tok-type">CATransform3DIdentity</span>
    side<span class="tok-number">3.</span>layer<span class="tok-operator">.</span>transform <span class="tok-operator">=</span> on <span class="tok-operator">?</span> <span class="tok-type">CATransform3D</span><span class="tok-operator">.</span><span class="tok-function">transformFor3DCubeSide</span>(<span class="tok-number">3</span>, z<span class="tok-type">Width</span><span class="tok-operator">:</span> <span class="tok-type">CACube3DView</span><span class="tok-operator">.</span>side<span class="tok-type">Width</span>) <span class="tok-operator">:</span> <span class="tok-type">CATransform3DIdentity</span>
    side<span class="tok-number">4.</span>layer<span class="tok-operator">.</span>transform <span class="tok-operator">=</span> on <span class="tok-operator">?</span> <span class="tok-type">CATransform3D</span><span class="tok-operator">.</span><span class="tok-function">transformFor3DCubeSide</span>(<span class="tok-number">4</span>, z<span class="tok-type">Width</span><span class="tok-operator">:</span> <span class="tok-type">CACube3DView</span><span class="tok-operator">.</span>side<span class="tok-type">Width</span>) <span class="tok-operator">:</span> <span class="tok-type">CATransform3DIdentity</span>
    side<span class="tok-number">5.</span>layer<span class="tok-operator">.</span>transform <span class="tok-operator">=</span> on <span class="tok-operator">?</span> <span class="tok-type">CATransform3D</span><span class="tok-operator">.</span><span class="tok-function">transformFor3DCubeSide</span>(<span class="tok-number">5</span>, z<span class="tok-type">Width</span><span class="tok-operator">:</span> <span class="tok-type">CACube3DView</span><span class="tok-operator">.</span>side<span class="tok-type">Width</span>) <span class="tok-operator">:</span> <span class="tok-type">CATransform3DIdentity</span>
    side<span class="tok-number">6.</span>layer<span class="tok-operator">.</span>transform <span class="tok-operator">=</span> on <span class="tok-operator">?</span> <span class="tok-type">CATransform3D</span><span class="tok-operator">.</span><span class="tok-function">transformFor3DCubeSide</span>(<span class="tok-number">6</span>, z<span class="tok-type">Width</span><span class="tok-operator">:</span> <span class="tok-type">CACube3DView</span><span class="tok-operator">.</span>side<span class="tok-type">Width</span>) <span class="tok-operator">:</span> <span class="tok-type">CATransform3DIdentity</span>
  }</code></pre>
<p>We either set the <code>identity transform</code>, which means <code>no transform</code>, to our cube sides, or the transform appropriate for that particular side.</p>
<h2>6. macOS Troubles</h2>
<p>I suppose I need to do further investigation in how <code>macOS</code> treats layers of <code>NSView</code>, for this little experiment is not working on the <code>macOS</code>, yet.
Here’s how the flattened cube looks on the <code>macOS</code></p>
<p><img src="https://aleahim.com/images/cacube/12_cube_macOS_flat_messed_up.png" alt=""></p>
<p>So, the positioning of the layers is not respected.</p>
<p>And here is the cube in 3D:</p>
<p><img src="https://aleahim.com/images/cacube/13_cube_macOS_3D_messed_up.png" alt=""></p>
<p>I did try to force the <code>isGeometryFlipped = true</code> everywhere. Anyway this needs more work.</p>
<p>If you want to help with the <code>macOS</code> implementation, please, be my guest.</p>
<p>Here’s a link to a Codeberg repo with the source code:
<a href="https://codeberg.org/Mihaela/CubeIn3DWithCoreAnimation">Core Animation 3D Cube</a></p>]]></content:encoded>
</item>
</channel>
</rss>
