Jekyll2023-07-22T11:07:19+00:00https://nerdyak.tech/atom.xmlPavel Zak’s dev blog#iOS development | #SwiftUI | #maybeMorePavel ZakSwiftUI transitions with distortion effect and Metal Shaders2023-06-16T00:00:00+00:002023-06-16T00:00:00+00:00https://nerdyak.tech/development/2023/06/16/distortionEffect-with-Metal-shaders-for-better-transitions<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 <metal_stdlib></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">></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">-></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">></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">></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>Pavel ZakThis 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).Avoid Spacers in SwiftUI Stacks2023-04-06T00:00:00+00:002023-04-06T00:00:00+00:00https://nerdyak.tech/development/2023/04/06/avoid-swiftui-spacers-in-stacks<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>Pavel ZakAs 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!Creating particles in SwiftUI - in less than 50 lines of code2020-12-12T00:00:00+00:002020-12-12T00:00:00+00:00https://nerdyak.tech/development/2020/12/12/create-particles-in-swiftui<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 canvas view</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>Pavel ZakI 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.Mastering transitions in SwiftUI2020-10-12T00:00:00+00:002020-10-12T00:00:00+00:00https://nerdyak.tech/development/2020/10/12/transitions-in-swiftui<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">-></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">-></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"><</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">-></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">-></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">-></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>Pavel ZakTransitions play a vital role in the user experience of our apps. They are visual keys signalizing that the app or screen context is changing.Creating onboarding screens in SwiftUI2020-07-17T00:00:00+00:002020-07-17T00:00:00+00:00https://nerdyak.tech/development/2020/07/17/creating-onboarding-screens-in-swiftui<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">..<</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">..<</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>Pavel ZakIn 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.TikTok logo-ish effect in SwiftUI2020-06-12T00:00:00+00:002020-06-12T00:00:00+00:00https://nerdyak.tech/development/2020/06/12/create-tiktok-logo-effect-in-swiftui<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">-></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">-></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">-></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">-></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>Pavel ZakHello 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.Morphing shapes in SwiftUI2020-05-07T00:00:00+00:002020-05-07T00:00:00+00:00https://nerdyak.tech/development/2020/05/07/morphing-shapes-in-swiftui<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">-></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">-></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"><</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">-></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">-></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">></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">-></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">..<</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>Pavel ZakHello 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.How to define custom LLDB command in Xcode2020-04-20T00:00:00+00:002020-04-20T00:00:00+00:00https://nerdyak.tech/development/2020/04/20/define-custom-lldb-command-xcode<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>Pavel ZakIt 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.Animating complex shapes in SwiftUI2020-01-12T00:00:00+00:002020-01-12T00:00:00+00:00https://nerdyak.tech/development/2020/01/12/animating-complex-shapes-in-swiftui<p>Hello and welcome to another blog post about SwiftUI. This time, we will talk about the animation of complex shapes in SwiftUI. You will understand animatableData property and will be able to implement animatable custom Shape struct that depends on multiple parameters.</p>
<h2 id="animatabledata">AnimatableData</h2>
<p>Animating simple shapes is easy thanks to <code class="language-plaintext highlighter-rouge">animatableData</code> property. We have seen an example of such animation in my previous post about <a href="https://nerdyak.tech/development/2019/11/28/creating-custom-views-in-swiftui.html">custom controls</a>. The magic behind <code class="language-plaintext highlighter-rouge">animatableData</code> is actually quite simple math. During the animation, the property value is being interpolated (or extrapolated in case of spring animation) from starting to the ending value according to the animation timing curve.</p>
<p>To demonstrate it once again, let’s start with a simple rectangle with cout-out rounded corners.</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">struct</span> <span class="kt">CoutOutRectangle</span><span class="p">:</span> <span class="kt">Shape</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">cornerRadius</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="n">cornerRadius</span> <span class="p">}</span>
<span class="k">set</span> <span class="p">{</span> <span class="n">cornerRadius</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">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">-></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="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="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">cornerRadius</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">addLine</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">width</span><span class="o">-</span><span class="n">cornerRadius</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">addQuadCurve</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">width</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">cornerRadius</span><span class="p">),</span> <span class="nv">control</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">width</span><span class="o">-</span><span class="n">cornerRadius</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">cornerRadius</span><span class="p">))</span>
<span class="c1">//</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="kt">CGPoint</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="n">width</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">height</span><span class="o">-</span><span class="n">cornerRadius</span><span class="p">))</span>
<span class="n">path</span><span class="o">.</span><span class="nf">addQuadCurve</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">width</span><span class="o">-</span><span class="n">cornerRadius</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="nv">control</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">width</span><span class="o">-</span><span class="n">cornerRadius</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">height</span><span class="o">-</span><span class="n">cornerRadius</span><span class="p">))</span>
<span class="c1">//</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="kt">CGPoint</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="n">cornerRadius</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="n">path</span><span class="o">.</span><span class="nf">addQuadCurve</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="mi">0</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">height</span><span class="o">-</span><span class="n">cornerRadius</span><span class="p">),</span> <span class="nv">control</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">cornerRadius</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">height</span><span class="o">-</span><span class="n">cornerRadius</span><span class="p">))</span>
<span class="c1">//</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="kt">CGPoint</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="n">cornerRadius</span><span class="p">))</span>
<span class="n">path</span><span class="o">.</span><span class="nf">addQuadCurve</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">cornerRadius</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">control</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">cornerRadius</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">cornerRadius</span><span class="p">))</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<p>Notice that I have created a single property called <code class="language-plaintext highlighter-rouge">cornerRadius</code> that is being set or read from <code class="language-plaintext highlighter-rouge">animatableData</code> setter and getter. That is enough for SwiftUI to perform the animation whenever we change the corner radius.</p>
<p>Let’s test it within a simple demo view that periodically animates corners from value 0 to 20 and back.</p>
<p>You can try to change the animation type or its parameters to see how the animation changes.</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">struct</span> <span class="kt">AnimationDemoView</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">cornerRadius</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="o">=</span> <span class="mf">0.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">CoutOutRectangle</span><span class="p">(</span><span class="nv">cornerRadius</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">cornerRadius</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">green</span><span class="p">)</span>
<span class="o">.</span><span class="n">onAppear</span><span class="p">{</span>
<span class="nf">withAnimation</span> <span class="p">(</span><span class="kt">Animation</span><span class="o">.</span><span class="nf">easeOut</span><span class="p">(</span><span class="nv">duration</span><span class="p">:</span> <span class="mf">0.4</span><span class="p">)</span><span class="o">.</span><span class="nf">repeatForever</span><span class="p">(</span><span class="nv">autoreverses</span><span class="p">:</span> <span class="kc">true</span><span class="p">)){</span>
<span class="k">self</span><span class="o">.</span><span class="n">cornerRadius</span> <span class="o">=</span> <span class="mf">20.0</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<p>It works!</p>
<p><img src="/assets/posts/07_cutoutrectangle.gif" alt="rectangle" title="Animated rectangle with cut out corners" /></p>
<h2 id="animatablepair">AnimatablePair</h2>
<p>Now, in the case things are more complicated and we need to animate the shape according to two values, we can utilize <code class="language-plaintext highlighter-rouge">AnimatablePair<T></code> type. Here the only obstacle is to define the right getter and setter to pass data between our control properties and values stored in AnimatablePair.</p>
<p>As an example, we will build a wedge shape that can be used to compose pie charts. The wedge has two main properties - <code class="language-plaintext highlighter-rouge">angleOffset</code> and <code class="language-plaintext highlighter-rouge">wedgeWidth</code> that we both want to be animatable. The best explanation of both properties gives the following illustration:</p>
<p><img src="/assets/posts/07_wedge.jpg" alt="wedgeExplanation" title="Explanation of wedge parameters" /></p>
<p>And the resulting code is here. Note how AnimatablePair`s first and second values are being mapped to our properties.</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">struct</span> <span class="kt">WedgeShape</span><span class="p">:</span> <span class="kt">Shape</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">angleOffset</span><span class="p">:</span> <span class="kt">Double</span> <span class="c1">// in radians</span>
<span class="k">var</span> <span class="nv">wedgeWidth</span><span class="p">:</span> <span class="kt">Double</span> <span class="c1">// in radians</span>
<span class="kd">public</span> <span class="k">var</span> <span class="nv">animatableData</span><span class="p">:</span> <span class="kt">AnimatablePair</span><span class="o"><</span><span class="kt">Double</span><span class="p">,</span> <span class="kt">Double</span><span class="o">></span> <span class="p">{</span>
<span class="k">get</span> <span class="p">{</span>
<span class="kt">AnimatablePair</span><span class="p">(</span><span class="kt">Double</span><span class="p">(</span><span class="n">angleOffset</span><span class="p">),</span> <span class="kt">Double</span><span class="p">(</span><span class="n">wedgeWidth</span><span class="p">))</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">angleOffset</span> <span class="o">=</span> <span class="n">newValue</span><span class="o">.</span><span class="n">first</span>
<span class="k">self</span><span class="o">.</span><span class="n">wedgeWidth</span> <span class="o">=</span> <span class="n">newValue</span><span class="o">.</span><span class="n">second</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">-></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="k">let</span> <span class="nv">width</span> <span class="o">=</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="k">let</span> <span class="nv">height</span> <span class="o">=</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="k">let</span> <span class="nv">middlePoint</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">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">height</span><span class="o">/</span><span class="mi">2</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">startingPoint</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">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="k">self</span><span class="o">.</span><span class="n">angleOffset</span><span class="p">)</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">height</span><span class="o">/</span><span class="mi">2</span> <span class="o">+</span> <span class="nf">sin</span><span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="n">angleOffset</span><span class="p">)</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">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="n">middlePoint</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="n">startingPoint</span><span class="p">)</span>
<span class="n">path</span><span class="o">.</span><span class="nf">addArc</span><span class="p">(</span><span class="nv">center</span><span class="p">:</span> <span class="n">middlePoint</span><span class="p">,</span> <span class="nv">radius</span><span class="p">:</span> <span class="n">rect</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">startAngle</span><span class="p">:</span> <span class="kt">Angle</span><span class="p">(</span><span class="nv">radians</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">angleOffset</span><span class="p">),</span> <span class="nv">endAngle</span><span class="p">:</span> <span class="kt">Angle</span><span class="p">(</span><span class="nv">radians</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">angleOffset</span><span class="o">+</span><span class="k">self</span><span class="o">.</span><span class="n">wedgeWidth</span><span class="p">),</span> <span class="nv">clockwise</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="nv">transform</span><span class="p">:</span> <span class="kt">CGAffineTransform</span><span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="nv">scaleX</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">rect</span><span class="o">.</span><span class="n">height</span><span class="o">/</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="nf">translatedBy</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="p">(</span><span class="n">rect</span><span class="o">.</span><span class="n">width</span><span class="o">-</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="mi">2</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="n">middlePoint</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<p>Having wedge shape ready, it is now possible to compose a <code class="language-plaintext highlighter-rouge">ZStack</code> of these views to create an animatable pie chart, like this:</p>
<p><img src="/assets/posts/07_piechart.gif" alt="pieChart" title="Animated pie chart composed from several wedges" /></p>
<h2 id="animatablevector">AnimatableVector</h2>
<p>The obvious question appears. What if our view depends on multiple values and we wish to animate according to all of them? There are two solutions.</p>
<p>First, it is possible to cascade multiple <code class="language-plaintext highlighter-rouge">AnimatablePairs</code> to pass more (like 3 or 4) values. Something like this:</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kt">AnimatablePair</span><span class="o"><</span><span class="kt">CGFloat</span><span class="p">,</span> <span class="kt">AnimatablePair</span><span class="o"><</span><span class="kt">CGFloat</span><span class="p">,</span> <span class="kt">AnimatablePair</span><span class="o"><</span><span class="kt">CGFloat</span><span class="p">,</span> <span class="kt">CGFloat</span><span class="o">>>></span></code></pre></figure>
<p>This is actually used within the implementation of <code class="language-plaintext highlighter-rouge">EdgeInsets</code> type, but as you can guess, it is not very flexible and usable for more than five or six values.</p>
<p>Luckily, there is a better approach. As <code class="language-plaintext highlighter-rouge">animatableData</code> can be set any type that implements to <code class="language-plaintext highlighter-rouge">VectorArithmetic</code> protocol. As we have seen, it is implemented by basic scalar types like <code class="language-plaintext highlighter-rouge">Double</code> or <code class="language-plaintext highlighter-rouge">CGFloat</code> and of course by <code class="language-plaintext highlighter-rouge">AnimatablePair</code>.</p>
<p>Knowing that, let us implement a brand new type called <code class="language-plaintext highlighter-rouge">AnimatableVector</code> that will be able to hold up to <code class="language-plaintext highlighter-rouge">N</code> values. The <code class="language-plaintext highlighter-rouge">VectorArithmetic</code> requires definition of <code class="language-plaintext highlighter-rouge">magnitudeSquared</code> property and <code class="language-plaintext highlighter-rouge">scale</code> method plus implementation of addition and subtraction operations from <code class="language-plaintext highlighter-rouge">AdditiveArithmetic</code>.</p>
<p>Our type is implemented as a standard Euclidean vector: addition, subtraction, and scale of these vectors are being done per-value and the magnitude of the vector is computed as a sum of all squared values. In case you need to refresh this part of high-school math, check this <a href="https://en.wikipedia.org/wiki/Euclidean_vector">wikipedia article</a>.</p>
<p>The whole implementation of <code class="language-plaintext highlighter-rouge">AnimatableVector</code>:</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">struct</span> <span class="kt">AnimatableVector</span><span class="p">:</span> <span class="kt">VectorArithmetic</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">values</span><span class="p">:</span> <span class="p">[</span><span class="kt">Double</span><span class="p">]</span> <span class="c1">// vector values</span>
<span class="nf">init</span><span class="p">(</span><span class="nv">count</span><span class="p">:</span> <span class="kt">Int</span> <span class="o">=</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">values</span> <span class="o">=</span> <span class="p">[</span><span class="kt">Double</span><span class="p">](</span><span class="nv">repeating</span><span class="p">:</span> <span class="mf">0.0</span><span class="p">,</span> <span class="nv">count</span><span class="p">:</span> <span class="n">count</span><span class="p">)</span>
<span class="k">self</span><span class="o">.</span><span class="n">magnitudeSquared</span> <span class="o">=</span> <span class="mf">0.0</span>
<span class="p">}</span>
<span class="nf">init</span><span class="p">(</span><span class="n">with</span> <span class="nv">values</span><span class="p">:</span> <span class="p">[</span><span class="kt">Double</span><span class="p">])</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">values</span> <span class="o">=</span> <span class="n">values</span>
<span class="k">self</span><span class="o">.</span><span class="n">magnitudeSquared</span> <span class="o">=</span> <span class="mi">0</span>
<span class="k">self</span><span class="o">.</span><span class="nf">recomputeMagnitude</span><span class="p">()</span>
<span class="p">}</span>
<span class="kd">func</span> <span class="nf">computeMagnitude</span><span class="p">()</span><span class="o">-></span><span class="kt">Double</span> <span class="p">{</span>
<span class="c1">// compute square magnitued of the vector</span>
<span class="c1">// = sum of all squared values</span>
<span class="k">var</span> <span class="nv">sum</span><span class="p">:</span> <span class="kt">Double</span> <span class="o">=</span> <span class="mf">0.0</span>
<span class="k">for</span> <span class="n">index</span> <span class="k">in</span> <span class="mi">0</span><span class="o">..<</span><span class="k">self</span><span class="o">.</span><span class="n">values</span><span class="o">.</span><span class="n">count</span> <span class="p">{</span>
<span class="n">sum</span> <span class="o">+=</span> <span class="k">self</span><span class="o">.</span><span class="n">values</span><span class="p">[</span><span class="n">index</span><span class="p">]</span><span class="o">*</span><span class="k">self</span><span class="o">.</span><span class="n">values</span><span class="p">[</span><span class="n">index</span><span class="p">]</span>
<span class="p">}</span>
<span class="k">return</span> <span class="kt">Double</span><span class="p">(</span><span class="n">sum</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">mutating</span> <span class="kd">func</span> <span class="nf">recomputeMagnitude</span><span class="p">(){</span>
<span class="k">self</span><span class="o">.</span><span class="n">magnitudeSquared</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="nf">computeMagnitude</span><span class="p">()</span>
<span class="p">}</span>
<span class="c1">// MARK: VectorArithmetic</span>
<span class="k">var</span> <span class="nv">magnitudeSquared</span><span class="p">:</span> <span class="kt">Double</span> <span class="c1">// squared magnitude of the vector</span>
<span class="k">mutating</span> <span class="kd">func</span> <span class="nf">scale</span><span class="p">(</span><span class="n">by</span> <span class="nv">rhs</span><span class="p">:</span> <span class="kt">Double</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// scale vector with a scalar</span>
<span class="c1">// = each value is multiplied by rhs</span>
<span class="k">for</span> <span class="n">index</span> <span class="k">in</span> <span class="mi">0</span><span class="o">..<</span><span class="n">values</span><span class="o">.</span><span class="n">count</span> <span class="p">{</span>
<span class="n">values</span><span class="p">[</span><span class="n">index</span><span class="p">]</span> <span class="o">*=</span> <span class="n">rhs</span>
<span class="p">}</span>
<span class="k">self</span><span class="o">.</span><span class="n">magnitudeSquared</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="nf">computeMagnitude</span><span class="p">()</span>
<span class="p">}</span>
<span class="c1">// MARK: AdditiveArithmetic</span>
<span class="c1">// zero is identity element for aditions</span>
<span class="c1">// = all values are zero</span>
<span class="kd">static</span> <span class="k">var</span> <span class="nv">zero</span><span class="p">:</span> <span class="kt">AnimatableVector</span> <span class="o">=</span> <span class="kt">AnimatableVector</span><span class="p">()</span>
<span class="kd">static</span> <span class="kd">func</span> <span class="o">+</span> <span class="p">(</span><span class="nv">lhs</span><span class="p">:</span> <span class="kt">AnimatableVector</span><span class="p">,</span> <span class="nv">rhs</span><span class="p">:</span> <span class="kt">AnimatableVector</span><span class="p">)</span> <span class="o">-></span> <span class="kt">AnimatableVector</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">retValues</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">..<</span><span class="nf">min</span><span class="p">(</span><span class="n">lhs</span><span class="o">.</span><span class="n">values</span><span class="o">.</span><span class="n">count</span><span class="p">,</span> <span class="n">rhs</span><span class="o">.</span><span class="n">values</span><span class="o">.</span><span class="n">count</span><span class="p">)</span> <span class="p">{</span>
<span class="n">retValues</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="n">lhs</span><span class="o">.</span><span class="n">values</span><span class="p">[</span><span class="n">index</span><span class="p">]</span> <span class="o">+</span> <span class="n">rhs</span><span class="o">.</span><span class="n">values</span><span class="p">[</span><span class="n">index</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">retValues</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">static</span> <span class="kd">func</span> <span class="o">+=</span> <span class="p">(</span><span class="nv">lhs</span><span class="p">:</span> <span class="k">inout</span> <span class="kt">AnimatableVector</span><span class="p">,</span> <span class="nv">rhs</span><span class="p">:</span> <span class="kt">AnimatableVector</span><span class="p">)</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">..<</span><span class="nf">min</span><span class="p">(</span><span class="n">lhs</span><span class="o">.</span><span class="n">values</span><span class="o">.</span><span class="n">count</span><span class="p">,</span><span class="n">rhs</span><span class="o">.</span><span class="n">values</span><span class="o">.</span><span class="n">count</span><span class="p">)</span> <span class="p">{</span>
<span class="n">lhs</span><span class="o">.</span><span class="n">values</span><span class="p">[</span><span class="n">index</span><span class="p">]</span> <span class="o">+=</span> <span class="n">rhs</span><span class="o">.</span><span class="n">values</span><span class="p">[</span><span class="n">index</span><span class="p">]</span>
<span class="p">}</span>
<span class="n">lhs</span><span class="o">.</span><span class="nf">recomputeMagnitude</span><span class="p">()</span>
<span class="p">}</span>
<span class="kd">static</span> <span class="kd">func</span> <span class="o">-</span> <span class="p">(</span><span class="nv">lhs</span><span class="p">:</span> <span class="kt">AnimatableVector</span><span class="p">,</span> <span class="nv">rhs</span><span class="p">:</span> <span class="kt">AnimatableVector</span><span class="p">)</span> <span class="o">-></span> <span class="kt">AnimatableVector</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">retValues</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">..<</span><span class="nf">min</span><span class="p">(</span><span class="n">lhs</span><span class="o">.</span><span class="n">values</span><span class="o">.</span><span class="n">count</span><span class="p">,</span> <span class="n">rhs</span><span class="o">.</span><span class="n">values</span><span class="o">.</span><span class="n">count</span><span class="p">)</span> <span class="p">{</span>
<span class="n">retValues</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="n">lhs</span><span class="o">.</span><span class="n">values</span><span class="p">[</span><span class="n">index</span><span class="p">]</span> <span class="o">-</span> <span class="n">rhs</span><span class="o">.</span><span class="n">values</span><span class="p">[</span><span class="n">index</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">retValues</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">static</span> <span class="kd">func</span> <span class="o">-=</span> <span class="p">(</span><span class="nv">lhs</span><span class="p">:</span> <span class="k">inout</span> <span class="kt">AnimatableVector</span><span class="p">,</span> <span class="nv">rhs</span><span class="p">:</span> <span class="kt">AnimatableVector</span><span class="p">)</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">..<</span><span class="nf">min</span><span class="p">(</span><span class="n">lhs</span><span class="o">.</span><span class="n">values</span><span class="o">.</span><span class="n">count</span><span class="p">,</span><span class="n">rhs</span><span class="o">.</span><span class="n">values</span><span class="o">.</span><span class="n">count</span><span class="p">)</span> <span class="p">{</span>
<span class="n">lhs</span><span class="o">.</span><span class="n">values</span><span class="p">[</span><span class="n">index</span><span class="p">]</span> <span class="o">-=</span> <span class="n">rhs</span><span class="o">.</span><span class="n">values</span><span class="p">[</span><span class="n">index</span><span class="p">]</span>
<span class="p">}</span>
<span class="n">lhs</span><span class="o">.</span><span class="nf">recomputeMagnitude</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<p>With <code class="language-plaintext highlighter-rouge">AnimatableVector</code> you have now complete freedom in building animated shapes and views. One of the handiest use cases is probably the creation of various animated charts, so let me demonstrate it here as well.</p>
<p>I present to you my implementation of <code class="language-plaintext highlighter-rouge">AnimatableGraph</code> that plots the values either as a chart line or whole area below it. As you can see, the chart values (here named as <code class="language-plaintext highlighter-rouge">controlPoints</code>) are stored and passed as <code class="language-plaintext highlighter-rouge">AnimatableVector</code>.</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">struct</span> <span class="kt">AnimatableGraph</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">closedArea</span><span class="p">:</span> <span class="kt">Bool</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">index</span><span class="p">:</span> <span class="kt">Int</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">-></span> <span class="kt">CGPoint</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">value</span> <span class="o">=</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">index</span><span class="p">]</span>
<span class="k">let</span> <span class="nv">x</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="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="p">)</span><span class="o">*</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="k">let</span> <span class="nv">y</span> <span class="o">=</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">value</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="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">-></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="k">let</span> <span class="nv">startPoint</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">index</span><span class="p">:</span> <span class="mi">0</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">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="n">startPoint</span><span class="p">)</span>
<span class="k">var</span> <span class="nv">i</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="k">while</span> <span class="n">i</span> <span class="o"><</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="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">index</span><span class="p">:</span> <span class="n">i</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">1</span><span class="p">;</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">closedArea</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// closed area below the chart line</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="kt">CGPoint</span><span class="p">(</span><span class="nv">x</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="nv">y</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="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="kt">CGPoint</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="n">rect</span><span class="o">.</span><span class="n">height</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="n">startPoint</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>Now, you can style this shape by setting <code class="language-plaintext highlighter-rouge">fill</code> and <code class="language-plaintext highlighter-rouge">stroke</code> properties and present eye-catching charts that can animate whenever its values are altered:</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="k">let</span> <span class="nv">areaGradient</span> <span class="o">=</span> <span class="kt">LinearGradient</span><span class="p">(</span><span class="nv">gradient</span><span class="p">:</span> <span class="kt">Gradient</span><span class="p">(</span><span class="nv">colors</span><span class="p">:</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">opacity</span><span class="p">(</span><span class="mf">0.1</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.4</span><span class="p">)]),</span> <span class="nv">startPoint</span><span class="p">:</span> <span class="kt">UnitPoint</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">endPoint</span><span class="p">:</span> <span class="kt">UnitPoint</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">1</span><span class="p">))</span>
<span class="k">let</span> <span class="nv">lineGradient</span> <span class="o">=</span> <span class="kt">LinearGradient</span><span class="p">(</span><span class="nv">gradient</span><span class="p">:</span> <span class="kt">Gradient</span><span class="p">(</span><span class="nv">colors</span><span class="p">:</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="kt">Color</span><span class="o">.</span><span class="n">orange</span><span class="p">]),</span> <span class="nv">startPoint</span><span class="p">:</span> <span class="kt">UnitPoint</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">endPoint</span><span class="p">:</span> <span class="kt">UnitPoint</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="mi">1</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="kd">struct</span> <span class="kt">DemoChart</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">vector</span><span class="p">:</span> <span class="kt">AnimatableVector</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">overlayLine</span> <span class="o">=</span> <span class="kt">AnimatableGraph</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">vector</span><span class="p">,</span> <span class="nv">closedArea</span><span class="p">:</span> <span class="kc">false</span><span class="p">)</span>
<span class="o">.</span><span class="nf">stroke</span><span class="p">(</span><span class="n">lineGradient</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="k">return</span> <span class="kt">AnimatableGraph</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">vector</span><span class="p">,</span> <span class="nv">closedArea</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span>
<span class="o">.</span><span class="nf">fill</span><span class="p">(</span><span class="n">areaGradient</span><span class="p">)</span>
<span class="o">.</span><span class="nf">overlay</span><span class="p">(</span><span class="n">overlayLine</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<p><img src="/assets/posts/07_chart.gif" alt="animatedChart" title="Animated chart" /></p>
<h2 id="the-challenge">The challenge</h2>
<p>Now try to play with the <code class="language-plaintext highlighter-rouge">AnimationVector</code> by yourself and as a challenge implement morphable shapes like this one below:</p>
<p><img src="/assets/posts/07_shapes.gif" alt="animatedShapes" title="Challenge with morphing shapes" /></p>
<p>Do not hesitate to share your solution or ask for help, I will gladly assist you.</p>
<p>I am looking forward to see your output!</p>
<p><em>Did you enjoy this article? Do you have anything to add?</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>Pavel ZakHello and welcome to another blog post about SwiftUI. This time, we will talk about the animation of complex shapes in SwiftUI. You will understand animatableData property and will be able to implement animatable custom Shape struct that depends on multiple parameters.Custom controls in SwiftUI2019-11-28T00:00:00+00:002019-11-28T00:00:00+00:00https://nerdyak.tech/development/2019/11/28/creating-custom-views-in-swiftui<p>In my recent project, I have created custom toggle for baby gender selection. In this post, I would like to demonstrate, how to create such a custom view in <a href="https://developer.apple.com/documentation/swiftui">SwiftUI</a> and comment it in the order how I usually approach these things.
But first, let’s have a look at what I am aiming for.</p>
<p><img src="/assets/posts/06_toggle.gif" alt="toggle" title="Toggle in action" /></p>
<h2 id="start-with-shape">Start with Shape</h2>
<p>I usually start with the preparation of custom shapes and paths that will be later used in view composition. Drawing shapes using lines and arcs is pretty straightforward, you only need to be cautious about the point coordinates. I always prepare myself small shape annotation on the paper before I start coding.</p>
<p><img src="/assets/posts/06_draft.jpg" alt="draft" title="Shape schematics" /></p>
<p>The Venus shape implementation consists of one ellipse and a set of lines forming the cross below. Note that the center of the bounds is aligned with the center of the ellipse - we will benefit from this later when we will rotate the whole shape.</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">struct</span> <span class="kt">GenderShape</span><span class="p">:</span> <span class="kt">Shape</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">stroke</span><span class="p">:</span> <span class="kt">CGFloat</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">-></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="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="o">=</span> <span class="p">(</span><span class="n">width</span><span class="o">-</span><span class="n">stroke</span><span class="p">)</span><span class="o">/</span><span class="mi">2</span>
<span class="k">let</span> <span class="nv">circlePad</span> <span class="o">=</span> <span class="mf">3*</span><span class="n">stroke</span>
<span class="n">path</span><span class="o">.</span><span class="nf">addEllipse</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="n">circlePad</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">circlePad</span><span class="p">,</span> <span class="nv">width</span><span class="p">:</span> <span class="n">width</span><span class="o">-</span><span class="mf">2*</span><span class="n">circlePad</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="n">height</span><span class="o">-</span><span class="mf">2*</span><span class="n">circlePad</span><span class="p">))</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="n">height</span><span class="p">))</span> <span class="c1">// move to the bottom left point of the cross</span>
<span class="n">path</span><span class="o">.</span><span class="nf">addLines</span><span class="p">([</span>
<span class="c1">// left part</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="o">-</span><span class="n">stroke</span><span class="o">+</span><span class="n">arrowShift</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">stroke</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">height</span><span class="o">-</span><span class="n">stroke</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">stroke</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">height</span><span class="o">-</span><span class="mf">2*</span><span class="n">stroke</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="o">-</span><span class="mf">2*</span><span class="n">stroke</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="o">-</span><span class="mf">4*</span><span class="n">stroke</span><span class="p">),</span>
<span class="c1">// right part</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">stroke</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">height</span><span class="o">-</span><span class="mf">4*</span><span class="n">stroke</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">stroke</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">height</span><span class="o">-</span><span class="mf">2*</span><span class="n">stroke</span><span class="o">+</span><span class="n">arrowShift</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="mf">2*</span><span class="n">stroke</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">height</span><span class="o">-</span><span class="mf">2*</span><span class="n">stroke</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="mf">2*</span><span class="n">stroke</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">height</span><span class="o">-</span><span class="n">stroke</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">stroke</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">height</span><span class="o">-</span><span class="n">stroke</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">stroke</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="c1">// and close path</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="p">])</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<h2 id="shape-transformation">Shape transformation</h2>
<p>Having the basic shape ready, it is time to allow it to transform from the starting Venus symbol to the Mars symbol. Here I will make things easier for me and I will accomplish the transformation from cross to arrow just by moving 4 points like this.</p>
<p><img src="/assets/posts/06_transformation.gif" alt="transformation" title="Shape transformation" /></p>
<p>Let’s add a new property called <code class="language-plaintext highlighter-rouge">arroweness</code>, that will control the position of transformed points and set it via <code class="language-plaintext highlighter-rouge">animatableData</code> property. This will allow SwiftUI to animate the shape whenever we change this property. We expect it to hold values from 0 to 1, where 0 corresponds to a cross symbol and 1 to an arrow symbol.</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">struct</span> <span class="kt">GenderShape</span><span class="p">:</span> <span class="kt">Shape</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">arroweness</span><span class="p">:</span> <span class="kt">CGFloat</span><span class="c1">// 0..1</span>
<span class="k">var</span> <span class="nv">stroke</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="o">=</span> <span class="mi">10</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="n">arroweness</span> <span class="p">}</span>
<span class="k">set</span> <span class="p">{</span> <span class="n">arroweness</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">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">-></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="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="o">=</span> <span class="p">(</span><span class="n">width</span><span class="o">-</span><span class="n">stroke</span><span class="p">)</span><span class="o">/</span><span class="mi">2</span>
<span class="k">let</span> <span class="nv">circlePad</span> <span class="o">=</span> <span class="mf">3*</span><span class="n">stroke</span>
<span class="k">let</span> <span class="nv">arrowShift</span> <span class="o">=</span> <span class="n">arroweness</span><span class="o">*</span><span class="n">stroke</span> <span class="c1">// !!!</span>
<span class="n">path</span><span class="o">.</span><span class="nf">addEllipse</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="n">circlePad</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">circlePad</span><span class="p">,</span> <span class="nv">width</span><span class="p">:</span> <span class="n">width</span><span class="o">-</span><span class="mf">2*</span><span class="n">circlePad</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="n">height</span><span class="o">-</span><span class="mf">2*</span><span class="n">circlePad</span><span class="p">))</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="n">height</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="c1">// left</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="o">-</span><span class="n">stroke</span><span class="o">+</span><span class="n">arrowShift</span><span class="p">),</span> <span class="c1">// !!!</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">stroke</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">height</span><span class="o">-</span><span class="n">stroke</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">stroke</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">height</span><span class="o">-</span><span class="mf">2*</span><span class="n">stroke</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="o">-</span><span class="mf">2*</span><span class="n">stroke</span><span class="o">+</span><span class="n">arrowShift</span><span class="p">),</span> <span class="c1">// !!!</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="o">-</span><span class="mf">4*</span><span class="n">stroke</span><span class="p">),</span>
<span class="c1">// right</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">stroke</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">height</span><span class="o">-</span><span class="mf">4*</span><span class="n">stroke</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">stroke</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">height</span><span class="o">-</span><span class="mf">2*</span><span class="n">stroke</span><span class="o">+</span><span class="n">arrowShift</span><span class="p">),</span> <span class="c1">// !!!</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="mf">2*</span><span class="n">stroke</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">height</span><span class="o">-</span><span class="mf">2*</span><span class="n">stroke</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="mf">2*</span><span class="n">stroke</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">height</span><span class="o">-</span><span class="n">stroke</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">stroke</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">height</span><span class="o">-</span><span class="n">stroke</span><span class="o">+</span><span class="n">arrowShift</span><span class="p">),</span> <span class="c1">// !!!</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">stroke</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="c1">// and close</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="p">])</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<h2 id="view-composition">View Composition</h2>
<p>The final Toggle View is Composed from two main views stacked in ZStack:</p>
<ul>
<li>First, there is a rounded rectangle with gradiend overlay</li>
<li>and on the top there is the gender shape overlayed with white circle, which will make the illusion of actual Toggle.</li>
</ul>
<p>The code below contains several let constants holding diomensions of the view and subviews. It is ok since the Toggle control usually do not change its size. The meaning of constants is captured on the following diagram:</p>
<p><img src="/assets/posts/06_anatomy.jpg" alt="anatomy" title="Dimensions" /></p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">struct</span> <span class="kt">GenderToggle</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">baseWidth</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="o">=</span> <span class="mi">50</span> <span class="c1">// width of toggle</span>
<span class="k">let</span> <span class="nv">baseHeight</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="o">=</span> <span class="mi">30</span> <span class="c1">// height of toggle</span>
<span class="k">let</span> <span class="nv">stroke</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="o">=</span> <span class="mi">6</span>
<span class="k">let</span> <span class="nv">gap</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="o">=</span> <span class="mi">2</span> <span class="c1">// gap between the background and foreground</span>
<span class="k">var</span> <span class="nv">gradient</span> <span class="o">=</span> <span class="p">[</span><span class="kt">Color</span><span class="p">(</span><span class="s">"Pink"</span><span class="p">),</span> <span class="kt">Color</span><span class="o">.</span><span class="n">gray</span><span class="p">,</span> <span class="kt">Color</span><span class="p">(</span><span class="s">"Blue"</span><span class="p">)]</span> <span class="c1">// custom colors from assets</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">whiteButton</span> <span class="o">=</span> <span class="kt">Circle</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">baseHeight</span><span class="o">-</span><span class="mf">2*</span><span class="n">gap</span> <span class="o">-</span> <span class="mf">2*</span><span class="n">stroke</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="n">baseHeight</span><span class="o">-</span><span class="mf">2*</span><span class="n">gap</span> <span class="o">-</span> <span class="mf">2*</span><span class="n">stroke</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">white</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">rectangleOverlay</span> <span class="o">=</span> <span class="kt">RoundedRectangle</span><span class="p">(</span><span class="nv">cornerRadius</span><span class="p">:</span> <span class="n">baseHeight</span><span class="o">/</span><span class="mi">2</span><span class="p">)</span>
<span class="o">.</span><span class="nf">fill</span><span class="p">(</span><span class="kt">RadialGradient</span><span class="p">(</span><span class="nv">gradient</span><span class="p">:</span> <span class="kt">Gradient</span><span class="p">(</span><span class="nv">colors</span><span class="p">:</span> <span class="p">[</span><span class="kt">Color</span><span class="o">.</span><span class="n">black</span><span class="o">.</span><span class="nf">opacity</span><span class="p">(</span><span class="mi">0</span><span class="p">),</span> <span class="kt">Color</span><span class="o">.</span><span class="n">black</span><span class="o">.</span><span class="nf">opacity</span><span class="p">(</span><span class="mf">0.2</span><span class="p">)]),</span>
<span class="nv">center</span><span class="p">:</span> <span class="kt">UnitPoint</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="mf">0.5</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="mf">0.5</span><span class="p">),</span> <span class="nv">startRadius</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="nv">endRadius</span><span class="p">:</span> <span class="n">baseWidth</span><span class="o">/</span><span class="mi">2</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">baseWidth</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="n">baseHeight</span><span class="p">)</span>
<span class="k">return</span> <span class="kt">ZStack</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="n">baseHeight</span><span class="o">/</span><span class="mi">2</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">secondary</span><span class="o">.</span><span class="nf">opacity</span><span class="p">(</span><span class="mf">0.5</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">baseWidth</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="n">baseHeight</span><span class="p">)</span>
<span class="o">.</span><span class="nf">overlay</span><span class="p">(</span><span class="n">rectangleOverlay</span><span class="p">)</span> <span class="c1">// add color gradient over background</span>
<span class="kt">GenderShape</span><span class="p">(</span><span class="nv">arroweness</span><span class="p">:</span> <span class="mf">0.0</span><span class="p">,</span> <span class="nv">stroke</span><span class="p">:</span> <span class="n">stroke</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">baseHeight</span><span class="o">+</span><span class="mf">6*</span><span class="n">stroke</span><span class="o">-</span><span class="mf">2*</span><span class="n">gap</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="n">baseHeight</span><span class="o">+</span><span class="mf">6*</span><span class="n">stroke</span><span class="o">-</span><span class="mf">2*</span><span class="n">gap</span><span class="p">)</span>
<span class="o">.</span><span class="nf">overlay</span><span class="p">(</span><span class="n">whiteButton</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">baseWidth</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="n">baseHeight</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<h2 id="state-switching">State switching</h2>
<p>By default, both the views are centered along the y-axis. To implement the state switch and changing animation we need to do the following:</p>
<ul>
<li>add property <code class="language-plaintext highlighter-rouge">gender</code> that will hold the state and can be linked to our model via <code class="language-plaintext highlighter-rouge">@Binding</code></li>
<li>change <code class="language-plaintext highlighter-rouge">y</code> coordinate of foreground arrow based on the <code class="language-plaintext highlighter-rouge">gender</code> value</li>
<li>change our arroweness, rotation and color of gender symbol based on the <code class="language-plaintext highlighter-rouge">gender</code> value</li>
<li>add tap gesture recognizer to perform the <code class="language-plaintext highlighter-rouge">gender</code> switch</li>
</ul>
<p>The final code:</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">enum</span> <span class="kt">Gender</span> <span class="p">:</span> <span class="kt">Int</span><span class="p">,</span> <span class="kt">Codable</span><span class="p">{</span>
<span class="k">case</span> <span class="n">boy</span>
<span class="k">case</span> <span class="n">girl</span>
<span class="p">}</span>
<span class="kd">struct</span> <span class="kt">GenderToggle</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kd">@Binding</span> <span class="k">var</span> <span class="nv">gender</span><span class="p">:</span> <span class="kt">Gender</span>
<span class="k">let</span> <span class="nv">baseWidth</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="o">=</span> <span class="mi">50</span> <span class="c1">// width of toggle</span>
<span class="k">let</span> <span class="nv">baseHeight</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="o">=</span> <span class="mi">30</span> <span class="c1">// height of toggle</span>
<span class="k">let</span> <span class="nv">stroke</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="o">=</span> <span class="mi">6</span>
<span class="k">let</span> <span class="nv">gap</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="o">=</span> <span class="mi">2</span> <span class="c1">// gap between the background and foreground</span>
<span class="k">var</span> <span class="nv">gradient</span> <span class="o">=</span> <span class="p">[</span><span class="kt">Color</span><span class="p">(</span><span class="s">"Pink"</span><span class="p">),</span> <span class="kt">Color</span><span class="o">.</span><span class="n">gray</span><span class="p">,</span> <span class="kt">Color</span><span class="p">(</span><span class="s">"Blue"</span><span class="p">)]</span> <span class="c1">// custom colors from assets</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">whiteButton</span> <span class="o">=</span> <span class="kt">Circle</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">baseHeight</span><span class="o">-</span><span class="mf">2*</span><span class="n">gap</span> <span class="o">-</span> <span class="mf">2*</span><span class="n">stroke</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="n">baseHeight</span><span class="o">-</span><span class="mf">2*</span><span class="n">gap</span> <span class="o">-</span> <span class="mf">2*</span><span class="n">stroke</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="n">gender</span> <span class="o">==</span> <span class="o">.</span><span class="n">boy</span> <span class="p">?</span> <span class="mi">10</span> <span class="p">:</span> <span class="o">-</span><span class="mi">10</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span><span class="mi">0</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">white</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">rectangleOverlay</span> <span class="o">=</span> <span class="kt">RoundedRectangle</span><span class="p">(</span><span class="nv">cornerRadius</span><span class="p">:</span> <span class="n">baseHeight</span><span class="o">/</span><span class="mi">2</span><span class="p">)</span>
<span class="o">.</span><span class="nf">fill</span><span class="p">(</span><span class="kt">RadialGradient</span><span class="p">(</span><span class="nv">gradient</span><span class="p">:</span> <span class="kt">Gradient</span><span class="p">(</span><span class="nv">colors</span><span class="p">:</span> <span class="p">[</span><span class="kt">Color</span><span class="o">.</span><span class="n">black</span><span class="o">.</span><span class="nf">opacity</span><span class="p">(</span><span class="mi">0</span><span class="p">),</span> <span class="kt">Color</span><span class="o">.</span><span class="n">black</span><span class="o">.</span><span class="nf">opacity</span><span class="p">(</span><span class="mf">0.2</span><span class="p">)]),</span>
<span class="nv">center</span><span class="p">:</span> <span class="kt">UnitPoint</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="mf">0.5</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="mf">0.5</span><span class="p">),</span> <span class="nv">startRadius</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="nv">endRadius</span><span class="p">:</span> <span class="n">baseWidth</span><span class="o">/</span><span class="mi">2</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">baseWidth</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="n">baseHeight</span><span class="p">)</span>
<span class="k">return</span> <span class="kt">ZStack</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="n">baseHeight</span><span class="o">/</span><span class="mi">2</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">secondary</span><span class="o">.</span><span class="nf">opacity</span><span class="p">(</span><span class="mf">0.5</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">baseWidth</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="n">baseHeight</span><span class="p">)</span>
<span class="o">.</span><span class="nf">overlay</span><span class="p">(</span><span class="n">rectangleOverlay</span><span class="p">)</span> <span class="c1">// add color gradient over background</span>
<span class="kt">GenderShape</span><span class="p">(</span><span class="nv">arroweness</span><span class="p">:</span> <span class="n">gender</span> <span class="o">==</span> <span class="o">.</span><span class="n">boy</span> <span class="p">?</span> <span class="mf">1.0</span> <span class="p">:</span> <span class="mf">0.0</span><span class="p">,</span> <span class="nv">stroke</span><span class="p">:</span> <span class="n">stroke</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">baseHeight</span><span class="o">+</span><span class="mf">6*</span><span class="n">stroke</span><span class="o">-</span><span class="mf">2*</span><span class="n">gap</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="n">baseHeight</span><span class="o">+</span><span class="mf">6*</span><span class="n">stroke</span><span class="o">-</span><span class="mf">2*</span><span class="n">gap</span><span class="p">)</span>
<span class="o">.</span><span class="nf">rotationEffect</span><span class="p">(</span><span class="kt">Angle</span><span class="p">(</span><span class="nv">degrees</span><span class="p">:</span> <span class="n">gender</span> <span class="o">==</span> <span class="o">.</span><span class="n">boy</span> <span class="p">?</span> <span class="o">-</span><span class="mi">135</span> <span class="p">:</span> <span class="mi">0</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="n">gender</span> <span class="o">==</span> <span class="o">.</span><span class="n">boy</span> <span class="p">?</span> <span class="mi">10</span> <span class="p">:</span> <span class="o">-</span><span class="mi">10</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span><span class="mi">0</span><span class="p">))</span>
<span class="o">.</span><span class="nf">foregroundColor</span><span class="p">(</span><span class="n">gender</span> <span class="o">==</span> <span class="o">.</span><span class="n">boy</span> <span class="p">?</span> <span class="kt">Color</span><span class="p">(</span><span class="s">"Blue"</span><span class="p">)</span> <span class="p">:</span> <span class="kt">Color</span><span class="p">(</span><span class="s">"Pink"</span><span class="p">))</span>
<span class="o">.</span><span class="nf">overlay</span><span class="p">(</span><span class="n">whiteButton</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">baseWidth</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="n">baseHeight</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="k">if</span> <span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="n">gender</span> <span class="o">==</span> <span class="o">.</span><span class="n">boy</span><span class="p">)</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">gender</span> <span class="o">=</span> <span class="o">.</span><span class="n">girl</span>
<span class="p">}</span>
<span class="k">else</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">gender</span> <span class="o">=</span> <span class="o">.</span><span class="n">boy</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><em>Did you like this article?</em></p>
<p><em>Feel free to comment or criticise so the next one is even better. Or share it with other SwiftUI adopters ;)</em></p>Pavel ZakIn my recent project, I have created custom toggle for baby gender selection. In this post, I would like to demonstrate, how to create such a custom view in SwiftUI and comment it in the order how I usually approach these things. But first, let’s have a look at what I am aiming for.