<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-US"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://nerdyak.tech/atom.xml" rel="self" type="application/atom+xml" /><link href="https://nerdyak.tech/" rel="alternate" type="text/html" hreflang="en-US" /><updated>2026-03-16T16:44:34+00:00</updated><id>https://nerdyak.tech/atom.xml</id><title type="html">Pavel Zak’s dev blog</title><subtitle>#iOS development | #SwiftUI | #maybeMore</subtitle><author><name>Pavel Zak</name></author><entry><title type="html">Expanding Animations in SwiftUI Lists</title><link href="https://nerdyak.tech/development/2026/03/16/expand-animation-in-SwiftUI-List.html" rel="alternate" type="text/html" title="Expanding Animations in SwiftUI Lists" /><published>2026-03-16T00:00:00+00:00</published><updated>2026-03-16T00:00:00+00:00</updated><id>https://nerdyak.tech/development/2026/03/16/expand-animation-in-SwiftUI-List</id><content type="html" xml:base="https://nerdyak.tech/development/2026/03/16/expand-animation-in-SwiftUI-List.html"><![CDATA[<h1 id="expanding-animations-in-swiftui-lists">Expanding Animations in SwiftUI Lists</h1>

<p>Ever tried to add a simple expand/collapse animation to a SwiftUI <code class="language-plaintext highlighter-rouge">List</code> and watched in horror as your beautiful animation turned into a janky mess? You’re not alone. I faced this issue a couple of months ago and thanks to Donny Wals, I decided to publish my findings.</p>

<p>Let me walk you through the journey of solving this problem, complete with all the failed attempts and eventual success.</p>

<h2 id="the-goal">The Goal</h2>

<p>We want a simple expandable view: a tappable title that reveals additional content when clicked. For our example, let’s use these simple components:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">TitleView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">Text</span><span class="p">(</span><span class="s">"Tap to expand/collapse"</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">font</span><span class="p">(</span><span class="o">.</span><span class="n">headline</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">,</span> <span class="nv">alignment</span><span class="p">:</span> <span class="o">.</span><span class="n">leading</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">padding</span><span class="p">()</span>
            <span class="o">.</span><span class="nf">contentShape</span><span class="p">(</span><span class="kt">Rectangle</span><span class="p">())</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">DetailView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">Text</span><span class="p">(</span><span class="s">"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">font</span><span class="p">(</span><span class="o">.</span><span class="n">body</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">fixedSize</span><span class="p">(</span><span class="nv">horizontal</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="nv">vertical</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">padding</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now let’s build a typical expandable view:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">TypicalExpandableView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">isExpanded</span> <span class="o">=</span> <span class="kc">false</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">VStack</span><span class="p">(</span><span class="nv">alignment</span><span class="p">:</span> <span class="o">.</span><span class="n">leading</span><span class="p">,</span> <span class="nv">spacing</span><span class="p">:</span> <span class="mi">8</span><span class="p">)</span> <span class="p">{</span>
            <span class="kt">TitleView</span><span class="p">()</span>
                <span class="o">.</span><span class="n">onTapGesture</span> <span class="p">{</span>
                    <span class="n">isExpanded</span><span class="o">.</span><span class="nf">toggle</span><span class="p">()</span>
                <span class="p">}</span>

            <span class="k">if</span> <span class="n">isExpanded</span> <span class="p">{</span>
                <span class="kt">Divider</span><span class="p">()</span>
                <span class="kt">DetailView</span><span class="p">()</span>
                    <span class="o">.</span><span class="nf">transition</span><span class="p">(</span><span class="o">.</span><span class="n">opacity</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="o">.</span><span class="nf">background</span><span class="p">(</span><span class="kt">AppColor</span><span class="o">.</span><span class="n">backgroundSecondary</span><span class="p">)</span>
        <span class="o">.</span><span class="nf">cornerRadius</span><span class="p">(</span><span class="mi">8</span><span class="p">)</span>
        <span class="o">.</span><span class="nf">animation</span><span class="p">(</span><span class="o">.</span><span class="k">default</span><span class="p">,</span> <span class="nv">value</span><span class="p">:</span> <span class="n">isExpanded</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Simple, right? And it works <em>beautifully</em> in a regular <code class="language-plaintext highlighter-rouge">VStack</code> or <code class="language-plaintext highlighter-rouge">LazyVStack</code>. The content smoothly animates in and out, everything looks great.</p>

<p>Let’s put our <code class="language-plaintext highlighter-rouge">TypicalExpandableView</code> into a List and see what happens.</p>

<center>
<video autoplay="" muted="" loop="" controls="controls" width="400">
	<source src="/assets/posts/17-expandable-broken.mov" />
	<source src="/assets/posts/17-expandable-broken.webm" type="video/webm" />
</video>
</center>

<p>Oof. That’s… not great. The content just <em>jumps</em> into existence. The List cell snaps to its new height without any smooth transition. It works perfectly in a <code class="language-plaintext highlighter-rouge">VStack</code> or <code class="language-plaintext highlighter-rouge">LazyVStack</code>, but in a <code class="language-plaintext highlighter-rouge">List</code>? Not so much.</p>

<h2 id="second-attempt-tweaking-the-animation">Second Attempt: Tweaking the Animation</h2>

<p>Maybe we just need to be more explicit with our animations? Let’s try wrapping the toggle in <code class="language-plaintext highlighter-rouge">withAnimation</code> and adding an <code class="language-plaintext highlighter-rouge">.id()</code> to help SwiftUI track the view:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">TweakedExpandableView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">isExpanded</span> <span class="o">=</span> <span class="kc">false</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">VStack</span><span class="p">(</span><span class="nv">alignment</span><span class="p">:</span> <span class="o">.</span><span class="n">leading</span><span class="p">,</span> <span class="nv">spacing</span><span class="p">:</span> <span class="mi">8</span><span class="p">)</span> <span class="p">{</span>
            <span class="kt">TitleView</span><span class="p">()</span>
                <span class="o">.</span><span class="n">onTapGesture</span> <span class="p">{</span>
                    <span class="n">withAnimation</span> <span class="p">{</span>
                        <span class="n">isExpanded</span><span class="o">.</span><span class="nf">toggle</span><span class="p">()</span>
                    <span class="p">}</span>
                <span class="p">}</span>

            <span class="k">if</span> <span class="n">isExpanded</span> <span class="p">{</span>
                <span class="kt">Divider</span><span class="p">()</span>
                <span class="kt">DetailView</span><span class="p">()</span>
                    <span class="o">.</span><span class="nf">transition</span><span class="p">(</span><span class="o">.</span><span class="n">opacity</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="o">.</span><span class="nf">background</span><span class="p">(</span><span class="kt">AppColor</span><span class="o">.</span><span class="n">backgroundSecondary</span><span class="p">)</span>
        <span class="o">.</span><span class="nf">cornerRadius</span><span class="p">(</span><span class="mi">8</span><span class="p">)</span>
        <span class="o">.</span><span class="nf">id</span><span class="p">(</span><span class="s">"static"</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<center>
<video autoplay="" muted="" loop="" controls="controls" width="400">
	<source src="/assets/posts/17-expandable-tweaked.mov" />
	<source src="/assets/posts/17-expandable-tweaked.webm" type="video/webm" />
</video>
</center>

<p>Better! But still not smooth. The animation is less jarring, but we’re still getting that awkward jump. List cells just don’t like conditional content appearing and disappearing.</p>

<h2 id="third-attempt-the-built-in-solution">Third Attempt: The Built-In Solution</h2>

<p>“Wait,” you might think, “doesn’t SwiftUI have a built-in component for this?”</p>

<p>Yes! Meet <code class="language-plaintext highlighter-rouge">DisclosureGroup</code>:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">DisclosureExpandableView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">isExpanded</span> <span class="o">=</span> <span class="kc">false</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">DisclosureGroup</span><span class="p">(</span>
            <span class="nv">isExpanded</span><span class="p">:</span> <span class="err">$</span><span class="n">isExpanded</span><span class="p">,</span>
            <span class="nv">content</span><span class="p">:</span> <span class="p">{</span>
                <span class="kt">DetailView</span><span class="p">()</span>
            <span class="p">},</span>
            <span class="nv">label</span><span class="p">:</span> <span class="p">{</span>
                <span class="kt">TitleView</span><span class="p">()</span>
            <span class="p">}</span>
        <span class="p">)</span>
        <span class="o">.</span><span class="nf">background</span><span class="p">(</span><span class="kt">AppColor</span><span class="o">.</span><span class="n">backgroundSecondary</span><span class="p">)</span>
        <span class="o">.</span><span class="nf">cornerRadius</span><span class="p">(</span><span class="mi">8</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<center>
<video autoplay="" muted="" loop="" controls="controls" width="400">
	<source src="/assets/posts/17-expandable-default.mov" />
	<source src="/assets/posts/17-expandable-default.webm" type="video/webm" />
</video>
</center>

<p>Finally, a smooth animation! <code class="language-plaintext highlighter-rouge">DisclosureGroup</code> works perfectly in Lists. So… problem solved?</p>

<p><strong>Not quite.</strong> <code class="language-plaintext highlighter-rouge">DisclosureGroup</code> comes with some limitations, mainly the lack of customization options. If you’re OK with the default disclosure indicator animation and animation timing, like on your rarely visited settings screen, you’re fine. But for a custom-designed UI, we need more control.</p>

<h2 id="fourth-attempt-animating-cell-height">Fourth Attempt: Animating cell height</h2>

<p>After some experimentation, I found that using an Animatable view that can animate its height works nicely even in a List. Here is the proof-of-concept implementation:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">AnimatableRedBox</span><span class="p">:</span> <span class="kt">View</span><span class="p">,</span> <span class="kt">Animatable</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">animatableData</span><span class="p">:</span> <span class="kt">CGFloat</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">Color</span><span class="o">.</span><span class="n">red</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">height</span><span class="p">:</span> <span class="n">animatableData</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">AnimatableExpandableView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">isExpanded</span> <span class="o">=</span> <span class="kc">false</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">VStack</span><span class="p">(</span><span class="nv">alignment</span><span class="p">:</span> <span class="o">.</span><span class="n">leading</span><span class="p">,</span> <span class="nv">spacing</span><span class="p">:</span> <span class="mi">8</span><span class="p">)</span> <span class="p">{</span>
            <span class="kt">TitleView</span><span class="p">()</span>
                <span class="o">.</span><span class="n">onTapGesture</span> <span class="p">{</span>
                    <span class="n">withAnimation</span> <span class="p">{</span>
                        <span class="n">isExpanded</span><span class="o">.</span><span class="nf">toggle</span><span class="p">()</span>
                    <span class="p">}</span>
                <span class="p">}</span>

            <span class="kt">AnimatableRedBox</span><span class="p">(</span><span class="nv">animatableData</span><span class="p">:</span> <span class="n">isExpanded</span> <span class="p">?</span> <span class="mi">100</span> <span class="p">:</span> <span class="mi">0</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="o">.</span><span class="nf">background</span><span class="p">(</span><span class="kt">AppColor</span><span class="o">.</span><span class="n">backgroundSecondary</span><span class="p">)</span>
        <span class="o">.</span><span class="nf">cornerRadius</span><span class="p">(</span><span class="mi">8</span><span class="p">)</span>
        <span class="o">.</span><span class="nf">id</span><span class="p">(</span><span class="s">"static"</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<center>
<video autoplay="" muted="" loop="" controls="controls" width="400">
	<source src="/assets/posts/17-expandable-red.mov" />
	<source src="/assets/posts/17-expandable-red.webm" type="video/webm" />
</video>
</center>

<p>Aha! The red box animates smoothly in the List. By implementing <code class="language-plaintext highlighter-rouge">Animatable</code> and animating a single <code class="language-plaintext highlighter-rouge">CGFloat</code> value, SwiftUI smoothly interpolates between states. The List knows exactly what height the cell should be at every frame of the animation.</p>

<h2 id="the-final-solution">The Final Solution</h2>

<p>Now we just need to generalize this approach to work with any content - not just a fixed-height red box, but dynamic text and self-sizing views. Here is my implementation of a simple expandable component I’m using in my codebase. Having the detail view in an overlay ensures that the detail text doesn’t change during expansion/collapsing. It allows you to tweak the detail animation even more with offsets/scales/distortions or more ;)</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">ExpandableBaseView</span><span class="o">&lt;</span><span class="kt">Header</span><span class="p">:</span> <span class="kt">View</span><span class="p">,</span> <span class="kt">Content</span><span class="p">:</span> <span class="kt">View</span><span class="o">&gt;</span><span class="p">:</span> <span class="kt">View</span><span class="p">,</span> <span class="kt">Animatable</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">animatableData</span><span class="p">:</span> <span class="kt">CGFloat</span>

    <span class="k">let</span> <span class="nv">header</span><span class="p">:</span> <span class="kt">Header</span>
    <span class="k">let</span> <span class="nv">content</span><span class="p">:</span> <span class="kt">Content</span>

    <span class="kd">@State</span> <span class="k">var</span> <span class="nv">baseFrame</span><span class="p">:</span> <span class="kt">CGRect</span> <span class="o">=</span> <span class="o">.</span><span class="n">zero</span>
    <span class="kd">@State</span> <span class="k">var</span> <span class="nv">expansionFrame</span><span class="p">:</span> <span class="kt">CGRect</span> <span class="o">=</span> <span class="o">.</span><span class="n">zero</span>

    <span class="nf">init</span><span class="p">(</span><span class="nv">animatableData</span><span class="p">:</span> <span class="kt">CGFloat</span><span class="p">,</span>
         <span class="kd">@ViewBuilder</span> <span class="nv">header</span><span class="p">:</span> <span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">Header</span><span class="p">,</span>
         <span class="kd">@ViewBuilder</span> <span class="nv">content</span><span class="p">:</span> <span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">Content</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">self</span><span class="o">.</span><span class="n">animatableData</span> <span class="o">=</span> <span class="n">animatableData</span>
        <span class="k">self</span><span class="o">.</span><span class="n">header</span> <span class="o">=</span> <span class="nf">header</span><span class="p">()</span>
        <span class="k">self</span><span class="o">.</span><span class="n">content</span> <span class="o">=</span> <span class="nf">content</span><span class="p">()</span>
    <span class="p">}</span>

    <span class="k">var</span> <span class="nv">expandedContent</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="n">content</span>
            <span class="o">.</span><span class="nf">opacity</span><span class="p">(</span><span class="n">animatableData</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">getFrame</span><span class="p">(</span><span class="err">$</span><span class="n">expansionFrame</span><span class="p">,</span> <span class="nv">space</span><span class="p">:</span> <span class="o">.</span><span class="n">local</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">offset</span><span class="p">(</span><span class="nv">y</span><span class="p">:</span> <span class="n">baseFrame</span><span class="o">.</span><span class="n">height</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">ZStack</span><span class="p">(</span><span class="nv">alignment</span><span class="p">:</span> <span class="o">.</span><span class="n">bottom</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">header</span>
                <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">getFrame</span><span class="p">(</span><span class="err">$</span><span class="n">baseFrame</span><span class="p">,</span> <span class="nv">space</span><span class="p">:</span> <span class="o">.</span><span class="n">local</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">overlay</span><span class="p">(</span><span class="n">expandedContent</span><span class="p">,</span> <span class="nv">alignment</span><span class="p">:</span> <span class="o">.</span><span class="n">top</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">height</span><span class="p">:</span> <span class="n">baseFrame</span><span class="o">.</span><span class="n">height</span> <span class="o">+</span> <span class="n">expansionFrame</span><span class="o">.</span><span class="n">height</span> <span class="o">*</span> <span class="n">animatableData</span><span class="p">,</span>
               <span class="nv">alignment</span><span class="p">:</span> <span class="o">.</span><span class="n">top</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">WorkingExpandableView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">isExpanded</span> <span class="o">=</span> <span class="kc">false</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">ExpandableBaseView</span><span class="p">(</span>
            <span class="nv">animatableData</span><span class="p">:</span> <span class="n">isExpanded</span> <span class="p">?</span> <span class="mi">1</span> <span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
            <span class="nv">header</span><span class="p">:</span> <span class="p">{</span>
                <span class="kt">TitleView</span><span class="p">()</span>
                    <span class="o">.</span><span class="n">onTapGesture</span> <span class="p">{</span>
                        <span class="nf">withAnimation</span><span class="p">()</span> <span class="p">{</span>
                            <span class="n">isExpanded</span><span class="o">.</span><span class="nf">toggle</span><span class="p">()</span>
                        <span class="p">}</span>
                    <span class="p">}</span>
            <span class="p">},</span>
            <span class="nv">content</span><span class="p">:</span> <span class="p">{</span>
                <span class="kt">DetailView</span><span class="p">()</span>
            <span class="p">}</span>
        <span class="p">)</span>
        <span class="o">.</span><span class="nf">background</span><span class="p">(</span><span class="kt">AppColor</span><span class="o">.</span><span class="n">backgroundSecondary</span><span class="p">)</span>
        <span class="o">.</span><span class="nf">cornerRadius</span><span class="p">(</span><span class="mi">8</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<center>
<video autoplay="" muted="" loop="" controls="controls" width="400">
	<source src="/assets/posts/17-expandable-final.mov" />
	<source src="/assets/posts/17-expandable-final.webm" type="video/webm" />
</video>
</center>

<p>Perfect! Smooth animation, works in Lists, and supports any content you throw at it.</p>

<h2 id="how-it-works">How It Works</h2>

<p>The magic happens by:</p>

<ol>
  <li><strong>Implementing <code class="language-plaintext highlighter-rouge">Animatable</code></strong> - Animates a single value from 0 to 1 when toggling isExpanded flag</li>
  <li><strong>Measuring content sizes</strong> - Uses SwiftUI’s <code class="language-plaintext highlighter-rouge">GeometryReader</code> to measure both header and expanded content</li>
  <li><strong>Calculating exact heights</strong> - Sets frame height to <code class="language-plaintext highlighter-rouge">headerHeight + contentHeight * progress</code></li>
  <li><strong>Positioning content</strong> - Uses overlays and offsets to position expanded content below the header</li>
</ol>

<p>The content exists during the entire animation - it’s just scaled from 0 to full height smoothly.</p>

<h3 id="the-frame-measurement-helper">The Frame Measurement Helper</h3>

<p>The <code class="language-plaintext highlighter-rouge">getFrame()</code> helper is a custom view modifier that uses <code class="language-plaintext highlighter-rouge">GeometryReader</code> and preference keys to measure view dimensions without affecting layout. Here’s how it works:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">FrameMeasurePreferenceKey</span><span class="p">:</span> <span class="kt">PreferenceKey</span> <span class="p">{</span>
    <span class="kd">typealias</span> <span class="kt">Value</span> <span class="o">=</span> <span class="p">[</span><span class="kt">String</span><span class="p">:</span> <span class="kt">CGRect</span><span class="p">]</span>

    <span class="kd">static</span> <span class="k">var</span> <span class="nv">defaultValue</span><span class="p">:</span> <span class="kt">Value</span> <span class="o">=</span> <span class="p">[:]</span>

    <span class="kd">static</span> <span class="kd">func</span> <span class="nf">reduce</span><span class="p">(</span><span class="nv">value</span><span class="p">:</span> <span class="k">inout</span> <span class="kt">Value</span><span class="p">,</span> <span class="nv">nextValue</span><span class="p">:</span> <span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">Value</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">value</span><span class="o">.</span><span class="nf">merge</span><span class="p">(</span><span class="nf">nextValue</span><span class="p">())</span> <span class="p">{</span> <span class="nv">$1</span> <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">MeasureFrameViewModifier</span><span class="p">:</span> <span class="kt">ViewModifier</span> <span class="p">{</span>
    <span class="kd">@Binding</span> <span class="k">var</span> <span class="nv">frame</span><span class="p">:</span> <span class="kt">CGRect</span>
    <span class="k">let</span> <span class="nv">space</span><span class="p">:</span> <span class="kt">CoordinateSpace</span>
    <span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">frameId</span> <span class="o">=</span> <span class="kt">UUID</span><span class="p">()</span>

    <span class="kd">func</span> <span class="nf">body</span><span class="p">(</span><span class="nv">content</span><span class="p">:</span> <span class="kt">Content</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="n">content</span>
            <span class="o">.</span><span class="nf">background</span><span class="p">(</span>
                <span class="kt">GeometryReader</span> <span class="p">{</span> <span class="n">geometry</span> <span class="k">in</span>
                    <span class="kt">Color</span><span class="o">.</span><span class="n">clear</span>
                        <span class="o">.</span><span class="nf">preference</span><span class="p">(</span>
                            <span class="nv">key</span><span class="p">:</span> <span class="kt">FrameMeasurePreferenceKey</span><span class="o">.</span><span class="k">self</span><span class="p">,</span>
                            <span class="nv">value</span><span class="p">:</span> <span class="p">[</span><span class="s">"frame_</span><span class="se">\(</span><span class="n">frameId</span><span class="se">)</span><span class="s">"</span><span class="p">:</span> <span class="n">geometry</span><span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">in</span><span class="p">:</span> <span class="n">space</span><span class="p">)]</span>
                        <span class="p">)</span>
                <span class="p">}</span>
            <span class="p">)</span>
            <span class="o">.</span><span class="nf">onPreferenceChange</span><span class="p">(</span><span class="kt">FrameMeasurePreferenceKey</span><span class="o">.</span><span class="k">self</span><span class="p">)</span> <span class="p">{</span> <span class="n">preferences</span> <span class="k">in</span>
                <span class="k">if</span> <span class="k">let</span> <span class="nv">frame</span> <span class="o">=</span> <span class="n">preferences</span><span class="p">[</span><span class="s">"frame_</span><span class="se">\(</span><span class="n">frameId</span><span class="se">)</span><span class="s">"</span><span class="p">]</span> <span class="p">{</span>
                    <span class="k">self</span><span class="o">.</span><span class="n">frame</span> <span class="o">=</span> <span class="n">frame</span>
                <span class="p">}</span>
            <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">extension</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">func</span> <span class="nf">getFrame</span><span class="p">(</span><span class="n">_</span> <span class="nv">frame</span><span class="p">:</span> <span class="kt">Binding</span><span class="o">&lt;</span><span class="kt">CGRect</span><span class="o">&gt;</span><span class="p">,</span> <span class="nv">space</span><span class="p">:</span> <span class="kt">CoordinateSpace</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="nf">modifier</span><span class="p">(</span><span class="kt">MeasureFrameViewModifier</span><span class="p">(</span><span class="nv">frame</span><span class="p">:</span> <span class="n">frame</span><span class="p">,</span> <span class="nv">space</span><span class="p">:</span> <span class="n">space</span><span class="p">))</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This utility:</p>
<ul>
  <li>Uses a hidden <code class="language-plaintext highlighter-rouge">GeometryReader</code> in the background to measure view size</li>
  <li>Reports the measurement through SwiftUI’s preference system</li>
  <li>Doesn’t interfere with the view’s layout or appearance</li>
  <li>Updates automatically when the view’s size changes</li>
</ul>

<p>With this helper, <code class="language-plaintext highlighter-rouge">ExpandableBaseView</code> can dynamically measure both the header and content sizes, then calculate the exact height needed at each point in the animation.</p>

<h2 id="wrapping-up">Wrapping Up</h2>

<p>I hope you find this useful! Let me know if you run into any troubles, or even a happy smiley face in DMs is rewarding. Get in touch on <a href="https://twitter.com/myridiphis">Twitter</a>!</p>

<p>Aaand Happy animating! 🎨</p>]]></content><author><name>Pavel Zak</name></author><category term="development" /><category term="SwiftUI" /><category term="List" /><category term="Animations" /><category term="Expanding" /><category term="DisclosureGroup" /><summary type="html"><![CDATA[Expanding Animations in SwiftUI Lists]]></summary></entry><entry><title type="html">Magical Particle Effects with SwiftUI Canvas</title><link href="https://nerdyak.tech/development/2024/06/27/particle-effects-with-SwiftUI-Canvas.html" rel="alternate" type="text/html" title="Magical Particle Effects with SwiftUI Canvas" /><published>2024-06-27T00:00:00+00:00</published><updated>2024-06-27T00:00:00+00:00</updated><id>https://nerdyak.tech/development/2024/06/27/particle-effects-with-SwiftUI-Canvas</id><content type="html" xml:base="https://nerdyak.tech/development/2024/06/27/particle-effects-with-SwiftUI-Canvas.html"><![CDATA[<p>In one of the previous posts, I shared a simple way of <a href="/development/2020/12/12/create-particles-in-swiftui.html">Creating particle effects in SwiftUI</a>. The approach is super easy and utilizes the power of viewModifiers, but I would not recommend it for production use as it is performance-greedy when having a bigger amount of particles in place (because each particle is a single view)</p>

<p>In this post, I will introduce you to an <strong>alternate and better</strong> approach - rendering the particles with the <code class="language-plaintext highlighter-rouge">Canvas</code> view. So let’s get into it 💪.</p>

<h2 id="setup">Setup</h2>

<p>We will start with the following view outline:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">struct</span> <span class="kt">ParticleCanvasView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">TimelineView</span><span class="p">(</span><span class="o">.</span><span class="n">animation</span><span class="p">)</span> <span class="p">{</span> <span class="n">context</span> <span class="k">in</span>
            <span class="kt">Canvas</span> <span class="p">{</span> <span class="n">context</span><span class="p">,</span> <span class="n">size</span> <span class="k">in</span>
	       <span class="k">let</span> <span class="nv">particleSymbol</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="nf">resolveSymbol</span><span class="p">(</span><span class="nv">id</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span><span class="o">!</span>
                <span class="k">let</span> <span class="nv">position</span> <span class="o">=</span> <span class="kt">CGPoint</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="n">size</span><span class="o">.</span><span class="n">width</span><span class="o">/</span><span class="mi">2</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">size</span><span class="o">.</span><span class="n">height</span><span class="o">/</span><span class="mi">2</span><span class="p">)</span>                    
                <span class="n">context</span><span class="o">.</span><span class="nf">draw</span><span class="p">(</span><span class="n">particleSymbol</span><span class="p">,</span> <span class="nv">at</span><span class="p">:</span> <span class="n">position</span><span class="p">,</span> <span class="nv">anchor</span><span class="p">:</span> <span class="o">.</span><span class="n">center</span><span class="p">)</span>
            <span class="p">}</span> <span class="nv">symbols</span><span class="p">:</span> <span class="p">{</span>
                <span class="kt">SingleParticleView</span><span class="p">()</span>
                    <span class="o">.</span><span class="nf">tag</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<p>This view features an outer <a href="https://developer.apple.com/documentation/swiftui/timelineview">TimelineView</a> that ensures its content (inner view) is regularly re-drawn. (note the <code class="language-plaintext highlighter-rouge">.animation</code> parameter allowing the system to decide the optimal refresh rate)
The content here is a <a href="https://developer.apple.com/documentation/swiftui/canvas/">Canvas</a> view. Those of you who come from good old UIKit days might be already familiar with the concept of the drawing context. In simple words, we get a canvas area with the view dimensions and we can draw/rasterize
various entities on it - like shapes and images.</p>

<p>In our case, we will draw a single particle represented with a <code class="language-plaintext highlighter-rouge">SingleParticleView</code>. Please note, how the SingleParticleView is being used as a <strong>drawing element</strong>. It is added to a symbols parameter allowing SwiftUI to <strong>pre-render</strong> it and thus be very performant later in the drawing calls - thus an ideal candidate for the many particles in the place ;)</p>

<p>At this moment, let’s just set the <code class="language-plaintext highlighter-rouge">SingleParticleView</code> as an orange dot, but we will tune it soon</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">struct</span> <span class="kt">SingleParticleView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">Circle</span><span class="p">()</span><span class="o">.</span><span class="nf">fill</span><span class="p">(</span><span class="kt">Color</span><span class="o">.</span><span class="n">orange</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span><span class="mi">35</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span><span class="mi">35</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<p>So far, we have managed to draw a small orange dot, but it is about to change soon ;)</p>

<p><img src="/assets/posts/16_01.png" alt="image1" title="Static particle" /></p>

<h2 id="i-like-to-move-it">I like to move it</h2>

<p>Now, let’s move that particle.</p>

<p>I will build here a fire-ish effect blending multiple upwards moving particles - so as a good start let’s periodically move the single particle up from the canvas bottom:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">struct</span> <span class="kt">ParticleCanvasView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">movementDuration</span> <span class="o">=</span> <span class="mf">2.0</span>
    
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">TimelineView</span><span class="p">(</span><span class="o">.</span><span class="n">animation</span><span class="p">)</span> <span class="p">{</span> <span class="n">context</span> <span class="k">in</span>
            <span class="k">let</span> <span class="nv">timeInterval</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">date</span><span class="o">.</span><span class="n">timeIntervalSinceReferenceDate</span><span class="p">;</span>
            
            <span class="k">let</span> <span class="nv">time</span> <span class="o">=</span> <span class="n">timeInterval</span><span class="o">.</span><span class="nf">truncatingRemainder</span><span class="p">(</span><span class="nv">dividingBy</span><span class="p">:</span> <span class="n">movementDuration</span><span class="p">)</span><span class="o">/</span><span class="n">movementDuration</span>
            
            <span class="kt">Canvas</span> <span class="p">{</span> <span class="n">context</span><span class="p">,</span> <span class="n">size</span> <span class="k">in</span>
                <span class="k">let</span> <span class="nv">particleSymbol</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="nf">resolveSymbol</span><span class="p">(</span><span class="nv">id</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span><span class="o">!</span>
                <span class="k">let</span> <span class="nv">position</span> <span class="o">=</span> <span class="kt">CGPoint</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="n">size</span><span class="o">.</span><span class="n">width</span><span class="o">/</span><span class="mi">2</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="p">(</span><span class="mi">1</span><span class="o">-</span><span class="n">time</span><span class="p">)</span><span class="o">*</span><span class="n">size</span><span class="o">.</span><span class="n">height</span><span class="p">)</span>
                    
                <span class="n">context</span><span class="o">.</span><span class="nf">draw</span><span class="p">(</span><span class="n">particleSymbol</span><span class="p">,</span> <span class="nv">at</span><span class="p">:</span> <span class="n">position</span><span class="p">,</span> <span class="nv">anchor</span><span class="p">:</span> <span class="o">.</span><span class="n">center</span><span class="p">)</span>
            <span class="p">}</span> <span class="nv">symbols</span><span class="p">:</span> <span class="p">{</span>
                <span class="kt">SingleParticleView</span><span class="p">()</span>
                    <span class="o">.</span><span class="nf">tag</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<p>You can see, that I am controlling the upwards movement with the <em>time</em> variable. What exactly is it in this context? Well, the timeline view already gives us access to the time property, but for our use, I want to have something normalized that can be easily bound with the particle movement.
I want the particle movement to take exactly 2 seconds (see movementDuration) so the code computes time as a truncating remainder, making sure it will periodically grow from 0 to 1 forever. As you can see in the following video:</p>

<center>
<video autoplay="" muted="" loop="" controls="controls" width="400">
	<source src="/assets/posts/16_video_01.mov" />
	<source src="/assets/posts/16_video_01.webm" type="video/webm" />
</video>
</center>

<h2 id="remember-goniometry">Remember goniometry?</h2>

<p>As a next step, we will upgrade the movement from the simple linear to something more firey :). My perception of fire movement is that it is waving, so let me change the code to move the particle along a cosinus wave whose amplitude is smaller the higher the particle is:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">struct</span> <span class="kt">ParticleCanvasView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">movementDuration</span> <span class="o">=</span> <span class="mf">2.0</span>
    
    <span class="kd">func</span> <span class="nf">particlePosition</span><span class="p">(</span><span class="nv">timeInterval</span><span class="p">:</span> <span class="kt">Double</span><span class="p">,</span> <span class="nv">canvasSize</span><span class="p">:</span> <span class="kt">CGSize</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">CGPoint</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">time</span> <span class="o">=</span> <span class="n">timeInterval</span><span class="o">.</span><span class="nf">truncatingRemainder</span><span class="p">(</span><span class="nv">dividingBy</span><span class="p">:</span> <span class="n">movementDuration</span><span class="p">)</span><span class="o">/</span><span class="n">movementDuration</span>
        <span class="k">let</span> <span class="nv">rotations</span><span class="p">:</span><span class="kt">CGFloat</span> <span class="o">=</span> <span class="mi">3</span>
        <span class="k">let</span> <span class="nv">amplitude</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="o">=</span> <span class="mf">0.1</span><span class="o">+</span><span class="mf">0.8</span><span class="o">*</span><span class="p">(</span><span class="mi">1</span><span class="o">-</span><span class="n">time</span><span class="p">)</span>
        <span class="k">let</span> <span class="nv">x</span> <span class="o">=</span> <span class="n">canvasSize</span><span class="o">.</span><span class="n">width</span><span class="o">/</span><span class="mi">2</span> <span class="o">+</span> <span class="nf">cos</span><span class="p">(</span><span class="n">rotations</span><span class="o">*</span><span class="n">time</span><span class="o">*</span><span class="kt">CGFloat</span><span class="o">.</span><span class="n">pi</span><span class="o">*</span><span class="mi">2</span><span class="p">)</span><span class="o">*</span><span class="n">canvasSize</span><span class="o">.</span><span class="n">width</span><span class="o">/</span><span class="mf">2*</span><span class="n">amplitude</span>
        
        <span class="k">return</span> <span class="kt">CGPoint</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="n">x</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="p">(</span><span class="mi">1</span><span class="o">-</span><span class="n">time</span><span class="p">)</span><span class="o">*</span><span class="n">canvasSize</span><span class="o">.</span><span class="n">height</span><span class="p">)</span>
    <span class="p">}</span>
    
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">TimelineView</span><span class="p">(</span><span class="o">.</span><span class="n">animation</span><span class="p">)</span> <span class="p">{</span> <span class="n">context</span> <span class="k">in</span>
            <span class="k">let</span> <span class="nv">timeInterval</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">date</span><span class="o">.</span><span class="n">timeIntervalSinceReferenceDate</span><span class="p">;</span>
            
            <span class="kt">Canvas</span> <span class="p">{</span> <span class="n">context</span><span class="p">,</span> <span class="n">size</span> <span class="k">in</span>
                <span class="k">let</span> <span class="nv">particleSymbol</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="nf">resolveSymbol</span><span class="p">(</span><span class="nv">id</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span><span class="o">!</span>
                <span class="k">let</span> <span class="nv">position</span> <span class="o">=</span> <span class="nf">particlePosition</span><span class="p">(</span><span class="nv">timeInterval</span><span class="p">:</span> <span class="n">timeInterval</span><span class="p">,</span> <span class="nv">canvasSize</span><span class="p">:</span> <span class="n">size</span><span class="p">)</span>
                
                <span class="n">context</span><span class="o">.</span><span class="nf">draw</span><span class="p">(</span><span class="n">particleSymbol</span><span class="p">,</span> <span class="nv">at</span><span class="p">:</span> <span class="n">position</span><span class="p">,</span> <span class="nv">anchor</span><span class="p">:</span> <span class="o">.</span><span class="n">center</span><span class="p">)</span>
            <span class="p">}</span> <span class="nv">symbols</span><span class="p">:</span> <span class="p">{</span>
                <span class="kt">SingleParticleView</span><span class="p">()</span>
                    <span class="o">.</span><span class="nf">tag</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<center>
<video autoplay="" muted="" loop="" controls="controls" width="400">
	<source src="/assets/posts/16_video_02.mov" />
	<source src="/assets/posts/16_video_02.webm" type="video/webm" />
</video>
</center>

<p>please note the position computation was moved to a separate function so the Canvas content remains clean.</p>

<h2 id="make-it-many">Make it many</h2>

<p>At this point, I am quite happy with the movement. I am sure we will fine-tune the constants here and there, but that can come later. Now, we want to draw more particles so let’s wrap the drawing into the for-cycle like this:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="k">let</span> <span class="nv">particleCount</span> <span class="o">=</span> <span class="mi">100</span>
<span class="c1">// …</span>
<span class="k">for</span> <span class="n">i</span> <span class="k">in</span> <span class="mi">0</span><span class="o">..&lt;</span><span class="n">particleCount</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">position</span> <span class="o">=</span> <span class="nf">particlePosition</span><span class="p">(</span><span class="nv">timeInterval</span><span class="p">:</span> <span class="n">timeInterval</span><span class="o">+</span><span class="p">(</span><span class="kt">Double</span><span class="p">(</span><span class="n">i</span><span class="p">)</span><span class="o">/</span><span class="kt">Double</span><span class="p">(</span><span class="n">particleCount</span><span class="p">)),</span> <span class="nv">canvasSize</span><span class="p">:</span> <span class="n">size</span><span class="p">)</span>
    <span class="n">context</span><span class="o">.</span><span class="nf">draw</span><span class="p">(</span><span class="n">particleSymbol</span><span class="p">,</span> <span class="nv">at</span><span class="p">:</span> <span class="n">position</span><span class="p">,</span> <span class="nv">anchor</span><span class="p">:</span> <span class="o">.</span><span class="n">center</span><span class="p">)</span>
<span class="p">}</span></code></pre></figure>

<center>
<video autoplay="" muted="" loop="" controls="controls" width="400">
	<source src="/assets/posts/16_video_03.mov" />
	<source src="/assets/posts/16_video_03.webm" type="video/webm" />
</video>
</center>

<h2 id="randomize">Randomize</h2>

<p>We can finally see more particles, but they all share the same path, so let me initiate each particle with random starting wave rotation and starting time offset:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">struct</span> <span class="kt">ParticleCanvasView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">movementDuration</span><span class="p">:</span> <span class="kt">Double</span>
    <span class="k">let</span> <span class="nv">particleCount</span><span class="p">:</span> <span class="kt">Int</span>
    <span class="k">let</span> <span class="nv">startingParticleOffsets</span><span class="p">:</span> <span class="p">[</span><span class="kt">CGFloat</span><span class="p">]</span>
    <span class="k">let</span> <span class="nv">startingParticleAlphas</span><span class="p">:</span> <span class="p">[</span><span class="kt">CGFloat</span><span class="p">]</span>
    
    <span class="nf">init</span><span class="p">(</span><span class="nv">particleCount</span><span class="p">:</span> <span class="kt">Int</span> <span class="o">=</span> <span class="mi">200</span><span class="p">,</span> <span class="nv">movementDuration</span><span class="p">:</span> <span class="kt">Double</span> <span class="o">=</span> <span class="mf">3.0</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">self</span><span class="o">.</span><span class="n">particleCount</span> <span class="o">=</span> <span class="n">particleCount</span>
        <span class="k">self</span><span class="o">.</span><span class="n">movementDuration</span> <span class="o">=</span> <span class="n">movementDuration</span>
        <span class="k">self</span><span class="o">.</span><span class="n">startingParticleOffsets</span> <span class="o">=</span> <span class="kt">Array</span><span class="p">(</span><span class="mi">0</span><span class="o">..&lt;</span><span class="n">particleCount</span><span class="p">)</span><span class="o">.</span><span class="n">map</span> <span class="p">{</span><span class="n">_</span> <span class="k">in</span> <span class="kt">CGFloat</span><span class="o">.</span><span class="nf">random</span><span class="p">(</span><span class="nv">in</span><span class="p">:</span> <span class="mi">0</span><span class="o">...</span><span class="mi">1</span><span class="p">)}</span>
        <span class="k">self</span><span class="o">.</span><span class="n">startingParticleAlphas</span> <span class="o">=</span> <span class="kt">Array</span><span class="p">(</span><span class="mi">0</span><span class="o">..&lt;</span><span class="n">particleCount</span><span class="p">)</span><span class="o">.</span><span class="n">map</span> <span class="p">{</span><span class="n">_</span> <span class="k">in</span> <span class="kt">CGFloat</span><span class="o">.</span><span class="nf">random</span><span class="p">(</span><span class="nv">in</span><span class="p">:</span> <span class="mi">0</span><span class="o">...</span><span class="kt">CGFloat</span><span class="o">.</span><span class="n">pi</span><span class="o">*</span><span class="mi">2</span><span class="p">)}</span>
    <span class="p">}</span>
    
    <span class="kd">func</span> <span class="nf">particlePosition</span><span class="p">(</span><span class="nv">index</span><span class="p">:</span> <span class="kt">Int</span><span class="p">,</span> <span class="nv">timeInterval</span><span class="p">:</span> <span class="kt">Double</span><span class="p">,</span> <span class="nv">canvasSize</span><span class="p">:</span> <span class="kt">CGSize</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">CGPoint</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">startingRotation</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="o">=</span>  <span class="n">startingParticleAlphas</span><span class="p">[</span><span class="n">index</span><span class="p">]</span><span class="c1">//CGFloat(index)/CGFloat(particleCount)*CGFloat.pi</span>
        <span class="k">let</span> <span class="nv">startingTimeOffset</span> <span class="o">=</span> <span class="n">startingParticleOffsets</span><span class="p">[</span><span class="n">index</span><span class="p">]</span><span class="o">*</span><span class="n">movementDuration</span>
        
        <span class="k">let</span> <span class="nv">time</span> <span class="o">=</span> <span class="p">(</span><span class="n">timeInterval</span><span class="o">+</span><span class="n">startingTimeOffset</span><span class="p">)</span><span class="o">.</span><span class="nf">truncatingRemainder</span><span class="p">(</span><span class="nv">dividingBy</span><span class="p">:</span> <span class="n">movementDuration</span><span class="p">)</span><span class="o">/</span><span class="n">movementDuration</span>
        <span class="k">let</span> <span class="nv">rotations</span><span class="p">:</span><span class="kt">CGFloat</span> <span class="o">=</span> <span class="mi">3</span>
        <span class="k">let</span> <span class="nv">amplitude</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="o">=</span> <span class="mf">0.1</span><span class="o">+</span><span class="mf">0.8</span><span class="o">*</span><span class="p">(</span><span class="mi">1</span><span class="o">-</span><span class="n">time</span><span class="p">)</span>
        
        <span class="k">let</span> <span class="nv">x</span> <span class="o">=</span> <span class="n">canvasSize</span><span class="o">.</span><span class="n">width</span><span class="o">/</span><span class="mi">2</span> <span class="o">+</span> <span class="nf">cos</span><span class="p">(</span><span class="n">rotations</span><span class="o">*</span><span class="n">time</span><span class="o">*</span><span class="kt">CGFloat</span><span class="o">.</span><span class="n">pi</span><span class="o">*</span><span class="mi">2</span><span class="o">+</span><span class="n">startingRotation</span><span class="p">)</span><span class="o">*</span><span class="n">canvasSize</span><span class="o">.</span><span class="n">width</span><span class="o">/</span><span class="mf">2*</span><span class="n">amplitude</span>
        
        <span class="k">return</span> <span class="kt">CGPoint</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="n">x</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="p">(</span><span class="mi">1</span><span class="o">-</span><span class="n">time</span><span class="p">)</span><span class="o">*</span><span class="n">canvasSize</span><span class="o">.</span><span class="n">height</span><span class="p">)</span>
    <span class="p">}</span></code></pre></figure>

<center>
<video autoplay="" muted="" loop="" controls="controls" width="400">
	<source src="/assets/posts/16_video_04.mov" />
	<source src="/assets/posts/16_video_04.webm" type="video/webm" />
</video>
</center>

<h2 id="improving-the-effect-appearance">Improving the effect appearance</h2>

<p>In terms of particle motion, I consider this done, but we still need to fine-tune this effect’s appearance to get some juiciness.</p>

<p>The first improvement is changing the particle opacity during the opacity movement - this is quite simple by changing the context opacity before a draw call:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="n">context</span><span class="o">.</span><span class="n">opacity</span> <span class="o">=</span> <span class="n">positionAndAlpha</span><span class="o">.</span><span class="mi">1</span></code></pre></figure>

<p>Next, let’s utilize the blending capabilities of SwiftUI and set the particle appearance like this:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">struct</span> <span class="kt">SingleParticleView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">Circle</span><span class="p">()</span><span class="o">.</span><span class="nf">fill</span><span class="p">(</span><span class="kt">Color</span><span class="o">.</span><span class="n">orange</span><span class="o">.</span><span class="nf">opacity</span><span class="p">(</span><span class="mf">0.4</span><span class="p">))</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span><span class="mi">35</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span><span class="mi">35</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">blendMode</span><span class="p">(</span><span class="o">.</span><span class="n">plusLighter</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">blur</span><span class="p">(</span><span class="nv">radius</span><span class="p">:</span> <span class="mi">10</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<p>We make particles here as a nice big blurry spots, that blends together to form a fire volume. The blendMode(.plusLighter) combines overlapping orange dots, effectively brightening the result where the patrticles intersect.</p>

<center>
<video autoplay="" muted="" loop="" controls="controls" width="400">
	<source src="/assets/posts/16_video_07.mov" />
	<source src="/assets/posts/16_video_07.webm" type="video/webm" />
</video>
</center>

<p>At this point, I am still not very happy with the result and will need to introduce more tweaks. This is very typical in such a creative process that your initial idea is not exactly aligned with the implementation and you are required to iterate on it.</p>

<p>The thing that bothers me is that the particles are more dense at the top of the view, while I would prefer otherwise. To fix that, let me get rid of the even y-axis distribution and adjust the y-coordinate like this:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="k">let</span> <span class="nv">y</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span><span class="o">-</span><span class="n">time</span><span class="o">*</span><span class="n">time</span><span class="p">)</span><span class="o">*</span><span class="n">canvasSize</span><span class="o">.</span><span class="n">height</span></code></pre></figure>

<p>Also, I would like to boost the particle vanishing effect so let me decrease the particle opacity with time.</p>

<p>The final effect I am happy with is:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">struct</span> <span class="kt">ParticleCanvasView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">movementDuration</span><span class="p">:</span> <span class="kt">Double</span>
    <span class="k">let</span> <span class="nv">particleCount</span><span class="p">:</span> <span class="kt">Int</span>
    <span class="k">let</span> <span class="nv">startingParticleOffsets</span><span class="p">:</span> <span class="p">[</span><span class="kt">CGFloat</span><span class="p">]</span>
    <span class="k">let</span> <span class="nv">startingParticleAlphas</span><span class="p">:</span> <span class="p">[</span><span class="kt">CGFloat</span><span class="p">]</span>
    
    <span class="nf">init</span><span class="p">(</span><span class="nv">particleCount</span><span class="p">:</span> <span class="kt">Int</span> <span class="o">=</span> <span class="mi">200</span><span class="p">,</span> <span class="nv">movementDuration</span><span class="p">:</span> <span class="kt">Double</span> <span class="o">=</span> <span class="mf">3.0</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">self</span><span class="o">.</span><span class="n">particleCount</span> <span class="o">=</span> <span class="n">particleCount</span>
        <span class="k">self</span><span class="o">.</span><span class="n">movementDuration</span> <span class="o">=</span> <span class="n">movementDuration</span>
        <span class="k">self</span><span class="o">.</span><span class="n">startingParticleOffsets</span> <span class="o">=</span> <span class="kt">Array</span><span class="p">(</span><span class="mi">0</span><span class="o">..&lt;</span><span class="n">particleCount</span><span class="p">)</span><span class="o">.</span><span class="n">map</span> <span class="p">{</span><span class="n">_</span> <span class="k">in</span> <span class="kt">CGFloat</span><span class="o">.</span><span class="nf">random</span><span class="p">(</span><span class="nv">in</span><span class="p">:</span> <span class="mi">0</span><span class="o">...</span><span class="mi">1</span><span class="p">)}</span>
        <span class="k">self</span><span class="o">.</span><span class="n">startingParticleAlphas</span> <span class="o">=</span> <span class="kt">Array</span><span class="p">(</span><span class="mi">0</span><span class="o">..&lt;</span><span class="n">particleCount</span><span class="p">)</span><span class="o">.</span><span class="n">map</span> <span class="p">{</span><span class="n">_</span> <span class="k">in</span> <span class="kt">CGFloat</span><span class="o">.</span><span class="nf">random</span><span class="p">(</span><span class="nv">in</span><span class="p">:</span> <span class="mi">0</span><span class="o">...</span><span class="kt">CGFloat</span><span class="o">.</span><span class="n">pi</span><span class="o">*</span><span class="mi">2</span><span class="p">)}</span>
    <span class="p">}</span>
    
    <span class="kd">func</span> <span class="nf">particlePositionAndAlpha</span><span class="p">(</span><span class="nv">index</span><span class="p">:</span> <span class="kt">Int</span><span class="p">,</span> <span class="nv">timeInterval</span><span class="p">:</span> <span class="kt">Double</span><span class="p">,</span> <span class="nv">canvasSize</span><span class="p">:</span> <span class="kt">CGSize</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="p">(</span><span class="kt">CGPoint</span><span class="p">,</span> <span class="kt">CGFloat</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">startingRotation</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="o">=</span> <span class="n">startingParticleAlphas</span><span class="p">[</span><span class="n">index</span><span class="p">]</span>
        <span class="k">let</span> <span class="nv">startingTimeOffset</span> <span class="o">=</span> <span class="n">startingParticleOffsets</span><span class="p">[</span><span class="n">index</span><span class="p">]</span><span class="o">*</span><span class="n">movementDuration</span>
        
        <span class="k">let</span> <span class="nv">time</span> <span class="o">=</span> <span class="p">(</span><span class="n">timeInterval</span><span class="o">+</span><span class="n">startingTimeOffset</span><span class="p">)</span><span class="o">.</span><span class="nf">truncatingRemainder</span><span class="p">(</span><span class="nv">dividingBy</span><span class="p">:</span> <span class="n">movementDuration</span><span class="p">)</span><span class="o">/</span><span class="n">movementDuration</span>
        <span class="k">let</span> <span class="nv">rotations</span><span class="p">:</span><span class="kt">CGFloat</span> <span class="o">=</span> <span class="mf">1.5</span>
        <span class="k">let</span> <span class="nv">amplitude</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="o">=</span> <span class="mf">0.1</span><span class="o">+</span><span class="mf">0.8</span><span class="o">*</span><span class="p">(</span><span class="mi">1</span><span class="o">-</span><span class="n">time</span><span class="p">)</span>
        
        <span class="k">let</span> <span class="nv">x</span> <span class="o">=</span> <span class="n">canvasSize</span><span class="o">.</span><span class="n">width</span><span class="o">/</span><span class="mi">2</span> <span class="o">+</span> <span class="nf">cos</span><span class="p">(</span><span class="n">rotations</span><span class="o">*</span><span class="n">time</span><span class="o">*</span><span class="kt">CGFloat</span><span class="o">.</span><span class="n">pi</span><span class="o">*</span><span class="mi">2</span> <span class="o">+</span> <span class="n">startingRotation</span><span class="p">)</span><span class="o">*</span><span class="n">canvasSize</span><span class="o">.</span><span class="n">width</span><span class="o">/</span><span class="mf">2*</span><span class="n">amplitude</span><span class="o">*</span><span class="mf">0.8</span>
        <span class="k">let</span> <span class="nv">y</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span><span class="o">-</span><span class="n">time</span><span class="o">*</span><span class="n">time</span><span class="p">)</span><span class="o">*</span><span class="n">canvasSize</span><span class="o">.</span><span class="n">height</span>
        
        <span class="nf">return</span> <span class="p">(</span><span class="kt">CGPoint</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="n">x</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">y</span><span class="p">),</span> <span class="mi">1</span><span class="o">-</span><span class="n">time</span><span class="p">)</span>
    <span class="p">}</span>
    
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">TimelineView</span><span class="p">(</span><span class="o">.</span><span class="n">animation</span><span class="p">)</span> <span class="p">{</span> <span class="n">context</span> <span class="k">in</span>
            <span class="k">let</span> <span class="nv">timeInterval</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">date</span><span class="o">.</span><span class="n">timeIntervalSinceReferenceDate</span><span class="p">;</span>
            
            <span class="kt">Canvas</span> <span class="p">{</span> <span class="n">context</span><span class="p">,</span> <span class="n">size</span> <span class="k">in</span>
                <span class="k">let</span> <span class="nv">particleSymbol</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="nf">resolveSymbol</span><span class="p">(</span><span class="nv">id</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span><span class="o">!</span>
                
                <span class="k">for</span> <span class="n">i</span> <span class="k">in</span> <span class="mi">0</span><span class="o">..&lt;</span><span class="n">particleCount</span> <span class="p">{</span>
                    <span class="k">let</span> <span class="nv">positionAndAlpha</span> <span class="o">=</span> <span class="nf">particlePositionAndAlpha</span><span class="p">(</span><span class="nv">index</span><span class="p">:</span> <span class="n">i</span><span class="p">,</span> <span class="nv">timeInterval</span><span class="p">:</span> <span class="n">timeInterval</span><span class="p">,</span> <span class="nv">canvasSize</span><span class="p">:</span> <span class="n">size</span><span class="p">)</span>
                    <span class="n">context</span><span class="o">.</span><span class="n">opacity</span> <span class="o">=</span> <span class="n">positionAndAlpha</span><span class="o">.</span><span class="mi">1</span>
                    <span class="n">context</span><span class="o">.</span><span class="nf">draw</span><span class="p">(</span><span class="n">particleSymbol</span><span class="p">,</span> <span class="nv">at</span><span class="p">:</span> <span class="n">positionAndAlpha</span><span class="o">.</span><span class="mi">0</span><span class="p">,</span> <span class="nv">anchor</span><span class="p">:</span> <span class="o">.</span><span class="n">center</span><span class="p">)</span>
                <span class="p">}</span>
            <span class="p">}</span> <span class="nv">symbols</span><span class="p">:</span> <span class="p">{</span>
                <span class="kt">SingleParticleView</span><span class="p">()</span>
                    <span class="o">.</span><span class="nf">tag</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<center>
<video autoplay="" muted="" loop="" controls="controls" width="400">
	<source src="/assets/posts/16_video_05.mov" />
	<source src="/assets/posts/16_video_05.webm" type="video/webm" />
</video>
</center>

<h2 id="your-turn">Your turn!</h2>

<p>Now it is your time to get creative!</p>

<p>Things to try</p>
<ul>
  <li>change particle appearance</li>
  <li>change particle movement paths</li>
  <li>combine multiple particle types</li>
  <li>react to user inputs</li>
  <li>💫 …</li>
</ul>

<p>Here is a result of more experiments and adjustments:</p>

<center>
<video autoplay="" muted="" loop="" controls="controls" width="400">
	<source src="/assets/posts/16_video_06.mov" />
	<source src="/assets/posts/16_video_06.webm" type="video/webm" />
</video>
</center>

<p>Enjoy! And let me know, if you find this article helpful, and send me your animations on <a href="https://twitter.com/myridiphis">Twitter</a>.</p>]]></content><author><name>Pavel Zak</name></author><category term="development" /><category term="SwiftUI" /><category term="Canvas" /><category term="Particles" /><category term="BlendMode" /><category term="TimelineView" /><summary type="html"><![CDATA[In one of the previous posts, I shared a simple way of Creating particle effects in SwiftUI. The approach is super easy and utilizes the power of viewModifiers, but I would not recommend it for production use as it is performance-greedy when having a bigger amount of particles in place (because each particle is a single view)]]></summary></entry><entry><title type="html">SwiftUI transitions with distortion effect and Metal Shaders</title><link href="https://nerdyak.tech/development/2023/06/16/distortionEffect-with-Metal-shaders-for-better-transitions.html" rel="alternate" type="text/html" title="SwiftUI transitions with distortion effect and Metal Shaders" /><published>2023-06-16T00:00:00+00:00</published><updated>2023-06-16T00:00:00+00:00</updated><id>https://nerdyak.tech/development/2023/06/16/distortionEffect-with-Metal-shaders-for-better-transitions</id><content type="html" xml:base="https://nerdyak.tech/development/2023/06/16/distortionEffect-with-Metal-shaders-for-better-transitions.html"><![CDATA[<p>This year DubDub is over and I am very excited about the new developer treats that iOS17 will bring us that expand the animation possibilities of SwiftUI. I am talking mainly about the <a href="https://developer.apple.com/documentation/swiftui/phaseanimator/">PhaseAnimator</a>, <a href="https://developer.apple.com/documentation/swiftui/keyframeanimator">KeyframeAnimator</a> and the ability to utilize Metal shaders on SwiftUI views through modifiers .distortionEffect, .layerEffect, and .colorEffect (<a href="https://developer.apple.com/documentation/swiftui/view-graphics-and-rendering#shaders">docs</a>).</p>

<p>In this post, we will play with the .distortionEffect and learn, how to utilize it for creating custom transitions, like this:</p>

<center>
<video autoplay="" muted="" loop="" controls="controls">
	<source src="/assets/posts/15_video1.mov" />
	<source src="/assets/posts/15_video1.webm" type="video/webm" />
</video>
</center>

<h2 id="kickstart">Kickstart</h2>

<p>Let’s start with the simple Hello world view and set a basic .distortion effect for the view:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">struct</span> <span class="kt">DemoView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    
    <span class="k">var</span> <span class="nv">shader</span><span class="p">:</span> <span class="kt">Shader</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">shaderLibrary</span> <span class="o">=</span> <span class="kt">ShaderLibrary</span><span class="o">.</span><span class="k">default</span>
        <span class="k">return</span> <span class="kt">Shader</span><span class="p">(</span><span class="nv">function</span><span class="p">:</span> <span class="kt">ShaderFunction</span><span class="p">(</span><span class="nv">library</span><span class="p">:</span> <span class="n">shaderLibrary</span><span class="p">,</span> <span class="nv">name</span><span class="p">:</span> <span class="s">"demoShader"</span><span class="p">),</span> <span class="nv">arguments</span><span class="p">:</span> <span class="p">[])</span>
    <span class="p">}</span>
    
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">VStack</span> <span class="p">{</span>
            <span class="kt">Image</span><span class="p">(</span><span class="nv">systemName</span><span class="p">:</span> <span class="s">"globe"</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">imageScale</span><span class="p">(</span><span class="o">.</span><span class="n">large</span><span class="p">)</span>
            <span class="kt">Text</span><span class="p">(</span><span class="s">"Hello, distortionEffect!"</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="o">.</span><span class="nf">padding</span><span class="p">()</span>
        <span class="o">.</span><span class="nf">background</span><span class="p">(</span><span class="kt">Color</span><span class="o">.</span><span class="n">blue</span><span class="o">.</span><span class="nf">opacity</span><span class="p">(</span><span class="mf">0.1</span><span class="p">))</span>
        <span class="o">.</span><span class="nf">distortionEffect</span><span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="n">shader</span><span class="p">,</span> <span class="nv">maxSampleOffset</span><span class="p">:</span> <span class="kt">CGSize</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="mi">500</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="mi">500</span><span class="p">))</span>
        <span class="o">.</span><span class="nf">overlay</span><span class="p">(</span><span class="kt">Rectangle</span><span class="p">()</span><span class="o">.</span><span class="nf">stroke</span><span class="p">(</span><span class="kt">Color</span><span class="o">.</span><span class="n">blue</span><span class="p">))</span> <span class="c1">// adding stroke so we see bounds of our view</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<p>We need to add a new Metal file to our Xcode project and define the shader function there. As the documentation says: For a shader function to act as a distortion effect it must have a function signature matching:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="p">[[</span> <span class="n">stitchable</span> <span class="p">]]</span> <span class="n">float2</span> <span class="nf">name</span><span class="p">(</span><span class="n">float2</span> <span class="n">position</span><span class="p">,</span> <span class="n">args</span><span class="o">...</span><span class="p">)</span></code></pre></figure>

<p>which tells us, that the function can “alter” the position of every pixel of our View. So in the minimum variation, the Shader can look like this</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="cp">#include &lt;metal_stdlib&gt;</span>
<span class="n">using</span> <span class="n">namespace</span> <span class="n">metal</span><span class="p">;</span>

<span class="p">[[</span> <span class="n">stitchable</span> <span class="p">]]</span> <span class="n">float2</span> <span class="nf">demoShader</span><span class="p">(</span><span class="n">float2</span> <span class="n">position</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">position</span><span class="p">;</span>
<span class="p">}</span></code></pre></figure>

<p>This is a fully working shader, but you cannot see any effect on the view as it is basically an identity function that just returns the input positions.</p>

<p><img src="/assets/posts/15_01.png" alt="image1" title="Main view" /></p>

<p>But with just a few simple alterations to the return value, you start seeing how the view image changes. For example, you can switch the x and y axis and get transposed view.</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="p">[[</span> <span class="n">stitchable</span> <span class="p">]]</span> <span class="n">float2</span> <span class="nf">demoShader</span><span class="p">(</span><span class="n">float2</span> <span class="n">position</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nf">float2</span><span class="p">(</span><span class="n">position</span><span class="o">.</span><span class="n">y</span><span class="p">,</span> <span class="n">position</span><span class="o">.</span><span class="n">x</span><span class="p">);</span>
<span class="p">}</span></code></pre></figure>

<p><img src="/assets/posts/15_02.png" alt="image2" title="Transposed view" /></p>

<p>Notice, that the shader cannot extend our view dimensions, so if we are transposing the rectangular view, the result will be cropped. But we learned an important lesson here: if we are returning an invalid position from the shader, it just leaves the pixel transparent. And this fact we are gonna utilize for the transitions later.</p>

<p>But before we move to that, let’s see, how to extend the shader with more parameters. From the initial function signature we see, that the shader is not aware of the view frame, so naturally, we want to provide at least that parameter. Adding a new param is simple as that, just extend the function with more parameters and provide those parameters in the SwiftUI part like so.</p>

<p>Very easily, we are now able to create waved distortion effect using the shader:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="p">[[</span> <span class="n">stitchable</span> <span class="p">]]</span> <span class="n">float2</span> <span class="nf">demoShader</span><span class="p">(</span><span class="n">float2</span> <span class="n">position</span><span class="p">,</span> <span class="n">float2</span> <span class="n">size</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">float</span> <span class="n">f</span> <span class="o">=</span> <span class="nf">sin</span><span class="p">(</span><span class="n">position</span><span class="o">.</span><span class="n">x</span><span class="o">/</span><span class="n">size</span><span class="o">.</span><span class="n">x</span><span class="o">*</span><span class="kt">M_PI_F</span><span class="o">*</span><span class="mi">2</span><span class="p">);</span>
    <span class="k">return</span> <span class="nf">float2</span><span class="p">(</span><span class="n">position</span><span class="o">.</span><span class="n">x</span><span class="p">,</span> <span class="n">position</span><span class="o">.</span><span class="n">y</span><span class="o">+</span><span class="n">f</span><span class="o">*</span><span class="mi">20</span><span class="p">);</span>
<span class="p">}</span></code></pre></figure>

<p><img src="/assets/posts/15_03.png" alt="image3" title="Flag effect" /></p>

<p>The full view with applied shader with parameter is now:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">struct</span> <span class="kt">DemoView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    
    <span class="k">let</span> <span class="nv">size</span><span class="p">:</span> <span class="kt">CGPoint</span> <span class="o">=</span> <span class="kt">CGPoint</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="mi">300</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="mi">200</span><span class="p">)</span>
    
    <span class="k">var</span> <span class="nv">shader</span><span class="p">:</span> <span class="kt">Shader</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">shaderLibrary</span> <span class="o">=</span> <span class="kt">ShaderLibrary</span><span class="o">.</span><span class="k">default</span>
        <span class="k">return</span> <span class="kt">Shader</span><span class="p">(</span><span class="nv">function</span><span class="p">:</span> <span class="kt">ShaderFunction</span><span class="p">(</span><span class="nv">library</span><span class="p">:</span> <span class="n">shaderLibrary</span><span class="p">,</span> <span class="nv">name</span><span class="p">:</span> <span class="s">"demoShader"</span><span class="p">),</span> <span class="nv">arguments</span><span class="p">:</span> <span class="p">[</span><span class="o">.</span><span class="nf">float2</span><span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="n">size</span><span class="p">)])</span>
    <span class="p">}</span>
    
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">VStack</span> <span class="p">{</span>
            <span class="kt">Image</span><span class="p">(</span><span class="nv">systemName</span><span class="p">:</span> <span class="s">"globe"</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">imageScale</span><span class="p">(</span><span class="o">.</span><span class="n">large</span><span class="p">)</span>
            <span class="kt">Text</span><span class="p">(</span><span class="s">"Hello, distortionEffect!"</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="n">size</span><span class="o">.</span><span class="n">x</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="n">size</span><span class="o">.</span><span class="n">y</span><span class="p">)</span>
        <span class="o">.</span><span class="nf">background</span><span class="p">(</span><span class="kt">Color</span><span class="o">.</span><span class="n">blue</span><span class="o">.</span><span class="nf">opacity</span><span class="p">(</span><span class="mf">0.1</span><span class="p">))</span>
        <span class="o">.</span><span class="nf">distortionEffect</span><span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="n">shader</span><span class="p">,</span> <span class="nv">maxSampleOffset</span><span class="p">:</span> <span class="kt">CGSize</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="mi">500</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="mi">500</span><span class="p">))</span>
        <span class="o">.</span><span class="nf">overlay</span><span class="p">(</span><span class="kt">Rectangle</span><span class="p">()</span><span class="o">.</span><span class="nf">stroke</span><span class="p">(</span><span class="kt">Color</span><span class="o">.</span><span class="n">blue</span><span class="p">))</span> <span class="c1">// adding stroke so we see bounds of our view</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<p>Try to experiment with the shader parameters and messing the output with various functions. <em>How fun</em>, right?</p>

<h2 id="transitions">Transitions</h2>

<p>Now, let’s utilize what we have learned to build a custom view transition. I want to create an effect, that shifts away the content of the view like so:</p>

<center>
<video autoplay="" muted="" loop="" controls="controls">
	<source src="/assets/posts/15_video2.mov" />
	<source src="/assets/posts/15_video2.webm" type="video/webm" />
</video>
</center>

<p>for that, I extend the shader with one more parameter named effectValue, which controls the offset and the skew of the view content. My shader looks like this:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="p">[[</span> <span class="n">stitchable</span> <span class="p">]]</span> <span class="n">float2</span> <span class="nf">demoShader</span><span class="p">(</span><span class="n">float2</span> <span class="n">position</span><span class="p">,</span> <span class="n">float2</span> <span class="n">size</span><span class="p">,</span> <span class="n">float</span> <span class="n">effectValue</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">float</span> <span class="n">skewF</span> <span class="o">=</span> <span class="mf">0.1</span><span class="o">*</span><span class="n">size</span><span class="o">.</span><span class="n">x</span><span class="p">;</span>
    <span class="n">float</span> <span class="n">yRatio</span> <span class="o">=</span> <span class="n">position</span><span class="o">.</span><span class="n">y</span><span class="o">/</span><span class="n">size</span><span class="o">.</span><span class="n">y</span><span class="p">;</span>
    
    <span class="n">float</span> <span class="n">positiveEffect</span> <span class="o">=</span> <span class="n">effectValue</span><span class="o">*</span><span class="nf">sign</span><span class="p">(</span><span class="n">effectValue</span><span class="p">);</span>
    <span class="n">float</span> <span class="n">skewProgress</span> <span class="o">=</span> <span class="nf">min</span><span class="p">(</span><span class="mf">0.5</span><span class="o">-</span><span class="nf">abs</span><span class="p">(</span><span class="n">positiveEffect</span><span class="o">-</span><span class="mf">0.5</span><span class="p">),</span> <span class="mf">0.2</span><span class="p">)</span><span class="o">/</span><span class="mf">0.2</span><span class="p">;</span>
    
    <span class="n">float</span> <span class="n">skew</span> <span class="o">=</span> <span class="n">effectValue</span><span class="o">&gt;</span><span class="mi">0</span> <span class="p">?</span> <span class="n">yRatio</span><span class="o">*</span><span class="n">skewF</span><span class="o">*</span><span class="nv">skewProgress</span> <span class="p">:</span> <span class="p">(</span><span class="mi">1</span><span class="o">-</span><span class="n">yRatio</span><span class="p">)</span><span class="o">*</span><span class="n">skewF</span><span class="o">*</span><span class="n">skewProgress</span><span class="p">;</span>
    <span class="n">float</span> <span class="n">shift</span> <span class="o">=</span> <span class="n">effectValue</span><span class="o">*</span><span class="n">size</span><span class="o">.</span><span class="n">x</span><span class="p">;</span>
    
    <span class="k">return</span> <span class="nf">float2</span><span class="p">(</span><span class="n">position</span><span class="o">.</span><span class="n">x</span><span class="o">+</span><span class="p">(</span><span class="n">shift</span><span class="o">+</span><span class="n">skew</span><span class="o">*</span><span class="nf">sign</span><span class="p">(</span><span class="n">effectValue</span><span class="p">)),</span> <span class="n">position</span><span class="o">.</span><span class="n">y</span><span class="p">);</span>
<span class="p">}</span></code></pre></figure>

<p>The shader is result of some experimentation session trying to tweak the behavior so it works also for the negative effect values. The goal is to use positive value for insertion transition and negative for removal. While testing, I am using a simple slider just to be sure that it behaves correctly for all effectValues between -1 and 1.</p>

<center>
<video autoplay="" muted="" loop="" controls="controls">
	<source src="/assets/posts/15_video3.mov" />
	<source src="/assets/posts/15_video3.webm" type="video/webm" />
</video>
</center>

<p>Once we are happy with the basic effect, we just wrap it into a view modifier. (I am keeping the size parameter as a constant just for the simplicity of the code snippet. OFC, you can set it dynamically from the actual geometry)</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">struct</span> <span class="kt">ShiftTransitionModifier</span><span class="p">:</span> <span class="kt">ViewModifier</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">size</span><span class="p">:</span> <span class="kt">CGPoint</span> <span class="o">=</span> <span class="kt">CGPoint</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="mi">300</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="mi">200</span><span class="p">)</span>
    <span class="k">var</span> <span class="nv">effectValue</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="o">=</span> <span class="mi">0</span>
    
    <span class="k">var</span> <span class="nv">shader</span><span class="p">:</span> <span class="kt">Shader</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">shaderLibrary</span> <span class="o">=</span> <span class="kt">ShaderLibrary</span><span class="o">.</span><span class="k">default</span>
        <span class="k">return</span> <span class="kt">Shader</span><span class="p">(</span><span class="nv">function</span><span class="p">:</span> <span class="kt">ShaderFunction</span><span class="p">(</span><span class="nv">library</span><span class="p">:</span> <span class="n">shaderLibrary</span><span class="p">,</span> <span class="nv">name</span><span class="p">:</span> <span class="s">"demoShader"</span><span class="p">),</span> <span class="nv">arguments</span><span class="p">:</span> <span class="p">[</span><span class="o">.</span><span class="nf">float2</span><span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="n">size</span><span class="p">),</span> <span class="o">.</span><span class="nf">float</span><span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="n">effectValue</span><span class="p">)])</span>
    <span class="p">}</span>
    
    <span class="kd">func</span> <span class="nf">body</span><span class="p">(</span><span class="nv">content</span><span class="p">:</span> <span class="kt">Content</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="n">content</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">size</span><span class="o">.</span><span class="n">x</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">size</span><span class="o">.</span><span class="n">y</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">distortionEffect</span><span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="n">shader</span><span class="p">,</span> <span class="nv">maxSampleOffset</span><span class="p">:</span> <span class="kt">CGSize</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="mi">500</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="mi">500</span><span class="p">))</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<p>And the last step is to use the modifier for a custom Transition definition:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="k">let</span> <span class="nv">insertionTransition</span><span class="p">:</span> <span class="kt">AnyTransition</span> <span class="o">=</span> <span class="o">.</span><span class="nf">modifier</span><span class="p">(</span><span class="nv">active</span><span class="p">:</span> <span class="kt">ShiftTransitionModifier</span><span class="p">(</span><span class="nv">effectValue</span><span class="p">:</span> <span class="mi">1</span><span class="p">),</span> <span class="nv">identity</span><span class="p">:</span> <span class="kt">ShiftTransitionModifier</span><span class="p">(</span><span class="nv">effectValue</span><span class="p">:</span> <span class="mi">0</span><span class="p">))</span>
<span class="k">let</span> <span class="nv">removalTransition</span><span class="p">:</span> <span class="kt">AnyTransition</span> <span class="o">=</span> <span class="o">.</span><span class="nf">modifier</span><span class="p">(</span><span class="nv">active</span><span class="p">:</span> <span class="kt">ShiftTransitionModifier</span><span class="p">(</span><span class="nv">effectValue</span><span class="p">:</span> <span class="o">-</span><span class="mi">1</span><span class="p">),</span> <span class="nv">identity</span><span class="p">:</span> <span class="kt">ShiftTransitionModifier</span><span class="p">(</span><span class="nv">effectValue</span><span class="p">:</span> <span class="mi">0</span><span class="p">))</span>

<span class="k">let</span> <span class="nv">shiftTransition</span><span class="p">:</span> <span class="kt">AnyTransition</span> <span class="o">=</span> <span class="o">.</span><span class="nf">asymmetric</span><span class="p">(</span><span class="nv">insertion</span><span class="p">:</span> <span class="n">insertionTransition</span><span class="p">,</span> <span class="nv">removal</span><span class="p">:</span> <span class="n">removalTransition</span><span class="p">)</span></code></pre></figure>

<p>And we are done.</p>

<center>
<video autoplay="" muted="" loop="" controls="controls">
	<source src="/assets/posts/15_video2.mov" />
	<source src="/assets/posts/15_video2.webm" type="video/webm" />
</video>
</center>

<h2 id="one-more-thing">One more thing</h2>

<p>If you are wondering, how does the shader look like for the transition from the beginning of this article, it is here:
(Notice, the shader looks much nicer due to after-experimentation optimizations and utilizing vector operations)</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="p">[[</span> <span class="n">stitchable</span> <span class="p">]]</span> <span class="n">float2</span> <span class="nf">slideAwayShader</span><span class="p">(</span><span class="n">float2</span> <span class="n">position</span><span class="p">,</span> <span class="n">float2</span> <span class="n">size</span><span class="p">,</span> <span class="n">float</span> <span class="n">time</span><span class="p">,</span> <span class="n">float</span> <span class="n">direction</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">float2</span> <span class="n">c</span> <span class="o">=</span> <span class="n">size</span><span class="o">/</span><span class="mi">2</span><span class="p">;</span>
    <span class="n">float2</span> <span class="n">v</span> <span class="o">=</span> <span class="n">position</span> <span class="o">-</span> <span class="n">c</span><span class="p">;</span>
    
    <span class="n">float</span> <span class="n">f</span> <span class="o">=</span> <span class="p">(</span><span class="n">direction</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="p">?</span> <span class="n">position</span><span class="o">.</span><span class="nv">x</span> <span class="p">:</span> <span class="p">(</span><span class="n">size</span><span class="o">.</span><span class="n">x</span> <span class="o">-</span> <span class="n">position</span><span class="o">.</span><span class="n">x</span><span class="p">)</span> <span class="p">)</span><span class="o">/</span><span class="n">size</span><span class="o">.</span><span class="n">x</span><span class="p">;</span>
    
    <span class="k">if</span> <span class="p">(</span> <span class="n">time</span> <span class="o">&gt;</span> <span class="n">f</span> <span class="p">)</span> <span class="p">{</span>
        <span class="n">float</span> <span class="n">mul</span> <span class="o">=</span> <span class="p">(</span><span class="n">time</span><span class="o">-</span><span class="n">f</span><span class="p">)</span><span class="o">/</span><span class="p">(</span><span class="mi">1</span><span class="o">-</span><span class="n">f</span><span class="p">);</span>
        <span class="k">return</span> <span class="n">c</span> <span class="o">+</span> <span class="n">v</span><span class="o">*</span><span class="n">mul</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">else</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nf">float2</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<h2 id="your-turn">Your turn!</h2>

<p>Now it is <strong>your time</strong> to get creative!</p>

<p>Let me know, if you find this article helpful, and send me your animations on <a href="https://twitter.com/myridiphis">Twitter</a>.</p>]]></content><author><name>Pavel Zak</name></author><category term="development" /><category term="SwiftUI" /><category term="distortionEffect" /><category term="Metal" /><category term="Shaders" /><category term="transitions" /><summary type="html"><![CDATA[This year DubDub is over and I am very excited about the new developer treats that iOS17 will bring us that expand the animation possibilities of SwiftUI. I am talking mainly about the PhaseAnimator, KeyframeAnimator and the ability to utilize Metal shaders on SwiftUI views through modifiers .distortionEffect, .layerEffect, and .colorEffect (docs).]]></summary></entry><entry><title type="html">Avoid Spacers in SwiftUI Stacks</title><link href="https://nerdyak.tech/development/2023/04/06/avoid-swiftui-spacers-in-stacks.html" rel="alternate" type="text/html" title="Avoid Spacers in SwiftUI Stacks" /><published>2023-04-06T00:00:00+00:00</published><updated>2023-04-06T00:00:00+00:00</updated><id>https://nerdyak.tech/development/2023/04/06/avoid-swiftui-spacers-in-stacks</id><content type="html" xml:base="https://nerdyak.tech/development/2023/04/06/avoid-swiftui-spacers-in-stacks.html"><![CDATA[<p>As I teach SwiftUI here and there I have noticed a particular pattern that is being used and I would like to comment on a possible issue it can lead to. Let’s explore it!</p>

<p><img src="/assets/posts/14_cell.png" alt="image1" title="Typical view layout" /></p>

<p>This is a widespread structure in various apps and a very common way how to code it in SwiftUI:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kt">HStack</span><span class="p">(</span><span class="nv">spacing</span><span class="p">:</span> <span class="mi">12</span><span class="p">)</span> <span class="p">{</span>
	<span class="kt">Text</span><span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="n">text</span><span class="p">)</span>
	<span class="kt">Spacer</span><span class="p">()</span>
	<span class="kt">Image</span><span class="p">(</span><span class="nv">systemName</span><span class="p">:</span> <span class="s">"tortoise.fill"</span><span class="p">)</span>
<span class="p">}</span></code></pre></figure>

<p>At first glance, everything looks fine. But what if the text starts to overflow? Suddenly it feels, that the wrapping leaves too much space between the text and the trailing icon even though we have set it to some fixed value…</p>

<p>Hmm, let’s examine by replacing the views with just colors:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kt">HStack</span><span class="p">(</span><span class="nv">spacing</span><span class="p">:</span> <span class="mi">12</span><span class="p">)</span> <span class="p">{</span>
	<span class="kt">Color</span><span class="o">.</span><span class="n">blue</span>
	<span class="kt">Spacer</span><span class="p">()</span>
	<span class="kt">Color</span><span class="o">.</span><span class="n">red</span>
<span class="p">}</span></code></pre></figure>

<p>Aha, we are right! The space between is twice as wide as it should be. So the Stack here, even though the spacer does not add anything, puts declared spaces around it.</p>

<p><img src="/assets/posts/14_colors.png" alt="image2" title="Expected layout and the actual issue" /></p>

<p>So how to fix it? You can either remove the spacing parameter from the stack and use explicit padding to its child views, or IMO more convenient way is to make one of the views stretchable using a <em>.frame</em> modifier and setting its <em>maxWidth</em> attribute to <em>.infinity</em> like so:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kt">HStack</span><span class="p">(</span><span class="nv">spacing</span><span class="p">:</span> <span class="mi">12</span><span class="p">)</span> <span class="p">{</span>
	<span class="kt">Text</span><span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="n">text</span><span class="p">)</span>
		<span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">,</span> <span class="nv">alignment</span><span class="p">:</span> <span class="o">.</span><span class="n">leading</span><span class="p">)</span>
	<span class="kt">Image</span><span class="p">(</span><span class="nv">systemName</span><span class="p">:</span> <span class="s">"tortoise.fill"</span><span class="p">)</span>
<span class="p">}</span></code></pre></figure>

<p>Let’s compare the behavior of before and after approaches:</p>

<p><img src="/assets/posts/14_comparison.png" alt="image3" title="Comparing solutions" /></p>

<p>I personally like the latter solution as it</p>
<ul>
  <li>brings simpler code structure</li>
  <li>has the flexibility to set the <em>alignment</em> of the stretched view (ideal for centering of the view content when needed)</li>
  <li>prevents padding/spacing issues when having optional views in the Stacks</li>
</ul>]]></content><author><name>Pavel Zak</name></author><category term="development" /><category term="SwiftUI" /><category term="Spacer" /><category term="Stack" /><category term="Frame" /><summary type="html"><![CDATA[As I teach SwiftUI here and there I have noticed a particular pattern that is being used and I would like to comment on a possible issue it can lead to. Let’s explore it!]]></summary></entry><entry><title type="html">Creating particles in SwiftUI - in less than 50 lines of code</title><link href="https://nerdyak.tech/development/2020/12/12/create-particles-in-swiftui.html" rel="alternate" type="text/html" title="Creating particles in SwiftUI - in less than 50 lines of code" /><published>2020-12-12T00:00:00+00:00</published><updated>2020-12-12T00:00:00+00:00</updated><id>https://nerdyak.tech/development/2020/12/12/create-particles-in-swiftui</id><content type="html" xml:base="https://nerdyak.tech/development/2020/12/12/create-particles-in-swiftui.html"><![CDATA[<p>I have put together a short video about creating particle effects with SwiftUI. It showcases the easiest/lazy way, which is great for simple eye candies and experimenting.</p>

<p>Please note, that it is not the most performant way how to deal with particle effects in SwiftUI. If you need to go crazy about blending and high particle count, there are better ways:</p>
<ul>
  <li>using the Canvas view (see <a href="/development/2024/06/27/particle-effects-with-SwiftUI-Canvas.html">Magical Particle Effects with SwiftUI Canvas</a></li>
  <li>embed SpriteKit view</li>
  <li>using MetalKit
But more on these another time ;)</li>
</ul>

<p>Enjoy!</p>

<div class="yt-container">
 <iframe src="https://www.youtube.com/embed/oj4HEqkDvBY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen=""></iframe>
 </div>]]></content><author><name>Pavel Zak</name></author><category term="development" /><category term="SwiftUI" /><category term="Particle" /><category term="ViewModifier" /><summary type="html"><![CDATA[I have put together a short video about creating particle effects with SwiftUI. It showcases the easiest/lazy way, which is great for simple eye candies and experimenting.]]></summary></entry><entry><title type="html">Mastering transitions in SwiftUI</title><link href="https://nerdyak.tech/development/2020/10/12/transitions-in-swiftui.html" rel="alternate" type="text/html" title="Mastering transitions in SwiftUI" /><published>2020-10-12T00:00:00+00:00</published><updated>2020-10-12T00:00:00+00:00</updated><id>https://nerdyak.tech/development/2020/10/12/transitions-in-swiftui</id><content type="html" xml:base="https://nerdyak.tech/development/2020/10/12/transitions-in-swiftui.html"><![CDATA[<p>Transitions play a vital role in the user experience of our apps. They are visual keys signalizing that the app or screen context is changing.</p>

<p>In this article, we will go through all important parts related to the implementation of <a href="https://developer.apple.com/documentation/swiftui/anytransition">transitions</a> in SwiftUI - from the very basics to more advanced techniques. At the end of this article, you will be able to implement transitions like this:</p>

<center>
<video autoplay="" muted="" loop="" controls="controls">
	<source src="/assets/posts/12_video.mov" />
	<source src="/assets/posts/12_video.webm" type="video/webm" />
</video>
</center>

<h2 id="triggering-the-transition">Triggering the transition</h2>

<p>Let us remind, what the transition is:</p>

<p><strong>Transition is an animation that might be triggered when some View is being added to or removed from the View hierarchy</strong></p>

<p>In practice, the change in the view hierarchy can usually come from the conditional statements, so let us have a look at the following example.</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">struct</span> <span class="kt">BasicTransitionView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">@State</span> <span class="k">var</span> <span class="nv">showText</span> <span class="o">=</span> <span class="kc">false</span>
    
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">VStack</span> <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="n">showText</span><span class="p">)</span> <span class="p">{</span>
                <span class="kt">Text</span><span class="p">(</span><span class="s">"HELLO WORLD"</span><span class="p">)</span>
            <span class="p">}</span>
            <span class="kt">Button</span><span class="p">(</span><span class="nv">action</span><span class="p">:</span> <span class="p">{</span>
                <span class="nf">withAnimation</span><span class="p">()</span> <span class="p">{</span>
                    <span class="k">self</span><span class="o">.</span><span class="n">showText</span><span class="o">.</span><span class="nf">toggle</span><span class="p">()</span>
                <span class="p">}</span>
            <span class="p">})</span> <span class="p">{</span>
                <span class="kt">Text</span><span class="p">(</span><span class="s">"Change me"</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<p><img src="/assets/posts/12_transition1.gif" alt="vid1" title="Default transition" /></p>

<p>There is a Text that is displayed only in the situation when <code class="language-plaintext highlighter-rouge">showText</code> is true. It is important to mention:</p>

<ul>
  <li>The state change needs to be done withing <code class="language-plaintext highlighter-rouge">withAnimation()</code> block. If you explicitly state the animation here, it will affect the transition timing curve and duration.</li>
  <li>By default, the transition is fade in /fade out. We will see below, how to change it</li>
  <li>Note how layout changes and adapts to additional view causing the button itself to jump a bit lower. This behavior is completely valid for our situation with VStack, just keep in mind that inserted/removed view may affect surrounding views.</li>
</ul>

<p>A transition may be triggered also when changing <code class="language-plaintext highlighter-rouge">.id()</code> of a view. In such situation, SwiftUI removes the old View and creates a new one which may trigger transition on both old and new views.</p>

<h2 id="basic-transitions">Basic transitions</h2>

<p>To change the default transition we can set up a new one using <code class="language-plaintext highlighter-rouge">.transition</code> view modifier. There are several types already available for basic view transformations</p>

<ul>
  <li>.scale</li>
  <li>.move</li>
  <li>.offset</li>
  <li>.slide</li>
  <li>.opacity</li>
</ul>

<p>In our example, let me demonstrate the usage of <code class="language-plaintext highlighter-rouge">move</code> transition that shifts view being added/removed from/towards the leading edge of <strong>its</strong> frame. We just need to apply <code class="language-plaintext highlighter-rouge">.transition(.move(edge: .leading))</code> to our text view.</p>

<p><img src="/assets/posts/12_transition7.gif" alt="vid8" title="Move transition" /></p>

<p>Feel free to experiment with the other types.</p>

<h2 id="combining-transitions">Combining transitions</h2>

<p>The list of basic transitions is pretty short and most probably would not be sufficient, but luckily we can create more complex transitions with two powerful mechanisms</p>

<ul>
  <li>transition combination</li>
  <li>transition created from any view modifier</li>
</ul>

<p>You can combine two transitions using <code class="language-plaintext highlighter-rouge">.combine(with:)</code> method that returns a new transition that is the result of <strong>both transitions being applied</strong>.</p>

<p>The amount of transition combinations is not limited, so for example this combination of opacity, scale and move transition is perfectly fine:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="o">.</span><span class="nf">transition</span><span class="p">(</span> <span class="kt">AnyTransition</span><span class="o">.</span><span class="nf">move</span><span class="p">(</span><span class="nv">edge</span><span class="p">:</span> <span class="o">.</span><span class="n">leading</span><span class="p">)</span><span class="o">.</span><span class="nf">combined</span><span class="p">(</span><span class="nv">with</span><span class="p">:</span> <span class="kt">AnyTransition</span><span class="o">.</span><span class="n">opacity</span><span class="p">)</span><span class="o">.</span><span class="nf">combined</span><span class="p">(</span><span class="nv">with</span><span class="p">:</span> <span class="o">.</span><span class="n">scale</span><span class="p">)</span> <span class="p">)</span></code></pre></figure>

<p><img src="/assets/posts/12_transition3.gif" alt="vid3" title="Transition combination" /></p>

<h2 id="asymetric-transitions">Asymetric transitions</h2>

<p>In case you require to perform different transition on removal than on insertion, it is possible to create asymetric transition using the static function <code class="language-plaintext highlighter-rouge">asymmetric(...)</code>, for example</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="o">.</span><span class="nf">transition</span><span class="p">(</span> <span class="kt">AnyTransition</span><span class="o">.</span><span class="nf">asymmetric</span><span class="p">(</span><span class="nv">insertion</span><span class="p">:</span> <span class="o">.</span><span class="n">scale</span><span class="p">,</span> <span class="nv">removal</span><span class="p">:</span> <span class="o">.</span><span class="n">opacity</span><span class="p">))</span></code></pre></figure>

<p><img src="/assets/posts/12_transition4.gif" alt="vid4" title="Assymetric transition" /></p>

<p>Asymetric transitions are especially handy in situations when the inserted view could overlap the removed view and thus produce unaesthetic animation.</p>

<h2 id="custom-transitions">Custom transitions</h2>

<p>The creative part begins with the possibility to define your own transition based on view modifiers. Let me start with a very simple example to demonstrate the basics.</p>

<p>Here is a custom view modifier that masks the view with a rounded rectangle shape. The value parameter is expected to have values between 0 and 1 when 0 clips the view entirely and 1 reveals the full view.</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">struct</span> <span class="kt">ClipEffect</span><span class="p">:</span> <span class="kt">ViewModifier</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">value</span><span class="p">:</span> <span class="kt">CGFloat</span>
    
    <span class="kd">func</span> <span class="nf">body</span><span class="p">(</span><span class="nv">content</span><span class="p">:</span> <span class="kt">Content</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="n">content</span>
            <span class="o">.</span><span class="nf">clipShape</span><span class="p">(</span><span class="kt">RoundedRectangle</span><span class="p">(</span><span class="nv">cornerRadius</span><span class="p">:</span> <span class="mf">100*</span><span class="p">(</span><span class="mi">1</span><span class="o">-</span><span class="n">value</span><span class="p">))</span><span class="o">.</span><span class="nf">scale</span><span class="p">(</span><span class="n">value</span><span class="p">))</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<p>Now, we can define custom transition that applies this effect as follows:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="o">.</span><span class="nf">transition</span><span class="p">(</span><span class="kt">AnyTransition</span><span class="o">.</span><span class="nf">modifier</span><span class="p">(</span><span class="nv">active</span><span class="p">:</span> <span class="kt">ClipEffect</span><span class="p">(</span><span class="nv">value</span><span class="p">:</span> <span class="mi">0</span><span class="p">),</span> <span class="nv">identity</span><span class="p">:</span> <span class="kt">ClipEffect</span><span class="p">(</span><span class="nv">value</span><span class="p">:</span> <span class="mi">1</span><span class="p">)))</span></code></pre></figure>

<p>Note: To ease future re-usability, it may be beneficial to define your transitions as static members of AnyTransition:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">extension</span> <span class="kt">AnyTransition</span> <span class="p">{</span>
    <span class="kd">static</span> <span class="k">var</span> <span class="nv">clipTransition</span><span class="p">:</span> <span class="kt">AnyTransition</span> <span class="p">{</span>
        <span class="o">.</span><span class="nf">modifier</span><span class="p">(</span>
            <span class="nv">active</span><span class="p">:</span> <span class="kt">ClipEffect</span><span class="p">(</span><span class="nv">value</span><span class="p">:</span> <span class="mi">0</span><span class="p">),</span>
            <span class="nv">identity</span><span class="p">:</span> <span class="kt">ClipEffect</span><span class="p">(</span><span class="nv">value</span><span class="p">:</span> <span class="mi">1</span><span class="p">)</span>
        <span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<p>when used at our example <code class="language-plaintext highlighter-rouge">.transition(.clipTransition)</code>, the result looks like this:</p>

<p><img src="/assets/posts/12_transition2.gif" alt="vid2" title="Transition from the effect" /></p>

<p>Please note that:</p>

<ul>
  <li>modifier transition depends on two states: <strong>active and identity</strong>. Identity is applied when the view is fully inserted in the view hierarchy and active when the view is gone.</li>
  <li>During the transition, SwiftUI interpolates between these two states, and thus the type of active and identity modifier <strong>needs to be the same</strong>! (XCode will complain otherwise)</li>
</ul>

<h2 id="mastering-transitions">Mastering transitions</h2>

<p>So far we went through quite basic stuff so maybe, you have not realized how powerful all these things are. Making a transition from custom view modifiers allows you to unleash your creativity and with a little help of <a href="/development/2019/08/29/tweaking-animations-with-GeometryEffect.html">Geometry Effect</a>, <a href="https://developer.apple.com/documentation/swiftui/animatablemodifier">AnimatableModifier</a> or blending modes you can create transitions that stand out. Let me showcase several examples:</p>

<h3 id="counting-up-transition">Counting up transition</h3>

<p>Handy for score or achievement screens, count up to the value of Text view using this simple AnimatableModifier:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">struct</span> <span class="kt">CountUpEffect</span><span class="p">:</span> <span class="kt">AnimatableModifier</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">value</span><span class="p">:</span> <span class="kt">CGFloat</span>
    
    <span class="k">var</span> <span class="nv">animatableData</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="p">{</span>
        <span class="k">get</span> <span class="p">{</span> <span class="k">return</span> <span class="n">value</span> <span class="p">}</span>
        <span class="k">set</span> <span class="p">{</span> <span class="n">value</span> <span class="o">=</span> <span class="n">newValue</span> <span class="p">}</span>
    <span class="p">}</span>
    
    <span class="kd">func</span> <span class="nf">body</span><span class="p">(</span><span class="nv">content</span><span class="p">:</span> <span class="kt">Content</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">Text</span><span class="p">(</span><span class="s">"</span><span class="se">\(</span><span class="k">self</span><span class="o">.</span><span class="n">value</span><span class="p">,</span> <span class="nv">specifier</span><span class="p">:</span> <span class="s">"%.1f"</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">font</span><span class="p">(</span><span class="o">.</span><span class="n">title</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">foregroundColor</span><span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="n">value</span><span class="o">&lt;</span><span class="mi">100</span> <span class="p">?</span> <span class="o">.</span><span class="nv">primary</span> <span class="p">:</span> <span class="o">.</span><span class="n">red</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<p>In this case, we need to provide a parameter to the transition itself, so I have prepared simple extension that will provide us with the right transition:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">extension</span> <span class="kt">AnyTransition</span> <span class="p">{</span>
    <span class="kd">static</span> <span class="kd">func</span> <span class="nf">countUpTransition</span><span class="p">(</span><span class="nv">value</span><span class="p">:</span> <span class="kt">CGFloat</span><span class="p">)</span><span class="o">-&gt;</span><span class="kt">AnyTransition</span> <span class="p">{</span>
        <span class="k">return</span> <span class="kt">AnyTransition</span><span class="o">.</span><span class="nf">modifier</span><span class="p">(</span><span class="nv">active</span><span class="p">:</span> <span class="kt">CountUpEffect</span><span class="p">(</span><span class="nv">value</span><span class="p">:</span> <span class="mi">0</span><span class="p">),</span> <span class="nv">identity</span><span class="p">:</span> <span class="kt">CountUpEffect</span><span class="p">(</span><span class="nv">value</span><span class="p">:</span> <span class="n">value</span><span class="p">))</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<p>And for the sake of excercise, we will combine it with the <code class="language-plaintext highlighter-rouge">.scale</code> transition when applied to the view:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="o">.</span><span class="nf">transition</span><span class="p">(</span><span class="kt">AnyTransition</span><span class="o">.</span><span class="nf">countUpTransition</span><span class="p">(</span><span class="nv">value</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">textValue</span><span class="p">)</span><span class="o">.</span><span class="nf">combined</span><span class="p">(</span><span class="nv">with</span><span class="p">:</span> <span class="o">.</span><span class="n">scale</span><span class="p">))</span></code></pre></figure>

<p><img src="/assets/posts/12_transition5.gif" alt="vid5" title="Count up transition" /></p>

<h3 id="taking-content-apart">Taking content apart</h3>

<p>Combining several layers of content in the view modifier is my absolute favorite way to create unique transitions. The sliding door transition is a nice way how to demonstrate this approach</p>

<p>Once again, let me start with the effect itself:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">struct</span> <span class="kt">SlidingDoorEffect</span><span class="p">:</span> <span class="kt">ViewModifier</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">shift</span><span class="p">:</span> <span class="kt">CGFloat</span>
    
    <span class="kd">func</span> <span class="nf">body</span><span class="p">(</span><span class="nv">content</span><span class="p">:</span> <span class="kt">Content</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">c</span> <span class="o">=</span> <span class="n">content</span>
        <span class="k">return</span> <span class="kt">ZStack</span> <span class="p">{</span>
            <span class="n">c</span><span class="o">.</span><span class="nf">clipShape</span><span class="p">(</span><span class="kt">HalfClipShape</span><span class="p">(</span><span class="nv">left</span><span class="p">:</span> <span class="kc">false</span><span class="p">))</span><span class="o">.</span><span class="nf">offset</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="o">-</span><span class="n">shift</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span>
            <span class="n">c</span><span class="o">.</span><span class="nf">clipShape</span><span class="p">(</span><span class="kt">HalfClipShape</span><span class="p">(</span><span class="nv">left</span><span class="p">:</span> <span class="kc">true</span><span class="p">))</span><span class="o">.</span><span class="nf">offset</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="n">shift</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">HalfClipShape</span><span class="p">:</span> <span class="kt">Shape</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">left</span><span class="p">:</span> <span class="kt">Bool</span>
        
    <span class="kd">func</span> <span class="nf">path</span><span class="p">(</span><span class="k">in</span> <span class="nv">rect</span><span class="p">:</span> <span class="kt">CGRect</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Path</span> <span class="p">{</span>
        <span class="c1">// shape covers lef or right part of rect</span>
        <span class="k">return</span> <span class="kt">Path</span> <span class="p">{</span> <span class="n">path</span> <span class="k">in</span>
            <span class="k">let</span> <span class="nv">width</span> <span class="o">=</span> <span class="n">rect</span><span class="o">.</span><span class="n">width</span>
            <span class="k">let</span> <span class="nv">height</span> <span class="o">=</span> <span class="n">rect</span><span class="o">.</span><span class="n">height</span>
            
            <span class="k">let</span> <span class="nv">startx</span><span class="p">:</span><span class="kt">CGFloat</span> <span class="o">=</span> <span class="n">left</span> <span class="p">?</span> <span class="mi">0</span> <span class="p">:</span> <span class="n">width</span><span class="o">/</span><span class="mi">2</span>
            <span class="k">let</span> <span class="nv">shapeWidth</span><span class="p">:</span><span class="kt">CGFloat</span> <span class="o">=</span> <span class="n">width</span><span class="o">/</span><span class="mi">2</span>
            
            <span class="n">path</span><span class="o">.</span><span class="nf">move</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="kt">CGPoint</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="n">startx</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="mi">0</span><span class="p">))</span>
            
            <span class="n">path</span><span class="o">.</span><span class="nf">addLines</span><span class="p">([</span>
                <span class="kt">CGPoint</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="n">startx</span><span class="o">+</span><span class="n">shapeWidth</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="mi">0</span><span class="p">),</span>
                <span class="kt">CGPoint</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="n">startx</span><span class="o">+</span><span class="n">shapeWidth</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">height</span><span class="p">),</span>
                <span class="kt">CGPoint</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="n">startx</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">height</span><span class="p">),</span>
                <span class="kt">CGPoint</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="n">startx</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span>
            <span class="p">])</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<p>This effect duplicates the view content into ZStack and masks/clips only the left part of the bottom layer and right part of the top layer. Both <em>copies</em> of the content are later moved apart depending on the shift parameter.</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kt">Text</span><span class="p">(</span><span class="s">"HELLO WORLD"</span><span class="p">)</span>
	<span class="o">.</span><span class="nf">padding</span><span class="p">()</span>
    <span class="o">.</span><span class="nf">background</span><span class="p">(</span><span class="kt">Color</span><span class="o">.</span><span class="n">orange</span><span class="p">)</span>
    <span class="o">.</span><span class="nf">transition</span><span class="p">(</span><span class="kt">AnyTransition</span><span class="o">.</span><span class="nf">modifier</span><span class="p">(</span><span class="nv">active</span><span class="p">:</span> <span class="kt">SlidingDoorEffect</span><span class="p">(</span><span class="nv">shift</span><span class="p">:</span> <span class="mi">170</span><span class="p">),</span> <span class="nv">identity</span><span class="p">:</span> <span class="kt">SlidingDoorEffect</span><span class="p">(</span><span class="nv">shift</span><span class="p">:</span> <span class="mi">0</span><span class="p">)))</span></code></pre></figure>

<p><img src="/assets/posts/12_transition6.gif" alt="vid6" title="Sliding door transition" /></p>

<p>You can go really crazy when mixing multiple occurrences of the content so please, bear in mind that</p>

<ul>
  <li>mixing many (hundrets) content layers may lead to performance issues. The identity modifier <strong>remains applied</strong> even when the transition finished</li>
  <li>even though I refer to the content layers as <em>copies</em> they are <strong>different views of the same type</strong>!</li>
  <li>if the content is animated, it will remain live during the whole transition</li>
  <li>there might be some <em>cuts</em> visible even after the transition ends. If you encouter this situation, the solution may be to clip the sub-areas with <em>slight</em> (1-2px) overlap</li>
</ul>

<p>The transition shown in the <a href="/development/2020/07/17/creating-onboarding-screens-in-swiftui.html">previous blog post</a> about onboarding screens was also done with this approach. The only difference is that the view is cut into quarters.</p>

<p><img src="/assets/posts/11_vid4.gif" alt="vid7" title="Transition used on onboarding screens" /></p>

<h2 id="the-task">The Task</h2>

<p>The best way to learn something new is to practice it. So for this topic, I have a challenge for you.</p>

<p>Implement transition that <em>explodes</em> the view on removal. Example:</p>

<center>
<video autoplay="" muted="" loop="" controls="controls">
	<source src="/assets/posts/12_explode.mov" />
	<source src="/assets/posts/12_explode.webm" type="video/webm" />
</video>
</center>

<p>How to do it?</p>

<ol>
  <li>start with a <a href="/development/2019/08/29/tweaking-animations-with-GeometryEffect.html">Geometry Effect</a> that animates a view along a curve</li>
  <li>prepare a masking shape for content sub-regions</li>
  <li>create an explosion effect that multiplies the content into many sub-regions, masking each one of them with relevant clipShape and offsetting using the effect from the first step</li>
  <li>wrap new effect into transition</li>
  <li>PROFIT</li>
</ol>]]></content><author><name>Pavel Zak</name></author><category term="development" /><category term="SwiftUI" /><category term="Transition" /><category term="ViewModifier" /><category term="ConditionalView" /><summary type="html"><![CDATA[Transitions play a vital role in the user experience of our apps. They are visual keys signalizing that the app or screen context is changing.]]></summary></entry><entry><title type="html">Creating onboarding screens in SwiftUI</title><link href="https://nerdyak.tech/development/2020/07/17/creating-onboarding-screens-in-swiftui.html" rel="alternate" type="text/html" title="Creating onboarding screens in SwiftUI" /><published>2020-07-17T00:00:00+00:00</published><updated>2020-07-17T00:00:00+00:00</updated><id>https://nerdyak.tech/development/2020/07/17/creating-onboarding-screens-in-swiftui</id><content type="html" xml:base="https://nerdyak.tech/development/2020/07/17/creating-onboarding-screens-in-swiftui.html"><![CDATA[<p>In this post, I would like to discuss several ways how to create onboarding/introduction screens for your app. Let me keep aside the discussion if such screens are good UX pattern, but let me rather examine SwiftUI capabilities for such task instead.</p>

<h2 id="the-task">The task</h2>

<p>The task for today’s SwiftUI exercise is simple. We would like our app to have <code class="language-plaintext highlighter-rouge">N</code> onboarding pages which our user can browse through at the first app launch. Browsing can be made either with a swipe gesture or by pressing the next button.</p>

<p>Something like this:</p>

<p><img src="/assets/posts/11_vid1.gif" alt="vid1" title="Demonstration of onboarding screen" /></p>

<h2 id="intropageview">IntroPageView</h2>

<p>We will start with the preparation of a single view that contains an illustration, the title, and description. It is a basic View, you can notice that I like to use a combination of <code class="language-plaintext highlighter-rouge">stacks</code> and <code class="language-plaintext highlighter-rouge">Spacers</code> for easy alignment of the subviews.</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">struct</span> <span class="kt">IntroPage</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">imageName</span><span class="p">:</span> <span class="kt">String</span>
    <span class="k">let</span> <span class="nv">title</span><span class="p">:</span> <span class="kt">String</span>
    <span class="k">let</span> <span class="nv">description</span><span class="p">:</span> <span class="kt">String</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">IntroPageView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">page</span><span class="p">:</span> <span class="kt">IntroPage</span>
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">VStack</span> <span class="p">{</span>
            <span class="kt">Spacer</span><span class="p">()</span>
            <span class="kt">Image</span><span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="n">page</span><span class="o">.</span><span class="n">imageName</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">resizable</span><span class="p">()</span>
                <span class="o">.</span><span class="nf">aspectRatio</span><span class="p">(</span><span class="nv">contentMode</span><span class="p">:</span> <span class="o">.</span><span class="n">fit</span><span class="p">)</span>
            <span class="kt">Spacer</span><span class="p">()</span>
            <span class="kt">Group</span> <span class="p">{</span>
                <span class="kt">HStack</span> <span class="p">{</span>
                    <span class="kt">Text</span><span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="n">page</span><span class="o">.</span><span class="n">title</span><span class="p">)</span>
                        <span class="o">.</span><span class="nf">font</span><span class="p">(</span><span class="o">.</span><span class="n">title</span><span class="p">)</span>
                        <span class="o">.</span><span class="nf">foregroundColor</span><span class="p">(</span><span class="o">.</span><span class="n">purple</span><span class="p">)</span>
                    <span class="kt">Spacer</span><span class="p">()</span>
                <span class="p">}</span>
                <span class="kt">HStack</span> <span class="p">{</span>
                    <span class="kt">Text</span><span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="n">page</span><span class="o">.</span><span class="n">description</span><span class="p">)</span>
                    <span class="kt">Spacer</span><span class="p">()</span>
                <span class="p">}</span>
            <span class="p">}</span>
            <span class="o">.</span><span class="nf">padding</span><span class="p">()</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<h2 id="using-tabview">Using TabView</h2>

<p>If you are familiar with UIKit, you would most probably look for a Scrollview with set property <code class="language-plaintext highlighter-rouge">isPagingEnabled = true</code> to start with. Unfortunately, SwiftUI does not have the exact counterpart, but that does not mean, there are no solutions for our case. Of course, one can use <code class="language-plaintext highlighter-rouge">UIScrollView</code> based component and wrap it using <code class="language-plaintext highlighter-rouge">UIViewRepresentable</code> to SwiftUI, but let me focus on pure SwiftUI solutions only.</p>

<p>Since the recent SwiftUI update, often denoted as SwiftUI2, we can use <code class="language-plaintext highlighter-rouge">TabView</code> for our purpose. Even though TabView is mainly purposed for Views organized in Tabbar, applying <code class="language-plaintext highlighter-rouge">.tabViewStyle(PageTabViewStyle())</code> creates exactly what we would expect from a paginated scroll view, it even creates a paging indicator at the bottom of the view!</p>

<p><em>(To remove or customize paging indicator appearance, set <code class="language-plaintext highlighter-rouge">.indexViewStyle(PageIndexViewStyle())</code> appropriately ;)</em></p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">struct</span> <span class="kt">ContentView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">pages</span><span class="p">:</span> <span class="p">[</span><span class="kt">IntroPage</span><span class="p">]</span>
    
    <span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">currentPage</span> <span class="o">=</span> <span class="mi">0</span>
    
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">VStack</span> <span class="p">{</span>
        
            <span class="kt">TabView</span><span class="p">(</span><span class="nv">selection</span><span class="p">:</span> <span class="err">$</span><span class="n">currentPage</span><span class="p">)</span> <span class="p">{</span>
                <span class="kt">ForEach</span> <span class="p">(</span><span class="mi">0</span> <span class="o">..&lt;</span> <span class="k">self</span><span class="o">.</span><span class="n">pages</span><span class="o">.</span><span class="n">count</span><span class="p">)</span> <span class="p">{</span> <span class="n">index</span> <span class="k">in</span>
                    <span class="kt">IntroPageView</span><span class="p">(</span><span class="nv">page</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">pages</span><span class="p">[</span><span class="n">index</span><span class="p">])</span>
                        <span class="o">.</span><span class="nf">tag</span><span class="p">(</span><span class="n">index</span><span class="p">)</span>
                        <span class="o">.</span><span class="nf">padding</span><span class="p">()</span>
                <span class="p">}</span>
            <span class="p">}</span>
            <span class="o">.</span><span class="nf">tabViewStyle</span><span class="p">(</span><span class="kt">PageTabViewStyle</span><span class="p">())</span> <span class="c1">// the important part</span>
        
			<span class="c1">// NEXT button</span>
            <span class="kt">HStack</span> <span class="p">{</span>
                <span class="kt">Spacer</span><span class="p">()</span>
                <span class="kt">Button</span><span class="p">(</span><span class="nv">action</span><span class="p">:</span> <span class="p">{</span>
                    <span class="nf">withAnimation</span> <span class="p">(</span><span class="o">.</span><span class="nf">easeInOut</span><span class="p">(</span><span class="nv">duration</span><span class="p">:</span> <span class="mf">1.0</span><span class="p">))</span> <span class="p">{</span>
                        <span class="k">self</span><span class="o">.</span><span class="n">currentPage</span> <span class="o">=</span> <span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="n">currentPage</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span><span class="o">%</span><span class="k">self</span><span class="o">.</span><span class="n">pages</span><span class="o">.</span><span class="n">count</span>
                    <span class="p">}</span>
                <span class="p">})</span> <span class="p">{</span>
                    <span class="kt">Image</span><span class="p">(</span><span class="nv">systemName</span><span class="p">:</span> <span class="s">"arrow.right"</span><span class="p">)</span>
                        <span class="o">.</span><span class="nf">font</span><span class="p">(</span><span class="o">.</span><span class="n">largeTitle</span><span class="p">)</span>
                        <span class="o">.</span><span class="nf">foregroundColor</span><span class="p">(</span><span class="kt">Color</span><span class="o">.</span><span class="n">white</span><span class="p">)</span>
                        <span class="o">.</span><span class="nf">padding</span><span class="p">()</span>
                        <span class="o">.</span><span class="nf">background</span><span class="p">(</span><span class="kt">Circle</span><span class="p">()</span><span class="o">.</span><span class="nf">fill</span><span class="p">(</span><span class="kt">Color</span><span class="o">.</span><span class="n">purple</span><span class="p">))</span>
                <span class="p">}</span>
            <span class="p">}</span>
            <span class="o">.</span><span class="nf">padding</span><span class="p">()</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<p>Nice, clean, and easy. There is only one drawback - this works on iOS14 only… So what if you want/need to support iOS13 as well?</p>

<h2 id="pagingscrollview-based-on-hstack">PagingScrollView based on HStack</h2>

<p>As already mentioned, the standard Scrollview in SwiftUI has very limited capabilities compared to UIScrollView. It is not possible to enable pagination and prior to SwiftUI2 it was also impossible to scroll to a specific subview. (Now you can use <code class="language-plaintext highlighter-rouge">ScrollViewReader</code> for that purpose, but again - supported on iOS14 only)</p>

<p>A bit harder way but with plenty of freedom is to take <code class="language-plaintext highlighter-rouge">HStack</code> and implement the scrolling using gesture recognizers. With this approach, you had to work with stack offset and change it according to user actions. You can inspire and/or directly use the component <a href="https://github.com/izakpavel/SwiftUIPagingScrollView">SwiftUIPagingScrollview</a> that I have prepared and is available on Github. The interface is far from ideal as it requires messing with <code class="language-plaintext highlighter-rouge">GeometryReader</code>, but for illustration:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kt">GeometryReader</span> <span class="p">{</span> <span class="n">geometry</span> <span class="k">in</span>
	<span class="kt">PagingScrollView</span><span class="p">(</span><span class="nv">activePageIndex</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="err">$</span><span class="n">currentPage</span><span class="p">,</span> 
					 <span class="nv">itemCount</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">pages</span><span class="o">.</span><span class="n">count</span><span class="p">,</span> 
					 <span class="nv">pageWidth</span><span class="p">:</span> <span class="n">geometry</span><span class="o">.</span><span class="n">size</span><span class="o">.</span><span class="n">width</span><span class="p">,</span> 
					 <span class="nv">tileWidth</span><span class="p">:</span> <span class="n">geometry</span><span class="o">.</span><span class="n">size</span><span class="o">.</span><span class="n">width</span><span class="p">,</span> 
					 <span class="nv">tilePadding</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    	<span class="kt">ForEach</span> <span class="p">(</span><span class="mi">0</span> <span class="o">..&lt;</span> <span class="k">self</span><span class="o">.</span><span class="n">pages</span><span class="o">.</span><span class="n">count</span><span class="p">)</span> <span class="p">{</span> <span class="n">index</span> <span class="k">in</span>
        	<span class="kt">IntroPageView</span><span class="p">(</span><span class="nv">page</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">pages</span><span class="p">[</span><span class="n">index</span><span class="p">])</span>
        <span class="p">}</span>
   	<span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<p>This solution gives us great scrolling experience:</p>

<p><img src="/assets/posts/11_vid2.gif" alt="vid2" title="Implementation using custom SwiftUIPagingScrollView" /></p>

<h2 id="transitions-and-id-modifier">Transitions and <code class="language-plaintext highlighter-rouge">id</code> modifier</h2>

<p>If you are not very strict about direct scrolling with your finger, an interesting approach is to use custom transitions that simulate the scrolling animation. The transitions are being triggered when views are being added or removed from the view hierarchy. You can create any fancy and asymmetric transition using composition of viewModifiers, but for now we will be OK just by combination of standard moving transition, like this:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">extension</span> <span class="kt">AnyTransition</span> <span class="p">{</span>
    <span class="kd">static</span> <span class="k">var</span> <span class="nv">pageTransition</span><span class="p">:</span> <span class="kt">AnyTransition</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">insertion</span> <span class="o">=</span> <span class="kt">AnyTransition</span><span class="o">.</span><span class="nf">move</span><span class="p">(</span><span class="nv">edge</span><span class="p">:</span> <span class="o">.</span><span class="n">trailing</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">combined</span><span class="p">(</span><span class="nv">with</span><span class="p">:</span> <span class="o">.</span><span class="n">opacity</span><span class="p">)</span>
        <span class="k">let</span> <span class="nv">removal</span> <span class="o">=</span> <span class="kt">AnyTransition</span><span class="o">.</span><span class="nf">move</span><span class="p">(</span><span class="nv">edge</span><span class="p">:</span> <span class="o">.</span><span class="n">leading</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">combined</span><span class="p">(</span><span class="nv">with</span><span class="p">:</span> <span class="o">.</span><span class="n">opacity</span><span class="p">)</span>
        <span class="k">return</span> <span class="o">.</span><span class="nf">asymmetric</span><span class="p">(</span><span class="nv">insertion</span><span class="p">:</span> <span class="n">insertion</span><span class="p">,</span> <span class="nv">removal</span><span class="p">:</span> <span class="n">removal</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<p>Please note that I have added there also opacity change just for the fun and demonstration of transition combinations.</p>

<p>Now, to setup several pages with transitions one could write something like this:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kt">Group</span> <span class="p">{</span>
	<span class="k">if</span> <span class="mi">0</span> <span class="o">==</span> <span class="k">self</span><span class="o">.</span><span class="n">currentPage</span> <span class="p">{</span>
    	<span class="kt">IntroPageView</span><span class="p">(</span><span class="nv">page</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">pages</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="mi">1</span> <span class="o">==</span> <span class="k">self</span><span class="o">.</span><span class="n">currentPage</span> <span class="p">{</span>
    	<span class="kt">IntroPageView</span><span class="p">(</span><span class="nv">page</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">pages</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="mi">2</span> <span class="o">==</span> <span class="k">self</span><span class="o">.</span><span class="n">currentPage</span> <span class="p">{</span>
    	<span class="kt">IntroPageView</span><span class="p">(</span><span class="nv">page</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">pages</span><span class="p">[</span><span class="mi">2</span><span class="p">])</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="mi">3</span> <span class="o">==</span> <span class="k">self</span><span class="o">.</span><span class="n">currentPage</span> <span class="p">{</span>
    	<span class="kt">IntroPageView</span><span class="p">(</span><span class="nv">page</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">pages</span><span class="p">[</span><span class="mi">3</span><span class="p">])</span>
	<span class="p">}</span>
<span class="p">}</span><span class="o">.</span><span class="nf">transition</span><span class="p">(</span><span class="kt">AnyTransition</span><span class="o">.</span><span class="n">pageTransition</span><span class="p">)</span></code></pre></figure>

<p>As you see, that is not very nice and scaleable. (But note the usage of <code class="language-plaintext highlighter-rouge">Group</code> view that sets the transition to each of its subviews)</p>

<p>Much nicer and more elegant solutuon is to use identity modifier <code class="language-plaintext highlighter-rouge">id</code> like so:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kt">IntroPageView</span><span class="p">(</span><span class="nv">page</span><span class="p">:</span> <span class="n">pages</span><span class="p">[</span><span class="k">self</span><span class="o">.</span><span class="n">currentPage</span><span class="p">])</span>
	<span class="o">.</span><span class="nf">transition</span><span class="p">(</span><span class="kt">AnyTransition</span><span class="o">.</span><span class="n">pageTransition</span><span class="p">)</span>
	<span class="o">.</span><span class="nf">id</span><span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="n">currentPage</span><span class="p">)</span></code></pre></figure>

<p><img src="/assets/posts/11_vid3.gif" alt="vid3" title="Implementation with custom transitions" /></p>

<p>Nice, right? Whenever assigned identifier changes, the view is being replaced with the new one and thus transitions are triggered both for the old view (removal) and new view (insertion).</p>

<h2 id="summary">Summary</h2>

<p>Today I have tried to present several ways of building up onboarding screens in SwiftUI. I have examined three approaches that can satisfy most of the use cases - at least I believe so. Let me review them once again:</p>

<h3 id="use-tabview">Use TabView</h3>

<p>➖ iOS14 only; low <em>coolness</em> factor (can be tweaked with parallax effects though); cannot set animation style to tab change</p>

<p>➕ quick and easy</p>

<h3 id="implement-custom-scrollview-based-on-hstack">Implement custom Scrollview based on HStack</h3>

<p>➖ non-trivial implementation; mixing with other scrollable components might lead to issues</p>

<p>➕ ability to fine-tune everything; great scrolling feeling</p>

<h3 id="use-just-transitions">Use just transitions</h3>

<p>➖ no way to mimic true scrollview behavior (if you need it)</p>

<p>➕ clean and easy; you can go crazy with custom transitions, like the one below. But that is for another story and my <a href="/development/2020/10/12/transitions-in-swiftui.html">next blog post</a> 😉.</p>

<p><img src="/assets/posts/11_vid4.gif" alt="vid4" title="You can be very creative with transitions" /></p>]]></content><author><name>Pavel Zak</name></author><category term="development" /><category term="SwiftUI" /><category term="Transition" /><category term="ScrollView" /><category term="TabView" /><category term="PageTabViewStyle" /><summary type="html"><![CDATA[In this post, I would like to discuss several ways how to create onboarding/introduction screens for your app. Let me keep aside the discussion if such screens are good UX pattern, but let me rather examine SwiftUI capabilities for such task instead.]]></summary></entry><entry><title type="html">TikTok logo-ish effect in SwiftUI</title><link href="https://nerdyak.tech/development/2020/06/12/create-tiktok-logo-effect-in-swiftui.html" rel="alternate" type="text/html" title="TikTok logo-ish effect in SwiftUI" /><published>2020-06-12T00:00:00+00:00</published><updated>2020-06-12T00:00:00+00:00</updated><id>https://nerdyak.tech/development/2020/06/12/create-tiktok-logo-effect-in-swiftui</id><content type="html" xml:base="https://nerdyak.tech/development/2020/06/12/create-tiktok-logo-effect-in-swiftui.html"><![CDATA[<p>Hello and welcome to another blog post about <a href="https://developer.apple.com/documentation/swiftui">SwiftUI</a>. This years WWDC is approaching fast and the expectation of new SwiftUI features and/or changes is tremendous. So I have decided to dedicate this post to something lighter yet still playful.</p>

<p>I will share with you a simple way of creating a TikTok logo-ish effect but the main takeaway of this article is meant to be understanding how to create reusable styling for your app.</p>

<h2 id="start-with-blending-of-layers">Start with blending of layers</h2>

<p>The key to creating our logo is in specific blending of two colored shapes. Please check the code first:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">struct</span> <span class="kt">CommandLogoView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">logo</span> <span class="o">=</span> <span class="kt">Image</span><span class="p">(</span><span class="nv">systemName</span><span class="p">:</span> <span class="s">"command"</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">font</span><span class="p">(</span><span class="o">.</span><span class="nf">system</span><span class="p">(</span><span class="nv">size</span><span class="p">:</span> <span class="mi">200</span><span class="p">,</span> <span class="nv">weight</span><span class="p">:</span> <span class="o">.</span><span class="n">bold</span><span class="p">,</span> <span class="nv">design</span><span class="p">:</span> <span class="o">.</span><span class="k">default</span><span class="p">))</span>
        
        <span class="k">return</span> <span class="n">logo</span>
            <span class="o">.</span><span class="nf">foregroundColor</span><span class="p">(</span><span class="kt">Color</span><span class="p">(</span><span class="s">"tiktokRed"</span><span class="p">))</span>
            <span class="o">.</span><span class="nf">blendMode</span><span class="p">(</span><span class="o">.</span><span class="n">plusLighter</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">overlay</span><span class="p">(</span>
                <span class="n">logo</span>
                    <span class="o">.</span><span class="nf">foregroundColor</span><span class="p">(</span><span class="kt">Color</span><span class="p">(</span><span class="s">"tiktokBlue"</span><span class="p">))</span>
                    <span class="o">.</span><span class="nf">blendMode</span><span class="p">(</span><span class="o">.</span><span class="n">plusLighter</span><span class="p">)</span>
                    <span class="o">.</span><span class="nf">offset</span><span class="p">(</span><span class="kt">CGSize</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="o">-</span><span class="mi">4</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="o">-</span><span class="mi">3</span><span class="p">))</span>
            <span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<p>As you see, I have used a simple SF Symbol as our logo. The resulting view takes this image, fills it with custom red color (defined in project assets) and as <code class="language-plaintext highlighter-rouge">.overlay</code> sets the same logo in blue, but slightly shifted with <code class="language-plaintext highlighter-rouge">.offset</code> viewModifier. The white area is the result of blend mode that is set to <code class="language-plaintext highlighter-rouge">.plusLighter</code>.</p>

<p><img src="/assets/posts/10_tiktok_effect.jpg" alt="logo" title="Custom logo with TikTok effect" /></p>

<p><em>Take this example and try to experiment with the viewModifier parameters, especially <code class="language-plaintext highlighter-rouge">.blendMode</code> to see how it affects the result. For instance, if you wish to use your logo on white background, you may want to set it to <code class="language-plaintext highlighter-rouge">.plusDarker</code> instead</em></p>

<h2 id="wrap-the-effect-as-custom-viewmodifier">Wrap the effect as custom ViewModifier</h2>

<p>We have achieved our logo with effect quite easily, but what if we need to apply the same effect to more elements in our app?</p>

<p>The beauty of SwiftUI is that you can specify custom <a href="https://developer.apple.com/documentation/swiftui/viewmodifier">ViewModifier</a> and hide almost any of your styling into its implementation and thus gain the ability to reuse it anywhere.</p>

<p>My implementation looks like this:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">struct</span> <span class="kt">TikTokEffect</span><span class="p">:</span> <span class="kt">ViewModifier</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">offset</span><span class="p">:</span> <span class="kt">CGSize</span>
    
    <span class="kd">func</span> <span class="nf">negOffset</span><span class="p">()</span><span class="o">-&gt;</span><span class="kt">CGSize</span> <span class="p">{</span>
        <span class="k">return</span> <span class="kt">CGSize</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="o">-</span><span class="k">self</span><span class="o">.</span><span class="n">offset</span><span class="o">.</span><span class="n">width</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="o">-</span><span class="k">self</span><span class="o">.</span><span class="n">offset</span><span class="o">.</span><span class="n">height</span><span class="p">)</span>
    <span class="p">}</span>
    
    <span class="kd">func</span> <span class="nf">body</span><span class="p">(</span><span class="nv">content</span><span class="p">:</span> <span class="kt">Content</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="n">content</span>
            <span class="o">.</span><span class="nf">offset</span><span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="nf">negOffset</span><span class="p">())</span>
            <span class="o">.</span><span class="nf">foregroundColor</span><span class="p">(</span><span class="kt">Color</span><span class="p">(</span><span class="s">"tiktokRed"</span><span class="p">))</span>
            <span class="o">.</span><span class="nf">blendMode</span><span class="p">(</span><span class="o">.</span><span class="n">plusLighter</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">overlay</span><span class="p">(</span>
                <span class="n">content</span>
                    <span class="o">.</span><span class="nf">foregroundColor</span><span class="p">(</span><span class="kt">Color</span><span class="p">(</span><span class="s">"tiktokBlue"</span><span class="p">))</span>
                    <span class="o">.</span><span class="nf">blendMode</span><span class="p">(</span><span class="o">.</span><span class="n">plusLighter</span><span class="p">)</span>
                    <span class="o">.</span><span class="nf">offset</span><span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="n">offset</span><span class="p">)</span>
            <span class="p">)</span>
            
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<p>You can see that the overall structure derives from our first example, but it is no longer dependent on specific Image. Instead, the view modifier takes the <code class="language-plaintext highlighter-rouge">Content</code> that it has been applied to and sets the same content also as its overlay (but with a different color).</p>

<p>Also, the offset is prepared to be set as viewModifier parameter so it can be changed from the outside.</p>

<p>I have also introduced a negative offset here, that shifts the base content in the opposite direction to its overlay. With this improvement, the resulting view remains centered which I personaly prefer but it is optional change.</p>

<p>Our view modifier can be used like this to any kind of view, even the root view if you wish;)</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kt">Image</span><span class="p">(</span><span class="nv">systemName</span><span class="p">:</span> <span class="s">"command"</span><span class="p">)</span>
   <span class="o">.</span><span class="nf">font</span><span class="p">(</span><span class="o">.</span><span class="nf">system</span><span class="p">(</span><span class="nv">size</span><span class="p">:</span> <span class="mi">200</span><span class="p">,</span> <span class="nv">weight</span><span class="p">:</span> <span class="o">.</span><span class="n">bold</span><span class="p">,</span> <span class="nv">design</span><span class="p">:</span> <span class="o">.</span><span class="k">default</span><span class="p">))</span>
   <span class="o">.</span><span class="nf">modifier</span><span class="p">(</span><span class="kt">TikTokEffect</span><span class="p">(</span><span class="nv">offset</span><span class="p">:</span> <span class="kt">CGSize</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="o">-</span><span class="mi">4</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="o">-</span><span class="mi">3</span><span class="p">)))</span></code></pre></figure>

<h2 id="reuse-view-modifier-within-buttonstyle">Reuse view modifier within ButtonStyle</h2>

<p>Custom view modifiers are especially handy when creating custom <a href="https://developer.apple.com/documentation/swiftui/buttonstyle">ButtonStyle</a>, transitions, or animations. Let me share here an example of a button, that applies our effect when pressed:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">struct</span> <span class="kt">TikTokButtonStyle</span><span class="p">:</span> <span class="kt">ButtonStyle</span> <span class="p">{</span>
    
    <span class="kd">func</span> <span class="nf">backgroundShift</span><span class="p">(</span><span class="n">_</span> <span class="nv">isPressed</span><span class="p">:</span> <span class="kt">Bool</span><span class="p">)</span><span class="o">-&gt;</span><span class="kt">CGSize</span> <span class="p">{</span>
        <span class="k">if</span> <span class="n">isPressed</span> <span class="p">{</span>
            <span class="k">return</span> <span class="kt">CGSize</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="o">-</span><span class="mi">4</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="o">-</span><span class="mi">3</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="k">else</span> <span class="p">{</span>
            <span class="k">return</span> <span class="kt">CGSize</span><span class="p">()</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="kd">func</span> <span class="nf">makeBody</span><span class="p">(</span><span class="nv">configuration</span><span class="p">:</span> <span class="k">Self</span><span class="o">.</span><span class="kt">Configuration</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="n">configuration</span><span class="o">.</span><span class="n">label</span>
            <span class="o">.</span><span class="nf">padding</span><span class="p">(</span><span class="mi">20</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">background</span><span class="p">(</span>
                <span class="kt">RoundedRectangle</span><span class="p">(</span><span class="nv">cornerRadius</span><span class="p">:</span> <span class="mi">10</span><span class="p">,</span> <span class="nv">style</span><span class="p">:</span> <span class="o">.</span><span class="n">continuous</span><span class="p">)</span>
                    <span class="o">.</span><span class="nf">modifier</span><span class="p">(</span><span class="kt">TikTokEffect</span><span class="p">(</span><span class="nv">offset</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="nf">backgroundShift</span><span class="p">(</span><span class="n">configuration</span><span class="o">.</span><span class="n">isPressed</span><span class="p">)))</span>
            <span class="p">)</span>
            <span class="o">.</span><span class="nf">scaleEffect</span><span class="p">(</span><span class="n">configuration</span><span class="o">.</span><span class="n">isPressed</span> <span class="p">?</span> <span class="mf">0.95</span><span class="p">:</span> <span class="mi">1</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">foregroundColor</span><span class="p">(</span><span class="o">.</span><span class="n">black</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">animation</span><span class="p">(</span><span class="o">.</span><span class="nf">spring</span><span class="p">())</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<p>Here we are changing the offset value of our effect based on <code class="language-plaintext highlighter-rouge">configuration.isPressed</code> value. The resulting button created as:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kt">Button</span><span class="p">(</span><span class="nv">action</span><span class="p">:</span> <span class="p">{})</span> <span class="p">{</span>
            <span class="kt">HStack</span> <span class="p">{</span>
                <span class="kt">Image</span><span class="p">(</span><span class="nv">systemName</span><span class="p">:</span> <span class="s">"command"</span><span class="p">)</span>
                <span class="kt">Text</span><span class="p">(</span><span class="s">"Command Button"</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="o">.</span><span class="nf">buttonStyle</span><span class="p">(</span><span class="kt">TikTokButtonStyle</span><span class="p">())</span></code></pre></figure>

<p>now behaves like this:</p>

<p><img src="/assets/posts/10_tiktok_button.gif" alt="button" title="Example of custom button style utilizing TikTok effect" /></p>

<h2 id="summary">Summary</h2>

<ul>
  <li>Styling the app and creating reusable styles can benefit heavily from custom view modifiers. This is only one of the many use-cases, but the same approach can be used for any app look and feel</li>
  <li>It is wise to keep configurable interface of your view modifier so it can be tuned for specific scenarios and foremost - ANIMATIONS. As an inspiration and your challenge, I present you a loading indicator based on our TikTokEffect. Can you achieve the same - or better?</li>
</ul>

<p><img src="/assets/posts/10_tiktok_loading.gif" alt="loading" title="Animating our TikTok effect" /></p>

<p><em>Did you like this article? What do you want me to focus on next?</em></p>

<p><em>Feel free to comment or criticize so the next one is even better. Or share it with other SwiftUI adopters ;)</em></p>]]></content><author><name>Pavel Zak</name></author><category term="development" /><category term="swiftUI" /><category term="tiktok" /><category term="effect" /><category term="BlendMode" /><category term="ViewModifier" /><category term="ButtonStyle" /><summary type="html"><![CDATA[Hello and welcome to another blog post about SwiftUI. This years WWDC is approaching fast and the expectation of new SwiftUI features and/or changes is tremendous. So I have decided to dedicate this post to something lighter yet still playful.]]></summary></entry><entry><title type="html">Morphing shapes in SwiftUI</title><link href="https://nerdyak.tech/development/2020/05/07/morphing-shapes-in-swiftui.html" rel="alternate" type="text/html" title="Morphing shapes in SwiftUI" /><published>2020-05-07T00:00:00+00:00</published><updated>2020-05-07T00:00:00+00:00</updated><id>https://nerdyak.tech/development/2020/05/07/morphing-shapes-in-swiftui</id><content type="html" xml:base="https://nerdyak.tech/development/2020/05/07/morphing-shapes-in-swiftui.html"><![CDATA[<p>Hello and welcome to another blog post about <a href="https://developer.apple.com/documentation/swiftui">SwiftUI</a> animations. In the <a href="https://nerdyak.tech/development/2020/01/12/animating-complex-shapes-in-swiftui.html">previous post</a> dedicated mainly to <code class="language-plaintext highlighter-rouge">AnimatableData</code>, we have constructed <code class="language-plaintext highlighter-rouge">AnimatableVector</code> that allowed us to create animatable charts. Today we will utilize the same class for morphing shapes.</p>

<h2 id="shape-representation">Shape representation</h2>

<p>Shapes in SwiftUI can be constructed as a composition of vector paths and/or shape primitives. If we want to create morphing animation between two shapes, we need to find universal representation first. This can be done either ad-hoc for specific shapes - as we saw in the previous post about <a href="https://nerdyak.tech/development/2019/11/28/creating-custom-views-in-swiftui.html">custom controls</a> - but it is better to find a more generic one that will allow us to morph any shape to any shape without limitations.</p>

<p>The most common approach is to describe the shape as an approximation of its outline using a finite set of line segments. This means, that we will distribute <code class="language-plaintext highlighter-rouge">N</code> points around the shape outline and when doing the morphing animation, we can easily move these outline points from the position given by shape A to the new position in the shape B. For better understanding, check the following illustration:</p>

<p><img src="/assets/posts/09_explanation.gif" alt="morphExplanation" title="Morphing using interpolation of control points" /></p>

<h2 id="implementation-of-morphable-shape-in-swiftui">Implementation of morphable shape in SwiftUI</h2>

<p>So how to implement all of this in SwiftUI? Let me start with a new shape representation based on multiple control points. This is quite easy, the only crucial part is that these control points are being represented using our <code class="language-plaintext highlighter-rouge">AnimatableVector</code> so it is capable to morph using animation.</p>

<p>But hey - the shape points are two-dimensional (having x and y coordinate), while our <code class="language-plaintext highlighter-rouge">AnimatableVector</code> holds only an array of Doubles. What can be done about that? Well, there are two solutions:</p>
<ul>
  <li>you can implement another custom vector holding array of <code class="language-plaintext highlighter-rouge">CGPoints</code> and let it conform to the <code class="language-plaintext highlighter-rouge">AnimatableData</code> protocol</li>
  <li>or use the same <code class="language-plaintext highlighter-rouge">AnimatableVector</code> and expect even indexed component to be x coordinate and odd-indexed components to by y coordinate. I choose this solution for the following <code class="language-plaintext highlighter-rouge">MorphableShape</code> implementation:</li>
</ul>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">struct</span> <span class="kt">MorphableShape</span><span class="p">:</span> <span class="kt">Shape</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">controlPoints</span><span class="p">:</span> <span class="kt">AnimatableVector</span>
    
    <span class="k">var</span> <span class="nv">animatableData</span><span class="p">:</span> <span class="kt">AnimatableVector</span> <span class="p">{</span>
        <span class="k">set</span> <span class="p">{</span> <span class="k">self</span><span class="o">.</span><span class="n">controlPoints</span> <span class="o">=</span> <span class="n">newValue</span> <span class="p">}</span>
        <span class="k">get</span> <span class="p">{</span> <span class="k">return</span> <span class="k">self</span><span class="o">.</span><span class="n">controlPoints</span> <span class="p">}</span>
    <span class="p">}</span>
    
    <span class="kd">func</span> <span class="nf">point</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="kt">Double</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="kt">Double</span><span class="p">,</span> <span class="nv">rect</span><span class="p">:</span> <span class="kt">CGRect</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">CGPoint</span> <span class="p">{</span>
        <span class="c1">// vector values are expected to by in the range of 0...1</span>
        <span class="k">return</span> <span class="kt">CGPoint</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="kt">Double</span><span class="p">(</span><span class="n">rect</span><span class="o">.</span><span class="n">width</span><span class="p">)</span><span class="o">*</span><span class="n">x</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="kt">Double</span><span class="p">(</span><span class="n">rect</span><span class="o">.</span><span class="n">height</span><span class="p">)</span><span class="o">*</span><span class="n">y</span><span class="p">)</span>
    <span class="p">}</span>
    
    <span class="kd">func</span> <span class="nf">path</span><span class="p">(</span><span class="k">in</span> <span class="nv">rect</span><span class="p">:</span> <span class="kt">CGRect</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Path</span> <span class="p">{</span>
        <span class="k">return</span> <span class="kt">Path</span> <span class="p">{</span> <span class="n">path</span> <span class="k">in</span>
            
            <span class="n">path</span><span class="o">.</span><span class="nf">move</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="nf">point</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">controlPoints</span><span class="o">.</span><span class="n">values</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> 
				                     <span class="nv">y</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">controlPoints</span><span class="o">.</span><span class="n">values</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="nv">rect</span><span class="p">:</span> <span class="n">rect</span><span class="p">))</span>
            
            <span class="k">var</span> <span class="nv">i</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
            <span class="k">while</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="k">self</span><span class="o">.</span><span class="n">controlPoints</span><span class="o">.</span><span class="n">values</span><span class="o">.</span><span class="n">count</span><span class="o">-</span><span class="mi">1</span> <span class="p">{</span>
                <span class="n">path</span><span class="o">.</span><span class="nf">addLine</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span>  <span class="k">self</span><span class="o">.</span><span class="nf">point</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">controlPoints</span><span class="o">.</span><span class="n">values</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> 
					                         <span class="nv">y</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">controlPoints</span><span class="o">.</span><span class="n">values</span><span class="p">[</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="p">],</span> <span class="nv">rect</span><span class="p">:</span> <span class="n">rect</span><span class="p">))</span>
                <span class="n">i</span> <span class="o">+=</span> <span class="mi">2</span><span class="p">;</span>
            <span class="p">}</span>
            
            <span class="n">path</span><span class="o">.</span><span class="nf">addLine</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span>  <span class="k">self</span><span class="o">.</span><span class="nf">point</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">controlPoints</span><span class="o">.</span><span class="n">values</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> 
				                         <span class="nv">y</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">controlPoints</span><span class="o">.</span><span class="n">values</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="nv">rect</span><span class="p">:</span> <span class="n">rect</span><span class="p">))</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<p>Let’s check the implementation using several random points:</p>

<p><img src="/assets/posts/09_example.gif" alt="morphExample" title="Morphable shape ready to be animated" /></p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="c1">// hepler func creating vector with random values</span>
<span class="kd">func</span> <span class="nf">randomVector</span><span class="p">(</span><span class="nv">count</span><span class="p">:</span> <span class="kt">Int</span><span class="p">)</span><span class="o">-&gt;</span><span class="kt">AnimatableVector</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">randomValues</span> <span class="o">=</span> <span class="kt">Array</span><span class="p">(</span><span class="mi">1</span><span class="o">...</span><span class="n">count</span><span class="p">)</span><span class="o">.</span><span class="n">map</span><span class="p">{</span><span class="n">_</span> <span class="k">in</span> <span class="kt">Double</span><span class="o">.</span><span class="nf">random</span><span class="p">(</span><span class="nv">in</span><span class="p">:</span> <span class="mi">0</span><span class="o">...</span><span class="mf">1.0</span><span class="p">)}</span>
    <span class="k">return</span> <span class="kt">AnimatableVector</span><span class="p">(</span><span class="nv">with</span><span class="p">:</span> <span class="n">randomValues</span><span class="p">)</span>
<span class="p">}</span>

<span class="c1">// demo view for our proof of concept of morphable shapes</span>
<span class="kd">struct</span> <span class="kt">DemoView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">static</span> <span class="k">let</span> <span class="nv">pointCount</span> <span class="o">=</span> <span class="mi">8</span> <span class="c1">// number of control points</span>
	<span class="c1">// vector holds twice as many elements</span>
    <span class="kd">@State</span> <span class="k">var</span> <span class="nv">controlPoints</span><span class="p">:</span> <span class="kt">AnimatableVector</span> <span class="o">=</span> <span class="nf">randomVector</span><span class="p">(</span><span class="nv">count</span><span class="p">:</span> <span class="n">pointCount</span><span class="o">*</span><span class="mi">2</span><span class="p">)</span> 
    
    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">VStack</span> <span class="p">{</span>
            <span class="kt">MorphableShape</span><span class="p">(</span><span class="nv">controlPoints</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">controlPoints</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">fill</span><span class="p">(</span><span class="kt">Color</span><span class="o">.</span><span class="n">orange</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="mi">256</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="mi">256</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">overlay</span><span class="p">(</span> <span class="c1">// overlay the shape with the same shape to create outline</span>
                    <span class="kt">MorphableShape</span><span class="p">(</span><span class="nv">controlPoints</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">controlPoints</span><span class="p">)</span>
                        <span class="o">.</span><span class="nf">stroke</span><span class="p">(</span><span class="kt">Color</span><span class="o">.</span><span class="n">white</span><span class="p">,</span> <span class="nv">lineWidth</span><span class="p">:</span> <span class="mi">3</span><span class="p">)</span>
                <span class="p">)</span>
            
            <span class="kt">Button</span> <span class="p">(</span><span class="nv">action</span><span class="p">:</span> <span class="p">{</span>
                <span class="n">withAnimation</span> <span class="p">{</span> <span class="c1">// feel free to play with animation curves here</span>
                    <span class="c1">// randomize points</span>
                    <span class="k">self</span><span class="o">.</span><span class="n">controlPoints</span> <span class="o">=</span> <span class="nf">randomVector</span><span class="p">(</span><span class="nv">count</span><span class="p">:</span> <span class="kt">DemoView</span><span class="o">.</span><span class="n">pointCount</span><span class="o">*</span><span class="mi">2</span><span class="p">)</span>
                <span class="p">}</span>
            <span class="p">}){</span>
                <span class="kt">Text</span><span class="p">(</span><span class="s">"Randomize"</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<h2 id="converting-standard-shape-to-morphableshape">Converting standard shape to MorphableShape</h2>

<p>All the above code is enough to morph any shape to any shape, but getting the shape representation as a set of control points may be tricky. Well, it is definitively something you cannot put together “by hand”.</p>

<p>Luckily, there is a way how to generate them using SwiftUI <a href="https://developer.apple.com/documentation/swiftui/path">Path</a> API. We can sample the shape outline using the <code class="language-plaintext highlighter-rouge">trimmedPath()</code> method, so let me create a simple Path extension, that provides a vector of N control points along any Path:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">extension</span> <span class="kt">Path</span> <span class="p">{</span>
    <span class="c1">// return point at the curve</span>
    <span class="kd">func</span> <span class="nf">point</span><span class="p">(</span><span class="n">at</span> <span class="nv">offset</span><span class="p">:</span> <span class="kt">CGFloat</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">CGPoint</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">limitedOffset</span> <span class="o">=</span> <span class="nf">min</span><span class="p">(</span><span class="nf">max</span><span class="p">(</span><span class="n">offset</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="mi">1</span><span class="p">)</span>
        <span class="k">guard</span> <span class="n">limitedOffset</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="n">cgPath</span><span class="o">.</span><span class="n">currentPoint</span> <span class="p">}</span>
        <span class="k">return</span> <span class="nf">trimmedPath</span><span class="p">(</span><span class="nv">from</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="nv">to</span><span class="p">:</span> <span class="n">limitedOffset</span><span class="p">)</span><span class="o">.</span><span class="n">cgPath</span><span class="o">.</span><span class="n">currentPoint</span>
    <span class="p">}</span>
    
    <span class="c1">// return control points along the path</span>
    <span class="kd">func</span> <span class="nf">controlPoints</span><span class="p">(</span><span class="nv">count</span><span class="p">:</span> <span class="kt">Int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">AnimatableVector</span> <span class="p">{</span>
        <span class="k">var</span> <span class="nv">retPoints</span> <span class="o">=</span> <span class="p">[</span><span class="kt">Double</span><span class="p">]()</span>
        <span class="k">for</span> <span class="n">index</span> <span class="k">in</span> <span class="mi">0</span><span class="o">..&lt;</span><span class="n">count</span> <span class="p">{</span>
            <span class="k">let</span> <span class="nv">pathOffset</span> <span class="o">=</span> <span class="kt">Double</span><span class="p">(</span><span class="n">index</span><span class="p">)</span><span class="o">/</span><span class="kt">Double</span><span class="p">(</span><span class="n">count</span><span class="p">)</span>
            <span class="k">let</span> <span class="nv">pathPoint</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="nf">point</span><span class="p">(</span><span class="nv">at</span><span class="p">:</span> <span class="kt">CGFloat</span><span class="p">(</span><span class="n">pathOffset</span><span class="p">))</span>
            <span class="n">retPoints</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="kt">Double</span><span class="p">(</span><span class="n">pathPoint</span><span class="o">.</span><span class="n">x</span><span class="p">))</span>
            <span class="n">retPoints</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="kt">Double</span><span class="p">(</span><span class="n">pathPoint</span><span class="o">.</span><span class="n">y</span><span class="p">))</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="kt">AnimatableVector</span><span class="p">(</span><span class="nv">with</span><span class="p">:</span> <span class="n">retPoints</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<p>Now, it can be utilized like this:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="k">let</span> <span class="nv">N</span> <span class="o">=</span> <span class="mi">100</span>
<span class="k">let</span> <span class="nv">shape</span> <span class="o">=</span> <span class="kt">Circle</span><span class="p">()</span> <span class="c1">// or any other shape</span>
<span class="k">let</span> <span class="nv">shapeControlPoints</span><span class="p">:</span> <span class="kt">AnimatableVector</span> <span class="o">=</span> <span class="n">shape</span><span class="o">.</span><span class="nf">path</span><span class="p">(</span><span class="nv">in</span><span class="p">:</span> <span class="kt">CGRect</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="nv">width</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="mi">1</span><span class="p">))</span>
                                                <span class="o">.</span><span class="nf">controlPoints</span><span class="p">(</span><span class="nv">count</span><span class="p">:</span> <span class="kt">N</span><span class="p">)</span></code></pre></figure>

<p>Please, note, that with setting the <code class="language-plaintext highlighter-rouge">CGRect</code> size to 1 we are assuring to have the control point coordinates in the range <code class="language-plaintext highlighter-rouge">0...1</code> which is exactly what our <code class="language-plaintext highlighter-rouge">MorphableShape</code> expects to get.</p>

<p>And that’s it!</p>

<p><img src="/assets/posts/07_shapes.gif" alt="animatedShapes" title="Demonstration of morphing of various shapes" /></p>

<h2 id="use-cases">Use cases</h2>

<p>If you are wondering, what is this all good for, I present here several ideas:</p>

<ul>
  <li>
    <p>morphing of icons (for multi-state buttons)
<img src="/assets/posts/09_recording.gif" alt="recording" title="Example of morphing icon on payer view" /></p>
  </li>
  <li>
    <p>custom transitions and morphing of clip masks
<img src="/assets/posts/09_heart.gif" alt="heart" title="Example of morphing transition" /></p>
  </li>
  <li>
    <p>creating a pointless music video in SwiftUI 🤪</p>
  </li>
</ul>

<center>
<iframe width="400" height="300" src="https://www.youtube.com/embed/r_XorK0cjv8" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe>
</center>

<h2 id="final-notes">Final notes</h2>

<ul>
  <li>Described algorithm works best for one-component/compact shapes. It works also for multi-component shapes, but the resulting animation may not be eye-pleasant.</li>
  <li>For large curved or complex shapes, the number of control points needs to be quite high (hundreds at least) to have a smooth result.</li>
  <li>The only tricky thing with this approach is that the shapes may differ in the origin and direction of how they are being constructed. If these parameters are different, the morphing animation would not work as expected and shape may “flip” during interpolation. When creating my custom shapes, I usually start the path at the top-left corner and construct the shape in the clockwise direction to avoid these issues. Nevertheles, the control points could be programatically rearranged so the vector always start around the same orientation - but so far I have not implemented this part:)</li>
  <li>It would be awesome, if we could get Path from SFSymbols….🤞</li>
</ul>

<p><em>Did you like this article? What do you want me to focus on next?</em></p>

<p><em>Feel free to comment or criticize so the next one is even better. Or share it with other SwiftUI adopters ;)</em></p>]]></content><author><name>Pavel Zak</name></author><category term="development" /><category term="swiftUI" /><category term="shape" /><category term="animation" /><category term="animatableData" /><category term="animatablePair" /><category term="animatableVector" /><category term="path" /><category term="morphing" /><category term="transition" /><summary type="html"><![CDATA[Hello and welcome to another blog post about SwiftUI animations. In the previous post dedicated mainly to AnimatableData, we have constructed AnimatableVector that allowed us to create animatable charts. Today we will utilize the same class for morphing shapes.]]></summary></entry><entry><title type="html">How to define custom LLDB command in Xcode</title><link href="https://nerdyak.tech/development/2020/04/20/define-custom-lldb-command-xcode.html" rel="alternate" type="text/html" title="How to define custom LLDB command in Xcode" /><published>2020-04-20T00:00:00+00:00</published><updated>2020-04-20T00:00:00+00:00</updated><id>https://nerdyak.tech/development/2020/04/20/define-custom-lldb-command-xcode</id><content type="html" xml:base="https://nerdyak.tech/development/2020/04/20/define-custom-lldb-command-xcode.html"><![CDATA[<p>It has been a couple of months since my last post. I have been quite occupied with a big project where I am helping to modernize its archaic code-base.</p>

<p>On a daily basis, I spend a lot of time debugging and I am usually using several LLDB commands over and over again. For this purpose, I have defined a couple of custom LLDB commands like printing out the contents of <code class="language-plaintext highlighter-rouge">Data</code> as <code class="language-plaintext highlighter-rouge">String</code> - so instead of <code class="language-plaintext highlighter-rouge">po String(data: jsonData, encoding: .utf8)</code> I simply write <code class="language-plaintext highlighter-rouge">printData jsonData</code></p>

<p>Since not many devs know about the opportunity to define own custom LLDB aliases or commands, I have put together a short video:</p>

<center>
<iframe width="560" height="315" src="https://www.youtube.com/embed/h9ggWxh8Evs" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe>
</center>

<p>(Fun fact: The video was made with Keynote which I found quite useful for such task)</p>

<p>The example of <code class="language-plaintext highlighter-rouge">.lldbinit</code> file can be seen on my <a href="https://gist.github.com/izakpavel/ceefe1f18bff4e69bb7432af7b65960a" title="gist with a command example">gist</a>.</p>

<p>Stay safe!</p>]]></content><author><name>Pavel Zak</name></author><category term="development" /><category term="Xcode" /><category term="LLDB" /><category term="command" /><category term="Swift" /><summary type="html"><![CDATA[It has been a couple of months since my last post. I have been quite occupied with a big project where I am helping to modernize its archaic code-base.]]></summary></entry></feed>