<![CDATA[Harshil Shah]]>https://harshil.netGatsbyJSFri, 06 Feb 2026 07:26:24 GMT<![CDATA[Less Janky Placeholders in SwiftUI]]>https://harshil.net/blog/swiftui-placeholder-jankhttps://harshil.net/blog/swiftui-placeholder-jankSat, 04 Oct 2025 14:00:00 GMT<p>I’ve been chipping away at an update for my app, <a href="https://sneakpeak.app">Peak</a>, which makes a bunch of things in the app <em>significantly</em> faster.</p> <p>Here, for instance, is how the Recaps screen in Peak loaded before the update. I’m opening my recap for 2024, and once that’s loaded, switching over to 2023:</p> <figure class="img-small"> <video muted controls playsInline> <source src="/6594bad7442ff0c88c4f102c10956b94/original.mp4" type="video/mp4"> </video> </figure> <p>And here’s what it looks like after the update:</p> <figure class="img-small"> <video muted controls playsInline> <source src="/ffdc38e27d6fabdd8f12e50831760ee8/faster.mp4" type="video/mp4"> </video> </figure> <p>That’s a lot faster! But it’s also a bit janky.</p> <p>The problem is straightforward. When the app doesn’t have a value to display for the current period, it reverts to showing some placeholder data.</p> <p>This works fine when the loading takes a while, as it did before the update. But when the data loads fast enough, it creates this jarring flicker, taking you from the previous value to the placeholder and then the new value, all in the span of a few frames.</p> <p>My first instinct was to solve this in the model. I could track the last non-nil value and display it until the new data arrives. But in practice, that got messy fast.</p> <p>It made my previous simple models a lot more complex, and this logic would need to be added to every single view model or TCA Reducer where I wanted the behavior as well. But at its core, the biggest issue was this was pushing a bunch of presentation layer work into the model.</p> <p>So instead I came up with a way to handle this in the view itself. Here’s the creatively named SwiftUI helper view I came up with:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="0"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">import</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">SwiftUI</span></span></span> <span class="grvsc-line"><span class="grvsc-source"></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">struct</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">CachingLastNonNilValue</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">&lt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">Value</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Equatable</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">, </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">Content</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">View</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">&gt;: View {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">var</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> value</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Value</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">?</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">var</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> placeholder</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Value</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">var</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> timeout</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> Duration</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">?</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">@State</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">private</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">var</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> lastNonNilValue</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Value</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">?</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">@ViewBuilder</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">var</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> content</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> (</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Value</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">) </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> Content</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">var</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> body</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> some View {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">content</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> value</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">??</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> lastNonNilValue</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">??</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> placeholder</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">onChange</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">of</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: value, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">initial</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-10">true</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-1">_</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">, value </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">in</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">guard</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> value </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">else</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> lastNonNilValue </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> value</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">task</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">id</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: value </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">==</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-7">nil</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">do</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">guard</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> value </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">==</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-7">nil</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">, </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> timeout </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">else</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">try</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> await Task.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">sleep</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">for</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: timeout</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> lastNonNilValue </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-7">nil</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> } </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">catch</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">print</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">Task was cancelled</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p>The view accepts an optional value as well as a placeholder. As your value updates, it keeps track of the last non-nil value, and uses it to power your content when the value changes to nil.</p> <p>It also lets you specify an optional timeout, which clears out the last non-nil value stored and reverts to the placeholder after a certain duration has passed, which can be useful if the loading process takes an unexpectedly long time.</p> <p>Here’s what it looks like at the call site:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="1"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">// Before</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">RecapSummary</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">metric</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: metric,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">recap</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: store.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">recaps</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">?</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">[metric] </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">??</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">placeholder</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">forMetric</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: metric</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">// After</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">CachingLastNonNilValue</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">value</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: store.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">recaps</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">?</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">[metric],</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">placeholder</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">placeholder</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">forMetric</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: metric</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">timeout</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">seconds</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">0.3</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { recap </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">in</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">RecapSummary</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">metric</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: metric, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">recap</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: recap</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p>With this view in place, here’s what the final shipping UI looks like:</p> <figure class="img-small"> <video muted controls playsInline> <source src="/07bff908fe24d8d62f56ca4089aa8d8f/final.mp4" type="video/mp4"> </video> </figure> <p>Now that’s much better!</p> <p>It’s being able to create abstractions like this that I love most about SwiftUI. This little 30 line helper can be plugged into any view in my app, and makes so much UI and model code simpler and easier to reason about.</p> <style class="grvsc-styles"> .nikso-plus { background-color: #ffffff; color: #24292e; } .nikso-plus .grvsc-ttxYAU-7 { color: #D73A49; } .nikso-plus .grvsc-ttxYAU-1 { color: #24292EFF; } .nikso-plus .grvsc-ttxYAU-4 { color: #6F42C1; } .nikso-plus .grvsc-ttxYAU-5 { color: #005CC5; } .nikso-plus .grvsc-ttxYAU-8 { color: #22863A; } .nikso-plus .grvsc-ttxYAU-3 { color: #6A737D; } .nikso-plus .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(0, 0, 0, 0.05)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(0, 0, 0, 0.2)); } /* Night Owl (No Italics) */ @media (prefers-color-scheme: dark) { .grvsc-mm-tGNH8N { background-color: #011627; color: #d6deeb; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-3 { color: #C792EA; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-1 { color: #D6DEEB; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-9 { color: #7FDBCA; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-5 { color: #C5E478; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-7 { color: #82AAFF; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-10 { color: #FF5874; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-37 { color: #D9F5DD; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-19 { color: #ECC48D; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-6 { color: #637777; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-8 { color: #F78C6C; } .grvsc-mm-tGNH8N .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); } } </style><![CDATA[The Year of Lifting]]>https://harshil.net/blog/the-year-of-liftinghttps://harshil.net/blog/the-year-of-liftingTue, 21 Jan 2025 15:30:00 GMT<p>2024 was the year I started lifting. Actually, that’s not quite right. I’ve started lifting on many occasions. 2024 was the first time I actually persisted with it.</p> <p>Growing up I had a weird relationship with my own body, in that I mostly viewed it as an external entity. Sure I needed it to carry around my brain, the real “me”, but that’s all it was, just an enclosure. I made sure it was in good enough condition to do that job, but didn’t really care much beyond that. I was never too fit or too unfit, and just comfortable hovering in the wide border between the two.</p> <p>I did learn over time that this wasn’t sustainable though. <a href="blog/apple-watch-one-thousand-days" title="A Thousand Days Wearing an Apple Watch">Getting an Apple Watch</a> to track my fitness helped me with the motivation quite a bit. All the various little trophies and challenges tickled just the right parts of my brain to keep me going. I was doing bodyweight workouts at home and going for longer and longer walks, but I could just feel I was reaching the point of diminishing returns.</p> <p>Going to a gym seemed like the next logical step, but it wasn’t an easy decision to make.</p> <h2>Showing Up</h2> <p>This wasn’t my first time going to the gym. I had tried it a few times before, and while I’d have some good stints of regular visits for a few weeks, but I’d eventually return back to my ways and daily schedule. I had some momentum from my home workouts this time which I knew would help, but I also needed a different way to think about the whole situation.</p> <p>This is where CGP Grey’s concept of <a href="https://www.youtube.com/watch?v=NVGuFdX5guE" title=" Your Theme video by CGP Grey">Yearly Themes</a> came in. The gist of the idea is that instead of setting concrete resolutions that you can fail early and give up on, you should help yourself achieve the larger goals you have by creating a broader, more vague theme, such as having a year of reading. This gives you more flexibility in your goals while still letting you count any progress as the win it is.</p> <p>My theme for 2023 was a year of health, and for 2024 I refined it a bit to be a year of lifting. My main goal was that I wanted to show up at the gym as often as I could. I wanted to grow some muscle, I wanted to lose some weight, and I had a bunch of ancillary goals for specific exercises and lifts, but all of that took a backseat to the main target of just showing up.</p> <p>The main part of this was just doing exercises that I enjoyed and would look forward to. I’d skip more “optimal” and tried-and-tested exercises if I found them to be uncomfortable or if they just didn’t feel right. I’m also incredibly careful around progressive overloading. I always make sure I can comfortably complete two whole sets with a few more reps left in the tank before even thinking about increasing the weights. As much as I want to push myself, the main goal is showing up, and an injury is the fastest way to prevent that.</p> <p>I applied the same ideas to my nutrition. While I didn’t cut out sugar and sweets entirely<sup id="fnref-1"><a href="#fn-1" class="footnote-ref">1</a></sup>, I made some smarter choices around them. As a vegetarian I knew I needed to supplement my diet with a lot of protein to properly recover, so I keep experimenting with different ways to make that work too.</p> <h2>Seeing The Results</h2> <p>The first big thing I noticed was sleep. As with previous attempts at going to the gym, the biggest impact it had was on my sleeping patterns. I went from erratic sleep schedules and durations to a more solid rhythm.</p> <p>It was a bit difficult to make sense of some of the other physical results. The first noticeable changes were in the weights I was lifting. Progressions in the first few months were fast and easy. For instance, I went from bench pressing with 5kg dumbbells to 7.5kg, 10kg, and then 12.5 in about 2 months. This whole time I didn’t notice any big changes in my weight or body though. I later had times where I’d look visibly thinner but my weight didn’t move much, and also times where the scale showed a difference but I just didn’t feel it.</p> <p>While I was initially obsessed with the numbers, I now mostly only view these metrics at a distance; as long as the trend line isn’t moving in the wrong direction, I’m not too bothered.</p> <p>It’s also changed my mindset. Exercise is no longer something I do out of a sense of obligation. It’s just something I <em>do</em>, an intrinsic part of me. I’ve lost a ton of weight and put on a ton of muscle, and between these things I just feel so much more comfortable in my skin and confident in myself.</p> <p>The biggest impact I’ve felt on my day-to-day mental state, really. There’s something incredibly empowering about pushing yourself to your limits every day and knowing you’ll survive. I feel calmer and more clear headed, though I don’t really know how much of that is a direct effect of the lifting or an effect of the other effects. I’m still prone to overthinking, but a bit less so, I think.</p> <h2>Some Advice I Wish I Had</h2> <p>The one big piece of advice I’d give anyone starting out is that most advice, even the stuff that seems like it’s pretty universal, isn’t always right for you.</p> <p>It’s easy to find a thousand different viewpoints about anything and everything on the internet, and this is especially true in the fitness space. It’s really easy to find yourself lost in a maze of contradicting opinions while you search for the one true ideal workout strategy that will give you the maximum gains you’re searching for.</p> <p>That said, you can’t really just walk into a gym and divine a workout plan knowing nothing, so my recommendation would be to seek <em>some</em> modicum of opinions, either from trainers at your gym or online, but then actually implement them and trust your gut. See which exercises work for you, which ones don’t, and adjust and adapt accordingly. You can check in periodically as you grow, but there’s little to be gained in constantly chasing the latest trends.</p> <p>There are definitely other fantastic people out there whose work you might resonate more with, but two people whose work I personally follow are:</p> <ul> <li><a href="https://www.youtube.com/@JeffNippard" title="Jeff Nippard on YouTube">Jeff Nippard</a>, whose channel and book <a href="https://jeffnippard.com/pages/the-muscle-ladder" title="The Muscle Ladder">The Muscle Ladder</a> are great resources especially for beginners and those building and tweaking their programs.</li> <li><a href="https://www.youtube.com/@SquatUniversity" title="Squat University on YouTube">Squat University</a>, whose channel has been really fantastic for more fine-grained topics and specifics around things like mobility and rehabilitation.</li> </ul> <h2>Looking Ahead</h2> <p>As I write this I’m in a bit of a weird place. I think I’m fairly close to the end of the honeymoon phase of lifting where progress comes easy.</p> <p>While I initially started out with a goal of just showing up, I think I’ve now got that part down and need to think more deeply about where to go from here: Do I want to put some weight back on to add more muscle? Do I go on a cut and see what it’s like to be shredded?</p> <p>I’m currently aiming for some smaller, more technical targets, like fixing pesky ankle stability imbalance and getting my first sissy squat, pistol squat, and pull up. I’m not too sure about the longer term stuff yet, but I guess I’ll just keep lifting in the meantime.</p> <p>2024 will always be the year I got into lifting. I hope that looking back in the future, I’ll see it as the year I just started lifting.</p> <div class="footnotes"> <hr> <ol> <li id="fn-1">I’m Gujarati after all<a href="#fnref-1" class="footnote-backref">↩</a></li> </ol> </div><![CDATA[A Thousand Days Wearing an Apple Watch]]>https://harshil.net/blog/apple-watch-one-thousand-dayshttps://harshil.net/blog/apple-watch-one-thousand-daysSat, 21 Sep 2024 15:30:00 GMT<p>Earlier today, I received a notification that I have been looking forward to for a long time.</p> <figure class="img-extrasmall"> <img src="/da183331f2cfc807fd06bd18a1058cbb/notification.png" alt="Apple Watch Fitness app notification: 1,000 Move Goals. You reached your daily Move goal for the 1,000th time!" /> <figcaption> Award notification from the Fitness app, for completing 1,000 move goals </figcaption> </figure> <p>I’ve now owned and worn an Apple Watch for exactly one thousand days, and I cannot emphasize enough how much this little gizmo has changed my life in this time. I’ve lost over 20 kilos of weight, put on a lot of muscle, and am stronger and fitter than I have ever been—yet!</p> <h2>Why Apple Watch</h2> <p>Coming out of lockdown, I wasn’t feeling the greatest. I hadn’t ever been a particularly fit or active person, and being at home for almost two years hadn’t helped with that. I knew I needed a change, and had already started a little home workout regiment, going on more walks, and eating better. Being the nerd I am, an Apple Watch just seemed like the perfect thing that could give me some impetus to keep going.</p> <p>And it worked, for the most part. I was able to increase my workouts and also started seeing small but real results fairly quickly, all with just the daily target of closing my rings every day.</p> <p>The rings, though, are not perfect. I started running into many issues where my and the Fitness app’s ideas for how my goals and rest should be structured didn’t always line up. I’d often end up working out through bad personal days or on days when I’d much rather rest, and I still don’t know how I feel about that. That tension eventually led me to make <a href="/blog/introducing-peak">my own fitness tracking app, Peak</a>, to get more control over my goals, both short and long term.</p> <p>This year I’ve also started going to the gym again after a few years away. I don’t think there’s many people out there unfamiliar with the cycle of joining a gym, paying for an annual membership, and then only showing up for a few months, if not weeks or even days. I’ve been down that path a few times in the past before, but between the watch, Peak, and also just the additional training I’ve done which has put me in a much better place to get started, I’m happy that this time I haven’t dropped out, and now, 8 months in, going to the gym every single morning is now just part of my daily routine.</p> <p>I’ve seen huge benefits already from adding weight training to my exercise routine, and in the process I’ve also started work on another app to help both beginners and experienced lifters. I can’t wait to share more about it later this year!</p> <h2>The Numbers</h2> <p>One of the nice things about working on fitness apps is that I have access to a large corpus of HealthKit code that can comb through my health data and pull out various interesting bits of insight. I modified some of that code to go through all of the data that my Apple Watch has recorded over the years, and put together some highlights:</p> <ul> <li>All rings closed on 1000/1000 days</li> <li>3334 Workouts</li> <li>49 days, 16 hours, and 32 minutes spent working out</li> <li>4,37,468 kcal burned in workouts</li> <li>8,04,469 kcal burned overall</li> <li>71,797 exercise minutes</li> <li>43,36,033 steps walked</li> <li>3,176.68 kilometres walked</li> <li>1,399 floors climbed</li> <li>21.2 kg lost</li> <li>-11bpm change in resting heart rate</li> <li>+16.2 change in VO<sub>2</sub> max</li> </ul> <h2>Wrapping Up</h2> <p>To be clear, I don’t want to suggest that just having a watch will suddenly bring about change you’ve always wanted to make. It takes a modicum of willpower to get started, and then some to keep showing up every day.</p> <p>But if you, like me, have a mind that can be motivated by little trinkets, fancy animations, and making a number go up every day, I think the Apple Watch can be very helpful in making that start.</p><![CDATA[Introducing Peak for Apple Watch]]>https://harshil.net/blog/introducing-peak-for-apple-watchhttps://harshil.net/blog/introducing-peak-for-apple-watchWed, 10 Jul 2024 10:50:00 GMT<p>On this day one year ago, <a href="http://harshil.net/blog/introducing-peak" title="Introducing Peak">I announced Peak</a>, an app that lets make your personal fitness dashboard and keep track of your fitness with a range of widgets.</p> <p>Today, I’m excited to announce <a href="https://apps.apple.com/us/app/peak/id6443923491" title="Download Peak">Peak 3</a>, which fulfils the most requested feature since the app’s launch a year ago, and which brings the app to the Apple Watch!</p> <p>Peak’s story began with my own experience using an Apple Watch, and the change it brought about in my fitness and habits. It started life on the iPhone because that’s the platform I am most familiar with as a developer, but bringing it to the watch was always a question of “when” and not “if”, and I’m so happy to finally see that vision become reality</p> <figure class="img-large"> <img src="/1da76c0ac5fb7e1435c97bae5d5cfc85/watch.png" /> <figcaption> Peak 3’s Today view, complications, and smart stack widgets </figcaption> </figure> <p>Peak for Apple Watch brings all of the power of the iPhone app to your wrist. You can track all of your stats for today at a glance in the Today view, and tap through for more details.</p> <p>You can see a wide variety of information by adding blocks to the app, such as:</p> <ul> <li>Goals, which are available across all metrics and can be created to suit exactly your fitness targets</li> <li>Benchmarks, which show a heat map of your daily progress</li> <li>Charts of your progress over the day, week, month, or year</li> <li>Trends, which show how your progress this week, month, or quarter compares to the previous four</li> <li>Totals, which show you aggregate stats for the week, month, and year</li> <li>Overviews, which show a statistics such as the average, maximum, minimum, and more, for a given time period</li> <li>Recents, which show your stats for the last few days</li> </ul> <p>All of these can be customised to fit your needs, and for workouts, you can limit them to see your progress for any specific workout types as well.</p> <p>All of your existing blocks carry over from your iPhone as well, powered by a brand new sync system which makes sure all of your metrics and blocks are available on all of your devices.</p> <p>The app’s is designed for watchOS 10’s new design language, and will feel familiar and intuitive right out of the box to anyone with an Apple Watch. Every part of the app has been tailored to show you the most important information about your progress. And there are 9 beautiful themes designed just for the watch app so you can match your style.</p> <p>Peak supports all of watchOS’s complication and widget styles, so all of your stats are just a glance away, on any watch face you like and in the smart stack.</p> <p>The Apple Watch app maintains Peak’s commitment to accessibility, supporting a wide variety of accessibility options like dynamic type, VoiceOver, and reduce motion.</p> <p>And of course, Peak for Apple Watch has the same strong privacy protections as the iPhone app, and includes zero tracking or data collection. The new sync system uses your iCloud storage, so there are no accounts to sign into or data stored on Peak’s servers (in fact, Peak doesn’t even have any servers).</p> <h2>Year One</h2> <p>This release also marks one year since the Peak’s initial release, and I’m overjoyed at both the response the app has received, and the progress it has made in this time.</p> <p>It didn’t start off as an intentional practise, but since the release and right up until I started working on the Apple Watch app, Peak has had a major release with new features every single month.</p> <p>Here’s the timeline so far:</p> <ul> <li>July 2023: Initial release</li> <li>August: Benchmarks, which show you GitHub-inspired heatmaps of your progress over time</li> <li>September: Peak 2 for iPad, along with support for StandBy mode on iOS 17 and new themes</li> <li>October: Weight tracking</li> <li>November: Charts &#x26; History widgets</li> <li>December: 2023 In Review, which let you view and share a summary of your progress over the whole year</li> <li>January 2024:: Resolutions, which allow you to break down long term targets into smaller goals which gradually build up in intensity</li> <li>February: Localisation support for Spanish &#x26; German</li> <li>March: Recaps, which generalise the 2023 In Review feature to show you summaries for any day, week, or month</li> <li>April: Sleep and cardiac health tracking</li> </ul> <p>I also put together a bento box illustration showing the App Store Event artwork I used for each of these releases:</p> <figure class="img-large"> <img src="/6261f30d036fec8351e69f52b2608f17/year-one.png" /> <figcaption> All the new features added in Peak’s first year </figcaption> </figure> <h2>We’re only just getting started</h2> <p>With this release, Peak now finally feels like the app I set out to build. Peak for Apple Watch sets up the platform for many more new exciting features.</p> <p>I have some ideas of my own for what to build for upcoming releases, but your feedback is also crucial in helping inform my decisions and priorities. If you run into any issues, have any feature requests, or have found the app useful, please let me know by <a href="mailto:support@sneakpeak.app">sending an email</a>, or by sending me a message on any of the various social media apps.</p> <p>You can <a href="https://apps.apple.com/us/app/peak/id6443923491">download Peak 3 on the App Store</a> now. I can’t wait for you to try Peak on your Apple Watch and hear what you think!</p><![CDATA[Building Peak]]>https://harshil.net/blog/building-peakhttps://harshil.net/blog/building-peakMon, 10 Jul 2023 10:50:00 GMT<p><em>This post goes into the technical details of how Peak was made. You can also <a href="/blog/introducing-peak">read the story behind the app</a> and <a href="https://apps.apple.com/us/app/peak/id6443923491">download it on the App Store</a>.</em></p> <h2>The Stack</h2> <p>Peak started out as a nights and weekends project. I knew that time was going to be short at hand, and that having some momentum and being able to make tangible changes in the short bursts of time I could scrounge together was going to be critical to keeping the project alive, and all that made the choice of tech stack fairly straightforward: SwiftUI and <a href="https://github.com/pointfreeco/swift-composable-architecture" title="The Composable Architecture on GitHub">The Composable Architecture</a>.</p> <p>I was familiar with both from using them at my job at the time, and I knew that while there’s a bit of boilerplate involved with TCA, the productivity gains over time made it worth it. While SwiftUI is great for quickly building and iterating on interfaces, there’s still all the business logic and data flow that you need to sort out yourself. It’s the combination of the two that truly makes my brain go almost on autopilot and quickly hammer out whole new features.</p> <p>I hope to write more in detail about my experience using both of these frameworks, but for now, know that I don’t exaggerate when I say that without them, this app likely wouldn’t exist right now, let alone be on the App Store, and that too in a state that I can genuinely be proud of.</p> <p>My deepest appreciation and thanks go out to everyone involved in making and maintaining them.</p> <h2>Design</h2> <p>The design goal for the app was for something that felt at home on iOS, while still not feeling too cookie cutter. I didn’t want the app to feel as extremely stock as SwiftUI apps are often (correctly) accused of being<sup id="fnref-1"><a href="#fn-1" class="footnote-ref">1</a></sup>.</p> <p>Another important design goal was theming. I didn’t want this app to have just a light and dark theme, or even just simple customisations such as an accent colour, but instead just entirely different, vibrant themes. This bit was made fairly simple by making themes available via an the Environment in SwiftUI, which propagates values across the hierarchy, while also allowing for overrides at any point.</p> <h2>Controls</h2> <p>Keeping with the design brief, there’s a fair mix of stock and custom UI in the app. In general I leaned towards stock UI where I could, but didn’t really hesitate too much to build something anew where I had to. In UIKit land the accessibility regressions and additional maintenance involved would make me a lot more hesitant, but the story around both those aspects is a lot better in SwiftUI.</p> <p>The charts are all rendered using the Swift Charts framework, which was a fortuitous little surprise at WWDC last year as I’d spent just the previous trying to figure out how to make my own.</p> <p>Apple offers a <a href="https://developer.apple.com/documentation/healthkit/" title="Apple’s documentation for HKActivityRingView">UIKit component</a> to render activity rings, however it’s a bit inflexible. It can only be populated with data from HealthKit, the animation timing is controlled by the system, and you can’t change any of the colours. I’ve previously <a href="https://github.com/HarshilShah/ActivityRings" title="My SpriteKit ActivityRings repo on GitHub">recreated the rings in SpriteKit</a> so I had a general idea of how to go about it, and I’m happy to say remaking them in SwiftUI was quite a bit simpler.</p> <p>All forms, such as the ones you see in Settings, or when you add metrics in onboarding, are custom. I tried using the standard <code>List</code> and <code>Form</code> components but wasn’t able to sufficiently customise them to fit the aesthetic I had in mind, so I ended up making my own set of controls, creatively called <code>Mesa</code> (Spanish for “table”). I really hope the system APIs are opened up more in the future to allow for more customisation, because you do miss out on a lot of built in niceties such as edit mode, swipe actions, and more, when you choose to build something using a <code>LazyVStack</code> instead.</p> <h2>Party Tricks</h2> <figure class="img-floating-left"> <video muted controls> <source src="/f33929f2132a24bd6ad57feb1411a228/jiggle.mp4" type="video/mp4"> </video> </figure> <p>Whenever I’m designing an app, I’m on the lookout for places to add fun little homages to interfaces I’ve been inspired by over the years.</p> <p>With <a href="https://harshil.net/pause">Pause</a>, I recreated the classic <a href="https://harshil.net/blog/recreating-the-mac-genie-effect">macOS genie effect</a>. Here, I stumbled upon an opportunity to add not one but two, in one go.</p> <p>I needed some way to let people reorder their metrics and blocks, and drag and drop was clearly the ideal interaction here. The app also includes interactive charts where you can tap or swipe on them to see the values over time, and the use of similar gestures made both of these interactions finicky. It was clear that they had to be exclusive, and so I needed an explicit edit mode. And what better inspiration for edit mode than Springboard’s jiggle mode.</p> <p>Getting SwiftUI to render it all correctly <a href="https://harshil.net/blog/swiftui-rotationeffect-is-kinda-funky" title="Adventures in Orienting Views in SwiftUI">took a bit of effort</a>, but it ended up being surprisingly simple to set up.</p> <p>One little additional touch is that when you exit enter mode, the metrics or blocks don’t snap to the regular state, but instead gracefully complete their current jiggle and stop when they reach the resting position. This behaviour was more difficult to get right, because I wanted the animation’s actual state to be based off of both an external state and a timer of some sort. I ended up making a new <a href="https://gist.github.com/HarshilShah/3e8a5ac15c55d2b56aab5cd342ea2534" title="The source for CycledAnimationTimelineView on GitHub"><code>CycledAnimationTimelineView</code></a> which wraps <code>TimelineView</code> and handles all the progress behaviour internally, exposing a simple API where I only need to specify a duration for the animation, whether it’s running, and a block that describes what should be rendered given the animation progress.</p> <p>In terms of accessibility, I was wary of how this would behave with the Reduce Motion setting. Intriguingly, Springboard’s jiggle mode seems to be unaffected by the setting. I generally tend to follow iOS’s lead for accessibility feature but I felt a bit unsure about that. Jiggle mode in Peak doesn’t animate at all when Reduce Motion is enabled, and just snaps to the rotated position. I’m not an expert here, so if anyone who uses the settings has strong opinions either way about the design of this feature, please do let me know.</p> <p>In addition to reordering, I added the ability to remove metrics and blocks in jiggle mode. This is not a big deal for most blocks, and they can be deleted and restored later fairly simply. Removing a metric or a goal however breaks any widgets configured for them. I needed to add a second confirmation step for these deletions.</p> <p>While a standard alert could work, there was some precedent from within iOS for an interaction that carried a bit more gravitas: Slide to unlock. This same interaction is also used to confirm that you want to restore from a backup. It’s even got the little shimmering effect suggesting the direction you need to swipe in.</p> <p>My favourite bit about this control might be the accessibility story. Under the hood, the Slide to Delete/Restore UI is implemented as just a button; more specifically it’s a custom <code>PrimitiveButtonStyle</code>. This means that for people using VoiceOver or Voice Control, there’s no need for them to figure out a way to actually perform the slide, as it just shows up as just another button.</p> <h2>My Side Project Has Side Projects</h2> <p>In general I’m a big fan of trying to smooth over any hard edges in my workflow. Any icky bits that can be automated tend to be automated. Sometimes this does get out of control but for Peak I think I’ve managed to get a really solid return on all 3 of the secondary apps I built to help me build it.</p> <h3>PeakBuilder</h3> <figure class="img-floating-right"> <img src="/a0c56cc37c2783c3dd602e33099a4a04/PeakBuilder.png" /> </figure> <p>As soon as I started pushing the first builds of the app up to TestFlight, I realised the upload process was way too finicky for me to go through with regularly. Archiving and waiting for that to complete, uploading and waiting again for that to complete, waiting once more for processing, and then adding release notes and groups was a process that didn’t take much time in itself but was repetitive manual work that required me to sit around and wait for the computer to do it’s thing and intermittently hit a button or two before it continued. The answer there was an easy one: <a href="https://fastlane.tools">Fastlane</a>. I’ve used it in projects but never set it up myself, and it ended up being a pretty quick and easy process.</p> <p>With Fastlane set up, all I had to do was fire off a build once with all the requisite input and it would handle all the steps for me. This was a lot faster, but it was still a bit annoying having to interact with all of this in via the command line. I didn’t like having to remember the correct syntax and making sure I added all the parameters every time.</p> <p>Enter Peak Builder: A SwiftUI app that handles the command line syntax for me. It displays a standard macOS form UI for all the fields. When I click build, it run some AppleScript that opens Terminal, sets the current directory, and executes the Fastlane command to cut a new build with all the correct parameters.</p> <p>At all of 160 lines that took me a half hour to put together, I think this might be one of the biggest time savers across the entire project. And if or when I do start using a CI provider, those generally tend to offer webhooks too, so I should be able to just reuse all the same code.</p> <h3>PeakThemer</h3> <p>Like with most other UI in the app, I started out designing themes right in Xcode, using SwiftUI previews. It was pleasant and fast enough to begin with, but I started noting some discrepancies when I’d run them on device. For some reason, colours that looked just right on the Mac felt incongruous on my iPhone, and would need a whole bunch of tweaking to work as expected.</p> <p>I’ve never quite figured out what the issue was here, whether it was something related to Xcode, or just related to the display technologies (OLED on the iPhone vs. Mini LED on the MacBook), or something else entirely, but it became clear that this wasn’t a sustainable development cycle.</p> <p>I needed to be able to make themes right on the device, and so I put together a simple little iOS app that lets me edit themes right on device using sliders. It shows a preview of what the theme looks right there, and also lets me copy the Swift representation of the generated Theme with a tap.</p> <figure class="img-inline"> <img src="/100ed7312021303535889126dd4844bf/PeakThemer.png" /> </figure> <h3>PeakSnapshotter</h3> <p>As I began the push towards launch, I realised there were gonna be a lot of screenshots and images to generate for promo art and App Store screenshots. I know my way around Sketch and so I knew this wouldn’t be too big an issue.</p> <p>However I also knew that I’d also need to keep these updated, and at some point later on, localise them to multiple languages too. Moreover because the app was designed almost entirely right in Xcode, I didn’t even have designs to use as a starting point. Recreating all of my existing UI just for the purpose of screenshots seemed pointless. I could take screenshots of the live app and use those in Sketch, however at that point I might as well cut out the middleman and render the extra device bezels and promo text right in code too. And so that’s what I did.</p> <p>I set up an app exclusively for testing which uses <a href="https://github.com/pointfreeco/swift-snapshot-testing">Point-Free’s Snapshot Testing</a> library to capture and automatically diff images. The UI you see across all screenshots and promo art is using the same exact components the app uses for the most part<sup id="fnref-2"><a href="#fn-2" class="footnote-ref">2</a></sup>.</p> <p>This setup has numerous benefits. As I update and iterate on the UI of the app, all my screenshots also update, without any work on my part. Updating screenshots to use a different theme takes just a single line of code. I also get to share the templates used across previews in code too, so I can update all screenshots in one shot, and the localisation work can also be unified with that for the rest of the app.</p> <p>It does have the drawback that I can’t use fancy 3D device mockups, but that’s something I’m okay with.</p> <hr> <p>That was a short look at how Peak was made!</p> <p>There’s no way I could’ve covered my experience using SwiftUI and TCA in this post without it being an order of magnitude longer, but I’m hoping to write separate posts about those in time as well. If there’s anything else that you’d like me to write about, or just any questions you have about the app, feel free to <a href="https://mastodon.social/@harshil" title="My Mastodon profile">contact me</a>.</p> <p>And also don’t forget to <a href="https://apps.apple.com/us/app/peak/id6443923491">download Peak on the App Store</a>, and let me know what you think about it.</p> <div class="footnotes"> <hr> <ol> <li id="fn-1">I haven’t been able to place why, but apps that look extremely stock often feel sterile on iOS, but at the same time they feel just right on macOS, and it’s apps that deviate from the system style that start feeling awkward. I’m very curious about which way things land for visionOS; having not tried it yet I’d guess it ends up being somewhere in the middle, though closer to macOS than iOS.<a href="#fnref-1" class="footnote-backref">↩</a></li> <li id="fn-2">Widgets on iOS use different typography metrics compared to the rest of the app itself. All fonts are smaller in widgets, but not proportionally so, and as they’re fairly simple bits of UI I decided to just remake those from scratch for the purpose of screenshots.<a href="#fnref-2" class="footnote-backref">↩</a></li> </ol> </div><![CDATA[Introducing Peak]]>https://harshil.net/blog/introducing-peakhttps://harshil.net/blog/introducing-peakFri, 07 Jul 2023 10:50:00 GMT<p>I’m excited today to announce my next app: <a href="https://apps.apple.com/us/app/peak/id6443923491">Peak</a>!</p> <p>Peak is an app that lets you build your personal fitness dashboard. It includes a whole bunch of widgets for your homescreen and lockscreen so you can always stay updated, as well as an assortment of themes to fully customise the app and every widget.</p> <figure class="img-large"> <img src="/7ca6eb2529f04fd343817fb417c57e62/dashboard.png" /> </figure> <h2>The Backstory</h2> <p>It started, as many an app does, with a personal itch to scratch.</p> <p>As 2021 drew to a close, I wanted to focus once again on my fitness. A year and a half of effectively being sedentary hadn’t been great for me, and being the nerd I am, the first step of the journey was an obvious one: I bought my first Apple Watch.</p> <p>I know myself well enough to know that if anything would work, it would be a little gizmo that gave me shiny medals for being active. And it actually did. Within weeks of getting the watch and signing up for Fitness+, I went from working out for about an hour and a half a week (20 minutes a day, on 3-5 days) to over 6 (over an hour on weekdays, 20-45 minutes on weekends).</p> <p>This wasn’t just a brief honeymoon period too. While they fluctuate a bit I have basically been able to maintain those numbers so far, and haven’t missed closing a single ring yet, 558 days in. If you’re starting from nothing and looking for some impetus to get a more active lifestyle, the Apple Watch absolutely works.</p> <p>Things do get a little bit dicey once you get beyond the initial hump though.</p> <h3>Close Your Rings</h3> <p>Easily the most recognisable bit of UI from the Apple Watch are the activity rings. They’re the centrepiece of the fitness functionality of the device, and also a really simple pitch: Meet these 3 daily goals and your watch gives you a little award. Having this consistent target you need to meet every single day helps out quite a lot in the initial stages. Later on though, that uniform goal becomes an issue.</p> <p>The initial goals the watch suggests seem to be fairly modest, or at least they were for me. I was set up with goals of 420 kcal, 15 minutes, and 12 hours for each ring, respectively. Meeting it every single day of the week wasn’t an issue. In fact most days I was going far above it, managing about 700–800 kcal and 60 exercise minutes.</p> <p>This was just <em>most</em> days though. I still stuck to around 500 or so on the weekends. As my stats improved and I was able to put in more time, I also wanted and would benefit more from additional rest days, but that’s just not something that the activity rings are set up to handle. Because of this, my activity rings goals remain the exact same as they were on the first day I got the watch.</p> <p>For a while last year it was impossible for me to regularly make time for the ~60 minute workouts I was used to. I tried to make up for this by doing more HIIT workouts, which would increase my active energy output for the day by a couple hundred calories. Doing this a couple days of the week would just about even out the average energy across the whole week back to my previous levels, but again, the activity rings have no provision for being able to set higher goals for certain days, or any other modifications.</p> <p>Over time, other hard edges started cropping up. There was no way for me to create my own custom goals. If I wanted to burn 4000 calories over the whole week regardless of the distribution, there’s no way to make the Fitness or Health apps track that for me. There’s also no way to make workout specific goals, such as say wanting to run 20 kilometres every week.</p> <p>There’s also the matter of streaks. While they’re great to get started, they create undue pressure to always keep pushing even when you’d rather not, or when you’re not in a position to do so.</p> <p>And it wasn’t just about goals. Trends work entirely differently in the Fitness and Health apps. There’s no simple way to just find aggregate values for certain stats, like say many strength workouts I’ve done this month, or how many steps I’ve walked in the year so far. There are no widgets for anything except the rings. And the list goes on.</p> <p>What initially started out as a bunch of minor annoyances slowly snowballed into enough problems that I just <em>had</em> to solve them, and that’s how Peak was started.</p> <h2>Meet Peak</h2> <p>Peak’s main interface is a customisable dashboard. To get started, pick the metrics you want, select the blocks you want for each of them, and the app shows them in an easy to browse dashboard.</p> <p>Each block represents some insight or way to view your data.</p> <p>The blocks included at launch are:</p> <ul> <li>Recents: Your stats for the last week</li> <li>Charts: Simple Day/Week/Month/Year charts</li> <li>Totals: Aggregate stats for the current and past week, month, and year</li> <li>Trends: See how your progress this week, month, or quarter compares to the last four</li> <li>Overview: This block is only available for workouts. It lets you see all your stats for the current week, month, or year, and quickly filter them down to view the stats for a single workout type</li> <li>And of course, goals</li> </ul> <p>Goals are supercharged in Peak. For starters, you can make any number of goals you want, and for all the metrics you have.</p> <p>Peak includes two flavours of goals:</p> <ul> <li>Show up goals, where you set daily goals and how often you’d like to accomplish them, be it daily, or at least a certain of days every week or month. These are great for building new habits, while still retaining some flexibility by allowing you to have rest days by choosing to only set a target of 5 days a week rather than daily.</li> <li>Build goals, where you set an overall goal for a whole week, month or year. These are recommended for anyone who doesn’t need to build a new habit but is looking to maintain one. Their less strict definition helps you create more flexible targets that you can distribute as you see fit.</li> </ul> <p>Workout goals have another power: You can limit them to certain workout types. This means that you can set specific goals to say go for a swim twice a week, run a 100km every month, or anything else that suits your targets.</p> <p>This extends out to all workout blocks as well, so you can see trends just in your walking workouts or see how many calories you’ve burned just in HIIT sessions.</p> <p>You can pin the blocks you always want to check in on to your homescreen as well, and it’ll show you a compact view with only the most relevant information from each block, and you can always tap through to see them in more detail, with extra stats and interactive charts.</p> <p>And of course, you can configure homescreen and lockscreen widgets for your blocks as well, so you can always stay updated on your progress without even opening the app.</p> <figure class="img-large"> <img src="/28ce49862e61715fc8f7f9f9dcadd5bc/widgets.png" /> </figure> <h2>Design</h2> <p>Peak feels right at home on your iPhone. Familiar, but fresh.</p> <p>An important design goal right from the start of the project was theming. I wanted the app to go beyond light and dark mode themes, or even, or simple customisations such as an accent colour, and offer wholly different, vibrant themes that change the entire aesthetic of the app. There are over 50 themes at launch with a wide variety of styles, and many more planned. And each widget can have its own themes too.</p> <figure class="img-large"> <img src="/2a39a217882ee137e5cfd7f293bd4b20/themes.png" /> </figure> <p>I also sneaked in homages to some iOS behaviours and designs I’ve long appreciated, such as jiggle mode and slide to unlock</p> <figure class="img-small"> <video muted controls> <source src="/f33929f2132a24bd6ad57feb1411a228/jiggle.mp4" type="video/mp4"> </video> </figure> <p>There’s broad support for dynamic type, bold text, voice over, reduce motion, etc. baked right in. If you use any of these assistive technologies and notice something doesn’t quite work right, please <a href="https://mastodon.social/@harshil" title="My Mastodon profile">feel free to contact me</a>.</p> <h2>Privacy</h2> <p>Health data is extremely sensitive information, and right from the start I wanted to make sure that it was handled appropriately, and there’s a fairly simple way to achieve that: Peak includes zero tracking or data collection. The app doesn’t have even any servers.</p> <h2>Pricing</h2> <p>Peak is available as a free download, and includes an optional subscription for Peak Pro.</p> <p>The free version of the app allows you to add up to 4 metrics, up to 5 blocks for each metric, and 5 standard themes.</p> <p>You can upgrade to Peak Pro to unlock:</p> <ul> <li>Unlimited metrics and blocks</li> <li>All 50+ themes</li> <li>Homescreen and lockscreen widgets</li> <li>Custom app icons</li> </ul> <h2>Behind the Scenes</h2> <p>Peak is built with SwiftUI and <a href="https://github.com/pointfreeco/swift-composable-architecture" title="The Composable Architecture on GitHub">The Composable Architecture</a>. My deepest appreciation and thanks go out to everyone involved in making and maintaining them. I intend to write more about the some of the technical aspects of building the app, so watch this space for more soon.</p> <hr> <p>So that’s Peak. It’s your personal fitness dashboard, with a wide range of widgets and themes.</p> <p>You can <a href="https://apps.apple.com/us/app/peak/id6443923491">download it from the App Store for free</a> now.</p> <p>If you run into any issues, have any ideas, or just find it useful, please do let me know!</p><![CDATA[Solving Advent of Code 2022 in Swift]]>https://harshil.net/blog/advent-of-code-2022https://harshil.net/blog/advent-of-code-2022Mon, 26 Dec 2022 12:00:00 GMT<p>It doesn’t feel that long ago that I was writing <a href="https://harshil.net/blog/advent-of-code-2021">my summary for last year’s Advent of Code</a>, and yet, here we are again already.</p> <p>For those unfamiliar, <a href="https://adventofcode.com" title="The Advent of Code website">Advent of Code</a> is an <a href="https://en.wikipedia.org/wiki/Advent_calendar" title="The Wikipedia article for ‘Advent Calendar’">advent calendar</a> where every day from December 1 to Christmas, you get a 2 part programming puzzle to solve. It’s essentially a bunch of Leetcode style interview puzzles, except they’re actually fun to do (usually) and with a competitive element too, as there’s a global leaderboard and you can create your own private ones too.</p> <p>As with every year before, I used Swift. This year I set up my solutions as a Swift package instead of a playground. I didn’t do any setup in advance so I wasn’t able to have a reusable library in place with some common types and data structures as I was hoping to do, but it was still immensely helpful to be able to have third party dependencies including Apple’s <a href="https://github.com/apple/swift-algorithms"><code>swift-algorithms</code></a> and <a href="https://github.com/apple/swift-collections"><code>swift-collections</code></a> packages which are instant imports in much every Swift project I’m working on<sup id="fnref-1"><a href="#fn-1" class="footnote-ref">1</a></sup>.</p> <p>This post goes through some of the trickier puzzles this year. If you’d just like to look at the code, you can find all my solutions on my <a href="https://github.com/HarshilShah/AdventOfCode">GitHub repo for Advent of Code</a>.</p> <h2><a href="https://adventofcode.com/2022/day/16">Day 16</a></h2> <p>I encourage you to read the description of the problem before going ahead (part 1 is open to everyone even without an account, you need to complete it to unlock part 2), but here’s a quick summary of part 1: We have 30 minutes to traverse a graph of pipes, opening them to create the most optimal lava flow. Each move to a neighbour costs a minute, as does opening a valve.</p> <p>Part 1 was fairly simple enough, I just set up a makeshift priority queue (<code>swift-collections</code>, alas, doesn’t ship with one) using a <code>Dictionary&#x3C;Int, Path></code>, where the key was the score for all the currently open valves. In every step I plucked out the path with the highest score, and visited all its neighbours. If the neighbour had a valve with a non-zero flow rate, I would also create another path that spent another minute opening it, and add it to the queue. Once I reached 30 minutes, I’d just update the <code>maxScore</code>.</p> <p>While a bit wasteful, this worked in a pinch and wasn’t particularly slow. Advent of Code however has a knack for punishing you for inefficient yet workable part 1 solutions in part 2, and this was one of those days for me.</p> <p>Part 2 now added a second player, and reduced the time to 26 minutes. While the time reduction certainly helped, the second player greatly expanded the number of possibilities for each step. If each node presented <code>n</code> possible next steps on average in part 1, the presence of two players means that goes up to <code>n<sup>2</sup></code> there. I needed a more efficient solution.</p> <p>Looking at my input was a big clue here. Out of 54 valves, 39 had a flow rate of 0, meaning that opening them wouldn’t add to my score. They only existed as a step on a way to another valve, so I could cut them out entirely.</p> <p>Taking this into account I created a reduced version of my graph with just the 15 valves with a non-zero flow rate. Each step would now take the minimum time for moving between the two valves, which further allowed me to eliminate the extra logic for 2 separate paths for valves with non-zero flow rates; since I could now jump straight to the next valve I wanted to, there was no need for travelling to a valve I didn’t need to open.</p> <p>My solution still isn’t ideal here. While it converges to the correct answer very quickly (~10 seconds), it takes a long time to fully exhaust the whole search space (~15 minutes). I need to improve the heuristic here someday, but for now, it works.</p> <p>You can read <a href="https://github.com/HarshilShah/AdventOfCode/blob/main/2022/Sources/2022/Day16.swift">my full solution here</a>.</p> <h2><a href="https://adventofcode.com/2022/day/19">Day 19</a></h2> <p>This is a problem where I would <em>really</em> recommend that you read the original text, but here’s a summary if you’d rather not: We’re given a bunch of blueprints, which define how we can construct various (ore, clay, obsidian, and geode) rock-extracting robots. Each robot can extract one rock of its type per minute, and requires some combination of the other rocks to build. Starting off with a single ore-collecting robot, and a machine that lets us construct one robot per minute, we have to maximise the number of geodes collected within 24 minutes.</p> <p>Part 2 reduces the blueprints down to 3, but also increases the time limit to 30.</p> <p>Like with day 16, this is not a particularly complex problem to write up a naive implementation for. The devil is in the <del>details</del> search space again.</p> <p>The key insight here is that we need to only maximise the number of geodes, we only really need enough of the rest to construct our geode bots. Consider this sample blueprint:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="" data-index="0"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source">Blueprint 1:</span></span> <span class="grvsc-line"><span class="grvsc-source"> Each ore robot costs 4 ore.</span></span> <span class="grvsc-line"><span class="grvsc-source"> Each clay robot costs 2 ore.</span></span> <span class="grvsc-line"><span class="grvsc-source"> Each obsidian robot costs 3 ore and 14 clay.</span></span> <span class="grvsc-line"><span class="grvsc-source"> Each geode robot costs 2 ore and 7 obsidian.</span></span></code></pre> <p>Remember that we can only construct one robot on any given step, so we can only really use 4 ores in any given step. If we have 4 ore robots, there’s no point in constructing any more of those, because we’re guaranteed to have enough ore to use at every step with just the ones we have already. And if we have more unused ore beyond 4 (while also having 4 bots; it’s possible that one bot created your 4 ores, and so if you use them up, you’ll only start the next step with 1), we can throw away the rest because it’s never gonna be used. This helps us treat two states where they only differ in having say 5 and 50 ores as identical, and thus ignore the one that we come across later.</p> <p>This insight helps us tremendously narrow down our search space, by both restricting growth for bots that we don’t need more of, and also letting us ignore the quantities we don’t really care about.</p> <p>You can find <a href="https://github.com/HarshilShah/AdventOfCode/blob/main/2022/Sources/2022/Day19.swift">my full solution here</a>.</p> <h2><a href="https://adventofcode.com/2022/day/22">Day 22</a></h2> <p>This was this year’s bastard puzzle™. Every once in a while we get a puzzle that can’t really be solved either generally or with just plain code, at least not easily or in a reasonable amount of time. It was <a href="https://harshil.net/blog/advent-of-code-2021">day 24 last year</a>, and this year it was day 22.</p> <p>Part 1 started off fairly simply. We are given a grid of points as an input, with <code>.</code> symbolising empty space and <code>#</code> a wall, and a set of instructions for moving. Starting off at the leftmost pointing, we had to move around the board following the instructions, looping around the board when running out of space, and returning a combination of the final position and direction we were facing in as the output. Fairly straightforward then.</p> <p>Part 2 tweaked the rules a tiny bit. The main goal remained the same, starting off from the same point and following the same instructions and returning the output in the same format. There was just one twist: The grid as it happens was divided into 6 squares of 50×50 points, which could be folded up into a cube. And so what we had to do for part 2 was move across the edge of this cube when running out of space.</p> <p>Here’s what my input looked like, zoomed out to represent each 50×50 point block with a number for which face of the cube it was:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="" data-index="1"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"> 12</span></span> <span class="grvsc-line"><span class="grvsc-source"> 3</span></span> <span class="grvsc-line"><span class="grvsc-source">45</span></span> <span class="grvsc-line"><span class="grvsc-source">6</span></span></code></pre> <p>I spent a solid 30 seconds trying to solve this one programatically but gave up. There was just no way I was gonna be able to generalise the math to figure out how a cube folded, let alone then apply it to the problem at hand.</p> <p>I ended up drawing out the cube on paper with all the connections labelled out, and then manually writing out the transition for each single outer edge. The infuriating thing about this puzzle was that the test input had a shape that didn’t match the puzzle input (which, as far as I can tell, had the same shape for everyone), which meant that there was no way to debug this except stepping through your code line by line.</p> <p>I ended up having a off-by-one error for one single edge transition which cost me a good hour or so, and that I still could only figure out by going through the corresponding transitions generated by a someone else’s code for my input. All in all, this was a hassle. But still, a pretty fun one. At least it didn’t take anywhere near as much time as the other two.</p> <p>You can read <a href="https://github.com/HarshilShah/AdventOfCode/blob/main/2022/Sources/2022/Day22.swift">my full solution here</a>.</p> <hr> <p>As always, I really enjoyed this year’s Advent of Code. The difficulty level seemed a bit up and down, with no real trend day-to-day, which felt a bit strange. Overall I’d say it had some more difficult puzzles than last year did, but wasn’t a lot more difficult in totality.</p> <p>I started off too late this year. In what is fast becoming an annual tradition now I’d even forgotten about day one once again despite setting multiple calendar alerts about it, so my setup wasn’t quite great. I had to repeat lots of bits of code across the project, and didn’t have any time to port over extensive library of extensions and data structures accumulated over the past years. I’m hoping to do all that before next year kicks off, unifying all my solutions into one single SPM package with a module for each year, and have a much more reliable base to start with. I’ve accumulated a pretty large library of extensions and common types across various projects, which I’m hoping to open source separately as well.</p> <style class="grvsc-styles"> .nikso-plus { background-color: #ffffff; color: #24292e; } .nikso-plus .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(0, 0, 0, 0.05)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(0, 0, 0, 0.2)); } /* Night Owl (No Italics) */ @media (prefers-color-scheme: dark) { .grvsc-mm-tGNH8N { background-color: #011627; color: #d6deeb; } .grvsc-mm-tGNH8N .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); } } </style> <div class="footnotes"> <hr> <ol> <li id="fn-1">My other instant import packages, though ones not particularly relevant for Advent of Code, are <a href="https://github.com/apple/swift-async-algorithms"><code>swift-async-algorithms</code></a> and <a href="https://github.com/pointfreeco/swift-tagged"><code>swift-tagged</code></a>.<a href="#fnref-1" class="footnote-backref">↩</a></li> </ol> </div><![CDATA[Adventures in Orienting Views in SwiftUI]]>https://harshil.net/blog/swiftui-rotationeffect-is-kinda-funkyhttps://harshil.net/blog/swiftui-rotationeffect-is-kinda-funkyWed, 21 Dec 2022 16:30:00 GMT<p>I like to break down my work on side projects into two phases. Once I’ve decided what features I want to focus on for the next few weeks, first I’ll just hammer away at the concept, making sure all the infrastructure is in place and solid albeit rendered in a sketchy interface just to make sure it all works, and then I like to take a week or two after that to polish and buff it out, making sure it’s pleasant to use as well.</p> <p>I’ve been on an extended version of one of the latter stages recently, adding a bunch of much missed niceties to an app I’m hoping to release early next year, when I ran into some weird behaviour.</p> <p>One of the little touches I was adding called for adding a little rotation to some views, sometimes. Nothing much, just a single degree’s worth<sup id="fnref-1"><a href="#fn-1" class="footnote-ref">1</a></sup>. But I noticed something weird seemed to be happening to the layouts when I did so. It’s a bit tricky to describe, so let me save a couple thousand words and show you an example instead.</p> <p>One of the views whose layout was breaking is a custom <code>PhotosStylePicker</code> view. As the name suggests, it’s a Picker that attempts to replicate the look and feel of the one at the bottom of the Library tab in the Photos app. Here’s what it looks like:</p> <figure class="img-inline"> <picture> <source srcset="/00d67a7ca95b279d66e594b83946839d/picker-dark.png" media="(prefers-color-scheme: dark)" /> <img src="/95938fd20dfea29dc7d9f27b9b7def29/picker.png" /> </picture> <figcaption> PhotosStylePicker in action </figcaption> </figure> <p>And now here’s what happenened when I applied a 10 degree rotation to it:</p> <figure class="img-inline"> <picture> <source srcset="/14360e759e8d2bda839ebfce36911aec/picker-borked-dark.png" media="(prefers-color-scheme: dark)" /> <img src="/abfeee3f6db2b1334d9e8468605f6419/picker-borked.png" /> </picture> <figcaption> PhotosStylePicker, rotated by 10 degrees </figcaption> </figure> <p>Not ideal. So what’s going on? Why is just the highlight broken there, and so strangely at that? I’ve posted the <a href="https://gist.github.com/HarshilShah/e997f321a747fb79a492e6cce5f4a6c5">code for the whole view on GitHub</a>, but here are the relevant bits.</p> <pre class="grvsc-container grvsc-has-line-highlighting nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="0"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">// PhotosPickerStyle.body</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">HStack</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">spacing</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">ForEach</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">items, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">id</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: \.</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">self</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { item </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">in</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Button</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">action</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: { selectedItem </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> item }</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Text</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">item.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">title</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">capitalized</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">font</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">callout</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">weight</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">semibold</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">))</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">lineLimit</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">1</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">foregroundStyle</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">foregroundStyle</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">for</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: item</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">))</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">environment</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">\.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">colorScheme</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">colorScheme</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">for</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: item</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">))</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">padding</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">horizontal</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">padding</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">vertical</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">5</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">frame</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">maxWidth</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-7">infinity</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">buttonStyle</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">plain</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line grvsc-line-highlighted"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">anchorPreference</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">key</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: ItemFramesPreferenceKey.</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">self</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">value</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: .</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">bounds</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line grvsc-line-highlighted"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> [</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">ItemFrame</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">itemTitle</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: item.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">title</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">anchorBounds</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">]</span></span></span> <span class="grvsc-line grvsc-line-highlighted"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span> <span class="grvsc-line grvsc-line-highlighted"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">backgroundPreferenceValue</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">ItemFramesPreferenceKey.</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">self</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { preferences </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">in</span></span></span> <span class="grvsc-line grvsc-line-highlighted"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> GeometryReader { proxy </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">in</span></span></span> <span class="grvsc-line grvsc-line-highlighted"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">if</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> anchorBounds </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> preferences.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">first</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">where</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: { </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">itemTitle</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">==</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> selectedItem.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">title</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> }</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">?</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">anchorBounds</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line grvsc-line-highlighted"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> bounds </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> proxy[anchorBounds]</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Capsule</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">()</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">fill</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">thinMaterial</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">opacity</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">0.5</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">))</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">environment</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">\.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">colorScheme</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, colorScheme.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">dual</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">frame</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">width</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: bounds.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">width</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">height</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: bounds.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">height</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">offset</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: bounds.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">minX</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: bounds.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">minY</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">animation</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">interactiveSpring</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">()</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">value</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: selectedItem</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p>A quick summary of what’s going on here: I’m rendering an <code>HStack</code> with a <code>Button</code> for every item, using an <code>anchorPreference</code> to collect their frames, and then using a <code>backgroundPreferenceValue</code> modifier to display a <code>Capsule</code> matching the frame of the current selection.</p> <p>Why go through all this trouble instead of just showing the capsule as a background for the selected button directly? Because this allows us to animate the position of the capsule as the selection changes, moving from the previous to the current selection instead of disappearing in one place and appearing at the other.</p> <p>There isn’t much there to debug. First, I’m looking for the <code>anchorBounds</code> for the current selection from the preferences handed to me, converting it to a <code>bounds</code> CGRect using a GeometryProxy, and using that to set the frame of my selection capsule.</p> <p>The anchor bounds are of the <code>Anchor</code> type, which is an opaque representation of a view’s frame that requires going through a GeometryProxy to obtain the frame relative to any given context. My code isn’t doing anything too complex with the frame which suggests that something in the conversion using the GeometryProxy has gone awry.</p> <p>There isn’t much info to go by here in the documentation. GeometryProxy has very minimal API, two properties for the size and safeAreaInsets, the aforementioned anchor subscript, and a <code>frame(in:)</code> function, which lets you calculate the frame of the GeometryReader in any coordinate space. It’s likely that the <code>frame(in:)</code> function and anchor conversion both use the same mechanism under the hood too.</p> <p>To test an assumption I <a href="https://gist.github.com/HarshilShah/14bceb365b5e00f6c7f10a6eeb2f06b0">set up a little Playground</a>. In it, I’m showing a GeometryReader on screen, with a fixed size of 400 points, and using the <code>frame(in:)</code> method to print a frame in both the local and global coordinate spaces.</p> <figure class="img-small"> <picture> <source srcset="/4a5fa605b228e178fe13f8dbd9878ef7/playground-dark.png" media="(prefers-color-scheme: dark)" /> <img src="/baedfbe2fa714c573f527a427309680b/playground.png" /> </picture> <figcaption> Playground with no rotation </figcaption> </figure> <p>Initially, it starts off showing showing a square 400 point frame. As you apply rotation however, things start to change:</p> <figure class="img-small"> <picture> <source srcset="/889da11771ec79aded08a599da7482c6/playground-rotated-dark.png" media="(prefers-color-scheme: dark)" /> <img src="/48ec5aa21d6f1cd065e87d614b55c930/playground-rotated.png" /> </picture> <figcaption> Playground with a 10 degree rotation applied </figcaption> </figure> <p>As you can see, the global frame now no longer matches the local frame, but instead reads 463 points. That’s not the size of the square, but as some basic maths confirms<sup id="fnref-2"><a href="#fn-2" class="footnote-ref">2</a></sup>, it’s the size of the bounding box of the transformed square. This lines up with what we were seeing above for the selection capsule as well, which was being rendered with a larger frame on applying a rotation.</p> <p>So for whatever reason, applying a rotation seems to change how the GeometryProxy converts frames across coordinate spaces. This is a really strange behaviour, especially if you, like me, have come from the UIKit world where transforms do not affect layout<sup id="fnref-3"><a href="#fn-3" class="footnote-ref">3</a></sup>.</p> <p>But the good thing is that since we know the bug, we know how we can work around it.</p> <h2>The Workaround</h2> <p>Since the bug happens when converting between the untransformed global and the rotated local coordinate spaces, we can work around this by just relying on a coordinate space that gets rotated whenever our view does.</p> <p>The local coordinate space is far too narrow as it only computes frames with respect to the view it’s used in, and thus the origin will always be zero. But SwiftUI does let us construct our own coordinate spaces too, and so we can construct one that’s local to our view, ensuring that any rotations also transform our custom coordinate space, which means that for the sake of our frame calculations, there is no transform applied.</p> <p>Here’s what the picker code looks like now:</p> <pre class="grvsc-container grvsc-has-line-highlighting nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="1"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">// PhotosStylePicker.body</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">HStack</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">spacing</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">ForEach</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">items, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">id</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: \.</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">self</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { item </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">in</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Button</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">action</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: { selectedItem </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> item }</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Text</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">item.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">title</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">capitalized</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">font</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">callout</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">weight</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">semibold</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">))</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">lineLimit</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">1</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">foregroundStyle</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">foregroundStyle</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">for</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: item</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">))</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">environment</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">\.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">colorScheme</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">colorScheme</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">for</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: item</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">))</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">padding</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">horizontal</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">padding</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">vertical</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">5</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">frame</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">maxWidth</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-7">infinity</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">buttonStyle</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">plain</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line grvsc-line-highlighted"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">onFrameChange</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">in</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">named</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">coordinateSpaceName</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">))</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line grvsc-line-highlighted"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> frames[item] </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">$0</span></span></span> <span class="grvsc-line grvsc-line-highlighted"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">background</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line grvsc-line-highlighted"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">if</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> frame </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> frames[selectedItem] {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Capsule</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">()</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">fill</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">thinMaterial</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">opacity</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">0.5</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">))</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">environment</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">\.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">colorScheme</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, colorScheme.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">dual</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">frame</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">width</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: frame.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-7">size</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">width</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">height</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: frame.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-7">size</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">height</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">position</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">CGPoint</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: frame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">midX</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: frame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">midY</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">))</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">animation</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">interactiveSpring</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">()</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">value</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: selectedItem</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span> <span class="grvsc-line grvsc-line-highlighted"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">coordinateSpace</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">name</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: coordinateSpaceName</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span></code></pre> <p>We can’t use anchor preferences anymore because it seems to always rely on the global coordinate space for conversions, but we can use regular GeometryReaders with the <code>frame(in:)</code> method. The <code>onFrameChange(in:perform)</code> there is a custom modifier I cooked up that just uses a GeometryReader under the hood to notify us whenever a view’s frame changes with respect to any coordinate space. Here’s what that looks like:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="2"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">struct</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">FrameKey</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: PreferenceKey {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">static</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">var</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> defaultValue</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> CGRect { .</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">zero</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">static</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">func</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">reduce</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">value</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">inout</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> CGRect, </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">nextValue</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: () </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> CGRect</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> value </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">nextValue</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">()</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span> <span class="grvsc-line"><span class="grvsc-source"></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">public</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">extension</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">View</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">func</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">onFrameChange</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">(</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">in</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-14 grvsc-tGNH8N-9">coordinateSpace</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: CoordinateSpace,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">perform</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-14 grvsc-tGNH8N-9">onChange</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">@escaping</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> (CGRect) </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> ()</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> some View {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> overlay {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> GeometryReader { proxy </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">in</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> Color.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">clear</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">preference</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">key</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: FrameKey.</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">self</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">value</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: proxy.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">frame</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">in</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: coordinateSpace</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">onPreferenceChange</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">FrameKey.</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">self</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">perform</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: onChange</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p>And here’s what the picker itself looks like, with a rotation applied.</p> <figure class="img-inline"> <picture> <source srcset="/980b5a5061b3f1fb75e7ab06440d8fd8/picker-fixed-dark.png" media="(prefers-color-scheme: dark)" /> <img src="/fbbaac3d557292d3c9e7bffa316a9c4b/picker-fixed.png" /> </picture> <figcaption> PhotosStylePicker, correctly rotated by 10 degrees </figcaption> </figure> <p>Problem solved, day saved, crises averted, right? Well, not so fast.</p> <h2>Turns Out…</h2> <p>While we can use avoid anchor preferences and the global coordinate space to avoid this rotation effect bug/behaviour/feature in our code, we can’t really do much about it for views we import.</p> <p>One such example I mentioned above was Swift Charts. It seems to be using anchor preferences to orient its axis marks, which get a similar wonky layout.</p> <p>As I was wondering how I could possibly work around this, even considering the possibility of rewriting a significant chunk of the app that relies quite heavily on Swift Charts, I got a <a href="https://mastodon.social/@erichoracek/109507057035893314">reply on Mastodon from Erik Horacek</a>:</p> <blockquote> <p>@harshil try calling modifier(<wbr>_RotationEffect(angle:)<wbr>.ignoredByLayout()), _RotationEffect is a frozen public type so you should be fine to use it!</p> </blockquote> <p>I tried that, and it actually fixes the bug. My original code, Swift Charts, etc. all render completely fine now.</p> <p>I didn’t mention it so far but I was also seeing a similar wonky layout from applying a <code>scaleEffect</code>. That too has an equivalent <code>_ScaleEffect(scale:).ignoredByLayout()</code> modifier that keeps layout working as expected.</p> <p>For those afraid about using an underscored property, note that they are frozen and thus reliable, and <code>ignoredByLayout()</code> is public API too. You can even wrap it up into a custom modifier if you don’t want to be using underscored code that doesn’t show up in autocomplete, you can package it up into a custom modifier like so:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="3"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">extension</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">View</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">func</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">rotationEffectIgnoringLayout</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">angle</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: Angle, </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">anchor</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: UnitPoint </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">center</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> some View {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">self</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">modifier</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">_RotationEffect</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">angle</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: angle, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">anchor</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: anchor</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">ignoredByLayout</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">())</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p>I’m was originally still a bit wary of <code>anchorPreferences</code> when I first stumbled across this behaviour, but I haven’t found any other issues apart from these two modifiers. If you’re vending components built with it though, either internally or externally, it might be worth adding a note about this behaviour and the modifier above in case your clients run into similar issues.</p> <p>I’m not quite sure why transforms affecting layout is the default behaviour, or why the default modifier doesn’t have a way to opt out of it, but the workarounds are all build using API available all the way since the introduction of SwiftUI since iOS 13, so they feel reliable enough for my use.</p> <p>I’ve filed <del>radar</del> feedback FB11882239 regarding this issue, so here’s hoping we get some better public APIs for this issue in the future.</p> <style class="grvsc-styles"> .nikso-plus { background-color: #ffffff; color: #24292e; } .nikso-plus .grvsc-ttxYAU-3 { color: #6A737D; } .nikso-plus .grvsc-ttxYAU-5 { color: #005CC5; } .nikso-plus .grvsc-ttxYAU-4 { color: #6F42C1; } .nikso-plus .grvsc-ttxYAU-1 { color: #24292EFF; } .nikso-plus .grvsc-ttxYAU-7 { color: #D73A49; } .nikso-plus .grvsc-ttxYAU-14 { color: #E27F2D; } .nikso-plus .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(0, 0, 0, 0.05)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(0, 0, 0, 0.2)); } /* Night Owl (No Italics) */ @media (prefers-color-scheme: dark) { .grvsc-mm-tGNH8N { background-color: #011627; color: #d6deeb; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-6 { color: #637777; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-5 { color: #C5E478; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-1 { color: #D6DEEB; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-7 { color: #82AAFF; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-8 { color: #F78C6C; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-9 { color: #7FDBCA; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-3 { color: #C792EA; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-37 { color: #D9F5DD; } .grvsc-mm-tGNH8N .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); } } </style> <div class="footnotes"> <hr> <ol> <li id="fn-1">What sort of effect can one achieve with just a single degree of rotation? That is left as an exercise to the reader.<a href="#fnref-1" class="footnote-backref">↩</a></li> <li id="fn-2">400 * (cos(10) + sin(10))<a href="#fnref-2" class="footnote-backref">↩</a></li> <li id="fn-3">Well... except in the case of safe area that is. For some reason, if you apply a transform on a view, its <code>safeAreaInsets</code> are recalculated with respect to its final presented frame. You can work around this by setting the transform for its layer instead.<a href="#fnref-3" class="footnote-backref">↩</a></li> </ol> </div><![CDATA[Adventures in Orienting Images on iOS]]>https://harshil.net/blog/image-orientationhttps://harshil.net/blog/image-orientationThu, 03 Feb 2022 15:30:00 GMT<p>A great thing about working with software is that despite it being a relatively young industry, there’s a staggering number of near-perfect abstractions we get to build upon. If you want to open a link in your app, you don’t need to know much about the intricacies of rendering engines, TCP/IP, and so on, let alone lower level frameworks, assembly code, or how a CPU works. Regardless of your platform and framework choices there’s likely something readymade that you can just drop in, wire up with a few lines, and forget about for the most part.</p> <p>Every once in a while though, something happens that makes you open up a previously perfectly opaque component to figure out exactly what the hell is going on in there, and this is a post about one of those instances.</p> <h2>I just wanted to flip an image</h2> <p>I’ve been working on a little project that involves some photo manipulation. Sometimes, I need to horizontally flip an image. I went through the documentation and turns out <code>UIImage</code> has a handy built-in method to do just that: <a href="https://developer.apple.com/documentation/uikit/uiimage/2113668-withhorizontallyflippedorientati" title="Apple’s documentation for UIImage.withHorizontallyFlippedOrientation()"><code>withHorizontallyFlippedOrientation()</code></a>.</p> <p>I put it into use and it seemed to be working alright. I tested it with some images from my library and it all seemed to work just fine. Except then I got to some images and it didn’t flip them horizontally, but vertically instead. This was really strange. But thankfully, it also always incorrectly rotated the same images, and in the same way, so I had a whole bunch of reproducible test cases to dig into.</p> <p>Here’s what the documentation for the method says:</p> <blockquote> <p>The returned image's <code>imageOrientation</code> property contains the mirrored version of the original image's orientation. For example, if the original orientation is <code>UIImage.Orientation.left</code>, the new orientation is <code>UIImage.Orientation.leftMirrored</code>.</p> </blockquote> <p>That all seems right, nothing seems to be amiss at a glance. What exactly is the <code>imageOrientation</code> property though?</p> <h2>Decoding Images</h2> <p>Most image formats contain a package of Exif metadata alongside the image data. Exif data can contain information such as the make of the camera, the capture settings, the time, location, and so on, and also an orientation. These orientations specify the direction an image is taken in, and whether it has been mirrored.</p> <p>The orientation property exists to make it possible to rotate or mirror an image without having to decode and then re-encode all of its data with respect to the new orientation, which can potentially be a lossy operation too</p> <p>You can read the orientation of a <code>UIImage</code> through the <code>imageOrientation</code> property. It has eight potential values: up, down, left, right, and the mirrored versions of all of them. It’s important here to take a second and look at what any specific orientation actually means. Here’s a snippet from Apple’s documentation for <a href="https://developer.apple.com/documentation/uikit/uiimage/orientation" title="Apple’s documentation for UIImage.Orientation"><code>UIImage.Orientation</code></a> (emphasis added):</p> <blockquote> <p>The UIImage class automatically handles the transform necessary to present an image in the correct display orientation according to its orientation metadata, so an image object's imageOrientation property simply <strong>indicates which transform was applied</strong>.</p> </blockquote> <p>An <code>imageOrientation</code> of left, then, means that the image data has been rotated leftwards i.e. anticlockwise by 90 degrees to display it.</p> <p>Back to <code>UIImage.withHorizontallyFlippedOrientation()</code> then. I started looking at these orientation values as I rotated images this time, and a pattern cropped up. All of the images that flipped correctly had an orientation with the rawValue of 0 (meaning <code>up</code>) to begin with, and ended up with 4 (<code>upMirrored</code>). Meanwhile the ones that ended up flipped vertically started off with 3 (<code>right</code>) and ended up with an orientation of 7 (<code>rightMirrored</code>).</p> <p>This lines up with the documentation for the method quoted earlier, the method seems to work as described. So what’s the issue here? Turns out, mirroring doesn’t quite do what you’d think it does.</p> <h2>Mirrored Orientations</h2> <p>First, let’s take a look at the documentation for the <a href="https://developer.apple.com/documentation/uikit/uiimage/orientation/right" title="Apple’s documentation for UIImage.Orientation.right"><code>right</code> orientation</a>:</p> <blockquote> <p>If an image is encoded with this orientation, then displayed by software unaware of orientation metadata, the image appears to be rotated 90° counter-clockwise. (That is, to present the image in its intended orientation, <strong>you must rotate it 90° clockwise</strong>.)</p> </blockquote> <p>An image with a <code>right</code> orientation has been rotated rightwards by 90 degrees to display it, which lines up with the documentation we’ve just read.</p> <p>And then let’s take a look at the documentation for the <a href="https://developer.apple.com/documentation/uikit/uiimage/orientation/rightmirrored" title="Apple’s documentation for UIImage.Orientation.rightMirrored"><code>rightMirrored</code> orientation</a>:</p> <blockquote> <p>If an image is encoded with this orientation, then displayed by software unaware of orientation metadata, the image appears to be horizontally mirrored, then rotated 90° clockwise. (That is, to present the image in its intended orientation, <strong>you can rotate 90° counter-clockwise, then flip horizontally</strong>.)</p> </blockquote> <p>Did you catch that? A <code>rightMirrored</code> image is rotated <em>left</em>, and not right, by 90 degrees and then mirrored horizontally.</p> <p>Working this out a bit, this transformation amounts to a vertical mirroring. You can see it in action below. This image with an original rightward orientation is flipped vertically instead of horizontally.</p> <figure class="img-medium"> <picture> <source srcset="/7699b6822750851c082269cb0b0775e0/wrong-dark.png" media="(prefers-color-scheme: dark)" /> <img src="/600fa98e07bf813cde05a11a768cf2df/wrong.png" /> </picture> <figcaption> Notice how the arrow points in the opposite direction, but the text, while also flipped vertically, remains left to right. </figcaption> </figure> <p>If you rejigger the operations a bit to perform the mirroring first though, it works out to the same as mirroring horizontally first and then rotating rightwards. I suppose that might justify calling it the <code>rightMirrored</code> orientation, considering both of those operations do take place, though you’d be forgiven for expecting that what should happen is rotating rightwards to obtain the original image, followed by the horizontal mirroring.</p> <p>It’s clear, then, that the <code>rightMirrored</code> orientation is not the horizontally flipped dual for <code>right</code>. So what is? <code>leftMirrored</code>, as it happens. From <a href="https://developer.apple.com/documentation/uikit/uiimage/orientation/leftmirrored" title="Apple’s documentation for UIImage.Orientation.leftMirrored">Apple’s documentation</a>:</p> <blockquote> <p>If an image is encoded with this orientation, then displayed by software unaware of orientation metadata, the image appears to be horizontally mirrored, then rotated 90° counter-clockwise. (That is, to present the image in its intended orientation, <strong>you can rotate it 90° clockwise, then flip horizontally</strong>.)</p> </blockquote> <h2>Correctly Flipping Images</h2> <p>Armed with this knowledge, we can write our own function to horizontally rotate images. Here’s what that looks like:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="0"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">extension</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">UIImage</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">.Orientation {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">var</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> horizontallyFlipped</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> UIImage.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">Orientation</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">switch</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">self</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">case</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-1">up</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">upMirrored</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">case</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">upMirrored</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-1">up</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">case</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-1">down</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">downMirrored</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">case</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">downMirrored</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-1">down</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">case</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">left</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">rightMirrored</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">case</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">rightMirrored</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">left</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">case</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">right</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">leftMirrored</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">case</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">leftMirrored</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">right</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">@unknown</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">default:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">self</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span> <span class="grvsc-line"><span class="grvsc-source"></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">extension</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">UIImage</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">func</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">flippedHorizontally</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">()</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> UIImage {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">if</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> cgImage </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">self</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">cgImage</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">UIImage</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">cgImage</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: cgImage, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">scale</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: scale, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">orientation</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: imageOrientation.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">horizontallyFlipped</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> format </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">UIGraphicsImageRendererFormat</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">()</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> format.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">scale</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> scale</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">UIGraphicsImageRenderer</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">size</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: size, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">format</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: format</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-1">image</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { context </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">in</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> context.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">cgContext</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">concatenate</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">CGAffineTransform</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">scaleX</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">-1</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">1</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">))</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">self</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">draw</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">at</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">CGPoint</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">size.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">width</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">))</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p>We now know the correct duals of each of the orientations, and so can correctly flip them. <code>up</code> and <code>down</code> are rotated by 0 and 180 degrees, respectively, so the order of operations doesn’t matter for those and the method works as expected.</p> <p>If we can’t retrieve a CGImage because the UIImage isn’t backed by one, we can spin up a <code>UIGraphicsImageRenderer</code>. The graphics renderer itself handles the orientation correctly and draws images in the correct way by default, and so we can flip its context horizontally to get the results we want<sup id="fnref-1"><a href="#fn-1" class="footnote-ref">1</a></sup>.</p> <p>Put this all together, and we now have horizontal image flipping, now working correctly for all orientations:</p> <figure class="img-medium"> <picture> <source srcset="/083262d5adfe0f9e1d03c674151dd66c/right-dark.png" media="(prefers-color-scheme: dark)"> <img src="/2c1d5a7dc5c9240676352c1ac1239bb9/right.png" /> </picture> </figure> <hr> <p>I’ve now figured out why the UIImage flipping method was bugging out and have a working method of my own, but I remain curious as to why the naming was set up that way.</p> <p>Based on my little research it seems that the Exif format doesn’t really have any fixed names for each of the 8 orientations, nor do there seem to be any unofficial or de facto standards, so I’m not quite sure why these particular ones were picked.</p> <p>I initially thought the discrepancy might have to do with Core Graphics’s inverted coordinate system with respect to UIKit, and some misunderstanding of the relation between UIImage.Orientation and CGImagePropertyOrientation on my part. But <a href="https://developer.apple.com/documentation/imageio/cgimagepropertyorientation" title="Apple’s documentation for CGImagePropertyOrientation">Apple’s documentation for the latter</a> says that the cases are supposed to line up, so I’m not quite sure what’s going on. Am I missing something clear as day to people with more experience in graphics/photography? If so, please <a href="https://twitter.com/harshil" title="Send me a tweet! I’m @harshil on Twitter">do let me know</a>!</p> <p>While I remain unsure about the origins of the naming for the orientation cases, I do think that the current implementation of the UIImage method is a bug. I’ve filed FB9877366 about this.</p> <p>Also, my original intention was to demonstrate this bug using Xcode Playgrounds’s inline previews, but turns out those ignore the orientation entirely. I’ve filed FB9877374 about that.</p> <figure class="img-inline"> <picture> <source srcset="/bf90eeeacf18e3575e6925cfb3682640/playgrounds-dark.png" media="(prefers-color-scheme: dark)"> <img src="/41f9f9e71fe4c1ff8bd884ab34ffbaea/playgrounds.png" /> </picture> </figure> <style class="grvsc-styles"> .nikso-plus { background-color: #ffffff; color: #24292e; } .nikso-plus .grvsc-ttxYAU-7 { color: #D73A49; } .nikso-plus .grvsc-ttxYAU-1 { color: #24292EFF; } .nikso-plus .grvsc-ttxYAU-4 { color: #6F42C1; } .nikso-plus .grvsc-ttxYAU-5 { color: #005CC5; } .nikso-plus .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(0, 0, 0, 0.05)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(0, 0, 0, 0.2)); } /* Night Owl (No Italics) */ @media (prefers-color-scheme: dark) { .grvsc-mm-tGNH8N { background-color: #011627; color: #d6deeb; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-3 { color: #C792EA; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-1 { color: #D6DEEB; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-5 { color: #C5E478; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-9 { color: #7FDBCA; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-7 { color: #82AAFF; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-37 { color: #D9F5DD; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-8 { color: #F78C6C; } .grvsc-mm-tGNH8N .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); } } </style> <div class="footnotes"> <hr> <ol> <li id="fn-1">I’m not entirely sure if this preserves the colour space and of the original image though, so beware of that when using this approach.<a href="#fnref-1" class="footnote-backref">↩</a></li> </ol> </div><![CDATA[Solving Advent of Code 2021 in Swift]]>https://harshil.net/blog/advent-of-code-2021https://harshil.net/blog/advent-of-code-2021Tue, 28 Dec 2021 12:30:00 GMT<p>Every year since 2018, I’ve been taking part in <a href="https://adventofcode.com" title="The Advent of Code website">Advent of Code</a>. For those unfamiliar, it's an <a href="https://en.wikipedia.org/wiki/Advent_calendar" title="The Wikipedia article for ‘Advent Calendar’">advent calendar</a> where every day from December 1 to Christmas, instead of a tiny gift, you get to solve a little 2 part programming puzzle. It’s essentially a bunch of Leetcode style interview puzzles, except they’re actually fun to do (usually) and with a competitive element too, as there’s a global leaderboard and you can create your own private ones too.</p> <p>As with every year before, I used Swift, and this post goes through some of the trickier puzzles this year. If you’d just like to look at the code, you can find all my solutions on my <a href="https://github.com/HarshilShah/AdventOfCode">GitHub repo for Advent of Code</a>.</p> <h3><a href="https://adventofcode.com/2021/day/18">Day 18</a></h3> <p>This was the first genuinely tricky problem this year, and the first that took me over an hour to solve. I encourage you to read the description of the problem before going ahead (part 1 is open to everyone even without an account, you need to complete it to unlock part 2).</p> <p>A quick summary of the task at hand, minus the puzzle specific language: We need to construct a binary tree from the input and repeatedly traverse it in order until no nodes have a depth greater than 4 (we achieve this by splitting the combined value of such nodes across the in order predecessor and successor) and no nodes have a value greater than 10 (we achieve this by splitting the value into two nodes). There are some more specifics here but this is the core of the problem.</p> <p>This seems fairly straightforward, except there’s just one tiny issue: Swift doesn’t come equipped with any tree types. The Swift standard library includes a lot of types and protocols for data structures, with even more potentially coming soon via the first-party <a href="https://github.com/apple/swift-collections">Swift Collections</a> package, which exists as a sort of staging area to refine new types until can be considered solid enough to move them to the standard library, but even there, trees, graphs, linked lists, etc. (basically anything that might use a reference type) aren’t included.</p> <p>I purposely limit myself to using just the standard library when solving Advent of Code puzzles and also try to avoid referencing any other material, which meant I had to implement the tree type myself. Doing that and traversing it in order was straightforward enough, but finding the in order successor and processor was a bit tricker. But given the challenge at hand necessitated a small enough tree (The maximum depth allowed was 5 nodes, which meant an upper bound of 63 nodes; practically it rarely crossed half that) I ended up just storing the results of in order traversal in an array and navigating through that, which ended up being performant enough.</p> <p>Part 2 was fairly trivial thereafter, you can read <a href="https://github.com/HarshilShah/AdventOfCode/blob/main/2021.playground/Pages/Day%2018.xcplaygroundpage/Contents.swift">my full solution here</a>.</p> <h2><a href="https://adventofcode.com/2021/day/19">Day 19</a></h2> <p>This was probably my favourite puzzle of the year. I’d encourage you to read the original text, but here’s a summary: We’re given a list of point clouds, with each point’s position being defined with respect to the cloud’s centre. Each cloud overlaps with at least one other, and at least 12 points make up the overlap. Our job is to work out the overlaps, and figure out the total number of unique points across all clouds. Adding to the complexity, another issue is we don’t know the orientation of the clouds; one cloud’s x-axis could be another cloud’s negative y-axis, for example.</p> <p>Ignoring the issue of rotations for a second, a naive approach here is to shift each point cloud along each axis until we manage to overlap 12 points. This might work but given the size of the problem (we’re told the clouds are up to 2000 points wide in each direction), it seems horribly inefficient. We can avoid this however by removing the absolute centre of each cloud out of the equation entirely, by instead considering the relative positions of the points with respect to each other.</p> <p>For a cloud of <code>n</code> points, we have a total of <code>n * (n - 1)</code> connections between the points. Since the overlapping subcloud has at least 12 points, this amounts to at least 12 * 11 = 132 overlapping connections, which we can check for while ignoring the absolute positions entirely. We can then find the actual overlapping points by seeing which ones contribute at least 11 connections to the overlap, and then find the positions of the centres by comparing the average magnitude of these sets of points.</p> <p>As for the rotations, I couldn’t really find a straightforward way to compare these. I haven’t done much work in 3D yet, and couldn’t find anything about them in the standard library either, so I had to resort to manually constructing all possible orientations of the clouds by rotating every single point within them.</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="0"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> rotations</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> [(Point) </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> Point] </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> [</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Point</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> },</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Point</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> },</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Point</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> },</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Point</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> },</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Point</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> },</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Point</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> },</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Point</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> },</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Point</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> },</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Point</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> },</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Point</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> },</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Point</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> },</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Point</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> },</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Point</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> },</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Point</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> },</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Point</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> },</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Point</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> },</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Point</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> },</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Point</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> },</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Point</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> },</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Point</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> },</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Point</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> },</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Point</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> },</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Point</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> },</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Point</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">z</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> },</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">]</span></span></span></code></pre> <p>You can find <a href="https://github.com/HarshilShah/AdventOfCode/blob/main/2021.playground/Pages/Day%2019.xcplaygroundpage/Contents.swift">my full solution here</a>.</p> <h2><a href="https://adventofcode.com/2021/day/22">Day 22</a></h2> <p>A thing you learn quickly when you start doing mobile development is that the O(<em>n</em>) complexity of certain algorithms matters significantly less at the tiny values of <em>n</em> youʼre usually dealing with. This problem, though, was one where <em>n</em> was large enough to matter.</p> <p>We’re given a list of cuboids and whether they should turn on or off all the points within them. Parts 1 and 2 are identical, except part 1 limits the size of the active area to 100 points along each axis, and part 2 is unbounded. Part 1 thus could be solved with a naive implementation looking at all the points in a cuboid with a maximum area of about a million points<sup id="fnref-1"><a href="#fn-1" class="footnote-ref">1</a></sup>, but part 2 goes well beyond a quadrillion points, so the approach needs to be a bit more considered.</p> <p>The way I went about this was subdividing each cuboid. Each new cuboid would be compared with the already processed ones to check for any overlaps. Two cubes overlap if their x, y, and z values all overlap. Next, the split. This is the core logic of the puzzle, and works out like so:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="1"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">extension</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">ClosedRange</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">where</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> Bound == Int {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">func</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">split</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">over</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-14 grvsc-tGNH8N-9">other</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">Self</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Set</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">&lt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">Self</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">&gt; {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">if</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> other.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">isSuperset</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">of</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">self</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> [</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">self</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">]</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> } </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">else</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">if</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> lowerBound </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">&lt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> other.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">lowerBound</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">, upperBound </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> other.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">upperBound</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> [</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> (lowerBound </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">...</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> other.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">lowerBound</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">1</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">),</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> other,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> (other.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">upperBound</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">+</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">1</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">...</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> upperBound)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> ]</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> } </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">else</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">if</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> other.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">lowerBound</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">&lt;=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> lowerBound {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> [(lowerBound </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">...</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> other.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">upperBound</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">), (other.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">upperBound</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">+</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">1</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">...</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> upperBound)]</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> } </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">else</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> [(lowerBound </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">...</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> other.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">lowerBound</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">1</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">), (other.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">lowerBound</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">...</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> upperBound)]</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p>Essentially we want to split each axis of the existing cuboid (<code>self</code>) into sub-axes that are either entirely within or entirely outside the new cuboid’s axis (<code>other</code>). This ensures that when we glue back the new axes, we can ignore the ones that are entirely contained within the new cuboid, thus only counting those overlapping points once or not at all, by either including or removing the new cuboid, respectively.</p> <p>You can read <a href="https://github.com/HarshilShah/AdventOfCode/blob/main/2021.playground/Pages/Day%2022.xcplaygroundpage/Contents.swift">my full solution here</a>.</p> <h2><a href="https://adventofcode.com/2021/day/24">Day 24</a></h2> <p>This was, by a margin, the toughest problem this year.</p> <p>We’re given a program for an ALU with 4 integer registers (w, x, y, and z) and 6 operations (assignment, addition, multiplication, division, modulus, and an equality check). The program requires an input of 14 single-digit integers between 1 and 9 to execute, and our task is to find the maximum (part 1) and minimum (part 2) input values that result in the value of z to be 0.</p> <p>A brute force solution is fairly easy to construct, except it’s also horribly inefficient, as the total search space is 9^14, or about 2.2 trillion values. The program is a pure function over the input, so there’s no obvious optimisation for a search here. We need to dig into the actual instructions.</p> <p>Compiling out the program, turns out that the calculations are fairly simple, and partially repetitive. The program has 14 parts. Each begins with receiving the value of w to the next input, and using it to build the value of z. The values of w, x, and y are entirely transitive and reset in each part. I was thus able to reduce the program to 14 operations where the value of z is calculated using just it’s own current value and the next input.</p> <p>Another insight from the compiled program is that 6 steps involve both multiplying z by 26 and adding the input and some constant value to it. The others always divide z by 26, but depending on the input they either do nothing else, or perform the same multiplication and addition as the rest<sup id="fnref-2"><a href="#fn-2" class="footnote-ref">2</a></sup>. We never multiply z by 0 and the program doesn’t allow for subtraction, so it’s clear that the input allowing for this division is the only way to make z reach 0.</p> <p>Moreover, since division only happens by 26 at most, we can also make other assumptions going off of that. Here’s the last step, for example:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="" data-index="2"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source">let m13 = (z12 % 26) - 14 == input[13] ? 1 : 26</span></span> <span class="grvsc-line"><span class="grvsc-source">let c13 = (z12 % 26) - 14 == input[13] ? 0 : input[13] + 13</span></span> <span class="grvsc-line"><span class="grvsc-source">let z13 = m13 * (z12/26) + c13</span></span></code></pre> <p><code>z13</code> is the value of z after the 14th step (I’ve indexed by zero here), <code>z12</code> is the value of z after the 13th step, <code>input[13]</code> is the 14th input, and <code>m13</code> and <code>c13</code> are the multiplier and constant.</p> <p>We know that <code>z12</code> is divided by 26 at most. We need the multiplier to be 1 and the constant to be 0 too, sure, but we also need z12 to be 26 or lower for the division to succeed. If z12 is greater than 26, we can avoid this calculation entirely. Going further, we can assert that it’s the value of z12 that’s the issue here; no possible value of input[13] can make z resolve to zero, so the values of the input before it need to be adjusted.</p> <p>We can make similar assertions for the 8 instructions that don’t guarantee to perform a multiplication. This vastly reduces our search space as we can go straight from evaluating say 99999999999999 to 99999989999999 (that’s an 8 in the 7th position), ignoring 10 million intermediary calculations that were guaranteed to fail.</p> <p>With this optimisation in place, it takes 0.3 seconds to calculate both parts of our answer.</p> <p>You can read <a href="https://github.com/HarshilShah/AdventOfCode/blob/main/2021.playground/Pages/Day%2024.xcplaygroundpage/Contents.swift">my full solution here</a>.</p> <hr> <p>All in all, I quite enjoyed this year’s Advent of Code. I thought the second week was a bit easier than it has been in past years, but the last few days more than made up for it in terms of difficulty. I’m already looking forward to next year.</p> <p>As for my tooling, I’m not sure I’ll want to use a language other than Swift as it is the language I feel most confident with, but I do hope to update my setup to use the Swift Package Manager next year. While I can mostly build or fake them in a good enough way in a pinch, I look forward to having access to solid implementations of common data structures like priority queues, graphs, and so on, both for Advent of Code and just in general.</p> <p>After trickier days I like to browse the <a href="https://www.reddit.com/r/adventofcode/" title="The Advent of Code subreddit">r/adventofcode</a> solutions mega thread for the day to see what solutions and techniques others used, and while there are rarely any Swift solutions in there anyways, I don’t recall seeing a single one this year. I realise the language is somewhat esoteric and largely used by people developing native apps for Apple platforms, but I hold out hope that that changes with time.</p> <style class="grvsc-styles"> .nikso-plus { background-color: #ffffff; color: #24292e; } .nikso-plus .grvsc-ttxYAU-7 { color: #D73A49; } .nikso-plus .grvsc-ttxYAU-1 { color: #24292EFF; } .nikso-plus .grvsc-ttxYAU-5 { color: #005CC5; } .nikso-plus .grvsc-ttxYAU-4 { color: #6F42C1; } .nikso-plus .grvsc-ttxYAU-14 { color: #E27F2D; } .nikso-plus .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(0, 0, 0, 0.05)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(0, 0, 0, 0.2)); } /* Night Owl (No Italics) */ @media (prefers-color-scheme: dark) { .grvsc-mm-tGNH8N { background-color: #011627; color: #d6deeb; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-3 { color: #C792EA; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-1 { color: #D6DEEB; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-9 { color: #7FDBCA; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-5 { color: #C5E478; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-7 { color: #82AAFF; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-37 { color: #D9F5DD; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-8 { color: #F78C6C; } .grvsc-mm-tGNH8N .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); } } </style> <div class="footnotes"> <hr> <ol> <li id="fn-1">This might seem like a lot too, but there weren’t many cuboids and my solution ran in well under a second.<a href="#fnref-1" class="footnote-backref">↩</a></li> <li id="fn-2">Note that this isn’t the same as not performing the division at all, since the division is a truncating integer division i.e. 26 * (27/26) gives us 26<a href="#fnref-2" class="footnote-backref">↩</a></li> </ol> </div><![CDATA[Recreating the macOS Genie Effect]]>https://harshil.net/blog/recreating-the-mac-genie-effecthttps://harshil.net/blog/recreating-the-mac-genie-effectFri, 16 Apr 2021 10:30:00 GMT<p>A few weeks ago <a href="/blog/introducing-pause" title="The announcement blog post I wrote for Pause">I released Pause</a>, a Mac app that I made to remind myself to take breaks periodically. If you’ve used the app, you know there just isn’t much UI in there. But I wanted to have one little party trick in there, which led to this:</p> <figure> <video muted controls> <source src="/7f0145f8dbdf95cdbc60e0bed4c12f41/pause.mp4" type="video/mp4"> </video> </figure> <p>This is the blog post I’d promised then, explaining how it works, and how you can build and use a similar animation in your app.</p> <h2>What Is the Genie Effect Anyway</h2> <p>Before we set about recreating it, we must first have a good understanding of how the animation works. Even if you’ve seen it multiple times a day for years, what it’s doing might not be entirely obvious.</p> <p>Here’s a clip of the standard macOS genie animation in action:</p> <figure> <video muted controls> <source src="/c43d923742c09a1704d62551cea4a912/sysprefs.mp4" type="video/mp4"> </video> </figure> <p>Here’s a rough breakdown of what’s happening.</p> <p>First up, the bottom edge of the window shears, shrinking in width and shifting towards its eventual final position, while the top edge remains fixed. The rest of the window contorts unevenly, forming a curve along either side.</p> <p>Next, the window begins to translate down and scale. This is where the real magic of the animation comes in. The window doesn’t <em>just</em> translate and scale down though, it follows the path created by the two sides. It’s almost like a liquid flowing through a funnel, albeit a rather rigid liquid.</p> <h2>Building It</h2> <p>So how do we build this sort of animation.</p> <p>If you’ve done any iOS or macOS UI work, this might seem impossible to do with a standard <code>CALayer</code> and the set of animation tools that ship with Core Animation. If we ignored the curved edges — and if I had the slightest clue about how those <code>CATransform3D</code> matrices work — that would be much more doable, but that’s not what we’re trying to do here. While perhaps not pixel perfect, we want something that at least comes close to matching the general vibe of the built in animation.</p> <p>While we can represent the curved path using a <code>UI/CGBezierPath</code>, there isn’t really any way to contort a view to fit one, let alone then animate all of this.</p> <p>One solution available to us while still sticking with the stock API here is that we don’t really need to use a single view: There’s nothing stopping us from taking a snapshot of our view and splitting it up a bunch of separate views, and then setting the frames and transforms for each of them. We can simulate the appearance of the entire shape being curved by splicing it up into small enough rectangles.</p> <p>I can already hear the guffaws from some of you reading this, and to be honest I wouldn’t blame you for that reaction. This just isn’t how animations are usually handled. I’d like to assure you though that this isn’t entirely kooky, and while it was a backup strategy I didn’t end up using, the concept behind it is sound and basically what we want to achieve: being able to manipulate a view as if it’s a composite formed of multiple pieces rather than as a monolith.</p> <p>As is turns out there is indeed a concept that represents this exact behaviour: Mesh transforms.</p> <p>The idea behind them is straightforward. First you apply a set of vertices to your original shape. These vertices connect to certain neighbours to form shapes, collectively subdividing your view into a number of partitions.</p> <p>Now you can move around any of the vertices, and all the partitions it touches will be distorted, and the partitions of the view they encompassed in the original shape will be distorted as well to fit the newly formed shapes.</p> <p>In a way, for our use case it can be seen as a different take at the snapshot slicing technique from above; we just skip the steps of manually slicing up the snapshot and calculating individual transforms that make it so that the partitions all align perfectly for every frame, since the mesh system handles all of those details for us.</p> <p>To be clear though, a mesh animation isn’t quite identical to the slicing technique; the slicing technique affords us much more control over the exact placement and contortion of the partition, although that power does require way more code on our part. If we wanted to recreate something like, say, the Passbook shredder animation from <del>the days of yore</del> iOS 6, a mesh transform simply won’t suffice, but slicing would do exactly what we need, allowing us to manipulate the slices as if they were actually cut apart, rather than separate but still connected. That said, a mesh is perfectly suited to our use case here, and so onwards.</p> <p>The next question would of course be: Is there any first party API that would let us achieve these mesh transform? I’m glad you asked, and as it turns out there is! Core Animation includes a <code>CAMeshTransform</code> type that does pretty much what you’d expect. Just set one of those to your layer’s <code>meshTransform</code> and you’re done. I’ve also been told that there’s API in AppKit to set a window’s transform directly.</p> <p>This seems like exactly what we’re looking for. There’s just one problem: It’s private API, all of it. Please do file a radar if you too would like to see that change.</p> <p>While it isn’t available there as of this writing, I would like for Pause to be on on the Mac App Store someday, so while I considered using all of this API even so, I ended up deciding against this approach too.</p> <p>After a bit of prodding around though I did find that there’s another way to use mesh transforms, although this one doesn’t involve Core Animation, UIKit, or AppKit…</p> <h2>Enter SpriteKit</h2> <p>SpriteKit, as it turns out, ships with some API to create mesh transforms, and public API at that.</p> <p>While it can be used pretty seamlessly with UIKit and AppKit, SpriteKit is seen as more of a game framework than something used for building interfaces, and so some of you might not be familiar with it. Fret not though, we’ll only be touching a small fraction of the API surface for this one.</p> <p>The biggest difference worth keeping in mind is that it uses a coordinate system with the origin at the bottom left, which might be a bit confusing for those coming from UIKit — or just right if you’re working with AppKit already. We only really care for a couple bits of API, so I’ll explain those in detail, and the others just enough to capture the point of what we want them for.</p> <h2>The Setup</h2> <p>Lets just get some of the stuff we need to set up in order to build the animation out of the way first. Since Pause is an AppKit app<sup id="fnref-1"><a href="#fn-1" class="footnote-ref">1</a></sup>, I’m building out the example app in an macOS playground. Regardless, the core of the animation is in SpriteKit and will thus work on both platforms with no issues.</p> <p>The first bit of SpriteKit code we need is an <code>SKView</code>. This is the encapsulation used to bridge SpriteKit into UIKit or AppKit (and thus SwiftUI too, if you’re so inclined) code as it inherits <code>UIView</code> and <code>NSView</code> when used with those frameworks, respectively<sup id="fnref-2"><a href="#fn-2" class="footnote-ref">2</a></sup>.</p> <p>Next, we need an <code>SKScene</code>. This is where the actual SpriteKit business goes down. All your SpriteKit nodes — these are the basic building blocks of SpriteKit code, analogous though not equivalent to <code>CALayer</code> — need to be rendered within a scene, which in turn needs to be presented within an <code>SKView</code>.</p> <p>Lastly, we need an <code>SKSpriteNode</code>, which is a node capable of displaying an image. For now we’re going to have it show a screenshot of the same System Preferences screen as before. In practise, you’d use a snapshot of the view you want to transition. One thing to keep in mind is for views with shadows, the shadow will necessarily extend beyond the frame, and so you’ll have to update your frames accordingly.</p> <p>All in all, this is what our basic setup looks like in code:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="0"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">import</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">SpriteKit</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">import</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">PlaygroundSupport</span></span></span> <span class="grvsc-line"><span class="grvsc-source"></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> frame </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">CGRect</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">width</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">800</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">height</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">600</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> skView </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">SKView</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">frame</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: frame</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">PlaygroundPage.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">current</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">liveView</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> skView</span></span></span> <span class="grvsc-line"><span class="grvsc-source"></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> scene </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">SKScene</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">size</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: frame.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-7">size</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">scene.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">backgroundColor</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">windowBackgroundColor</span></span></span> <span class="grvsc-line"><span class="grvsc-source"></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> imageNode </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">SKSpriteNode</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">imageNamed</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">SysPrefs.png</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">imageNode.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">position</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">CGPoint</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: frame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">midX</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: frame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">midY</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">imageNode.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-1">size</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> frame.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-1">size</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">scene.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">addChild</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">imageNode</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">skView.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">presentScene</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">scene</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span></code></pre> <p>And here’s how it renders:</p> <figure class="img-medium"> <img src="/d6caa9d079f87de60cdfa5f98b278338/unmorphed.png" /> </figure> <p>It looks borked for now, but that’s intentional; we’ll get to that in a moment.</p> <h2>The Mesh</h2> <p>With all that out of the way, let us look at the mesh transform APIs for a second.</p> <p>SpriteKit calls its mesh transformation API “warp geometry”, and it manifests in the form of 3 types:</p> <ul> <li> <p><code>SKWarpable</code> is a protocol that types that can be warped conform to. We don’t use this directly, <code>SKSpriteNode</code> conforms to it which is what allows us to animate one.</p> </li> <li> <p><code>SKWarpGeometry</code>, which is the base class for warp geometries. We don’t use this one directly either.</p> </li> <li> <p><code>SKWarpGeometryGrid</code>, a subclass of <code>SKWarpGeometry</code> which lets you define a two dimensional mesh geometry grid, where every vertex connects to its neighbours along all 4 directions.</p> <p>There aren’t any other (public) subclasses of <code>SKWarpGeometry</code>, but this design does leave the door open to other, different kinds of grids; you might want to split your mesh into triangles, or hexagons, or some other kind of design with a mix of shapes, or maybe even let SpriteKit figure it out itself.</p> </li> </ul> <p><code>SKWarpGeometryGrid</code> is the only one we’ll be using, and here’s how we’re going to do that.</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="1"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">class</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">SKWarpGeometryGrid</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">convenience</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">init</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">(</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">columns</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Int</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">rows</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Int</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">sourcePositions</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: [SIMD2&lt;</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Float</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">&gt;] </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> [SIMD2</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">&lt;</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Float</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">]</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">()</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">destinationPositions</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: [SIMD2&lt;</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Float</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">&gt;] </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> [SIMD2</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">&lt;</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Float</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">]</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">()</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p>To instantiate a grid we need to pass in the number of columns and rows<sup id="fnref-3"><a href="#fn-3" class="footnote-ref">3</a></sup>, and alongside those we can optionally pass in the source and destination positions; if we skip either, it’ll assume that we mean the vertices to be positioned at regular intervals.</p> <p>The array of positions is organised from as rows from bottom to top, and each row itself is organised from left to right. What this means is that the very first element reflects the position of the bottom left vertex, the second does it’s next neighbour to the right, and so on. If this seems a bit off, remember again that the origin is at the bottom left.</p> <p>Each individual vertex position is represented as a <code>SIMD2&#x3C;Float></code> value. If you haven’t come across it before, <code>SIMD2</code> is part of the set of <code>SIMD</code> vector types in the Swift standard library designed for faster parallel processing; for our use here though you can just think of it as a tuple of <code>(Float, Float)</code>. The first value is the <code>x</code> coordinate of a vertex, and the second is the <code>y</code>.</p> <p>The coordinates aren’t absolute values but relative to the node’s frame; if you had a node rendered at a size of 400×400 points, and wanted to place one of its vertices at 100 points off the left and 100 points off the bottom, you’d set the nodeʼs position to <code>SIMD2(0.25, 0.25)</code>.</p> <p>We can give this a run to make sure everything is working as expected. Our Playground is running at a size of 800×600, while the image has a square aspect ratio. Lets warp it to be centered and have a size of 400×400, using a warp geometry grid with 4 vertices.</p> <p>But first, because we’re dealing with normalised<sup id="fnref-4"><a href="#fn-4" class="footnote-ref">4</a></sup> positions with respect to the node’s bounds, I wrote up a small helper function to convert those:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="2"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">public</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">extension</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">CGRect</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">func</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">normalized</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">in</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-14 grvsc-tGNH8N-9">other</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: CGRect</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> CGRect {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">CGRect</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: (origin.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> other.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">origin</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">) </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">/</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> other.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">width</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: (origin.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> other.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">origin</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">) </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">/</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> other.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">height</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">width</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: width </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">/</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> other.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">width</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">height</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: height </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">/</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> other.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">height</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p>Now to set the initial frame using it:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="3"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> initialFrame </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">CGRect</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">200</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">100</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">width</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">400</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">height</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">400</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">normalized</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">in</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: skView.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">bounds</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> initialPositions </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> [</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">SIMD2</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Float</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">initialFrame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">minX</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Float</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">initialFrame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">minY</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">))</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">SIMD2</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Float</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">initialFrame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">maxX</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Float</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">initialFrame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">minY</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">))</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">SIMD2</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Float</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">initialFrame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">minX</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Float</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">initialFrame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">maxY</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">))</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">SIMD2</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Float</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">initialFrame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">maxX</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Float</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">initialFrame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">maxY</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">))</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">]</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">imageNode.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">warpGeometry</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">SKWarpGeometryGrid</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">columns</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">1</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">rows</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">1</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">destinationPositions</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: initialPositions</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span></code></pre> <figure class="img-medium"> <img src="/56219c896254e8a76e4420b36cba060c/initial.png" /> <figcaption> The initial state for our animation </figcaption> </figure> <p>The items in the <code>initialPositions</code> array are the positions for the bottom left, bottom right, top left, and then top right vertices, in that order. The explicit <code>Float</code> casting makes it even harder to read but that’s the best we can do until <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0307-allow-interchangeable-use-of-double-cgfloat-types.md">SE-307: Allow interchangeable use of CGFloat and Double types</a> is merged.</p> <p>We can also see what it’ll look like when minimised:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="4"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> finalFrame </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">CGRect</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">640</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">width</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">50</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">height</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">50</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">normalized</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">in</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: skView.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">frame</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> finalPositions </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> [</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">SIMD2</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Float</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">finalFrame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">minX</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Float</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">finalFrame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">minY</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">))</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">SIMD2</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Float</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">finalFrame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">maxX</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Float</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">finalFrame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">minY</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">))</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">SIMD2</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Float</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">finalFrame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">minX</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Float</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">finalFrame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">maxY</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">))</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">SIMD2</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Float</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">finalFrame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">maxX</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Float</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">finalFrame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">maxY</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">))</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">]</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">imageNode.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">warpGeometry</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">SKWarpGeometryGrid</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">columns</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">1</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">rows</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">1</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">destinationPositions</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: finalPositions</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span></code></pre> <figure class="img-medium"> <img src="/b5173af75fc5ce8f718e04692975c279/final.png" /> <figcaption> And the final state </figcaption> </figure> <p>Now that we have a bit more of a hang of how these warps work, on to the actual animation.</p> <h2>The Animation</h2> <p>Now for the fun stuff. First, a very brief look at the animation API.</p> <p>Animations in SpriteKit are carried out using the <code>SKAction</code> class. It’s a different system from what you may be used to with Core Animation, so rather than diving into the details we’ll just look at the bits we care about, which is this one function:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="5"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">class</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">SKAction</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">class</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">func</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">animate</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">(</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">withWarps</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-14 grvsc-tGNH8N-9">warps</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: [SKWarpGeometry],</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">times</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: [NSNumber]</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> SKAction</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">?</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p>It lets us create an action from an array of warps and accompanying timestamps, which when run by a node, animates the geometries we’ve specified. So, yeah, we’re doing a keyframe animation.</p> <p>Here’s what the basic setup for our animation looks like:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="6"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> slideAnimationEndFraction </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">0.5</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> translateAnimationStartFraction </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">0.4</span></span></span> <span class="grvsc-line"><span class="grvsc-source"></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> duration </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">0.7</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> fps </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">60.0</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> frameCount </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> duration </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">*</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> fps</span></span></span> <span class="grvsc-line"><span class="grvsc-source"></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> rowCount </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">1</span></span></span> <span class="grvsc-line"><span class="grvsc-source"></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> positions</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> [[SIMD2</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">&lt;</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Float</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">]] </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">stride</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">from</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">to</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: frameCount, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">by</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">1</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">map</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { frame </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">in</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> fraction </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> (frame </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">/</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> (frameCount </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">1</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">))</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> slideProgress </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">max</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">min</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">1</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, fraction</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">/</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">slideAnimationEndFraction</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">))</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> translateProgress </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">max</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">min</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">1</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, (fraction </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> translateAnimationStartFraction)</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">/</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">1</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> translateAnimationStartFraction)</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">))</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">/// calculate actual positions here</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span> <span class="grvsc-line"><span class="grvsc-source"></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> warps </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> positions.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">map</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">SKWarpGeometryGrid</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">columns</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">1</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">rows</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: rowCount, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">destinationPositions</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span> <span class="grvsc-line"><span class="grvsc-source"></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> warpAction </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> SKAction.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">animate</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">withWarps</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: warps,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">times</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: warps.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">enumerated</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">()</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">map</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> { </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">NSNumber</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">value</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Double</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">offset</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">/</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> fps</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">!</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">imageNode.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">run</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">warpAction</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span></code></pre> <p>To start things off, pretty much all the stuff at the top is an estimate on my part, so feel free to play around with those. For the timing for the two sub-animations, I tested a bunch of different values and those are the ones that seemed closest to me. The duration is eyeballed too. For the display’s FPS, I know macOS is now better about supporting higher frame rates but I haven’t found a simple way to find the value for the current display, so we’re going with the trusted 60fps. If you know of a technique to find it, please do let me know.</p> <p>As for the row count, in practise it’ll depend on the full size of the view you want to animate and the kind of performance characteristics you’re seeing on the various devices you’re targeting, but for this particular demo, I’ve found 50 is a good enough value. As a rough estimate, I’ve found that a row for every 5–10 pixels is good enough.</p> <p>Next, we runs through every single frame of our animation, 42 in our case here, and calculates the current progression<sup id="fnref-5"><a href="#fn-5" class="footnote-ref">5</a></sup> of both sub-animations. We’ll get to calculating the actual positions in a second.</p> <p>Next, we <code>map</code> our positions to create an array of warps, and then use that to create an <code>SKAction</code> that handles our animation, and have our node <code>run</code> it.</p> <p>As for calculating the positions, there are a lot of moving parts here, so let us break it down. As we noted earlier, there are two main parts to the animation, firstly the bottom edge shearing, and then the translation of the whole window along the path created by the sides. These two parts aren’t sequential though, the translation begins just a bit before the shrinking finishes, which is why the effect doesn’t appear as a combination of two disjointed animations. We’ll have to account for this as well.</p> <p>The most important part of this whole setup though, is the curvature of the “funnel” we talked about earlier. Sure, the two subanimations drive the horizontal and vertical deformation of the view in rough terms, but they aren’t enough to derive the position of all vertices for a given frame.</p> <p>We’re going to have to model for the curved paths in any case, so rather than doing so implicitly we’re going to design the whole animation around it, and as it turns out doing so will make this whole business much simpler too.</p> <p>First, we can restate what the shearing sub-animation does in terms of what it does to the funnel rather than to the view itself. Instead of compressing and shifting the bottom edge of the view, we can see it as moving the bottom points of both curved paths from being collinear with the initial position’s edges to being at the top of the final position’s edges. The translation sub-animation now has the job of moving the view along between the paths.</p> <p>As for the actual curvature, we’re going to model that using an easing function. While you’re probably most familiar with as timing functions for animations, an easing function is pretty much just a way to map a number, traditionally between 0 and 1, to another number, and can be used anywhere such a situation crops up<sup id="fnref-6"><a href="#fn-6" class="footnote-ref">6</a></sup>. We’re going to assume that the top of our curve is 0, and the bottom is 1. For any values in between, we’ll obtain their representation on this scale, apply our easing function, and then convert it back to the bezier’s full scale.</p> <p>So, writing up all of that, here’s what our final code looks like.</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="7"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> slideAnimationEndFraction </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">0.5</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> translateAnimationStartFraction </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">0.4</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> leftBezierTopX </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Double</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">initialFrame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">minX</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> rightBezierTopX </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Double</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">initialFrame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">maxX</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> duration </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">0.7</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> fps </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">60.0</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> frameCount </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> duration </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">*</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> fps</span></span></span> <span class="grvsc-line"><span class="grvsc-source"></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> rowCount </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">50</span></span></span> <span class="grvsc-line"><span class="grvsc-source"></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">/// 1</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> leftEdgeDistanceToMove </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Double</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">finalFrame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">minX</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> initialFrame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">minX</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> rightEdgeDistanceToMove </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Double</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">finalFrame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">maxX</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> initialFrame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">maxX</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> verticalDistanceToMove </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Double</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">finalFrame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">maxY</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> initialFrame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">maxY</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> bezierTopY </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Double</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">initialFrame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">maxY</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> bezierBottomY </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Double</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">finalFrame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">maxY</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> bezierHeight </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> bezierTopY </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> bezierBottomY</span></span></span> <span class="grvsc-line"><span class="grvsc-source"></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> positions</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> [[SIMD2</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">&lt;</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Float</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">]] </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">stride</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">from</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">to</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: frameCount, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">by</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">1</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">map</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { frame </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">in</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> fraction </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> (frame </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">/</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> (frameCount </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">1</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">))</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> slideProgress </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">max</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">min</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">1</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, fraction</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">/</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">slideAnimationEndFraction</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">))</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> translateProgress </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">max</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">min</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">1</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, (fraction </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> translateAnimationStartFraction)</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">/</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">1</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> translateAnimationStartFraction)</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">))</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">/// 2</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> translation </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> translateProgress </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">*</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> verticalDistanceToMove</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> topEdgeVerticalPosition </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Double</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">initialFrame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">maxY</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">+</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> translation</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> bottomEdgeVerticalPosition </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">max</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Double</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">initialFrame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">minY</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">+</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> translation,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Double</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">finalFrame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">minY</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">/// 3</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> leftBezierBottomX </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> leftBezierTopX </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">+</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> (slideProgress </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">*</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> leftEdgeDistanceToMove)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> rightBezierBottomX </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Double</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">initialFrame.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">maxX</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">+</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> (slideProgress </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">*</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> rightEdgeDistanceToMove)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">/// 4</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">func</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">leftBezierPosition</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">forY</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-14 grvsc-tGNH8N-9">y</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Double</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Double</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">switch</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> y {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">case</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">..&lt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">bezierBottomY</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> leftBezierBottomX</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">case</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> bezierBottomY </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">..&lt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> bezierTopY</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> progress </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> ((y </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> bezierBottomY) </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">/</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> bezierHeight).</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">quadraticEaseInOut</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">progress </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">*</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> (leftBezierTopX </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> leftBezierBottomX)</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">+</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> leftBezierBottomX</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">default:</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> leftBezierTopX</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">func</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">rightBezierPosition</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">forY</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-14 grvsc-tGNH8N-9">y</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Double</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Double</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">switch</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> y {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">case</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">..&lt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">bezierBottomY</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> rightBezierBottomX</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">case</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> bezierBottomY </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">..&lt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> bezierTopY</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> progress </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> ((y </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> bezierBottomY) </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">/</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> bezierHeight).</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">quadraticEaseInOut</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">progress </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">*</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> (rightBezierTopX </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> rightBezierBottomX)</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">+</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> rightBezierBottomX</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">default:</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> rightBezierTopX</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">/// 5</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">...</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> rowCount</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">map</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Double</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">/</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Double</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">rowCount</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">flatMap</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { position </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> [SIMD2</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">&lt;</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Double</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">] </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">in</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> y </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> (topEdgeVerticalPosition </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">*</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> position) </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">+</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> (bottomEdgeVerticalPosition </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">*</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> (</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">1</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> position))</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> xMin </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">leftBezierPosition</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">forY</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> xMax </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">rightBezierPosition</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">forY</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> [</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">SIMD2</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">xMin, y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">SIMD2</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">xMax, y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">]</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">map</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">SIMD2</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">&lt;</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Float</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">&gt;</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">init</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span> <span class="grvsc-line"><span class="grvsc-source"></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> warps </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> positions.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">map</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">SKWarpGeometryGrid</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">columns</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">1</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">rows</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: rowCount, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">destinationPositions</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span> <span class="grvsc-line"><span class="grvsc-source"></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> warpAction </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> SKAction.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">animate</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">withWarps</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: warps,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">times</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: warps.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">enumerated</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">()</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">map</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">NSNumber</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">value</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Double</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-9">$0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">offset</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">/</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> fps</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">!</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">imageNode.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">run</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">warpAction</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span></code></pre> <p>One note before getting to anything else: We’re using <code>Doubles</code> everywhere and then mapping back to <code>Float</code> at the end because I noticed a bug once or twice in some testing when using <code>Float</code>. I haven’t been able to repro it since so it might be a fluke, but the <code>Double</code> version works just fine so I’m fine with it as well.</p> <p>Lets go through the other changes in parts. In the first code block, we’re defining a bunch of constants related to the full animation such as the total translation for the top, left, and right edges, and the fixed top coordinates and height of the bezier curve.</p> <p>In the second block, we’re calculating the translation of the top and bottom edges, based only off of the translation subanimation’s progress.</p> <p>In the third block, we’re calculating the x axis values of the bottom coordinate of the two beziers, based only off of the shearing subanimation progress.</p> <p>In the fourth block, we’re setting up functions to calculate the <code>x</code> value for a point at a particular height given the bezier mathematics we have, as we’d discussed earlier. Within the bezier, i.e. between the top edge of the initial position and top edge of the final position, we’re easing the relative position to find the position along the curve, and beyond the edges we’re extending the beziers vertically, to match the final position being a rectangle with straight edges.</p> <p>It all comes together in the fifth block. We calculate positions in a row-wise fashion, starting from the bottom edge and moving upwards. For every row, we first calculate the <code>y</code> position by interpolating linearly between the top and bottom edges. Then we use the bezier functions defined in the fourth block to find the matching <code>x</code> positions.</p> <p>And with that, let’s see this looks like in action:</p> <figure class="img-large"> <video muted controls> <source src="/37c7178716d018e412509b4fe45f1d16/genie.mp4" type="video/mp4"> </video> </figure> <h2>Conclusion</h2> <p>With the exception that there are some changes because the animation moves upwards, and is wrapped in a single view, that is pretty much the exact code that ships in Pause. If you’d want to the animation with UIKit, you’d do something similar, along with some coordinate system transformations<sup id="fnref-7"><a href="#fn-7" class="footnote-ref">7</a></sup> to ensure that the frames lines up.</p> <p>You can look at the accompanying Playground for this blog post on <a href="https://github.com/HarshilShah/Genie" title="Genie on GitHub">GitHub</a>. The Playground also covers some additional variations on the animation, such as being able to maximise, and also animate in or out of any edge of the minimised window. So you can do this:</p> <figure class="img-large"> <video muted controls> <source src="/f9f9856f14db286075d58cedd53ab6de/quattro.mp4" type="video/mp4"> </video> </figure> <p>Additionally, I’d recommend taking a look at Bartosz Ciechanowski’s <a href="https://github.com/Ciechan/BCGenieEffect" title="BCGenieEffect on GitHub">BCGenieEffect</a>. He recreated the effect over 8 years ago, using the image slicing method and <code>CATransform3D</code> as we’d discussed earlier.</p> <style class="grvsc-styles"> .nikso-plus { background-color: #ffffff; color: #24292e; } .nikso-plus .grvsc-ttxYAU-7 { color: #D73A49; } .nikso-plus .grvsc-ttxYAU-1 { color: #24292EFF; } .nikso-plus .grvsc-ttxYAU-4 { color: #6F42C1; } .nikso-plus .grvsc-ttxYAU-5 { color: #005CC5; } .nikso-plus .grvsc-ttxYAU-8 { color: #22863A; } .nikso-plus .grvsc-ttxYAU-14 { color: #E27F2D; } .nikso-plus .grvsc-ttxYAU-3 { color: #6A737D; } .nikso-plus .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(0, 0, 0, 0.05)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(0, 0, 0, 0.2)); } /* Night Owl (No Italics) */ @media (prefers-color-scheme: dark) { .grvsc-mm-tGNH8N { background-color: #011627; color: #d6deeb; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-3 { color: #C792EA; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-1 { color: #D6DEEB; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-9 { color: #7FDBCA; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-5 { color: #C5E478; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-7 { color: #82AAFF; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-8 { color: #F78C6C; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-37 { color: #D9F5DD; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-19 { color: #ECC48D; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-6 { color: #637777; } .grvsc-mm-tGNH8N .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); } } </style> <div class="footnotes"> <hr> <ol> <li id="fn-1"> <p>And because macOS Playgrounds are significantly more stable than the iOS kind.<a href="#fnref-1" class="footnote-backref">↩</a></p> </li> <li id="fn-2"> <p>How exactly does that work? Is it just a bunch of <code>#if canImport(AppKit)</code> macros strewn all over? Your guess is as good as mine.<a href="#fnref-2" class="footnote-backref">↩</a></p> </li> <li id="fn-3"> <p>Note that this is the number of columns and rows for the <em>grid</em> itself, not the number of columns and rows of vertices; i.e. if you have a setup of 3x3 vertices, you’d have a grid of 2 columns and 2 rows.<a href="#fnref-3" class="footnote-backref">↩</a></p> </li> <li id="fn-4"> <p>Yes, I write prose in Indian English and code in US English. Yes, I hate it too. And no, I won’t switch either of them.<a href="#fnref-4" class="footnote-backref">↩</a></p> </li> <li id="fn-5"> <p>The <code>frameCount - 1</code> is there is there to ensure that the <code>fraction</code> is 0 for the zeroth frame and 1 for the last i.e. 41st frame.<a href="#fnref-5" class="footnote-backref">↩</a></p> </li> <li id="fn-6"> <p>Galaxy brain: We’re <em>always</em> using easing functions when dealing with numbers, it’s just a linear easing function most times.<a href="#fnref-6" class="footnote-backref">↩</a></p> </li> <li id="fn-7"> <p>Something like:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="8"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">extension</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">CGRect</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">func</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">invertingCoordinateSystem</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">in</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-14 grvsc-tGNH8N-9">superviewBounds</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: CGRect</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> CGRect {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">CGRect</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: origin.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">x</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: superviewBounds.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">height</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> (origin.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">y</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">+</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> height),</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">width</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: width,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">height</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: height</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p><a href="#fnref-7" class="footnote-backref">↩</a></p> </li> </ol> </div><![CDATA[Introducing Pause]]>https://harshil.net/blog/introducing-pausehttps://harshil.net/blog/introducing-pauseThu, 25 Mar 2021 14:30:00 GMT<p>If you work in tech, sitting at a desk typing away at a computer all day, you’re aware of the looming spectre of RSI. I’m also personally extremely worried about eye strain. Before Retina screens were a thing, using a computer for more than a couple hours just wasn’t possible for me.</p> <p>However if anything screen time has gone up for me over the years, especially this last year given <em>points all around generally</em>. The nature of my work means I can’t significantly alter my total usage, but I can change the way I go about it.</p> <p>The 20-20-20 rule is a commonly cited technique. It recommends taking a break every 20 minutes, looking at an object about 20 feet away, for 20 seconds. I’m not really sure where it came from or if it’s entirely scientific, but I’d tried and found it helpful in the past, and so I started following the technique again.</p> <p>I tried a few methods of keeping track of time using clocks, timers, setting reminders, and trying to bend a whole bunch of apps, but they were finicky at best. I did find a few more dedicated apps, but none of them really gelled with me.</p> <p>Meanwhile I had always wanted to make a Mac app, even more so with SwiftUI out now. And so eventually I got to a point where it started to make perfect sense to combat the problem of spending too much time on the computer, by spending a bit more time on the computer making an app that’d help me do so.</p> <h2>Pause</h2> <p>Enter <a href="/pause" title="The Pause website">Pause</a>. It’s a simple little app that lives in your menu bar and sends you notifications reminding you to take a break.</p> <figure class="img-small"> <img src="/df89eb857e5f91c777bb5d40eb8745a6/pause@3x.png" > <figcaption> <span class="caption">The app icon</span> </figcaption> </figure> <p>While you’re taking a break, Pause covers up the screen, showing a blurred version of your wallpaper, along with a suggestion for an activity to do. The app ships with a few of these suggestions baked in, but you have the ability to fully customise them to your pleasure, and even disable them entirely.</p> <figure class="img-large"> <img src="/ac84972c48c48b1985bcc62b07e945b1/break.png"> </figure> <p>You can also customise how long each break lasts, and their frequency. And for times when you just need to focus, there’s Focus Mode, which stops any break notifications until you disable it.</p> <p>Also when you finish the onboarding, this happens:</p> <figure> <video autoplay muted controls> <source src="/7f0145f8dbdf95cdbc60e0bed4c12f41/genie.mp4" type="video/mp4"> </video> </figure> <h2>Code Stuff</h2> <p>The UI is done in SwiftUI and AppKit. This is the first app I’ve shipped that uses either, so that was a fun learning experience.</p> <p>The app is built on the excellent <a href="http://github.com/pointfreeco/swift-composable-architecture" title="The Composable Architecture on GitHub">Composable Architecture</a>. This might seem a bit like overkill for such a simple app — there’s not much UI to compose there — but the main benefit of it was being able to use <a href="https://github.com/pointfreeco/combine-schedulers" title="Combine Schedulers on Github">Combine Schedulers</a>.</p> <p>Rather than relying on the system <code>Timer</code> based APIs, and thus introducing that bit of asynchronicity in my local as well as unit tests, the package ships with a <code>TestScheduler</code> that lets me have full control of the flow of time. Rather than wait for say 20 minutes to pass to see if a break reminder fires off correctly, I can manually advance time by that much in an instance and verify that it does.</p> <p>In addition to helping with unit tests, this has also let me build a little debug mode into the app where I can test various scenarios live by advancing the time for the app, rather than either having mess around with the system clock or set up short durations, which still have some amount of wait baked in, and are also ineffective for catching subtle bugs.</p> <p>As for the genie animation, that was a little touch added at the end (hat tip to <a href="https://twitter.com/neilsardesai" title="Neil Sardesai on Twitter">Neil Sardesai</a> for the idea). It’s not a perfect match for Apple’s but I’m happy with the outcome and kinda surprised at how well it turned out eventually. More about how I built that in a later blog post. [Update: Itʼs live now, <a href="/blog/recreating-the-mac-genie-effect">Recreating the macOS Genie Effect</a>]</p> <hr> <p>That’s about it, really. I have a couple more ideas for things the app should do but it’s meant to be a simple little utility that does one thing, and does it well.</p> <p>Pause is out now for free, available for Macs running Big Sur or newer. You can download it <a href="/pause" title="The Pause website">here</a>.</p> <p>If you run into any issues, have any ideas, or just find it useful, please do let me know!</p><![CDATA[Wrangling Time]]>https://harshil.net/blog/foundation-datehttps://harshil.net/blog/foundation-dateFri, 15 Jan 2021 10:30:00 GMT<p>The overarching theme of what I write about on this website is things that I used to once hate, then understood, and now continue to hate but for different reasons. And there’s no topic that fits the bill better than this.</p> <h2><code>Date</code> Is Not Really A Date</h2> <p>I’m a pretty big fan of using Xcode Playgrounds for prototyping. They let you quickly mess around when you just have a few lines of an idea without having to go through the hassle of setting up a brand new project<sup id="fnref-1"><a href="#fn-1" class="footnote-ref">1</a></sup>.</p> <p>They also have these little previews that show you what happens when each line when your code is executed, which can be really nifty. If you ran the following line:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="0"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> now </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Date</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">()</span></span></span></code></pre> <p>You’d see something like <code>01-Jan-2021 at 12:00 AM</code> (or rather the equivalently formatted version of the current time for you) in the sidebar.</p> <p>This is slightly misleading, though. Despite what the preview shows, <code>Date</code> has no concept of days, months, years, any of that stuff. It deals solely with time, and more specifically, the number of seconds that have passed since midnight of 1 January 2001, in UTC.</p> <h2>How You’re Supposed To Deal With Time</h2> <p><code>Date</code> is a very simple type that models the passage of time rather than whatever concepts of dates we have. If everyone on the planet created a Date instance at the same moment, they’d be exactly the same.</p> <p>While dealing time measured in seconds can be reduced to pure maths, date maths isn’t quite simple. We have time zones, leap seconds<sup id="fnref-2"><a href="#fn-2" class="footnote-ref">2</a></sup>, leap years, daylight savings time, and to top it all multiple calendars, each with their own concepts of how days and months should be organised. A single moment in time can be represented in a host of different ways and so rather than creating mechanisms for handling them as such it makes a ton more sense to use the time-based representation. It’s much easier to store, compare, and operate on one single 64-bit number than all of the various formats. As a bonus you also get some semantic guarantees: Every single time interval represents one single date and vice versa. You cannot possibly, accidentally, create say February 30 with this representation.</p> <p>This is also why <code>Date</code> is fairly barebones in terms of functionality. There is the one <code>addingTimeInterval</code> method, and it does have a few valid uses, but otherwise you canʼt really do much with just a <code>Date</code>.</p> <p>All date-related information and smarts come from <code>Calendar</code>, <code>DateFormatter</code>, and related types. In fact, Playgrounds uses them too, and here’s how you can manually generate the string representation used in the preview:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="1"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">var</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> formatter </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">DateFormatter</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">()</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">formatter.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">dateStyle</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">medium</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">formatter.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">timeStyle</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">short</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">formatter.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">string</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">from</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: now</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">// &quot;01-Jan-2021 at 12:00 AM&quot;</span></span></span></code></pre> <p>Both <code>Calendar</code> and <code>DateFormatter</code> do a host of things. If you want to manipulate a date, you should be using the former, and if you want to convert one to or from a string, you should be using the latter (and <code>ISO8601DateFormatter</code>, <code>RelativeDateTimeFormatter</code>, and so on). Your use case is probably already covered and localised by those types.</p> <p>That said, I do have one pretty massive gripe with this whole setup…</p> <h2><code>Date</code> <em>Really</em> Is Not a Date</h2> <figure class="img-small"> <picture> <source srcset="/e78054f25724737880199bfb4e66a9f8/steps-dark.png" media="(prefers-color-scheme: dark)"> <img src="/7be1dba22c27270eff3178b386d69a98/steps-light.png" title="My step count data for June 8, 2017."> </picture> </figure> <p>Here’s a screenshot from the Health app, showing my step counts for the day of June 8, 2017. It was a fairly active day, as I got in a bit over 20,000 steps. The distribution of those steps, though, feels a bit off. It reads like I was up walking all night, and then asleep throughout the day. Scrolling around in the app a bit, that’s the story for the rest of the week too. I don’t remember spending the week that way.</p> <p>As it happens I was in San Jose attending WWDC at the time, which has a -12:30 time difference with India<sup id="fnref-3"><a href="#fn-3" class="footnote-ref">3</a></sup>, and as a result all of my health data from that trip appears time-shifted.</p> <p>I say appears because in a way, it is accurate. The step counts did happen as the graph reads, except when it says 12 AM it doesn’t refer to 12 AM as I experienced it, but rather 12 AM <em>with respect to my current time zone</em>.</p> <p>Going back to how <code>Date</code> works, it doesn’t model the actual clock time but rather a fixed point in time that can be interpreted in any time zone. And so what’s happening here is that the data is being interpreted as if it happened in my current time zone, which is the default time zone that <code>Calendar</code> and <code>DateFormatter</code> use.</p> <p>And as such, a <code>Date</code> alone isn’t sufficient for modelling historical data, or at least personal historical data: You need time zone information too.</p> <p>HealthKit acknowledges this too. You do have the ability to specify a time zone when constructing the appropriate <code>HKSample</code> subclass for the health data you’re modelling. It just so happens that while you are required to submit the start and end dates for any sample, the time zone information is entirely optional and buried within a metadata dictionary, that you can even omit entirely.</p> <p>All of the step data shown in the screenshot was captured by the Health app right on my phone, stored in HealthKit, and displayed by the Health app. Somewhere in this pipeline, the time zone information was ignored or discarded.</p> <p>So clearly the type itself isn’t at fault for this bug<sup id="fnref-4"><a href="#fn-4" class="footnote-ref">4</a></sup>, and while it also isn’t a result of any of individual pieces of the existing setup, the existing design of the whole date-time handling apparatus does feel a little incomplete.</p> <h2>Dates As We Interpret Them</h2> <p>We can accommodate for this use case with a simple custom type that is composed of a <code>Date</code> and a <code>TimeZone</code>.</p> <p>I think giving <code>Date</code> that name was a mistake, with <code>Moment</code> or <code>Instant</code> or another synonym being a much better and more descriptive fit for what it does, and <code>Date</code> being the ideal name for this new type, but since a Foundation rewrite probably isn’t on the horizon I’ve been content with calling it <code>LocalizedDate</code>.</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="2"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">struct</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">LocalizedDate</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: Codable {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">var</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> date</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> Date</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">var</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> timeZone</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> TimeZone</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p>A great thing about this setup is that we can still continue relying on <code>Calendar</code> and the other Foundation goodies to perform operations and format dates.</p> <p>For operations relying on one date you just need to change the calendar or formatter’s <code>timeZone</code> and you’ll be good to go. This has the downside of requiring a mutable instance and having to write <code>mutating</code> functions, but there’s no workaround for that. For operations involving multiple dates such as <code>Calendar.isDate(_:inSameDayAs:)</code> you need a bit more work since we can have dates in different time zones and so we can’t fall back to the existing methods, but <code>DateComponents</code> gets you a lot of the way there.</p> <p>As for the actual archival, this is where things get interesting.</p> <p>Swift will autosynthesise a <code>Codable</code> implementation for a struct where all of its stored properties are <code>Codable</code> as well, which means that we get conformance for free just be declaring it. Here’s what serialising an instance of our new <code>LocalizedDate</code> type produces:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="3"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> date </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">LocalizedDate</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">date</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: now, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">timeZone</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: .</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">current</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> encoder </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">JSONEncoder</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">()</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">encoder.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">outputFormatting</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">prettyPrinted</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> json </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">try</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">!</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> encoder.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">encode</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">date</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">print</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">String</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">decoding</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: json, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">as</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">UTF8</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">self</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">))</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">// {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">// &quot;date&quot; : 631132200,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">// &quot;timeZone&quot; : {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">// &quot;identifier&quot; : &quot;Asia\/Kolkata&quot;</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">// }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">// }</span></span></span></code></pre> <p>This is composed of the individual <code>Codable</code> conformances for <code>Date</code> and <code>TimeZone</code>, and as such you can rely on it to keep working regardless of any future changes to date math or time zones.</p> <p>If you export data in this format though, you’ll have to require any importers to also read it in the same way. While the individual components themselves are based on standards, the shape is not.</p> <p>As it happens though there is a standardised format that lets you specify the date with a time zone, and there’s some support for it within Foundation too: <a href="https://www.iso.org/iso-8601-date-and-time-format.html" title="The official website for the ISO-8601 standard">ISO-8601</a>.</p> <p>Foundation ships with an <code>ISO8601Formatter</code> which, as it says on the tin, lets you convert dates to and from this format. While there are multiple representations within the standard for different date and time related quantities, the one we care for is the default setup, and here’s how it works:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="4"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> isoFormatter </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">ISO8601Formatter</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">()</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">isoFormatter.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">timeZone</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">current</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">isoFormatter.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">string</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">from</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: now</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">// 2021-01-01T00:00:00+05:30</span></span></span></code></pre> <p>Instead of the time interval for the <code>Date</code> and the name of the <code>TimeZone</code>, this representation uses a string with the date, time, and time zone offset.</p> <p>It’s <em>mostly</em> the same information, and in a standardised representation. One big difference though is that a time zone offset is a fixed difference from UTC, whereas a time zone is more abstract. While it will have a fixed offset for a fixed point in time, a time zone can have different offsets at different points in time, be it temporary (daylight savings) or permanent (a timezone can change its offsets). This means that while storing the offset does accurately represent that point in time, any date maths has based off of it the potential to break in the future.</p> <p>There is also another issue with using this representation, and this one I’m inclined to actually label a bug: When parsing a string, the <code>ISO8601Formatter</code> returns just a <code>Date</code>, which, as we’ve discussed already, is a fixed point in time, devoid of any time zone information. If any time zone information is present in the string it is used to calculate the correct time (or else the formatter’s <code>timeZone</code> is used), but then it is discarded.</p> <p>I don’t know why it works this way, but given that it does, you’d have to rely on any importers, including your own code, to be aware of this behaviour and manually parse the timezone information. It still gives you the correct <code>Date</code> no matter what, but this is another drawback worth noting.</p> <p>So, in terms of both correctness and for practical reasons, while it does require adding a whole new type and will require a bit more work from anyone importing your data, something like the <code>LocalizedDate</code> type is your best bet, at least until we have a better standard.</p> <p>As of this writing there is a proposal in place to <a href="https://tc39.es/proposal-temporal/docs/" title="The Temporal ECMAScript proposal">add a similar type to ECMAScript</a>, (h/t to <a href="https://twitter.com/sindresorhus/status/1381078393899245570" title="Sindre Sorhus’s tweet about the ECMAScript Temporal proposal">Sindre Sorhus</a>), and its <code>ZonedDateTime</code> type looks an awful lot similar to <code>LocalizedDate</code> as defined here<sup id="fnref-5"><a href="#fn-5" class="footnote-ref">5</a></sup>. Hopefully Swift gains something similar in the future as well.</p> <h2>Conclusion</h2> <p>Dates and times form a massively complicated subject that I cannot hope to cover in its entirety, but I hope this post was helpful in understanding some of the basics.</p> <p>The <a href="https://developer.apple.com/documentation/foundation/dates_and_times" title="Apple’s official Dates and Times documentation">Foundation documentation</a> is pretty solid so it’s a good place to start if you want to learn more.</p> <p>While Foundation itself is closed source, <a href="https://github.com/apple/swift-corelibs-foundation/" title="Swift CoreLibs Foundation on GitHub">swift-corelibs-foundation</a> is an open source project from Apple that aims to bring parity with Foundation in a pure Swift project. Notably it is still a work in progress, and so not all of the API has been ported over yet.</p> <p>I’d also recommend taking a look at <a href="https://yourcalendricalfallacyis.com" title="Your Calendrical Fallacy Is">Your Calendrical Fallacy Is</a>, which debunks some beliefs you might have about calendars, in case you still need to be sold on the idea of relying on the system types for any date math.</p> <style class="grvsc-styles"> .nikso-plus { background-color: #ffffff; color: #24292e; } .nikso-plus .grvsc-ttxYAU-7 { color: #D73A49; } .nikso-plus .grvsc-ttxYAU-1 { color: #24292EFF; } .nikso-plus .grvsc-ttxYAU-5 { color: #005CC5; } .nikso-plus .grvsc-ttxYAU-4 { color: #6F42C1; } .nikso-plus .grvsc-ttxYAU-3 { color: #6A737D; } .nikso-plus .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(0, 0, 0, 0.05)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(0, 0, 0, 0.2)); } /* Night Owl (No Italics) */ @media (prefers-color-scheme: dark) { .grvsc-mm-tGNH8N { background-color: #011627; color: #d6deeb; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-3 { color: #C792EA; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-1 { color: #D6DEEB; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-9 { color: #7FDBCA; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-5 { color: #C5E478; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-7 { color: #82AAFF; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-6 { color: #637777; } .grvsc-mm-tGNH8N .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); } } </style> <div class="footnotes"> <hr> <ol> <li id="fn-1"> <p>The only item on my desktop is a Playground called “Scratchpad” used exactly as the name suggests.</p> <p>Full disclaimer that I stole this idea from <a href="https://twitter.com/jaredsinclair" title="Jared Sinclair’s Twitter">Jared Sinclair</a>.<a href="#fnref-1" class="footnote-backref">↩</a></p> </li> <li id="fn-2"> <p>And maybe even a <a href="https://www.livescience.com/earth-spinning-faster-negative-leap-second.html" title="Live Science article about the Earth’s speed of rotation">negative leap second</a> soon.<a href="#fnref-2" class="footnote-backref">↩</a></p> </li> <li id="fn-3"> <p>At that time, at least. The difference is -13:30 when DST is active.<a href="#fnref-3" class="footnote-backref">↩</a></p> </li> <li id="fn-4"> <p>Or behaviour, depending on how you choose to look at it.<a href="#fnref-4" class="footnote-backref">↩</a></p> </li> <li id="fn-5"> <p>It also includes a calendar optionally, whereas with the Foundation setup dates are almost always interpreted with reference to the currently selected one. I can see the utility but I’ve also never given much thought to the idea of a single corpus of data having multiple calendars, so I can’t wait to see the edge cases that creates.<a href="#fnref-5" class="footnote-backref">↩</a></p> </li> </ol> </div><![CDATA[Swiftʼs Collection Types]]>https://harshil.net/blog/swift-sequence-collection-arrayhttps://harshil.net/blog/swift-sequence-collection-arrayThu, 06 Aug 2020 03:30:00 GMT<p>A while ago, Paul Hudson posted <a href="https://twitter.com/twostraws/status/1273378038760247298" title="Paul Hudson’s tweet">a poll on Twitter</a>, asking people what topic they found most difficult when they’d started learning Swift. An overwhelming number of responses were for generics, with a bunch of votes for optionals, closures, and some for the pattern matching techniques<sup id="fnref-1"><a href="#fn-1" class="footnote-ref">1</a></sup>.</p> <p>The thing that had me personally scratching my head for hours and hours on end was the whole suite of protocols included in the standard library backing common data structures such as Array, Set, Dictionary, and String.</p> <p>I ran into the limitations or found a use case for all of the others, forcing me to spend some time studying them up, but the Sequence protocols offered had an escape hatch: The concrete types. The four aforementioned types covered about 99% of my use cases, and for a significant part of the remaining percent, building a wrapper around an Array was simple enough in a pinch. And for the rare case where I ended up with a <code>Slice</code> or a <code>Substring</code>, it was fairly easy to get back to familiar territory by just wrapping it in the appropriate type.</p> <p>Since those days I’ve spent quite a bit of time understanding those protocols and so this post is an attempt to explain the functionality of some of the most important ones, and the reason for their existence.</p> <h2>Why So Many Protocols</h2> <p>Before going into the what and how, in this case I think it’s important to look at the why first. There are two largely interrelated reasons behind this elaborate structure we’re just about to dive into:</p> <h3>Designing API for minimum requirements</h3> <p>The extremely granular nature of the protocol hierarchy means that you can design your API to accept data that matches your functional and performance requirements, rather than just the one concrete type you had in mind at the time.</p> <p>Aside from letting you provide an implementation that works with multiple concrete types that also meet those requirements, it also allows your API’s users to use their own custom types, ones that you couldn’t have even thought of — and because of this design, also don’t need to think about.</p> <p>Because Swift isn’t widely used in Apple’s own frameworks so far there aren’t many first-party examples of this, but a good one is <code>ForEach</code> in SwiftUI. Rather than just an Array, the data argument can be of any type that conforms to <code>RandomAccessCollection</code>:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="0"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">struct</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">ForEach</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">init</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">&lt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">Data</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">, </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">Content</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">_</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-14 grvsc-tGNH8N-9">data</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: Data, </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">content</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">@escaping</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> (Data.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Element</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">) </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> Content</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">where</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> Data</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">RandomAccessCollection</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">, </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> Data.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Element</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> Identifiable,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> Content</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> View {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">// implementation</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p>At this time of writing <code>RandomAccessCollection</code> has 43 conforming types across all of Apple’s SDKs, all of which can now be used with <code>ForEach</code> because of this design choice.</p> <h3>Shared and specialised implementations</h3> <p>Just as you can use the exact protocol required to design your APIs, so does the standard library, offering a range of algorithms appropriate for the given protocol.</p> <p>The first benefit of this approach is that the concrete types themselves can be lightweight as an adopter only needs to implement behaviour specific to the data structure they’re designing. Simply conforming to the appropriate protocols gives you access to a range of functions such as <code>allSatisfy</code>, <code>map</code>, <code>filter</code>, etc. with no extra cost. But what makes it especially worthwhile is being able to customise implementations as needed by overloading functions.</p> <p>Swift doesn’t allow for overloading functions with the same name, arguments, and return value within the same type, however a type can overload a function from another type that it inherits from.</p> <p>Written out in code, this is what it means:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="1"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">protocol</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">A</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {}</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">extension</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">A</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">func</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">display</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">()</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">print</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">A</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span> <span class="grvsc-line"><span class="grvsc-source"></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">protocol</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">B</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: A {}</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">extension</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">B</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">func</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">display</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">()</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">print</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">B</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span> <span class="grvsc-line"><span class="grvsc-source"></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">struct</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">C</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: B {}</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">C</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">()</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">display</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">()</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">// prints &quot;B&quot;</span></span></span></code></pre> <p>While <code>C.display()</code> can point to either implementation of the <code>display</code> function, because <code>B</code> inherits from <code>A</code>, the compiler will prefer the implementation from <code>B</code><sup id="fnref-2"><a href="#fn-2" class="footnote-ref">2</a></sup>.</p> <p>Additionally, it is possible for overloaded implementations to have different return types, and the compiler will pick the correct implementation based on the context of the call site.</p> <p>This is where the real magic happens. The lower levels of the protocol hierarchy offer little information, and so while certain algorithms can be offered, their implementations are necessarily constrained by this knowledge limitation. However, as we go move through the hierarchy and more information is made available, the implementations can be refined and made more performant, while remaining transparent to us Swift users, for the most part, thanks to overloaded implementations and type inference.</p> <h2>Sequences &#x26; Iterators</h2> <p>Sequences and Iterators are the foundation on top of which all the other protocols and concrete types are built.</p> <p><code>IteratorProtocol</code> is a protocol, as the name suggests<sup id="fnref-3"><a href="#fn-3" class="footnote-ref">3</a></sup>, and despite its rather fundamental nature, implementing one is fairly straightforward, with just one requirement:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="2"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">protocol</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">IteratorProtocol</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">associatedtype</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Element</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">mutating</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">func</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">next</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">()</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Element</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">?</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p>Every time <code>next</code> is called, the iterator can return an instance of the type <code>Element</code>, or nil. Once it returns <code>nil</code>, the iterator is considered to be finished, having exhausted all its elements. The function is marked as mutating because all of the state required to derive the next value is held by the iterator itself, and so any time a value is vended, the iterator can use that as an opportunity to update its internal state in preparation for the next call.</p> <p>There’s no stipulation that you have to return <code>nil</code>; this is intentional, and allows you to create infinite iterators.</p> <p>While you probably don’t often create iterators directly, you do use them indirectly. A <code>for ... in</code> loop, for example, works by creating an iterator under the hood. Essentially every eager operation on a sequence (more on this in a bit) relies on the creation of an iterator.</p> <p>An iterator thus is a one-way ordered walk through a list of elements.</p> <p>The <code>Sequence</code> protocol has a bunch of requirements, most of which have sensible default implementations, and so a simplified representation of the minimal requirements is as shown below:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="3"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">protocol</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">Sequence</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">associatedtype</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Element</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">associatedtype</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Iterator</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">IteratorProtocol</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">where</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Iterator</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Element</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">==</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Element</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">func</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">makeIterator</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">()</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Iterator</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p>In essence, a sequence’s sole job is to vend an iterator. There’s also an additional convenience built in, where an <code>IteratorProtocol</code> can also conform to <code>Sequence</code> with no extra code, by returning itself in the <code>makeIterator</code> function:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="4"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">extension</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Sequence</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">where</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> Self.Iterator == Self {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">public</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">func</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">makeIterator</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">()</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">Self</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">self</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p>While you don’t have to add much or any code on top of your <code>IteratorProtocol</code> conformance to get a sequence, you get a fair amount of power with it. Functions such as <code>prefix</code>, <code>dropFirst/Last</code>, <code>reversed</code>, <code>contains</code>, <code>map</code>, <code>filter</code>, and many more are made available at the sequence level. However because of the fairly low amount of knowledge available to us at this level, in that you can only ask for the next element one at a time, the implementations must do just that.</p> <p><code>dropFirst(_:)</code>, for instance, gives you a new iterator, which iterates over the base iterator after eagerly executing and discarding as many values as specified.</p> <p><code>dropLast(_:)</code>, meanwhile, traverses the whole sequence, adding every encountered value to an Array, thus having a time and space complexity of O(<em>n</em>).</p> <h2>Collection</h2> <p>While in practise it is often used that way, <code>Sequence</code> doesn’t make any promises regarding repeated access. To get the guarantee of repeated access, you need to conform to the <code>Collection</code> protocol.</p> <p><code>Collection</code> has a bunch more requirements, but a simplified representation of them is as below:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="5"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">protocol</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">Collection</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: Sequence {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">associatedtype</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Element</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">associatedtype</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Index</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Comparable</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">associatedtype</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Indices</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Collection</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">associatedtype</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">SubSequence</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Collection</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">var</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> startIndex</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Index</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">get</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">var</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> endIndex</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Index</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">get</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">var</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> indices</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Indices</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">get</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">func</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">index</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">after</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-14 grvsc-tGNH8N-9">i</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Index</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Index</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">subscript</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">position</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Index</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Element</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">get</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">subscript</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">bounds</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Range</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">&lt;</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Index</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">&gt;</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">SubSequence</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">get</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p>The fundamental improvement over <code>Sequence</code> is that a collection lets you access values at any given position, rather than just the one after the one you requested last.</p> <p>Note here that the <code>Index</code> type isn’t <code>Int</code>, it can be any type that conforms to <code>Comparable</code>. Another important thing here is that you’re never supposed to alter or form indices yourself, you should always ask the collection instance you intend to subscript to do that for you. This ensures that the index you receive is valid, and in some cases it’s required to the point where you can’t even construct an index without querying the collection.</p> <p>So to enable you to calculate indices, a collection of all of the collection’s valid indices is exposed, as are the starting and the ending indices (which is the index after the last valid one; i.e. it is the first invalid index). The index right after a particular one can also be calculated using the <code>index(after:)</code> function. There is also an <code>index(_:offsetBy:)</code> convenience function which lets you compute the index at some positive integral offset, along with two <code>firstIndex(where:)</code> and <code>firstIndex(of:)</code> functions which let you find the first index matching some condition, or containing some value, respectively.</p> <p>You can use these indices to subscript values either individually or as a range. Going back to the point about always asking the collection to form an index for you: it is technically considered undefined behaviour to subscript a collection with an index that wasn’t derived from it.</p> <p>Subscripting a single index returns a single value. Subscripting a range of indices doesn’t return an Array, however, but rather a <code>SubSequence</code>. A collection can use a custom subsequence type, but the standard library provides a good default with the <code>Slice</code> type. The requirements for using a custom <code>SubSequence</code> type are that it must be a collection itself having the same index and element types as the collection, and it must be its own subsequence, which means that if a subsequence is further subscripted, it shouldn’t generate another new type, but rather a new instance of itself.</p> <p>Slices store the entirety of their backing collections and information regarding the subset of them to be used. Thanks to Swift’s copy-on-write behaviour for all of the standard library collections, this generally doesn’t involve creating an actual copy, but rather the compiler manages multiple references to the same underlying storage transparently, creating actual copies only as needed, when a mutation occurs. This retains value semantics from a Swift developer’s perspective, while optimising memory usage as well. This is where a lot of the power of collections comes into play, allowing you to subscript a range of values with little memory overhead.</p> <p>Consider the <code>dropLast(_:)</code> algorithm from earlier. If your type conforms to <code>Collection</code>, in addition to the <code>Sequence.dropLast(_:)</code> function which returns an Array of elements, you’ll also have at your disposal an additional <code>Collection.dropLast(_:)</code> function which returns a subsequence of your collection, for example an <code>ArraySlice</code> for Arrays. The implementation is as below:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="6"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">extension</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Collection</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">public</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">func</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">dropLast</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">_</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-14 grvsc-tGNH8N-9">k</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Int</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">1</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">SubSequence</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">_precondition</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> k </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">&gt;=</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">Can&#39;t drop a negative number of elements from a collection</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> amount </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> Swift.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">max</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, count </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> k</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> end </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">index</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">startIndex, </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">offsetBy</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: amount, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">limitedBy</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: endIndex</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">??</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> endIndex</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">self</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">[startIndex</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">..&lt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">end]</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p>The <code>Collection.dropLast(_:)</code> algorithm computes a new end index by offsetting the startIndex <code>n - k</code> times, where <code>n</code> is the size of the collection, and <code>k</code> is the number of elements to be dropped, and then subscripts the collection over this new range. This still has a time complexity of O(<em>n</em>), as the <code>index(_:offsetBy:)</code> function itself needs to call <code>index(after:)</code> function <code>count - k</code> times, however the underlying storage is shared until there are any mutations, reducing storage complexity to O(1).</p> <p>One thing to keep in mind when using them is that subsequences share indices with their parent collections. Consider the following snippet:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="7"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> list </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> [</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">0</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">1</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">2</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">3</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">4</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">]</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> withoutFirst </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> list.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">dropFirst</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">()</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">print</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">withoutFirst</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">[</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">])</span></span></span></code></pre> <p>You might assume that it would print <code>1</code>, but in fact it crashes, logging an out of bounds access fatal error. This is because the zeroth index isn’t part of the collection’s indices at all, since it has been dropped. The <code>indices</code> of the <code>withoutFirst</code> subsequence are <code>1 ..&#x3C; 4</code>. You can still request the first element using the <code>first</code> property, and you can fetch elements at an arbitrary offset by computing the index using the aforementioned <code>index(_:offsetBy:)</code> function.</p> <h2>BidirectionalCollection</h2> <p><code>BidirectionalCollection</code> is one of those types where the name really tells you the whole story. It’s a collection that can also be traversed in the opposite direction, from back to front.</p> <p>As such, conforming to <code>BidirectionCollection</code> is fairly straightforward. If your type conforms to <code>Collection</code>, you only need to add one single new function. As you specialise your collection further, you need to update the <code>Indices</code> and <code>SubSequence</code> types to also conform to the same protocols, and so in this case they must both be upgraded to be bidirectional collections as well.</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="8"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">protocol</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">BidirectionalCollection</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: Collection {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">func</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">index</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">before</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-14 grvsc-tGNH8N-9">i</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Index</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Index</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p>In addition to computing the index before a given one, this conformance also lets you pass in negative distances in the aforementioned <code>index(_:offsetBy:)</code> function (whereas <code>Collection</code> would crash if you did so), and exposes a convenient <code>last</code> property, mirroring the <code>first</code> available on all collections. Additionally, there are <code>lastIndex(where:)</code> and <code>lastIndex(of:)</code> functions to match the corresponding <code>first...</code> functions on <code>Collection</code>.</p> <p>Going back to the <code>dropLast(_:)</code> algorithm from the earlier two examples, the implementation is further refined here. Rather than advancing the startIndex <code>n - k</code> times, it moves the endIndex backwards <code>k</code> times, further reducing time complexity down to O(<em>k</em>)<sup id="fnref-4"><a href="#fn-4" class="footnote-ref">4</a></sup>.</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="9"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">extension</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">BidirectionalCollection</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">public</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">func</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">dropLast</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">_</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-14 grvsc-tGNH8N-9">k</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Int</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">SubSequence</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">_precondition</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> k </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">&gt;=</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">0</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">Can&#39;t drop a negative number of elements from a collection</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> end </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">index</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> endIndex,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">offsetBy</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">k,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">limitedBy</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: startIndex</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">??</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> startIndex</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">self</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">[startIndex</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">..&lt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">end]</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p><code>suffix(_:)</code> is also similarly faster, requiring only the traversal of the last <code>k</code> elements directly, rather than traversing the whole collection from start to end.</p> <p>Another algorithm that gets a refined implementation is <code>reversed()</code>. <code>Sequence</code> has an implementation of <code>reversed()</code> as well, one which eagerly executes the whole sequence to form an Array, and then traverses it using two indices, one from the start and one from the end, swapping items until the indices meet in the middle. This approach has an O(<em>n</em>) hit in terms of both time and storage complexity.</p> <p>Instead of an Array, calling <code>reversed()</code> on a <code>BidirectionalCollection</code> gets you a <code>ReversedCollection</code> which is generic over the type that you reversed i.e. <code>Array.reversed()</code> gives you a <code>ReversedCollection&#x3C;Array></code>.</p> <p>Rather than eagerly iterating through the collection and manually reversing its contents, this <code>ReversedCollection</code> operates lazily, simply storing the whole collection, and converting indices as need be. Going back to the earlier point about copy-on-write semantics, if you’re reversing a standard library collection or a custom type that implements that behaviour, this also means that you don’t pay any cost for the storage either, reducing execution complexity across the board for the initial reversal to O(1).</p> <p>It defines a custom <code>Index</code> type which wraps the index of the <code>base</code> collection being reversed, and this index type cannot be instantiated by you from the outside; you can only use the start and end indices combined with the <code>index(after:)</code>, <code>index(_:offsetBy:)</code>, and related functions to form new indices. Collection algorithms such as <code>prefix(_:)</code>, <code>suffix(_:)</code>, <code>dropFirst(_:)</code>, etc. use the appropriate functions under the hood though, so you rarely need to compute indices directly.</p> <p>At the same time, there are cases where you might actively want such the Sequence function’s behaviour — if, for instance, you have another function that requires an Array as a parameter, or if you really want the integer-based subscripting that Array provides — and you can always opt into it by manually spelling out your variable’s type as <code>Array&#x3C;Element></code>, which will make the compiler pick the eager <code>Sequence</code> implementation.</p> <h2>RandomAccessCollection</h2> <p><code>RandomAccessCollection</code> is a bit of a weird protocol, in that it’s requirements aren’t really representable within the type system. It is purely a performance guarantee, and one that can’t be verified by the compiler.</p> <p><code>Collection</code> only requires that you be able to move forward, <code>BidirectionalCollection</code> requires the ability to move in either direction, and <code>RandomAccessCollection</code> requires that those moves be in constant time.</p> <p>To conform to the protocol, you need to update your <code>BidirectionalCollection</code>’s <code>Indices</code> and <code>SubSequence</code> associated types to also conform to <code>RandomAccessCollection</code>, but that’s it in terms of new requirements.</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="10"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">protocol</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">RandomAccessCollection</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: BidirectionalCollection {}</span></span></span></code></pre> <p>Just doing this will make your code compile, however it isn’t semantically correct at this point. You also need to make sure the <code>index(_:offsetBy:)</code> function we’d talked about earlier operates in constant time. If your <code>Index</code> type conforms to the <code>Strideable</code> protocol, you get this behaviour for free as well.</p> <p>Unlike the previous protocols, <code>RandomAccessCollection</code> doesn’t really unlock any new functionality. Because its requirements are entirely performance-related, so are the gains. Forming an index at a given offset from another becomes a constant time operation, which means that algorithms such as <code>dropFirst/Last(_:)</code>, <code>prefix(_:)</code>, <code>suffix(_:)</code>, all become constant time operations as well.</p> <p>The primary benefit of this protocol thus is for API authors to communicate that their algorithms require this higher level of performance. As discussed above, SwiftUI’s <code>ForEach</code> is a great example of where this might be useful. SwiftUI automatically diffs and updates all your views as your state updates, and so especially for long lists, being able to calculate those diffs quickly thanks to the constant time guarantee is critical.</p> <p>At this point you might be thinking that this sounds great, why would you ever <em>not</em> want to conform to this protocol? An example for this is String.</p> <p>If you’re familiar with how Unicode strings work, you know that lots of characters are actually composed from multiple individual “scalars”. These scalars can be characters in their own right, and sometimes they need to be conjoined with others using to have valid meaning. Diacritics are a good example of this; “á” and “é” can both be formed by conjoining “a” and “e” with the same diacritic<sup id="fnref-5"><a href="#fn-5" class="footnote-ref">5</a></sup>. Similarly, lots of emoji are composed. All the various skin toned emoji like 👍🏽, 👎🏽, and 👋🏽 are formed by conjoining their default yellow forms with a skin tone modifier. Family emoji such as 👨‍👩‍👧‍👦 are also formed by composing the various standalone people emoji such as 👨, 👩, 👧, and 👦.</p> <p>Because of this behaviour where a character doesn’t have a fixed sized, it isn’t possible to go to the any given position in a String without traversing every single position before it, and thus, String cannot conform to <code>RandomAccessCollection</code>.</p> <p>Another example from the standard library is the <code>LazyFilterCollection</code>, which is what you obtain if you lazily filter a collection. Because of its lazy operation, it simply stores the whole base collection being filtered, and so must iterate through it every item in the base collection to check if it exists in the filtered version.</p> <h2>MutableCollection</h2> <p>So far in this post we have only discussed reading values from a collection, and seen nothing about writing to one.</p> <p>Despite what the name might suggest, <code>MutableCollection</code> isn’t required to make collections mutable. Mutability is still enforced by <code>let</code> and <code>var</code> declarations and using value versus reference types, and you can implement custom <code>mutating</code> functions that don’t require this protocol. All the concrete standard library collections you’ve worked with such as String or Dictionary necessarily have to be mutable to be useful, but that doesn’t mean that they conform to <code>MutableCollection</code>; in fact, both of those types don’t.</p> <p>MutableCollection has a very specific semantic requirement, in that it allows for specific positions of a collection to be mutated while maintaining its length. Additionally, it requires that any such mutations retain the positions they were made at. Like with <code>RandomAccessCollection</code>, there’s no way the compiler can guarantee this, so it is up to implementers to maintain this behaviour.</p> <p>To conform to the <code>MutableCollection</code> protocol, you need to update your <code>Collection</code>’s subscripts to also be writeable:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="11"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">protocol</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">MutableCollection</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: Collection {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">subscript</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">position</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Index</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Element</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">get</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">set</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">subscript</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">bounds</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Range</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">&lt;</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Index</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">&gt;</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">SubSequence</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">get</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">set</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p>This seems like a very limited set of mutations but it unlocks a whole range of functionality. The most simple of the bunch is the <code>swapAt(_:_)</code> operation, which, lets you swap the elements at two indices.</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="12"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">extension</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">MutableCollection</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">public</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">mutating</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">func</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">swapAt</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">_</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-14 grvsc-tGNH8N-9">i</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Index</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">, </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">_</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-14 grvsc-tGNH8N-9">j</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Index</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">guard</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> i </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">!=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> j </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">else</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> tmp </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">self</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">[i]</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">self</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">[i] </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">self</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">[j]</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">self</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">[j] </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> tmp</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p>You can also sort a mutable collection in place, and reverse or shuffle it as well, and you can partition it, making it so that elements matching a given predicate are all placed after elements that don’t.</p> <p>Like with <code>RandomAccessCollection</code>, to understand why this protocol exists, you need to look not at conforming types, but types that don’t conform to it.</p> <p>Going back to the example from that section, String also doesn’t conform to <code>MutableCollection</code>, and for the same reasons that it doesn’t conform to <code>RandomAccessCollection</code>. Because a String is made up of characters of variable size, replacing a character with another might change the length of the whole collection. String implements a custom <code>Index</code> type, which holds private information about the corresponding position in the underlying storage buffer. As such, replacing a character with one of a different length would invalidate all indices after it, making them point to invalid or incorrect data.</p> <p>It’s worth noting though that <code>MutableCollection</code> isn’t just a requirement on fixed sized elements.</p> <p>While subscripting a Dictionary using the <code>Key</code> produces a value of type <code>Element?</code>, these aren’t actually the Dictionary’s Index and Element types. These subscripts are overloads on the regular collection ones. Dictionary’s Index type is a custom <code>Index</code> like with String, and its Element type is a tuple of the type <code>(Key, Value)</code><sup id="fnref-6"><a href="#fn-6" class="footnote-ref">6</a></sup>. As such, replacing a given element would make it possible to potentially create multiple entries with the same key, and any attempt to remove that duplication would require removing all but one value, thereby altering the collection’s length, and so Dictionary also doesn’t conform to <code>RandomAccessCollection</code>.</p> <h2>RangeReplaceableCollection</h2> <p>We’re back to familiar territory with the protocol doing exactly what it says on the tin: <code>RangeReplaceableCollection</code> lets you replace all elements within a given range with elements from another collection.</p> <p>Conforming to the protocol is fairly simple. You need to define an initialiser to create an empty collection, and one other function:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="13"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">protocol</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">RangeReplaceableCollection</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: Collection {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">init</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">()</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">mutating</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">func</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">replaceSubrange</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">&lt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">C</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">(</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">_</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-14 grvsc-tGNH8N-9">subrange</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Range</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">&lt;</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Index</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">&gt;,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">with</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-14 grvsc-tGNH8N-9">newElements</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: C</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">where</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> C: Collection, C.Element == Element</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p>While it may appear similar to the settable range subscript from <code>MutableCollection</code>, not only can ‌<code>replaceSubrange(_:with:)</code> accept any arbitrary collection, it is also allowed to change the length of the collection. It is perfectly legal to replace a range with a collection that is shorter or longer, whereas that would be incorrect behaviour for <code>MutableCollection</code>, and would even crash if you attempted to do so using the default implementation.</p> <p>This one simple function opens the door to a lot of functionality. You can for instance remove a range of values, by passing in an empty collection as the replacement. You can also insert values at any arbitrary <code>index</code>, which is accomplished by making it so that the range to be replaced is <code>index ..&#x3C; index</code>. And you can append values to a collection by making it so that the index at which values are inserted is the <code>endIndex</code>.</p> <p>One thing to note is that while <code>MutableCollection</code> and <code>RangeReplaceableCollection</code> may appear interrelated, they both extend <code>Collection</code>, and can be implemented individually.</p> <p>We saw that String for instance cannot implement <code>MutableCollection</code>, but it does however conform to <code>RangeReplaceableCollection</code>. Inserting, removing, or performing any other mutations on a String could change the length and invalidate indices, however this is legal behaviour for <code>RangeReplaceableCollection</code>, and so String can safely conform to the protocol.</p> <p>Dictionary, however, does not conform to <code>RangeReplaceableCollection</code>, since it is a requirement that any replaced items should remain at the same indices they were placed in, and this wouldn’t be possible to achieve for dictionaries while retaining their core functionality of only having a single value for any given key: If a duplicate key was inserted, it would need to remove either the new or old one, which would necessarily break other indices.</p> <h2>Conclusion</h2> <p>That about covers all of the public types making the standard library’s <code>Collection</code> protocol hierarchy<sup id="fnref-7"><a href="#fn-7" class="footnote-ref">7</a></sup>.</p> <p>While they can seem a bit daunting at first and take some time to imbibe, I hope this article helped in explaining why the various protocols exist, and the kinds of refinements they enable.</p> <p>At the same time, it’s worth considering some of the drawbacks of this approach too.</p> <p>You could argue that the granularity of the protocols is too fine and creating too complex a structure that isn’t well understood and thus underused in the community at large. It’s really common for instance to see sequence algorithms being written as extensions on Array or some other concrete type, when you can often generalise them out to one or some combination of these protocols, allowing them to also be used by any other conformances without writing a single extra line of code, and also allowing you to use them on subsequences as well.</p> <p>At the same time you could also argue that the granularity isn’t fine enough. While you rarely interact with them directly, it does feel a bit off for unordered types such as Dictionary and Set to have a <code>firstIndex</code> and a <code>lastIndex</code>. Both of those types also have mutation implemented using custom-designed, bespoke functions not belonging to any of the protocols we’ve seen.</p> <p>This one is more of an issue with protocols in general rather than the hierarchy as it is, but consider also the fact that types can only conform to a protocol in one way. The standard library doesn’t currently have one, but it isn’t too outrageous to imagine it having a collection to represent binary trees. Generally these can be traversed in a pre-order, in-order, and post-order fashion, with none being more valid than the other. How would such a behaviour fit with the Collection or Sequence protocols, which require one single implementation? Like with Dictionary and Set, you would probably need some custom logic and code here, as it doesn’t really neatly fit in with the model at hand.</p> <p>For further reading, I’d highly recommend diving into the source code. While Swift itself is written in C++, the standard library is (mostly) written in idiomatic Swift, and the directory containing sources for public API can be <a href="http://github.com/apple/swift/blob/main/stdlib/public/core" title="The Swift standard library source directory on GitHub">found here</a>. A great place to start is with the source for the <a href="http://github.com/apple/swift/blob/main/stdlib/public/core/Sequence.swift" title="The source code for the Sequence protocol">Sequence</a> and <a href="http://github.com/apple/swift/blob/main/stdlib/public/core/Collection.swift" title="The source code for the Collection protocol">Collection</a> types. Additionally, I’d recommend watching Dave Abrahams’s excellent <a href="https://developer.apple.com/videos/play/wwdc2018/223/" title="Embracing Algorithms, session 223 from WWDC 2018">Embracing Algorithms</a> session from WWDC 2018, which was what inspired me to dive into this subject in the first place.</p> <style class="grvsc-styles"> .nikso-plus { background-color: #ffffff; color: #24292e; } .nikso-plus .grvsc-ttxYAU-7 { color: #D73A49; } .nikso-plus .grvsc-ttxYAU-1 { color: #24292EFF; } .nikso-plus .grvsc-ttxYAU-4 { color: #6F42C1; } .nikso-plus .grvsc-ttxYAU-14 { color: #E27F2D; } .nikso-plus .grvsc-ttxYAU-5 { color: #005CC5; } .nikso-plus .grvsc-ttxYAU-3 { color: #6A737D; } .nikso-plus .grvsc-ttxYAU-8 { color: #22863A; } .nikso-plus .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(0, 0, 0, 0.05)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(0, 0, 0, 0.2)); } /* Night Owl (No Italics) */ @media (prefers-color-scheme: dark) { .grvsc-mm-tGNH8N { background-color: #011627; color: #d6deeb; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-3 { color: #C792EA; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-1 { color: #D6DEEB; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-9 { color: #7FDBCA; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-37 { color: #D9F5DD; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-7 { color: #82AAFF; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-5 { color: #C5E478; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-6 { color: #637777; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-19 { color: #ECC48D; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-8 { color: #F78C6C; } .grvsc-mm-tGNH8N .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); } } </style> <div class="footnotes"> <hr> <ol> <li id="fn-1"> <p><a href="http://fuckingifcaseletsyntax.com" title="Fucking If Case Let Syntax">For your future reference</a> (<a href="http://goshdarnifcaseletsyntax.com" title="Gosh Darn If Case Let Syntax">SFW version</a>).<a href="#fnref-1" class="footnote-backref">↩</a></p> </li> <li id="fn-2"> <p>One caveat here is that if <code>C</code> is stored in a variable typed specifically as <code>A</code>, then <code>display()</code> will use <code>A</code>’s implementation.<a href="#fnref-2" class="footnote-backref">↩</a></p> </li> <li id="fn-3"> <p>The <code>Protocol</code> suffix seems to have persisted through the grand renaming in Swift 3.0, though I’m not quire sure why.<a href="#fnref-3" class="footnote-backref">↩</a></p> </li> <li id="fn-4"> <p>You might think here that for certain cases moving forward <code>n - k</code> times with the Collection implementation might be more performant.</p> <p>To use that function we do first need to calculate the total length, <code>n</code>, and at this point we have no guarantees that that is a constant time operation or that the value will be cached, so in general, the <code>BidirectionalCollection</code> implementation is faster.<a href="#fnref-4" class="footnote-backref">↩</a></p> </li> <li id="fn-5"> <p>A previous version of this article said that “á” and “é” only existed as composed characters. Turns out, while they can be formed by composition, they do have separate characters as well. Thanks to <a href="https://twitter.com/tonirogel/status/1291332102571593729" title="Toni Rogelʼs tweet about accented Unicode characters">Toni Rogel on Twitter</a> for this correction.<a href="#fnref-5" class="footnote-backref">↩</a></p> </li> <li id="fn-6"> <p>This is a lot to digest but covering that whole deal is a bit out of scope for this post. If you’d be interested in reading another article about how Dictionary and other standard library collections work, let me know!<a href="#fnref-6" class="footnote-backref">↩</a></p> </li> <li id="fn-7"> <p>String has a lot of it’s own apparatus because of it’s Unicode-correct nature, but it is all String-specific.<a href="#fnref-7" class="footnote-backref">↩</a></p> </li> </ol> </div><![CDATA[How the SwiftUI DSL Works]]>https://harshil.net/blog/swiftui-dsl-function-buildershttps://harshil.net/blog/swiftui-dsl-function-buildersWed, 27 May 2020 18:29:00 GMT<p>I dunno about you but I distinctly remember where I was and what I was doing when SwiftUI was announced. My first reaction upon seeing Craig Federighi show how a view controller with multiple hundred lines of code could be replaced with a single 20 line view was just pure joy<sup id="fnref-1"><a href="#fn-1" class="footnote-ref">1</a></sup>.</p> <p>My second reaction was: Wait, hang on, how the hell does that code even work?</p> <p>This was the first ever public SwiftUI sample code, and even as — or perhaps especially as — someone who has been writing Swift for a long time, it appeared mostly alien to me.</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="0"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">struct</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">Content</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> : View {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">@State</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">var</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> model </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> Themes.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">listModel</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">var</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> body</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> some View {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">List</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">model.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">items</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">action</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: model.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">selectItem</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> { item </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">in</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Image</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">item.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-7">image</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">VStack</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">alignment</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: .</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">leading</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Text</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">item.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">title</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Text</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">item.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">subtitle</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-1">color</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">gray</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p>Since then I’ve gotten some answers and come up with more questions, and this post attempts to explain why and how SwiftUI views are constructed like they are.</p> <p><em>Note: There’s one bit of syntax from that sample I’m not going to cover: The <code>@State</code> declaration there and the related property wrappers and types introduced for data propagation in SwiftUI. I’d recommend reading Jared Sinclair’s excellent <a href="https://jaredsinclair.com/2020/05/07/swiftui-cheat-sheet.html" title="”When Should I Use @State, @Binding, @ObservedObject, @EnvironmentObject, or @Environment?” by Jared Sinclair">blog post</a> to learn more about those.</em></p> <h2>The Missing <code>Return</code></h2> <p>Apart from the <code>@State</code>, the first bit of new syntax when you in that block is the <code>some</code> keyword, but we’re gonna keep that aside for now; we’re going to need to learn some other information first to understand why that is needed.</p> <p>The next new bit of syntax then is the lack of a <code>return</code>. The <code>body</code> in the snippet is meant to return a value of the type <code>some View</code>, whatever that is, but the <code>return</code> keyword is never used.</p> <p>This change was included in Swift in the open and long before WWDC, via <a href="https://github.com/apple/swift-evolution/blob/master/proposals/0255-omit-return.md">SE-0255: Implicit returns from single-expression functions</a>. The title is pretty descriptive as to what it does: It enables you to skip the <code>return</code> keyword for any functions or computed properties made of a single expression, unifying their behaviour with closures.</p> <h2>Result Builders</h2> <p>Visually it appears similar to the skipped <code>return</code>, but the next bit of syntax, involving nested components such as the <code>Image</code> and <code>VStack</code> within the <code>List</code>, are a separate and much more complex feature altogether.</p> <p>Result builders are a planned feature<sup id="fnref-2"><a href="#fn-2" class="footnote-ref">2</a></sup> that enables developers to provide a simpler interface for creating tree like structures. It can be thought of as an implicit macro system, where any declarations within a result builder call are expanded using the rules provided by the implementer.</p> <p>Every SwiftUI view which can show a bunch of other views has as part of its initialiser a parameter attributed with <code>@ViewBuilder</code>, enabling the custom result builder implementation in SwiftUI.</p> <p>For instance, this is what the initialiser for <code>VStack</code> looks like:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="1"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">struct</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">VStack</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">&lt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">Content</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">View</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">&gt;: View {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">init</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">(</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">alignment</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: HorizontalAlignment </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> .</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">center</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">, </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">spacing</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: CGFloat</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">?</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-7">nil</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">, </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> @</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">ViewBuilder</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-14 grvsc-tGNH8N-9">content</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: () </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-&gt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> Content</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-37">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p>The <code>content</code> closure there is responsible for interpreting the DSL syntax, and generating another view, of the type <code>Content</code>, which must be a <code>View</code> as well. More about this <code>Content</code> type later in the post.</p> <p>Consider this snippet below:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="2"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">VStack {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Text</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">some text</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p>It is equivalent to writing out the following:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="3"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">VStack {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> ViewBuilder.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">buildBlock</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Text</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">some text</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">))</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p>Not just the implementation, but the very existence of the various <code>buildBlock</code> calls is abstracted out.</p> <h2>Control Flow</h2> <p>While <code>if</code> is the only control flow statement that currently works in result builders, support for <code>if let</code>, <code>if case</code>, <code>switch</code>, and <code>for ... in</code> loops <a href="https://twitter.com/dgregor79/status/1240477096704503809" title="Doug Gregor’s tweet about if let, if case, and switch being added to result builders">has been added</a> <a href="https://twitter.com/dgregor79/status/1257421359870808069" title="Doug Gregor’s tweet about for ... in loops being added to result builders">to Swift</a>, and will thus likely be available in SwiftUI with the next release.</p> <p>Each of these are used by result builder types via their own methods similar to <code>buildBlock</code>, such as <code>buildIf</code>, <code>buildEither</code>, <code>buildArray</code>, and so on.</p> <p>This pretty much brings result buildersʼs control flow powers up to par with the rest of the language — other structures like <code>guard</code>, <code>repeat</code>, etc. don’t quite mesh with the goal of creating a DSL and so are not planned — but at this point you might be wondering: What’s the point? It seems like an awful lot of work to do something that we should be able to do with just vanilla Swift.</p> <p>A common refrain is that it seems like the existing <code>Array</code> syntax could be repurposed to fit this use case, without the need of inventing a whole new feature that needs reimplementation for common things like control flow. There has even been some confusion about the feature actually being exactly that under the hood: Arrays with trailing commas elided, as was pitched in the rejected <a href="https://github.com/apple/swift-evolution/blob/master/proposals/0257-elide-comma.md">SE-0257: Eliding commas from multiline expression lists</a>.</p> <p>There are two reasons this isn’t the case.</p> <p>Firstly, Array literals also don’t have support for control flow labels as well, and they would need to be built in. Building a new feature that is designed specifically for the use case of DSLs, and can thus also be extended in more ways that Array syntax can’t, seems like a better choice.</p> <p>Secondly, and more importantly: Arrays in Swift are generic over a single element type. This seems like a minor thing but it is crucial for how result builders are used in SwiftUI.</p> <h2>Type Information and Opaque Return Types</h2> <p>Unlike with an Array which requires all of its elements to be of the same type, the various <code>ViewBuilder.build...</code> methods operate on each individual element in the builder, meaning that not only can the types be different, that type information is retained and not erased down to a common base type.</p> <p>Additionally, the custom implementations of the control flow elements aren’t a drawback but rather a massive positive, as they expose the outputs of all branches to the result builder, rather than just the successful one.</p> <p>Consider this snippet below:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="4"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">struct</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">ContentView</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">: View {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> shouldShowImage</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Bool</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">var</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> body</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> some View {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> VStack {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">if</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> shouldShowImage {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Image</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">some image</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> } </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">else</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Text</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">no image for you!</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p>You might think that the <code>VStack</code> expands out to something like this:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="5"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">VStack {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">if</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> shouldShowImage {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> ViewBuilder.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">buildBlock</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Image</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">some image</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">))</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> } </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">else</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> ViewBuilder.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">buildBlock</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Text</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">no image for you!</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">))</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p>However that’s not quite how it works. Instead, the result looks more like this<sup id="fnref-3"><a href="#fn-3" class="footnote-ref">3</a></sup>:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="6"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">VStack {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> partialView</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> _ConditionalContent</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">&lt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">Image, Text</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">&gt;</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">if</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> shouldShowImage {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> partialView </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> ViewBuilder.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">buildEither</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">first</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Image</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">some image</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">))</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> } </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">else</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> partialView </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> ViewBuilder.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">buildEither</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">second</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">Text</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">no image for you!</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">))</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> ViewBuilder.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">buildBlock</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">partialView</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">)</span></span></span></code></pre> <p>This means that regardless of which paths your code follows for a particular run given some state, the type of all possible results is known beforehand, and additionally encoded into the <code>VStack</code> when the <code>ViewBuilder</code> output is used in its initialiser. The views you’re displaying may change, but the <code>Content</code> type of your <code>VStack</code> is always going to be the same.</p> <p>This is also where the <code>some View</code> from above comes into play. The <code>some</code> means that the property returns an opaque type, meaning that while it doesn’t need to specify it, the <code>body</code> property is required to return a <code>View</code> with the exact same type every time is is called, and <code>ViewBuilder</code> does all the hard work of figuring out just what type it is based on your DSL.</p> <p>The type of any view’s <code>body</code> is stored as an associated type on the <code>View</code> protocol known as <code>Body</code>. If you printed out <code>ContentView.Body.self</code>, here’s what the result looks like:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="7"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">VStack</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">&lt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">_ConditionalContent</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">&lt;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">Image, Text</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">&gt;&gt;</span></span></span></code></pre> <p>A view’s <code>Body</code> type is a direct representation of the shape of its view tree, and it is what it returned every single time a view’s <code>body</code> is fetched.</p> <p>This was a simple example, but consider now that your SwiftUI app is essentially one giant tree of nested views, the type of each of which is known at compile time<sup id="fnref-4"><a href="#fn-4" class="footnote-ref">4</a></sup>.</p> <p>This also means that regardless of the current state of every single bit of data in your app, it has the same exact structure. The exact views being rendered may change, but the underlying shape of the entire view tree remains the same.</p> <p>This is what enables SwiftUI to quickly diff your view bodies when the underlying model changes, relying on Swift’s type system rather than manual identifiers<sup id="fnref-5"><a href="#fn-5" class="footnote-ref">5</a></sup> or other heuristics to determine the difference between the existing and the new view trees.</p> <h2>Conclusion</h2> <p>Result builders are just the gateway to understanding the world of SwiftUI, and I hope this post gives you a good understanding of the DSL and why it looks and works the way it does.</p> <p>If you’d like to learn more about result builders, you can read the <a href="https://github.com/apple/swift-evolution/blob/9992cf3c11c2d5e0ea20bee98657d93902d5b174/proposals/XXXX-function-builders.md" title="Function builders draft proposal by John McCall and Doug Gregor">original draft proposal</a>, and read <a href="https://forums.swift.org/t/function-builders-implementation-progress/32981" title="Function builders implementation progress thread by Doug Gregor">this Swift forums thread</a> to follow along with progress on the implementation.</p> <p>Lastly, while result builders aren’t meant to be publicly used yet, their availability in development toolchains for all of Apple’s platforms means folks have been building a lot of cool stuff using them already, a list of which can be <a href="https://github.com/carson-katri/awesome-function-builders" title="A GitHub repository by Carson Katri keeping track of projects built using result builders">viewed on this GitHub repo</a>.</p> <p><em>Note: At the time of publication, this feature was called function builders. The article has been updated to use the new name.</em></p> <style class="grvsc-styles"> .nikso-plus { background-color: #ffffff; color: #24292e; } .nikso-plus .grvsc-ttxYAU-7 { color: #D73A49; } .nikso-plus .grvsc-ttxYAU-1 { color: #24292EFF; } .nikso-plus .grvsc-ttxYAU-4 { color: #6F42C1; } .nikso-plus .grvsc-ttxYAU-5 { color: #005CC5; } .nikso-plus .grvsc-ttxYAU-14 { color: #E27F2D; } .nikso-plus .grvsc-ttxYAU-8 { color: #22863A; } .nikso-plus .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(0, 0, 0, 0.05)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(0, 0, 0, 0.2)); } /* Night Owl (No Italics) */ @media (prefers-color-scheme: dark) { .grvsc-mm-tGNH8N { background-color: #011627; color: #d6deeb; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-3 { color: #C792EA; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-1 { color: #D6DEEB; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-9 { color: #7FDBCA; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-5 { color: #C5E478; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-7 { color: #82AAFF; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-37 { color: #D9F5DD; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-19 { color: #ECC48D; } .grvsc-mm-tGNH8N .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); } } </style> <div class="footnotes"> <hr> <ol> <li id="fn-1">As documented <a href="https://twitter.com/Harshil/status/1135624376252977152" title="A Twitter thread where I’m losing my mind about SwiftUI as it was being announced">on Twitter</a>.<a href="#fnref-1" class="footnote-backref">↩</a></li> <li id="fn-2">The proposal for the feature hasn’t passed Swift Evolution yet, and thus isn’t part of Swift proper. Apple includes an underscored implementation just for SwiftUI in the iOS 13 toolchain, which isn’t meant to be used by 3rd parties.<a href="#fnref-2" class="footnote-backref">↩</a></li> <li id="fn-3">This code won’t actually compile if you run it, since <code>_ConditionalContent</code> is a private view.<a href="#fnref-3" class="footnote-backref">↩</a></li> <li id="fn-4">You can cheat the system a bit by using an <code>AnyView</code>, which erases all type information. However this means that diffs are much more expensive, which is why it’s recommended to avoid these if you can, and limit usage to leaf nodes as much as possible.<a href="#fnref-4" class="footnote-backref">↩</a></li> <li id="fn-5">Though those are still used when displaying a list of views using a <code>ForEach</code>.<a href="#fnref-5" class="footnote-backref">↩</a></li> </ol> </div><![CDATA[Dynamic Wallpapers in macOS Catalina]]>https://harshil.net/blog/dynamic-wallpapers-in-macos-catalinahttps://harshil.net/blog/dynamic-wallpapers-in-macos-catalinaSat, 15 Jun 2019 18:29:00 GMT<p>To go with dark mode, macOS Mojave introduced a feature called “dynamic wallpapers”. Once enabled, a dynamic wallpaper would cycle between a number of related images<sup id="fnref-1"><a href="#fn-1" class="footnote-ref">1</a></sup>, showing one that was appropriate for the time of day.</p> <p>Keeping with tradition macOS Catalina includes a new default wallpaper, and while it is a dynamic desktop, it works a bit differently: It only has two images rather than sixteen, and rather than switching between them based on time, the wallpaper is set based on whether your appearance preference is set to light or dark mode. This style is even acknowledged separately in System Preferences as “Automatic” rather than “Dynamic”.</p> <p>While I was enamoured with Mojave’s dynamic desktops at first, I ended up switching to a regular wallpaper after some time. I don’t use dark mode only at night<sup id="fnref-2"><a href="#fn-2" class="footnote-ref">2</a></sup>, and so I’d often be left with a dark UI and a searing bright wallpaper.</p> <p>So naturally I was excited to create my own dynamic desktops with this new style, but like with the previous ones, Apple hasn’t said anything about how one would go about doing that.</p> <h2>Mojave’s Dynamic Desktop Format</h2> <p>As it turns out though <a href="https://nshipster.com/macos-dynamic-desktop/" title="”macOS Dynamic Desktop” on NSHipster">Mattt at NSHipster</a> had done some digging around into the format for Mojave and that proved to be a good starting place.</p> <p>Encoded within the <code>heic</code> file for the default dynamic wallpaper for Mojave was a metadata item named “solar”, which detailed the position of the sun in the sky in terms of its altitude and azimuth, for each of the images.</p> <p>The general format for the solar metadata was as follows:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="" data-index="0"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source">(</span></span> <span class="grvsc-line"><span class="grvsc-source"> ap = {</span></span> <span class="grvsc-line"><span class="grvsc-source"> d // ??</span></span> <span class="grvsc-line"><span class="grvsc-source"> l // ??</span></span> <span class="grvsc-line"><span class="grvsc-source"> };</span></span> <span class="grvsc-line"><span class="grvsc-source"> si = (</span></span> <span class="grvsc-line"><span class="grvsc-source"> {</span></span> <span class="grvsc-line"><span class="grvsc-source"> a // altitude</span></span> <span class="grvsc-line"><span class="grvsc-source"> i // index</span></span> <span class="grvsc-line"><span class="grvsc-source"> z // azimuth</span></span> <span class="grvsc-line"><span class="grvsc-source"> },</span></span> <span class="grvsc-line"><span class="grvsc-source"> ...</span></span> <span class="grvsc-line"><span class="grvsc-source"> )</span></span> <span class="grvsc-line"><span class="grvsc-source">)</span></span></code></pre> <p>The <code>d</code> and <code>l</code> were bits that Mattt wasn’t able to figure out; more about those in a bit.</p> <p>And here’s the data in it’s XMP form:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="xml" data-index="1"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&lt;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-14">x:xmpmeta</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">xmlns:x</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">adobe:ns:meta/</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">x:xmptk</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">XMP Core 5.4.0</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&gt;</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&lt;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-14">rdf:RDF</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">xmlns:rdf</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">http://www.w3.org/1999/02/22-rdf-syntax-ns#</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&gt;</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&lt;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-14">rdf:Description</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">rdf:about</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;&quot;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">xmlns:apple_desktop</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">http://ns.apple.com/namespace/1.0/</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&gt;</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&lt;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-14">apple_desktop:solar</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&gt;</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">&lt;!-- (Base64-Encoded Metadata) --&gt;</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&lt;/</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-14">apple_desktop:solar</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&gt;</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&lt;/</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-14">rdf:Description</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&gt;</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&lt;/</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-14">rdf:RDF</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&gt;</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&lt;/</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-14">x:xmpmeta</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&gt;</span></span></span></code></pre> <p>Equipped with this information and the companion <a href="http://github.com/NSHipster/DynamicDesktop" title="“Dynamic Desktop” on GitHub">Playgrounds</a>, I set about trying to figure out Catalina’s dynamic desktop format. It’s worth reading the NSHipster post before proceeding any further since I’m leaning heavily on that.</p> <h2>Catalina’s Dynamic Desktop Format</h2> <p>I hadn’t installed Catalina at this time, so I obtained the wallpaper from <a href="https://www.dropbox.com/sh/zml0g0o7ovdfn08/AAC1UwEVvxTubzoncDabDS28a" title="macOS Catalina wallpapers on Dropbox">here</a> (it’s the <code>Dynamic.heic</code> file).</p> <p>Reading the metadata, while there wasn’t a <code>solar</code> item to be found, there was one named <code>apr</code>. Here’s the data included with that:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="" data-index="2"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source">YnBsaXN0MDDSAQIDBFFsUWQQABABCA0PERMAAAAAAAABAQAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAFQ==</span></span></code></pre> <p>Here's what I got after putting it through a <code>PropertyListDecoder</code>:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="" data-index="3"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source">{</span></span> <span class="grvsc-line"><span class="grvsc-source"> d = 1;</span></span> <span class="grvsc-line"><span class="grvsc-source"> l = 0;</span></span> <span class="grvsc-line"><span class="grvsc-source">}</span></span></code></pre> <p>Gone is all the solar positioning data from Mojave’s format, and this is much simpler. Just two keys, with two integer values.</p> <p><code>d</code> and <code>l</code>, it turns out, are the indices for the <code>dark</code> and <code>light</code> wallpapers, respectively. Their inclusion in the Mojave format suggests that the “Automatic” style might also be enabled for these wallpapers in the future, however this doesn’t seem to be true as of the first beta for Catalina.</p> <p>The XMP format is also slightly tweaked, with the <code>apple_desktop:solar</code> tag being replaced with an <code>apple_desktop:apr</code> tag</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="xml" data-index="4"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&lt;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-14">x:xmpmeta</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">xmlns:x</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">adobe:ns:meta/</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">x:xmptk</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">XMP Core 5.4.0</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&gt;</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&lt;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-14">rdf:RDF</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">xmlns:rdf</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">http://www.w3.org/1999/02/22-rdf-syntax-ns#</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&gt;</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&lt;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-14">rdf:Description</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">rdf:about</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;&quot;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">xmlns:apple_desktop</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">http://ns.apple.com/namespace/1.0/</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&gt;</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&lt;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-14">apple_desktop:apr</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&gt;</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">&lt;!-- (Base64-Encoded Metadata) --&gt;</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&lt;/</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-14">apple_desktop:apr</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&gt;</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&lt;/</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-14">rdf:Description</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&gt;</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&lt;/</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-14">rdf:RDF</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&gt;</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&lt;/</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-14">x:xmpmeta</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-9">&gt;</span></span></span></code></pre> <p>That’s all the information we need to make new wallpapers of our own!</p> <h2>Generating Dynamic Wallpapers</h2> <p>The code below is tweaked from Mattt’s aforementioned <a href="http://github.com/NSHipster/DynamicDesktop" title="“Dynamic Desktop” on GitHub">Playground</a>.</p> <p>First we have references to the two light and dark wallpapers. These must be stored in the Playground’s <code>Resources</code> folder:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="5"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> lightImage </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">NSImage</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">imageLiteralResourceName</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">Mojave Day.jpg</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> darkImage </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">NSImage</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">imageLiteralResourceName</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">Mojave Night.jpg</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span></code></pre> <p>Next, a location for where the final image must be stored:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="6"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> outputURL </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">URL</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">fileURLWithPath</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">/Users/harshil/Desktop/output.heic</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span></code></pre> <p>Catalina makes some changes to how permissions work for certain folders including the desktop, so you might need to change the location.</p> <p>We then create a <code>CGImageDestination</code>:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="7"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">guard</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> imageDestination </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">CGImageDestinationCreateWithURL</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> outputURL </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">as</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> CFURL, </span><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">// url</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> AVFileType.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">heic</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">as</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> CFString, </span><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">// type</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">2</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">// count</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-7">nil</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">// options</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">else</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">fatalError</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">Error creating image destination</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p>Then we create a metadata item and populate it as per the XML structure:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="8"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> imageMetadata </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">CGImageMetadataCreateMutable</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">()</span></span></span> <span class="grvsc-line"><span class="grvsc-source"></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">guard</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> metadata </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">try</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">?</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">DynamicDesktopMetadata</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">()</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">base64EncodedMetadata</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">()</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">as</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> CFString,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> tag </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">CGImageMetadataTagCreate</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">http://ns.apple.com/namespace/1.0/</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">as</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> CFString, </span><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">// xmlns</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">apple_desktop</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">as</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> CFString, </span><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">// prefix</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">apr</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">as</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> CFString, </span><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">// name</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> .</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">string</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">// type</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> metadata</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">, </span><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">// value</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">CGImageMetadataSetTagWithPath</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">imageMetadata, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-7">nil</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">xmp:apr</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">as</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7"> CFString, tag</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">else</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">fatalError</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">Error creating image metadata</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p>We then convert the images to <code>CGImage</code>s and write them to file, including the metadata along with the first image:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="9"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">guard</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> lightCGImage </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> lightImage.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">cgImage</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">,</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">let</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> darkCGImage </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> darkImage.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">cgImage</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">else</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">fatalError</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">Error converting images</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span> <span class="grvsc-line"><span class="grvsc-source"></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">CGImageDestinationAddImageAndMetadata</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">imageDestination, lightCGImage, imageMetadata, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-7">nil</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">CGImageDestinationAddImage</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">imageDestination, darkCGImage, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-7">nil</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span></code></pre> <p>And lastly we finalise the conversion:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="10"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">guard</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">CGImageDestinationFinalize</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">imageDestination</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">else</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">fatalError</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-19">Error finalizing image</span><span class="grvsc-ttxYAU-8 grvsc-tGNH8N-37">&quot;</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <p>Once this has finished executing, we should have our image at the destination URL, ready for use!</p> <p>The automatic wallpapers work in Mojave too, although setting them somewhat glitches out the UI in System Preferences.</p> <p>Even though iOS 13 ships with its own dynamic wallpapers, neither the official Catalina wallpaper nor any that I’ve generated seem to work there. Hopefully that’s just a beta bug.</p> <p>You can find the full source code for the above on <a href="https://github.com/HarshilShah/DynamicDesktop" title="My fork of DynamicDesktop on GitHub">my fork of Mattt’s repo</a>. I have also made some dynamic wallpapers from the wallpapers shipping with iOS 13, which can be <a href="https://www.dropbox.com/s/hj8i9y3cctdh12a/iOS%2013%20Dynamic%20Wallpapers.zip" title="Download dynamic versions of the iOS 13 wallpapers">downloaded here</a>.</p> <style class="grvsc-styles"> .nikso-plus { background-color: #ffffff; color: #24292e; } .nikso-plus .grvsc-ttxYAU-1 { color: #24292EFF; } .nikso-plus .grvsc-ttxYAU-8 { color: #22863A; } .nikso-plus .grvsc-ttxYAU-4 { color: #6F42C1; } .nikso-plus .grvsc-ttxYAU-3 { color: #6A737D; } .nikso-plus .grvsc-ttxYAU-7 { color: #D73A49; } .nikso-plus .grvsc-ttxYAU-5 { color: #005CC5; } .nikso-plus .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(0, 0, 0, 0.05)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(0, 0, 0, 0.2)); } /* Night Owl (No Italics) */ @media (prefers-color-scheme: dark) { .grvsc-mm-tGNH8N { background-color: #011627; color: #d6deeb; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-9 { color: #7FDBCA; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-14 { color: #CAECE6; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-5 { color: #C5E478; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-37 { color: #D9F5DD; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-19 { color: #ECC48D; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-1 { color: #D6DEEB; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-6 { color: #637777; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-3 { color: #C792EA; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-7 { color: #82AAFF; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-8 { color: #F78C6C; } .grvsc-mm-tGNH8N .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); } } </style> <div class="footnotes"> <hr> <ol> <li id="fn-1">The two wallpapers bundled with Mojave, the eponymous “Mojave” and “Solar Gradients”, include 16 images each.<a href="#fnref-1" class="footnote-backref">↩</a></li> <li id="fn-2">I switch between themes enough that I even made my first Mac app, <a href="https://github.com/HarshilShah/Nocturnal" title="View Nocturnal on GitHub">Nocturnal</a>, to make it easier to do so.<a href="#fnref-2" class="footnote-backref">↩</a></li> </ol> </div><![CDATA[Updating UI for iPhone X]]>https://harshil.net/blog/updating-ui-for-iphone-xhttps://harshil.net/blog/updating-ui-for-iphone-xThu, 02 Nov 2017 06:30:00 GMT<p>The safe area API introduced at WWDC 2017 seemed at the time like a lot of work to accommodate the status bar, so I happily ignored it right until the introduction of iPhone X with its notch (ahem, “sensor housing”) and home indicator.</p> <p>Since then, I’ve updated a whole bunch of projects for iPhone X, and this post compiles a few of the techniques I’ve been using across the board.</p> <p>It’s going to take a while to adjust to the new constraints of an edge-to-edge display, rounded corners, and a much taller aspect ratio, and for new patterns to emerge from those. But in the meantime, I hope this post helps you in updating your existing designs to work on iPhone X.</p> <h2>Backwards Compatibility</h2> <p>One of the biggest issues when getting starting with using the new safe area API was backwards compatibility. Initially my code was littered with <code>if #available(iOS 11, *)</code> statements, and massive, mostly illegible lines such as:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="0"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">controlsView.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">safeAreaLayoutGuide</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">bottomAnchor</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">.</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">constraint</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">equalTo</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: view.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">bottomAnchor</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">, </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">constant</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">: </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">-</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-7">Constants.</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-5">bottomPadding</span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">)</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">isActive</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">=</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-10">true</span></span></span></code></pre> <p>Eventually I found a solution that solves both issues. Since I was just pinning to the normal anchors<sup id="fnref-1"><a href="#fn-1" class="footnote-ref">1</a></sup> instead of safe area anchors for iOS &#x3C;11, it made sense to use a unified anchor which handled that fallback case automatically.</p> <p>Additionally, it simplified the syntax quite a bit, and now instead of calling <code>view.safeAreaLayoutGuide.bottomAnchor</code>, I can just use <code>view.safeBottomAnchor</code> and have the iOS &#x3C;11 fallback case automatically covered as well.</p> <p>All this was achieved with the following 30 line UIView extension:</p> <pre class="grvsc-container nikso-plus grvsc-mm-tGNH8N" data-language="swift" data-index="1"><code class="grvsc-code"><span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">//</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">// UIView+SafeAnchors.swift</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-3 grvsc-tGNH8N-6">//</span></span></span> <span class="grvsc-line"><span class="grvsc-source"></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">import</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">UIKit</span></span></span> <span class="grvsc-line"><span class="grvsc-source"></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">extension</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-4 grvsc-tGNH8N-1">UIView</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">var</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> safeTopAnchor</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> NSLayoutYAxisAnchor { </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> optionalSafeAreaLayoutGuide</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">?</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">topAnchor</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">??</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> topAnchor }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">var</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> safeBottomAnchor</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> NSLayoutYAxisAnchor { </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> optionalSafeAreaLayoutGuide</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">?</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">bottomAnchor</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">??</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> bottomAnchor }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">var</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> safeLeftAnchor</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> NSLayoutXAxisAnchor { </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> optionalSafeAreaLayoutGuide</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">?</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">leftAnchor</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">??</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> leftAnchor }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">var</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> safeRightAnchor</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> NSLayoutXAxisAnchor { </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> optionalSafeAreaLayoutGuide</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">?</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">rightAnchor</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">??</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> rightAnchor }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">var</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> safeLeadingAnchor</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> NSLayoutXAxisAnchor { </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> optionalSafeAreaLayoutGuide</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">?</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">leadingAnchor</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">??</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> leadingAnchor }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">var</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> safeTrailingAnchor</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> NSLayoutXAxisAnchor { </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> optionalSafeAreaLayoutGuide</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">?</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">trailingAnchor</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">??</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> trailingAnchor }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">var</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> safeCenterXAnchor</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> NSLayoutXAxisAnchor { </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> optionalSafeAreaLayoutGuide</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">?</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">centerXAnchor</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">??</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> centerXAnchor }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">var</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> safeCenterYAnchor</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> NSLayoutYAxisAnchor { </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> optionalSafeAreaLayoutGuide</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">?</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">centerYAnchor</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">??</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> centerYAnchor }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">var</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> safeWidthAnchor</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> NSLayoutDimension { </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> optionalSafeAreaLayoutGuide</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">?</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">widthAnchor</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">??</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> widthAnchor }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">var</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> safeHeightAnchor</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> NSLayoutDimension { </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> optionalSafeAreaLayoutGuide</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">?</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">.</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-5">heightAnchor</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">??</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> heightAnchor }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">private</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">var</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> optionalSafeAreaLayoutGuide</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">:</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> UILayoutGuide</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-9">?</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">if</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-5">#available</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">(</span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">iOS</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-8">11</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">, </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">*</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">) {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> safeAreaLayoutGuide</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> } </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">else</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> {</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-7 grvsc-tGNH8N-3">return</span><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> </span><span class="grvsc-ttxYAU-5 grvsc-tGNH8N-7">nil</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1"> }</span></span></span> <span class="grvsc-line"><span class="grvsc-source"><span class="grvsc-ttxYAU-1 grvsc-tGNH8N-1">}</span></span></span></code></pre> <!-- https://gist.github.com/HarshilShah/6d75593d4c78a8015f54a090b115a40b.js --> <h2>To <code>.safeAreaLayoutGuide</code> or not</h2> <p>Ok, now that the how is sorted out, on to deciding when the safe area anchors should be used instead of the normal ones.</p> <p>It’s best to think that your actual app exists within the safe area; the parts of the display on either side of the notch and around home indicator are just meant to extend the appearance by blurring overscrolled content or showing a flat background color.</p> <p>The easiest case is for static elements i.e. those which do not scroll. These should always be pinned to the safe area anchors. <code>UIScrollView</code> instances, however, should be pinned to the normal anchors.</p> <p>Views within cells should be pinned to the normal anchors along the direction of scrolling, and to the safe area anchors otherwise. So for a vertical scrolling table/collection view, you’d pin the leading, trailing, left, right, width, and centerX anchors using safe area anchors, and the top, bottom, height, and centerY anchors using normal anchors.</p> <h2>Conclusion</h2> <p>This was a super brief post only meant to cover some techniques I’ve been using in updating my apps for iOS 11 and iPhone X.</p> <p>For those looking for more, I’d recommend the following pieces:</p> <ul> <li><a href="https://designcode.io/ios11-iphone-x">Designing for iPhone X</a> by Meng To</li> <li>UI design for iPhone X: <a href="http://blog.maxrudberg.com/post/166045445103/ui-design-for-iphone-x-top-elements-and-the-notch">Top Elements and the Notch</a> and <a href="http://blog.maxrudberg.com/post/165590234593/ui-design-for-iphone-x-bottom-elements">Bottom Elements</a> pieces by Max Rudberg</li> <li><a href="https://medium.com/@robnorback/the-5-most-important-changes-you-need-to-make-for-the-iphone-x-ba5cdc7b5811">The 5 Most Important Changes You Need to Make for the iPhone X</a> by Rob Norback</li> </ul> <style class="grvsc-styles"> .nikso-plus { background-color: #ffffff; color: #24292e; } .nikso-plus .grvsc-ttxYAU-1 { color: #24292EFF; } .nikso-plus .grvsc-ttxYAU-5 { color: #005CC5; } .nikso-plus .grvsc-ttxYAU-4 { color: #6F42C1; } .nikso-plus .grvsc-ttxYAU-7 { color: #D73A49; } .nikso-plus .grvsc-ttxYAU-3 { color: #6A737D; } .nikso-plus .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(0, 0, 0, 0.05)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(0, 0, 0, 0.2)); } /* Night Owl (No Italics) */ @media (prefers-color-scheme: dark) { .grvsc-mm-tGNH8N { background-color: #011627; color: #d6deeb; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-1 { color: #D6DEEB; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-5 { color: #C5E478; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-7 { color: #82AAFF; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-9 { color: #7FDBCA; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-10 { color: #FF5874; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-6 { color: #637777; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-3 { color: #C792EA; } .grvsc-mm-tGNH8N .grvsc-tGNH8N-8 { color: #F78C6C; } .grvsc-mm-tGNH8N .grvsc-line-highlighted::before { background-color: var(--grvsc-line-highlighted-background-color, rgba(255, 255, 255, 0.1)); box-shadow: inset var(--grvsc-line-highlighted-border-width, 4px) 0 0 0 var(--grvsc-line-highlighted-border-color, rgba(255, 255, 255, 0.5)); } } </style> <div class="footnotes"> <hr> <ol> <li id="fn-1">This is a weird name, but since I can’t think of a better or official one, I’m gonna be using it throughout this post. Let me know if you have any better ideas<a href="#fnref-1" class="footnote-backref">↩</a></li> </ol> </div>