Dimi Chakarov I code, I design, I lead, I write, I follow, I play, I deliver, I understand, I teach. Currently @ Just Eat. Say hi. https://dchakarov.com/ Thu, 09 Apr 2026 12:22:55 +0000 Thu, 09 Apr 2026 12:22:55 +0000 Jekyll v3.10.0 A Refined Authentication Experience | Dan Lages <div class="link-preview"> <a href="https://danlages.com/engineering/authentication/2026/01/04/refined-authentication-experience.html" target="_blank" rel="noopener"> <img src="/assets/images/logo.svg" alt="" loading="lazy" onerror="this.remove()"> <span class="link-preview-text"> <strong>A Refined Authentication Experience | Dan Lages</strong> <span class="link-preview-desc">In a large-scale consumer application, it is vital that the authentication experience is both secure and intuitive. The Just Eat Takeaway applicati...</span> <span class="link-preview-domain">danlages.com</span> </span> </a> </div> <div class="link-preview"> <a href="https://danlages.com/engineering/authentication/2026/01/04/refined-authentication-experience.html" target="_blank" rel="noopener"> <img src="/assets/images/logo.svg" alt="" loading="lazy" onerror="this.remove()"> <span class="link-preview-text"> <strong>A Refined Authentication Experience | Dan Lages</strong> <span class="link-preview-desc">In a large-scale consumer application, it is vital that the authentication experience is both secure and intuitive. The Just Eat Takeaway applicati...</span> <span class="link-preview-domain">danlages.com</span> </span> </a> </div> Mon, 30 Mar 2026 12:27:39 +0000 https://dchakarov.com/blog/a-refined-authentication-experience-dan-lages-1227/ https://dchakarov.com/blog/a-refined-authentication-experience-dan-lages-1227/ notes I got inspired by my friend Costa’s write-up and decided to try it on my own blo <p>I got inspired by my friend Costa’s write-up and decided to try it on my own blog. A more detailed article coming soon… for now, enjoy the result</p> <div class="link-preview"> <a href="https://www.costafotiadis.com/things/" target="_blank" rel="noopener"> <img src="https://www.costafotiadis.com/content/images/size/w1200/2026/03/Gemini_Generated_Image_hejjiehejjiehejj.png" alt="" loading="lazy" onerror="this.remove()"> <span class="link-preview-text"> <strong>Things</strong> <span class="link-preview-desc">Or how I made a telegram bot update my website</span> <span class="link-preview-domain">costafotiadis.com</span> </span> </a> </div> <p>I got inspired by my friend Costa’s write-up and decided to try it on my own blog. A more detailed article coming soon… for now, enjoy the result</p> Sun, 29 Mar 2026 19:36:12 +0000 https://dchakarov.com/blog/things-1936/ https://dchakarov.com/blog/things-1936/ notes AI is making CEOs delusional <div class="video-embed"> <iframe src="https://www.youtube.com/embed/Q6nem-F8AG8" title="AI is making CEOs delusional" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> </div> <div class="video-embed"> <iframe src="https://www.youtube.com/embed/Q6nem-F8AG8" title="AI is making CEOs delusional" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> </div> Sun, 29 Mar 2026 19:06:52 +0000 https://dchakarov.com/blog/ai-is-making-ceos-delusional-1906/ https://dchakarov.com/blog/ai-is-making-ceos-delusional-1906/ notes Universal Links are hard. Good that we have Alberto to help us out! <p>Universal Links are hard. Good that we have Alberto to help us out!</p> <div class="link-preview"> <a href="https://albertodebortoli.com/2026/01/15/universal-links-at-scale-the-challenges-nobody-talks-about/" target="_blank" rel="noopener"> <img src="https://albertodebortoli.com/content/images/size/w1200/2026/01/0_5RZ69ENMLrgyZwGV.webp" alt="" loading="lazy" onerror="this.remove()"> <span class="link-preview-text"> <strong>Universal Links At Scale: The Challenges Nobody Talks About</strong> <span class="link-preview-desc">A deep dive into the practical challenges of implementing, testing, and maintaining Universal Links at scale</span> <span class="link-preview-domain">albertodebortoli.com</span> </span> </a> </div> <p>Universal Links are hard. Good that we have Alberto to help us out!</p> Sun, 29 Mar 2026 18:31:28 +0000 https://dchakarov.com/blog/universal-links-at-scale-the-challenges-nobody-tal-1831/ https://dchakarov.com/blog/universal-links-at-scale-the-challenges-nobody-tal-1831/ notes How I Built a Telegram Bot to Post Notes to My Jekyll Blog <p><span class="dropcap">I</span>‘ve been wanting to add a “micro-blog” to my site for a while – a place for quick thoughts, links I’ve found interesting, or a video worth sharing. Not a full article, just a line or two with a link. The kind of thing you’d share in a group chat.</p> <p>Posting on my site isn’t that hard right now. I duplicate the last post (as an MD file), rename it, open it in Panda, and start writing. Once I’m done, I open Fork and push my changes to GitHub. A few seconds later, the article is live.</p> <p>That’s all good when I have my laptop with me. If I only have my phone, I would have to do all that in the GitHub web interface or develop a complicated workflow in order to use an app on the phone. Another problem is styling. The current blog theme works for long technical articles. To a lesser extent, it also works for a short story or a review. It doesn’t work for link posts or for hot takes.</p> <p>Then my friend Costa wrote about <a href="https://www.costafotiadis.com/things/">how he built something similar</a> using a Telegram bot, and I decided to build my own version. His setup uses Railway to host the bot and appends entries to a single page. Mine is slightly different: each note becomes its own Jekyll post, and the bot runs locally on my Mac. That way, I avoid having to sign up for a new service.</p> <h2 id="the-architecture">The Architecture</h2> <p>The setup is simple:</p> <ol> <li>I send a message to my Telegram bot (from my phone, desktop, wherever)</li> <li>A Python script running on my Mac picks it up</li> <li>The script creates a Jekyll post file via the GitHub Contents API</li> <li>GitHub Pages rebuilds the site automatically</li> </ol> <p>No servers, no containers, no CI pipelines. The bot talks directly to GitHub’s REST API to create files in my <code class="language-plaintext highlighter-rouge">_posts/</code> directory, and GitHub Pages does the rest.</p> <h2 id="the-bot">The Bot</h2> <p>The bot is about 150 lines of Python code, built on two frameworks: a Python wrapper for the Telegram API called <a href="https://github.com/python-telegram-bot/python-telegram-bot">python-telegram-bot</a> and <a href="https://www.python-httpx.org/">httpx</a>. It handles three types of messages (for now):</p> <ul> <li><strong>Just a link</strong> - fetches the page’s Open Graph metadata (title, description, image) and creates a note with a link preview card</li> <li><strong>A link + my thoughts</strong> - As above, but use my notes as the post body</li> <li><strong>Plain text</strong> - saved as a quick thought, no link involved</li> </ul> <h3 id="creating-posts-via-the-github-api">Creating Posts via the GitHub API</h3> <p>The clever bit (borrowed from Costa’s approach) is that the bot never touches a local git repo. It uses the <a href="https://docs.github.com/en/rest/repos/contents">GitHub Contents API</a> to create files directly:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="k">def</span> <span class="nf">gh_create_file</span><span class="p">(</span><span class="n">client</span><span class="p">,</span> <span class="n">path</span><span class="p">,</span> <span class="n">content</span><span class="p">,</span> <span class="n">message</span><span class="p">):</span> <span class="n">url</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"https://api.github.com/repos/</span><span class="si">{</span><span class="n">GITHUB_REPO</span><span class="si">}</span><span class="s">/contents/</span><span class="si">{</span><span class="n">path</span><span class="si">}</span><span class="s">"</span> <span class="n">payload</span> <span class="o">=</span> <span class="p">{</span> <span class="s">"message"</span><span class="p">:</span> <span class="n">message</span><span class="p">,</span> <span class="s">"content"</span><span class="p">:</span> <span class="n">base64</span><span class="p">.</span><span class="n">b64encode</span><span class="p">(</span><span class="n">content</span><span class="p">.</span><span class="n">encode</span><span class="p">(</span><span class="s">"utf-8"</span><span class="p">)).</span><span class="n">decode</span><span class="p">(</span><span class="s">"utf-8"</span><span class="p">),</span> <span class="p">}</span> <span class="n">r</span> <span class="o">=</span> <span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="n">put</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="n">GH_HEADERS</span><span class="p">,</span> <span class="n">json</span><span class="o">=</span><span class="n">payload</span><span class="p">,</span> <span class="n">timeout</span><span class="o">=</span><span class="mi">15</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">raise_for_status</span><span class="p">()</span> </code></pre></div></div> <p>A single <code class="language-plaintext highlighter-rouge">PUT</code> request creates the file and commits it. GitHub Pages picks up the new commit and rebuilds the site within a couple of minutes.</p> <h3 id="fetching-link-previews">Fetching Link Previews</h3> <p>When you share a link in Telegram, WhatsApp, or Slack, you get a nice preview card with the page’s title, description, and image. I wanted the same thing on my blog.</p> <p>The bot fetches Open Graph metadata from the URL:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="k">def</span> <span class="nf">fetch_og_metadata</span><span class="p">(</span><span class="n">client</span><span class="p">,</span> <span class="n">url</span><span class="p">):</span> <span class="n">r</span> <span class="o">=</span> <span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">timeout</span><span class="o">=</span><span class="mi">10</span><span class="p">,</span> <span class="n">follow_redirects</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="s">"User-Agent"</span><span class="p">:</span> <span class="s">"Mozilla/5.0 ..."</span><span class="p">})</span> <span class="n">page</span> <span class="o">=</span> <span class="n">r</span><span class="p">.</span><span class="n">text</span> <span class="k">return</span> <span class="p">{</span> <span class="s">"title"</span><span class="p">:</span> <span class="n">_get_meta</span><span class="p">(</span><span class="n">page</span><span class="p">,</span> <span class="p">[</span><span class="s">"og:title"</span><span class="p">,</span> <span class="s">"twitter:title"</span><span class="p">]),</span> <span class="s">"description"</span><span class="p">:</span> <span class="n">_get_meta</span><span class="p">(</span><span class="n">page</span><span class="p">,</span> <span class="p">[</span><span class="s">"og:description"</span><span class="p">,</span> <span class="s">"twitter:description"</span><span class="p">]),</span> <span class="s">"image"</span><span class="p">:</span> <span class="n">_get_meta</span><span class="p">(</span><span class="n">page</span><span class="p">,</span> <span class="p">[</span><span class="s">"og:image"</span><span class="p">,</span> <span class="s">"twitter:image"</span><span class="p">]),</span> <span class="p">}</span> </code></pre></div></div> <p>It then generates an HTML preview card that gets embedded in the post. One gotcha: sites behind Cloudflare (like Medium) return a “Just a moment…” challenge page instead of real content. The bot detects these bad titles and falls back to extracting a human-readable title from the URL path.</p> <h3 id="security">Security</h3> <p>Since the bot publishes directly to my site, I needed some restrictions. Posts are only allowed from my Telegram user ID. And all the keys to connect to GitHub and Telegram are stored on my Mac.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ALLOWED_USER_ID</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">[</span><span class="s">"ALLOWED_USER_ID"</span><span class="p">])</span> <span class="k">async</span> <span class="k">def</span> <span class="nf">handle_message</span><span class="p">(</span><span class="n">update</span><span class="p">,</span> <span class="n">context</span><span class="p">):</span> <span class="k">if</span> <span class="n">update</span><span class="p">.</span><span class="n">effective_user</span><span class="p">.</span><span class="nb">id</span> <span class="o">!=</span> <span class="n">ALLOWED_USER_ID</span><span class="p">:</span> <span class="k">await</span> <span class="n">update</span><span class="p">.</span><span class="n">message</span><span class="p">.</span><span class="n">reply_text</span><span class="p">(</span><span class="s">"Not authorised."</span><span class="p">)</span> <span class="k">return</span> <span class="c1"># ... create the post </span></code></pre></div></div> <h2 id="the-jekyll-side">The Jekyll Side</h2> <p>I needed a few tweaks on my blog templates to make this work.</p> <h3 id="a-separate-layout-for-notes">A Separate Layout for Notes</h3> <p>Regular blog posts use my <code class="language-plaintext highlighter-rouge">post.html</code> layout, which shows a featured image, read time estimate, and prev/next navigation. That’s too heavy for a one-liner. I created a minimal <code class="language-plaintext highlighter-rouge">note.html</code> layout:</p> <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>--- layout: default --- <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"post note"</span><span class="nt">&gt;</span> <span class="nt">&lt;p</span> <span class="na">class=</span><span class="s">"meta"</span><span class="nt">&gt;</span>{{ page.date | date: '%B %d, %Y' }} <span class="ni">&amp;middot;</span> Note<span class="nt">&lt;/p&gt;</span> {{ content }} <span class="nt">&lt;/div&gt;</span> </code></pre></div></div> <p>No title heading (the body is the content), no read time, no post navigation. Just the date and the text.</p> <h3 id="a-notes-tab">A Notes Tab</h3> <p>I added a <code class="language-plaintext highlighter-rouge">notes.md</code> page that lists only posts in the <code class="language-plaintext highlighter-rouge">notes</code> category, and added it to my site navigation. The filter is a simple Liquid condition:</p> <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{% for post in site.posts %} {% if post.categories contains 'notes' %} <span class="c">&lt;!-- show the note --&gt;</span> {% endif %} {% endfor %} </code></pre></div></div> <p>Notes also appear on the homepage alongside regular posts. The homepage template detects notes and shows them differently – just the date and content text, without the usual title-as-link treatment.</p> <h3 id="link-preview-cards">Link Preview Cards</h3> <p>The preview cards are styled with CSS to look like the link previews you see in messaging apps – a border, rounded corners, the page’s image on top, and the title/description/domain below. The key CSS:</p> <div class="language-scss highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">.link-preview</span> <span class="p">{</span> <span class="nl">display</span><span class="p">:</span> <span class="nb">block</span><span class="p">;</span> <span class="nl">border</span><span class="p">:</span> <span class="m">1px</span> <span class="nb">solid</span> <span class="nf">darken</span><span class="p">(</span><span class="no">white</span><span class="o">,</span> <span class="m">15%</span><span class="p">);</span> <span class="nl">border-radius</span><span class="p">:</span> <span class="m">8px</span><span class="p">;</span> <span class="nl">overflow</span><span class="p">:</span> <span class="nb">hidden</span><span class="p">;</span> <span class="nl">margin</span><span class="p">:</span> <span class="m">1</span><span class="mi">.5rem</span> <span class="m">0</span><span class="p">;</span> <span class="nt">img</span> <span class="p">{</span> <span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span> <span class="nl">max-height</span><span class="p">:</span> <span class="m">300px</span><span class="p">;</span> <span class="nl">object-fit</span><span class="p">:</span> <span class="n">cover</span><span class="p">;</span> <span class="nl">display</span><span class="p">:</span> <span class="nb">block</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>One thing to watch out for: Jekyll uses kramdown for Markdown processing, and kramdown can mangle raw HTML if you’re not careful. The fix is to wrap the HTML card in kramdown’s <code class="language-plaintext highlighter-rouge">{::nomarkdown}...{:/nomarkdown}</code> tags, which tells it to pass the HTML through untouched.</p> <h2 id="running-it-locally">Running It Locally</h2> <p>Costa uses <a href="https://railway.com/">Railway</a> to host his bot. I didn’t want to sign up for another service, so I run mine locally on my Mac using <code class="language-plaintext highlighter-rouge">launchd</code>.</p> <p>A <code class="language-plaintext highlighter-rouge">.plist</code> file tells launchd to start the bot on login and restart it if it crashes:</p> <div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;dict&gt;</span> <span class="nt">&lt;key&gt;</span>Label<span class="nt">&lt;/key&gt;</span> <span class="nt">&lt;string&gt;</span>com.dchakarov.notebot<span class="nt">&lt;/string&gt;</span> <span class="nt">&lt;key&gt;</span>ProgramArguments<span class="nt">&lt;/key&gt;</span> <span class="nt">&lt;array&gt;</span> <span class="nt">&lt;string&gt;</span>/path/to/my-bot/.venv/bin/python<span class="nt">&lt;/string&gt;</span> <span class="nt">&lt;string&gt;</span>/path/to/my-bot/bot.py<span class="nt">&lt;/string&gt;</span> <span class="nt">&lt;/array&gt;</span> <span class="nt">&lt;key&gt;</span>RunAtLoad<span class="nt">&lt;/key&gt;</span> <span class="nt">&lt;true/&gt;</span> <span class="nt">&lt;key&gt;</span>KeepAlive<span class="nt">&lt;/key&gt;</span> <span class="nt">&lt;true/&gt;</span> <span class="nt">&lt;/dict&gt;</span> </code></pre></div></div> <p>Install it with:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cp </span>com.dchakarov.notebot.plist ~/Library/LaunchAgents/ launchctl load ~/Library/LaunchAgents/com.dchakarov.notebot.plist </code></pre></div></div> <p>The bot runs as long as my Mac is on. Since Telegram uses polling, it works regardless of whether I send the message from my phone, tablet, or desktop. Messages sent while the Mac is off simply queue up in Telegram and get processed the next time the bot starts. And yes, my Mac is a laptop, so my thoughts don’t go live immediately. So what?</p> <h2 id="setting-it-up-yourself">Setting It Up Yourself</h2> <p>If you want to build your own, here’s what you need:</p> <h3 id="prerequisites">Prerequisites</h3> <ol> <li>A Jekyll blog on GitHub Pages</li> <li>Python 3.9+</li> <li>A Telegram account</li> </ol> <h3 id="steps">Steps</h3> <p><strong>0. Think of a good name for your bot.</strong> Don’t be lame.</p> <p><strong>1. Create a Telegram bot.</strong> Message <a href="https://t.me/BotFather">@BotFather</a> on Telegram, send <code class="language-plaintext highlighter-rouge">/newbot</code>, and follow the prompts. Copy the bot token.</p> <p><strong>2. Get your Telegram user ID.</strong> Message <a href="https://t.me/userinfobot">@userinfobot</a> on Telegram. It replies with your numeric ID.</p> <p><strong>3. Create a GitHub personal access token.</strong> Go to <a href="https://github.com/settings/tokens?type=beta">GitHub Settings &gt; Developer settings &gt; Fine-grained tokens</a>. Create a token scoped to your blog repository with “Contents: Read and write” permission.</p> <p><strong>4. Set up the bot.</strong></p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir </span>cool-bot <span class="o">&amp;&amp;</span> <span class="nb">cd </span>cool-bot python3 <span class="nt">-m</span> venv .venv .venv/bin/pip <span class="nb">install </span>python-telegram-bot httpx python-dotenv </code></pre></div></div> <p><strong>5. Create a <code class="language-plaintext highlighter-rouge">.env</code> file</strong> with your credentials:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>TELEGRAM_BOT_TOKEN=your-token-here GITHUB_TOKEN=your-github-pat GITHUB_REPO=yourusername/yourusername.github.io ALLOWED_USER_ID=your-telegram-id SITE_URL=https://yoursite.com </code></pre></div></div> <p><strong>6. Add the bot script</strong> - you can find the full source code here: <a href="https://github.com/dchakarov/telegram-notes-bot">Telegram notes bot</a>. Feel free to contribute!</p> <p><strong>7. Test it.</strong> Run <code class="language-plaintext highlighter-rouge">.venv/bin/python bot.py</code>, send a test message to your bot, and check your GitHub repo for the new file. Since one of the features of this setup is that posts get published immediately, be ready to delete your test posts from GitHub.</p> <p><strong>8. Set up launchd</strong> to run it automatically once you’re happy (see the section above).</p> <h2 id="what-i-learned">What I Learned</h2> <p>I didn’t learn Python, that’s for sure. With Costa’s article and code on one side and Claude on the other, implementing this setup was a fun journey. Testing something that publishes live is challenging, so I had to pull locally and delete the test posts rather quickly on each iteration. There are still a few wrinkles to fix, and a few that will never get fixed, but overall, I am quite pleased with the result. Expect more frequent, shorter posts in the near future.</p> <p>Check out the <a href="/notes">Notes</a> section to see it in action.</p> <p>Hero image by <a href="https://unsplash.com/@dtravisphd?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">David Travis</a> on <a href="https://unsplash.com/photos/brown-fountain-pen-on-notebook-5bYxXawHOQg?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></p> <p><span class="dropcap">I</span>‘ve been wanting to add a “micro-blog” to my site for a while – a place for quick thoughts, links I’ve found interesting, or a video worth sharing. Not a full article, just a line or two with a link. The kind of thing you’d share in a group chat.</p> Sat, 28 Mar 2026 08:00:00 +0000 https://dchakarov.com/blog/telegram-bot-jekyll-notes/ https://dchakarov.com/blog/telegram-bot-jekyll-notes/ From Algorithms to Adventures <p><a href="https://apps.apple.com/us/app/mz-maze-adventures/id6746577798"><img src="/assets/img/AppStore.svg" /></a></p> <p><span class="dropcap">I</span>n my <a href="https://dchakarov.com/blog/maze-algorithms/">previous post</a>, I shared the story of how I fell in love with maze algorithms. I wrote about porting Jamis Buck’s Ruby code to Swift, building a visualisation engine, and the intellectual joy of understanding “perfect” mazes.</p> <p>I ended that project with a robust <code class="language-plaintext highlighter-rouge">MazeGenerator</code> framework and a technical demo. I felt like 90% of the work was done. I just needed to wrap some UI around it, add a “Start” button, and I’d have a game.</p> <p>I was wrong. I had a simulation, not a game. And turning the former into the latter would take me on a journey through bad design decisions, harsh feedback, and a complete pivot in how I thought about “fun.”</p> <h2 id="the-nerd-trap-v10">The “Nerd” Trap (v1.0)</h2> <p>When you build a game engine before you build a game, you tend to value the wrong things. I was obsessed with the algorithms. I wanted players to appreciate the difference between the <strong>Recursive Backtracker</strong> (long, winding rivers) and <strong>Prim’s Algorithm</strong> (short, organic branches).</p> <p>My first version of the game was essentially my tech demo with added controls.</p> <ul> <li><strong>The Core Loop:</strong> Select an algorithm, generate a maze, solve it, and try not to touch any walls as you get points deducted for that.</li> <li><strong>The “Cool” Feature:</strong> A “Draw Mode” where you could sketch a shape with your finger, and the framework would generate a maze around it.</li> <li><strong>The Educational Angle:</strong> Info screens explaining “Diagonal Bias” and “Spanning Trees.”</li> </ul> <p>I thought it was brilliant. It wasn’t. It was boring.</p> <h3 id="the-reality-check">The Reality Check</h3> <p>You can’t convince people to use your app or play your game if you don’t do it yourself. I installed the game on my phone and tried to play it instead of Balatro or Mini Motorways. I didn’t last one hour. There wasn’t anything to keep me entertained or to make me want to win more points. I was bored.</p> <p>Also, the “Draw Mode”? It turns out that drawing a mask with your fat finger on a phone screen results in a blocky, ugly shape. It was a technical marvel that resulted in a terrible user experience.</p> <h2 id="finding-the-fun">Finding the Fun</h2> <p>I was three months in. The codebase was sprawling with features nobody cared about (algorithm selectors, learning tabs, map editors). I had a choice to make: release a tech demo that no one would play, or strip back the nerdy features and make the game fun.</p> <p>I chose the latter.</p> <p>I stripped out the algorithm selector. I hid the educational content. I removed the Draw screen.</p> <h3 id="the-move-limit">The Move Limit</h3> <p>The breakthrough came when I stopped punishing the player for hitting a wall and introduced a <strong>Move-Based</strong> system.</p> <p>In the new version, you are given a strict budget of moves to solve the maze.</p> <ul> <li><strong>Efficiency matters:</strong> You can’t just wander; you have to plan.</li> <li><strong>Fairness is guaranteed:</strong> For any specific maze seed, the optimal path is a fixed number of steps. I could calculate the “Perfect Score” mathematically using Dijkstra’s algorithm.</li> <li><strong>Tension:</strong> Running out of moves is far more stressful (in a good way) than a timer counting up.</li> </ul> <p>Suddenly, it wasn’t a racing game anymore. It was a strategy game.</p> <h2 id="building-the-game-part">Building the “Game” Part</h2> <p>Once I had the core mechanic, I needed to wrap it in a progression system. A game needs to feel like a journey, not a loop.</p> <p>I introduced:</p> <ol> <li><strong>Power-ups:</strong> Since moves are vital to the game, power-ups became about economy. “Demolish Wall” costs coins to buy, but can help you get to that hidden crate and back before you run out of moves. “Jump” lets you skip long corridors and cash in on any remaining moves you have.</li> <li><strong>Terrain:</strong> I added ice (you slide, saving moves but losing control) and mud (costs double moves).</li> <li><strong>Progression:</strong> I added progressively harder levels - the mazes get bigger, the crates are further from the happy path, the terrain is rocky. Every ten levels, you get a prize. Every now and then, you unlock a rare avatar or stumble upon a hidden gem.</li> </ol> <h3 id="the-tech-stack-swiftui">The Tech Stack: SwiftUI</h3> <p>I built the entire game in <strong>SwiftUI</strong>.</p> <p>Many developers warned me against this, suggesting SpriteKit or Unity. But for a grid-based puzzle game, SwiftUI is surprisingly capable.</p> <ul> <li><strong>State Management:</strong> The grid is composed of <code class="language-plaintext highlighter-rouge">VStacks</code> and <code class="language-plaintext highlighter-rouge">HStacks</code>, rendered based on the game state.</li> <li><strong>Animations:</strong> SwiftUI’s implicit animations made the player movement (sliding from tile to tile) buttery smooth.</li> <li><strong>Iterative Speed:</strong> I could change the “Mud” tile view in one file and see it update across the entire game instantly in the Preview.</li> </ul> <p>The hardest part was positioning the “items” and the player layers on top of the maze layer. I ended up using a complex layering of <code class="language-plaintext highlighter-rouge">ZStacks</code> that I don’t recommend to the faint of heart, but it worked.</p> <h2 id="launching">Launching</h2> <p>The game, <strong>MZ: Maze Adventures</strong>, is finished.</p> <p>All the cute avatars in the game were designed by my wife; the not-so-cute ones - by me. The music was composed by my son (who also decided to download the game to play, not just to listen to his music in action). It has no algorithm selectors, no “Draw Mode,” and no educational lectures.</p> <p>It just has mazes. Perfect, frustrating, beautiful mazes.</p> <p>You can download it now on the App Store.</p> <p><a href="https://apps.apple.com/us/app/mz-maze-adventures/id6746577798"><img src="/assets/img/AppStore.svg" /></a></p> <p><a href="https://apps.apple.com/us/app/mz-maze-adventures/id6746577798"><img src="/assets/img/AppStore.svg" /></a></p> Mon, 08 Dec 2025 10:00:01 +0000 https://dchakarov.com/blog/maze-adventures/ https://dchakarov.com/blog/maze-adventures/ Fitting User Content Without Clipping <p><span class="dropcap">F</span>or an app I’m working on, I needed to solve a deceptively tricky problem: how many list items can I show in a widget without clipping? The app lets users create lists - shopping, to-do, Christmas, you name it. I added a widget so they could track their current list and even tick off an item or two without opening the app. Since this is user-generated content, I control neither the number of items nor how long each one is. A shopping list might have many single-word items whilst a to-do list can have longer entries, sometimes spanning multiple lines.</p> <h2 id="the-naive-implementation">The Naive Implementation</h2> <p>My initial implementation didn’t account for any of this. I used strict hardcoded limits depending on the widget family: 4 for small and medium, 8 for large. Here’s the code:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">ListView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span> <span class="kd">@Environment</span><span class="p">(\</span><span class="o">.</span><span class="n">widgetFamily</span><span class="p">)</span> <span class="k">var</span> <span class="nv">family</span> <span class="k">let</span> <span class="nv">items</span><span class="p">:</span> <span class="kt">ListItem</span> <span class="k">var</span> <span class="nv">itemLimit</span><span class="p">:</span> <span class="kt">Int</span> <span class="p">{</span> <span class="k">switch</span> <span class="n">family</span> <span class="p">{</span> <span class="k">case</span> <span class="o">.</span><span class="nv">systemSmall</span><span class="p">:</span> <span class="mi">4</span> <span class="k">case</span> <span class="o">.</span><span class="nv">systemMedium</span><span class="p">:</span> <span class="mi">4</span> <span class="k">case</span> <span class="o">.</span><span class="nv">systemLarge</span><span class="p">:</span> <span class="mi">8</span> <span class="k">default</span><span class="p">:</span> <span class="mi">0</span> <span class="p">}</span> <span class="p">}</span> <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">ZStack</span><span class="p">(</span><span class="nv">alignment</span><span class="p">:</span> <span class="o">.</span><span class="n">topLeading</span><span class="p">)</span> <span class="p">{</span> <span class="k">switch</span> <span class="n">family</span> <span class="p">{</span> <span class="k">case</span> <span class="o">.</span><span class="n">systemSmall</span><span class="p">,</span> <span class="o">.</span><span class="n">systemMedium</span><span class="p">,</span> <span class="o">.</span><span class="n">systemLarge</span><span class="p">,</span> <span class="o">.</span><span class="nv">systemExtraLarge</span><span class="p">:</span> <span class="k">let</span> <span class="nv">visibleItems</span> <span class="o">=</span> <span class="kt">Array</span><span class="p">(</span><span class="n">items</span><span class="p">[</span><span class="mi">0</span><span class="o">..&lt;</span><span class="n">itemLimit</span><span class="p">])</span> <span class="kt">VStack</span><span class="p">(</span><span class="nv">alignment</span><span class="p">:</span> <span class="o">.</span><span class="n">leading</span><span class="p">,</span> <span class="nv">spacing</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="kt">ForEach</span><span class="p">(</span><span class="n">visibleItems</span><span class="p">,</span> <span class="nv">id</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="n">id</span><span class="p">)</span> <span class="p">{</span> <span class="p">(</span><span class="nv">item</span><span class="p">:</span> <span class="kt">ListItem</span><span class="p">)</span> <span class="k">in</span> <span class="kt">ItemRowView</span><span class="p">(</span><span class="nv">item</span><span class="p">:</span> <span class="n">item</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="k">default</span><span class="p">:</span> <span class="kt">EmptyView</span><span class="p">()</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>This worked fine until I started testing with longer items. Here are a few screenshots of it “working fine” and… well… not. <img src="/assets/img/widget-sizing/first-pass-small-short.png" alt="" /> <img src="/assets/img/widget-sizing/first-pass-small-long.png" alt="" /> <img src="/assets/img/widget-sizing/first-pass-medium-short.png" alt="" /> <img src="/assets/img/widget-sizing/first-pass-medium-long.png" alt="" /></p> <p>It’s harder to break with the medium or large widget, but it’s still possible. Not ideal.</p> <h2 id="research-and-solve">Research and Solve</h2> <p>After some research, I discovered there’s no automatic way to handle this. I needed to do the calculations myself, accounting for:</p> <ul> <li>The widget dimensions</li> <li>Padding and spacing</li> <li>Font size and line height</li> <li>How many lines each item requires</li> </ul> <p>Let’s apply this to the code.</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">var</span> <span class="nv">itemLimit</span><span class="p">:</span> <span class="kt">Int</span> <span class="p">{</span> <span class="nf">itemLimitForWidgetFamily</span><span class="p">(</span><span class="nv">items</span><span class="p">:</span> <span class="n">items</span><span class="p">,</span> <span class="nv">family</span><span class="p">:</span> <span class="n">family</span><span class="p">)</span> <span class="p">}</span> <span class="c1">// ...</span> <span class="kd">private</span> <span class="kd">func</span> <span class="nf">itemLimitForWidgetFamily</span><span class="p">(</span><span class="nv">items</span><span class="p">:</span> <span class="p">[</span><span class="kt">ListItem</span><span class="p">],</span> <span class="nv">family</span><span class="p">:</span> <span class="kt">WidgetFamily</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Int</span> <span class="p">{</span> <span class="k">var</span> <span class="nv">result</span> <span class="o">=</span> <span class="mi">0</span> <span class="k">var</span> <span class="nv">currentHeight</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="o">=</span> <span class="mi">0</span> <span class="k">for</span> <span class="n">item</span> <span class="k">in</span> <span class="n">items</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">lines</span> <span class="o">=</span> <span class="nf">estimatedLineCount</span><span class="p">(</span><span class="nv">for</span><span class="p">:</span> <span class="n">item</span><span class="p">,</span> <span class="nv">width</span><span class="p">:</span> <span class="n">widgetWidth</span><span class="p">)</span> <span class="k">let</span> <span class="nv">itemHeight</span> <span class="o">=</span> <span class="n">lines</span> <span class="o">*</span> <span class="n">lineHeight</span> <span class="n">currentHeight</span> <span class="o">+=</span> <span class="n">itemHeight</span> <span class="o">+</span> <span class="n">itemSpacing</span> <span class="k">if</span> <span class="n">currentHeight</span> <span class="o">&gt;</span> <span class="n">widgetHeight</span> <span class="p">{</span> <span class="k">break</span> <span class="p">}</span> <span class="n">result</span> <span class="o">+=</span> <span class="mi">1</span> <span class="p">}</span> <span class="k">return</span> <span class="n">result</span> <span class="p">}</span> </code></pre></div></div> <p>The logic is straightforward: iterate through items, calculate each one’s height based on how many lines it needs, add it to our running total, and stop when we exceed the widget’s available space. The beauty of this approach? It works with your actual content, not arbitrary test data.</p> <p>Notice we’re referencing some unknowns: <code class="language-plaintext highlighter-rouge">widgetWidth</code>, <code class="language-plaintext highlighter-rouge">widgetHeight</code>, <code class="language-plaintext highlighter-rouge">lineHeight</code>, and <code class="language-plaintext highlighter-rouge">itemSpacing</code>. Let’s resolve them.</p> <h3 id="widget-dimensions">Widget Dimensions</h3> <p>We don’t need to be extra precise with these; ballpark it. So let’s trust the internet and use some estimated numbers here.</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">func</span> <span class="nf">widgetHeight</span><span class="p">(</span><span class="k">for</span> <span class="nv">family</span><span class="p">:</span> <span class="kt">WidgetFamily</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">CGFloat</span> <span class="p">{</span> <span class="k">switch</span> <span class="n">family</span> <span class="p">{</span> <span class="k">case</span> <span class="o">.</span><span class="nv">systemSmall</span><span class="p">:</span> <span class="mi">155</span> <span class="k">case</span> <span class="o">.</span><span class="nv">systemMedium</span><span class="p">:</span> <span class="mi">155</span> <span class="k">case</span> <span class="o">.</span><span class="nv">systemLarge</span><span class="p">:</span> <span class="mi">345</span> <span class="k">default</span><span class="p">:</span> <span class="mi">0</span> <span class="p">}</span> <span class="p">}</span> <span class="kd">private</span> <span class="kd">func</span> <span class="nf">widgetWidth</span><span class="p">(</span><span class="k">for</span> <span class="nv">family</span><span class="p">:</span> <span class="kt">WidgetFamily</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">CGFloat</span> <span class="p">{</span> <span class="k">switch</span> <span class="n">family</span> <span class="p">{</span> <span class="k">case</span> <span class="o">.</span><span class="nv">systemSmall</span><span class="p">:</span> <span class="mi">155</span> <span class="k">case</span> <span class="o">.</span><span class="nv">systemMedium</span><span class="p">:</span> <span class="mi">329</span> <span class="k">case</span> <span class="o">.</span><span class="nv">systemLarge</span><span class="p">:</span> <span class="mi">329</span> <span class="k">default</span><span class="p">:</span> <span class="mi">0</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <h3 id="measuring-text-how-many-lines">Measuring Text: How Many Lines?</h3> <p>Next challenge: determining whether each item needs one line or two. I decided to cap items at 2 lines maximum - if users want to see the full text, they can open the app. This keeps the widget clean and readable.</p> <p>Here’s where things get interesting. We need UIKit to measure text (I know, I know):</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">func</span> <span class="nf">estimatedLineCount</span><span class="p">(</span><span class="k">for</span> <span class="nv">item</span><span class="p">:</span> <span class="kt">ListItem</span><span class="p">,</span> <span class="nv">width</span><span class="p">:</span> <span class="kt">CGFloat</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Int</span> <span class="p">{</span> <span class="nf">stringFitsOnOneLine</span><span class="p">(</span><span class="n">item</span><span class="o">.</span><span class="n">name</span><span class="p">,</span> <span class="nv">width</span><span class="p">:</span> <span class="n">width</span><span class="p">)</span> <span class="p">?</span> <span class="mi">1</span> <span class="p">:</span> <span class="mi">2</span> <span class="p">}</span> <span class="kd">private</span> <span class="kd">func</span> <span class="nf">stringFitsOnOneLine</span><span class="p">(</span><span class="n">_</span> <span class="nv">text</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> <span class="nv">width</span><span class="p">:</span> <span class="kt">CGFloat</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Bool</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">constraintRect</span> <span class="o">=</span> <span class="kt">CGSize</span><span class="p">(</span> <span class="nv">width</span><span class="p">:</span> <span class="kt">CGFloat</span><span class="o">.</span><span class="n">greatestFiniteMagnitude</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="kt">CGFloat</span><span class="o">.</span><span class="n">greatestFiniteMagnitude</span> <span class="p">)</span> <span class="k">let</span> <span class="nv">boundingBox</span> <span class="o">=</span> <span class="p">(</span><span class="n">text</span> <span class="k">as</span> <span class="kt">NSString</span><span class="p">)</span><span class="o">.</span><span class="nf">boundingRect</span><span class="p">(</span> <span class="nv">with</span><span class="p">:</span> <span class="n">constraintRect</span><span class="p">,</span> <span class="nv">options</span><span class="p">:</span> <span class="p">[</span><span class="o">.</span><span class="n">usesLineFragmentOrigin</span><span class="p">,</span> <span class="o">.</span><span class="n">usesFontLeading</span><span class="p">],</span> <span class="nv">attributes</span><span class="p">:</span> <span class="p">[</span><span class="o">.</span><span class="nv">font</span><span class="p">:</span> <span class="kt">UIFont</span><span class="o">.</span><span class="nf">preferredFont</span><span class="p">(</span><span class="nv">forTextStyle</span><span class="p">:</span> <span class="o">.</span><span class="n">body</span><span class="p">)],</span> <span class="nv">context</span><span class="p">:</span> <span class="kc">nil</span> <span class="p">)</span> <span class="k">return</span> <span class="n">boundingBox</span><span class="o">.</span><span class="n">width</span> <span class="o">&lt;=</span> <span class="n">width</span> <span class="p">}</span> </code></pre></div></div> <p>We’re using <code class="language-plaintext highlighter-rouge">NSString</code>’s <code class="language-plaintext highlighter-rouge">boundingRect</code> method to calculate text width with the system body font. If you’re using a custom font or text style, adjust accordingly.</p> <p>The final piece of the puzzle is <code class="language-plaintext highlighter-rouge">lineHeight</code>, which we get from <code class="language-plaintext highlighter-rouge">UIFont.preferredFont(forTextStyle: .body).lineHeight</code>. Again, update if you’re using custom typography.</p> <h2 id="first-results">First Results</h2> <p>Let’s test it:</p> <p><img src="/assets/img/widget-sizing/second-pass-small-short.png" alt="" /> <img src="/assets/img/widget-sizing/second-pass-small-long.png" alt="" /> <img src="/assets/img/widget-sizing/second-pass-medium-short.png" alt="" /></p> <p>Almost perfect! But before shipping, I ran one more test. Good thing I did.</p> <p><img src="/assets/img/widget-sizing/second-pass-medium-long.png" alt="" /></p> <h2 id="the-missing-piece">The Missing Piece</h2> <p>In the words of my favourite coding agent: <em>I see the real issue now!</em></p> <p>We forgot to account for padding around elements and the size of those checkboxes. The calculation assumed the full widget space was available for text, but that’s not true. We need to subtract the UI chrome.</p> <p>Here’s the corrected calculation:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">func</span> <span class="nf">itemLimitForWidgetFamily</span><span class="p">(</span><span class="nv">items</span><span class="p">:</span> <span class="p">[</span><span class="kt">ListItem</span><span class="p">],</span> <span class="nv">family</span><span class="p">:</span> <span class="kt">WidgetFamily</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Int</span> <span class="p">{</span> <span class="k">var</span> <span class="nv">result</span> <span class="o">=</span> <span class="mi">0</span> <span class="k">var</span> <span class="nv">currentHeight</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="o">=</span> <span class="mi">0</span> <span class="k">let</span> <span class="nv">allowedWidth</span> <span class="o">=</span> <span class="n">widgetWidth</span> <span class="o">-</span> <span class="n">itemPadding</span> <span class="k">let</span> <span class="nv">allowedHeight</span> <span class="o">=</span> <span class="n">widgetHeight</span> <span class="o">-</span> <span class="n">listPadding</span> <span class="k">for</span> <span class="n">item</span> <span class="k">in</span> <span class="n">items</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">lines</span> <span class="o">=</span> <span class="nf">estimatedLineCount</span><span class="p">(</span><span class="nv">for</span><span class="p">:</span> <span class="n">item</span><span class="p">,</span> <span class="nv">width</span><span class="p">:</span> <span class="n">allowedWidth</span><span class="p">)</span> <span class="k">let</span> <span class="nv">itemHeight</span> <span class="o">=</span> <span class="n">lines</span> <span class="o">*</span> <span class="n">lineHeight</span> <span class="n">currentHeight</span> <span class="o">+=</span> <span class="n">itemHeight</span> <span class="o">+</span> <span class="n">itemSpacing</span> <span class="k">if</span> <span class="n">currentHeight</span> <span class="o">&gt;</span> <span class="n">allowedHeight</span> <span class="p">{</span> <span class="k">break</span> <span class="p">}</span> <span class="n">result</span> <span class="o">+=</span> <span class="mi">1</span> <span class="p">}</span> <span class="k">return</span> <span class="n">result</span> <span class="p">}</span> </code></pre></div></div> <p>With this adjustment, we account for the checkbox width, the spacing between the checkbox and the text, and the horizontal padding (<code class="language-plaintext highlighter-rouge">itemPadding</code>), and the vertical padding around the list (<code class="language-plaintext highlighter-rouge">listPadding</code>). Let’s verify one more time:</p> <p><img src="/assets/img/widget-sizing/final-pass-medium-long.png" alt="" /></p> <p>Perfect. No clipping, clean edges, and it works with any content length.</p> <h2 id="reflections">Reflections</h2> <p>Working with widget code always feels a bit constrained. You have to accept certain limitations and adapt your expectations. But by using some lesser-known APIs and a bit of maths, we ended up in a good spot. The widget now gracefully handles user content of any length, showing as many items as will fit without breaking the visual design.</p> <p>If you’re building widgets with dynamic content, I hope this approach saves you some debugging time. And if you have questions or improvements to suggest, feel free to <a href="https://dchakarov.com/contact/">send me a message</a>.</p> <p><em>Header photo by <a href="https://unsplash.com/@tinkerman?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Immo Wegmann</a> on <a href="https://unsplash.com/photos/a-person-using-a-machine-to-cut-a-piece-of-paper-ASAni-6OvNM?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></em></p> <p><span class="dropcap">F</span>or an app I’m working on, I needed to solve a deceptively tricky problem: how many list items can I show in a widget without clipping? The app lets users create lists - shopping, to-do, Christmas, you name it. I added a widget so they could track their current list and even tick off an item or two without opening the app. Since this is user-generated content, I control neither the number of items nor how long each one is. A shopping list might have many single-word items whilst a to-do list can have longer entries, sometimes spanning multiple lines.</p> Tue, 18 Nov 2025 19:25:01 +0000 https://dchakarov.com/blog/dynamic-widget-sizing/ https://dchakarov.com/blog/dynamic-widget-sizing/ Building a Maze Generation Framework <p><span class="dropcap">I</span>wasn’t looking for a game idea when I picked up <strong>Mazes for Programmers</strong> by Jamis Buck. I was just curious about algorithms and looking for something different from the usual data structures textbooks. And the book gave me that - it was incredibly well-written. I couldn’t stop reading. But there was one problem: all the code examples were in Ruby.</p> <p>Ruby is not exactly my cup of tea. As an iOS developer, I am familiar with it, but I never got fluent. But I was itching for a new project, and I wanted to truly understand these algorithms, not just copy code in a language I didn’t know. So I embraced the challenge and started converting the code to Swift. What began as a simple translation exercise eventually evolved into a full Swift framework, then a demo app, and ultimately became an iOS game called MZ: Maze Adventures.</p> <p>This is the story of that journey - specifically, the technical foundation that made everything else possible.</p> <hr /> <h2 id="what-makes-maze-algorithms-fascinating">What Makes Maze Algorithms Fascinating</h2> <p>Before diving into the code, let me share what captured my imagination about maze algorithms. They don’t just generate mazes; they generate <strong>perfect mazes</strong>. In graph theory terms, a perfect maze is a spanning tree of all cells in the grid. This means:</p> <ul> <li>The maze is connected (you can reach any cell from any other cell)</li> <li>It’s acyclic (no loops)</li> <li>It has exactly V-1 edges for V vertices</li> <li>Most importantly: there’s exactly one path between any two cells</li> </ul> <p>But here’s where it gets interesting: different algorithms produce mazes with vastly different characteristics, almost like different “personalities.”</p> <h3 id="three-algorithms-three-personalities">Three Algorithms, Three Personalities</h3> <p><strong>Recursive Backtracker (Depth-First Search)</strong><br /> This algorithm explores as far as possible before backtracking, creating long, winding corridors with fewer branches. The result is what’s called a “river pattern”: paths that players can get trapped in for extended exploration. It has high complexity and is ideal for more challenging puzzles.</p> <p><strong>Prim’s Algorithm (Randomised)</strong><br /> Prim’s randomly expands from the growing maze, creating many short dead ends with a tree-like structure. It feels more organic and natural, offering frequent decision points for the player. The complexity is medium, with many branching choices.</p> <p><strong>Binary Tree</strong><br /> Each cell makes a binary choice (typically north or east), resulting in a strong diagonal bias and predictable texture. Its main benefit? It’s very fast to generate. The downside is that the bias makes solutions easier to find. You can often solve these mazes by following the long corridors on the top or right edges.</p> <p>For MZ: Maze Adventures, I ultimately chose Recursive Backtracker because I liked how the long corridors looked, and the higher complexity made it more challenging for players.</p> <hr /> <h2 id="the-ruby-to-swift-translation-challenge">The Ruby to Swift Translation Challenge</h2> <p>When I began translating the algorithms, I had to consider every detail carefully. Ruby and Swift are fundamentally different languages with different paradigms, and that meant more than just syntax changes.</p> <p>Here’s a side-by-side comparison of the Recursive Backtracker algorithm:</p> <p><strong>Ruby (from the book):</strong></p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">RecursiveBacktracker</span> <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">on</span><span class="p">(</span><span class="n">grid</span><span class="p">,</span> <span class="ss">start_at: </span><span class="n">grid</span><span class="p">.</span><span class="nf">random_cell</span><span class="p">)</span> <span class="n">stack</span> <span class="o">=</span> <span class="p">[]</span> <span class="n">stack</span><span class="p">.</span><span class="nf">push</span> <span class="n">start_at</span> <span class="k">while</span> <span class="n">stack</span><span class="p">.</span><span class="nf">any?</span> <span class="n">current</span> <span class="o">=</span> <span class="n">stack</span><span class="p">.</span><span class="nf">last</span> <span class="n">neighbors</span> <span class="o">=</span> <span class="n">current</span><span class="p">.</span><span class="nf">neighbors</span> <span class="p">.</span><span class="nf">select</span> <span class="p">{</span> <span class="o">|</span><span class="n">n</span><span class="o">|</span> <span class="n">n</span><span class="p">.</span><span class="nf">links</span><span class="p">.</span><span class="nf">empty?</span> <span class="p">}</span> <span class="k">if</span> <span class="n">neighbors</span><span class="p">.</span><span class="nf">empty?</span> <span class="n">stack</span><span class="p">.</span><span class="nf">pop</span> <span class="k">else</span> <span class="n">neighbor</span> <span class="o">=</span> <span class="n">neighbors</span><span class="p">.</span><span class="nf">sample</span> <span class="n">current</span><span class="p">.</span><span class="nf">link</span><span class="p">(</span><span class="n">neighbor</span><span class="p">)</span> <span class="n">stack</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="n">neighbor</span><span class="p">)</span> <span class="k">end</span> <span class="k">end</span> <span class="n">grid</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div></div> <p><strong>Swift (my framework):</strong></p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="kt">RecursiveBacktrackerMazeGenerator</span><span class="p">:</span> <span class="kt">MazeGenerating</span> <span class="p">{</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">stack</span><span class="p">:</span> <span class="p">[</span><span class="kt">Cell</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span> <span class="kd">public</span> <span class="kd">func</span> <span class="nf">generateStep</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="p">(</span><span class="nv">generated</span><span class="p">:</span> <span class="p">[</span><span class="kt">Cell</span><span class="p">],</span> <span class="nv">evaluating</span><span class="p">:</span> <span class="p">[</span><span class="kt">Cell</span><span class="p">])?</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">current</span> <span class="o">=</span> <span class="n">stack</span><span class="o">.</span><span class="n">last</span><span class="o">!</span> <span class="k">let</span> <span class="nv">unvisitedNeighbours</span> <span class="o">=</span> <span class="n">grid</span> <span class="o">.</span><span class="nf">neighbours</span><span class="p">(</span><span class="nv">of</span><span class="p">:</span> <span class="n">current</span><span class="p">)</span> <span class="o">.</span><span class="n">filter</span> <span class="p">{</span> <span class="nv">$0</span><span class="o">.</span><span class="n">links</span><span class="o">.</span><span class="n">isEmpty</span> <span class="p">}</span> <span class="k">if</span> <span class="n">unvisitedNeighbours</span><span class="o">.</span><span class="n">isEmpty</span> <span class="p">{</span> <span class="n">_</span> <span class="o">=</span> <span class="n">stack</span><span class="o">.</span><span class="nf">popLast</span><span class="p">()</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">neighbour</span> <span class="o">=</span> <span class="n">unvisitedNeighbours</span><span class="o">.</span><span class="nf">randomElement</span><span class="p">()</span><span class="o">!</span> <span class="n">grid</span><span class="o">.</span><span class="nf">link</span><span class="p">(</span><span class="nv">cell1</span><span class="p">:</span> <span class="n">current</span><span class="p">,</span> <span class="nv">cell2</span><span class="p">:</span> <span class="n">neighbour</span><span class="p">)</span> <span class="n">stack</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="n">neighbour</span><span class="p">)</span> <span class="p">}</span> <span class="nf">return</span> <span class="p">(</span><span class="nv">generated</span><span class="p">:</span> <span class="p">[</span><span class="n">current</span><span class="p">],</span> <span class="nv">evaluating</span><span class="p">:</span> <span class="n">stack</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>The logic is identical - both use a stack to track visited cells, both explore neighbours, and both backtrack when stuck. But the differences reveal each language’s philosophy:</p> <ul> <li>Ruby’s <code class="language-plaintext highlighter-rouge">neighbors.sample</code> is elegant and expressive</li> <li>Swift requires <code class="language-plaintext highlighter-rouge">randomElement()!</code> with explicit unwrapping</li> <li>Ruby uses class methods (<code class="language-plaintext highlighter-rouge">def self.on</code>), while I used instance methods</li> <li>Swift’s type safety means more explicit handling of optionals</li> </ul> <h3 id="beyond-one-to-one-translation">Beyond One-to-One Translation</h3> <p>Initially, I translated the algorithms one-to-one. But then I wanted to visualise the generation of mazes step by step, not all at once. This forced me to restructure the code - instead of generating a complete maze in one pass, I needed a <code class="language-plaintext highlighter-rouge">generateStep()</code> method that returned intermediate states.</p> <p>For three of the twelve algorithms I translated, I abandoned the Ruby code completely and implemented them based purely on the algorithm descriptions in the book. By that point, I understood the patterns well enough that it was actually easier than getting stuck in translation.</p> <hr /> <h2 id="building-a-framework-not-just-code">Building a Framework, Not Just Code</h2> <p>The real insight came when I decided to build this as a proper Swift framework rather than just a collection of files. This decision paid massive dividends later.</p> <h3 id="protocol-oriented-design">Protocol-Oriented Design</h3> <p>At the core of the framework is the <code class="language-plaintext highlighter-rouge">MazeGenerating</code> protocol:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">protocol</span> <span class="kt">MazeGenerating</span> <span class="p">{</span> <span class="k">var</span> <span class="nv">grid</span><span class="p">:</span> <span class="kt">Grid</span> <span class="p">{</span> <span class="k">get</span> <span class="k">set</span> <span class="p">}</span> <span class="kd">func</span> <span class="nf">generateMaze</span><span class="p">(</span><span class="k">in</span> <span class="nv">grid</span><span class="p">:</span> <span class="kt">Grid</span><span class="p">)</span> <span class="kd">func</span> <span class="nf">generateStep</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="p">(</span><span class="nv">generated</span><span class="p">:</span> <span class="p">[</span><span class="kt">Cell</span><span class="p">],</span> <span class="nv">evaluating</span><span class="p">:</span> <span class="p">[</span><span class="kt">Cell</span><span class="p">])?</span> <span class="p">}</span> </code></pre></div></div> <p>This simple protocol allowed for easy swapping of algorithms. Need a different maze style? Just change the algorithm. Want to compare algorithms visually? Iterate through them. The flexibility was incredible.</p> <h3 id="key-design-principles">Key Design Principles</h3> <p><strong>Principle 1: Separation of Concerns</strong></p> <ul> <li><code class="language-plaintext highlighter-rouge">Grid</code> handles cell structure and topology</li> <li><code class="language-plaintext highlighter-rouge">Cell</code> manages individual cell state and links</li> <li>Algorithms implement <code class="language-plaintext highlighter-rouge">MazeGenerating</code></li> <li>Solvers (like Dijkstra’s) work on any valid maze</li> </ul> <p><strong>Principle 2: Type Safety Prevents Errors</strong></p> <ul> <li>Can’t link cells from different grids</li> <li>Can’t access out-of-bounds cells</li> <li>Can’t create invalid maze states</li> <li>Swift’s type system catches these at compile time</li> </ul> <p><strong>Principle 3: Reference Semantics with Observable State for Real-Time Visualisation</strong></p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@Observable</span> <span class="kd">final</span> <span class="kd">class</span> <span class="kt">Grid</span> <span class="p">{</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">cells</span><span class="p">:</span> <span class="p">[[</span><span class="kt">Cell</span><span class="p">]]</span> <span class="kd">func</span> <span class="nf">link</span><span class="p">(</span><span class="nv">cell1</span><span class="p">:</span> <span class="kt">Cell</span><span class="p">,</span> <span class="nv">cell2</span><span class="p">:</span> <span class="kt">Cell</span><span class="p">)</span> <span class="p">{</span> <span class="n">cell1</span><span class="o">.</span><span class="n">links</span><span class="o">.</span><span class="nf">insert</span><span class="p">(</span><span class="n">cell2</span><span class="p">)</span> <span class="n">cell2</span><span class="o">.</span><span class="n">links</span><span class="o">.</span><span class="nf">insert</span><span class="p">(</span><span class="n">cell1</span><span class="p">)</span> <span class="c1">// Changes automatically notify SwiftUI views</span> <span class="p">}</span> <span class="p">}</span> <span class="kd">@Observable</span> <span class="kd">final</span> <span class="kd">class</span> <span class="kt">Cell</span> <span class="p">{</span> <span class="k">var</span> <span class="nv">links</span><span class="p">:</span> <span class="kt">Set</span><span class="o">&lt;</span><span class="kt">Cell</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">[]</span> <span class="k">var</span> <span class="nv">visited</span><span class="p">:</span> <span class="kt">Bool</span> <span class="o">=</span> <span class="kc">false</span> <span class="p">}</span> </code></pre></div></div> <p>Why this design choice:</p> <ul> <li>Grid and cells are classes because they’re mutated during maze generation</li> <li>The algorithm modifies the grid in place; changes happen instantly</li> <li><code class="language-plaintext highlighter-rouge">@Observable</code> allows SwiftUI views to automatically redraw as cells are linked</li> <li>In the demo app, watching the maze generate step-by-step in real time was only possible because SwiftUI could react to these observable state changes</li> <li>The visualisation loop becomes trivial: algorithm mutates observable grid → SwiftUI redraws → user sees generation in progress</li> </ul> <p>The power of this approach: Separating the generative algorithm from UI concerns through <code class="language-plaintext highlighter-rouge">@Observable</code> meant the demo app didn’t need complex bindings or manual refresh logic. SwiftUI’s reactive framework automatically handled the visualisation.</p> <hr /> <h2 id="from-framework-to-demo-app">From Framework to Demo App</h2> <p>In order to see what I was generating - and this will sound very familiar to iOS developers - I added a demo app to the framework. The demo app was written in SwiftUI and imported the framework.</p> <p>The demo app lets you:</p> <ul> <li>Select different maze generation algorithms</li> <li>Generate mazes with a button press</li> <li>Visualise the maze structure</li> </ul> <p>Then, I added a solver using Dijkstra’s algorithm to show the optimal paths through the maze. Watching the solver trace the perfect path through a Recursive Backtracker maze was mesmerising.</p> <h3 id="the-aha-moment">The “Aha” Moment</h3> <p>Even before finishing the book, I was already thinking: <strong>This could be a game, right?</strong></p> <p>Once I could visualise these mazes and watch the solver find optimal paths, the game idea crystallised. Players could attempt to solve mazes with a limited number of moves. The solver could calculate fair move limits. Power-ups could let players bend the rules.</p> <p>The framework was no longer just a learning project. It was the foundation for something playable.</p> <hr /> <h2 id="lessons-learned">Lessons Learned</h2> <h3 id="framework-separation-pays-off">Framework Separation Pays Off</h3> <p>Building a separate framework compelled me to consider interfaces and contracts. When I later built the game, I could import the framework and immediately have working maze generation - no copy-pasting, no coupling. The framework could be tested independently, and game logic stayed cleanly separated from maze generation.</p> <h3 id="translation-forces-deep-understanding">Translation Forces Deep Understanding</h3> <p>Translating the Ruby algorithms to Swift made me understand them far more deeply than if I’d just read the Ruby code. I had to think about data structures, error handling, performance, and architecture. Every design decision was intentional, not just inherited from the source.</p> <h3 id="visualisation-changes-everything">Visualisation Changes Everything</h3> <p>The demo app wasn’t just a nice-to-have; it fundamentally changed how I understood the algorithms. Seeing the mazes generate, watching the solver work, experimenting with parameters - this hands-on interaction revealed insights I would never have gained from code alone.</p> <h3 id="start-simple-iterate-quickly">Start Simple, Iterate Quickly</h3> <p>The framework started with just three algorithms and basic functionality. But because the design was sound (thank you, protocol-oriented programming), I added eight more algorithms over time. Each took 30-60 minutes to implement with no changes to existing code.</p> <hr /> <h2 id="whats-next">What’s Next</h2> <p>This framework became the foundation for <a href="https://testflight.apple.com/join/Rs497D4E">MZ: Maze Adventures</a>, an iOS game that’s currently in beta testing. In future articles, I’ll dive into:</p> <ul> <li>Building the game layer on top of the framework</li> <li>SwiftUI for game development (pros and cons)</li> <li>Progression, power-ups, and game economy</li> </ul> <p>But it all started here: with a curious developer, a Ruby book, and the decision to translate algorithms into Swift.</p> <p><strong>Want to explore maze generation yourself?</strong> <a href="https://github.com/swiftyaf/MazeAlgorithms">The framework is open-source on GitHub</a>. Start with the Recursive Backtracker - it’s the most rewarding to watch in action. Or if you are into Ruby, I can recommend a great book! Find it here: <a href="https://pragprog.com/titles/jbmaze/mazes-for-programmers/">Mazes for Programmers</a>.</p> <p><span class="dropcap">I</span>wasn’t looking for a game idea when I picked up <strong>Mazes for Programmers</strong> by Jamis Buck. I was just curious about algorithms and looking for something different from the usual data structures textbooks. And the book gave me that - it was incredibly well-written. I couldn’t stop reading. But there was one problem: all the code examples were in Ruby.</p> Sun, 09 Nov 2025 00:00:01 +0000 https://dchakarov.com/blog/maze-algorithms/ https://dchakarov.com/blog/maze-algorithms/ Visual debugging with Swift Charts <p><span class="dropcap">W</span>hen I decided to implement the algorithm Soroush Khanlou so brilliantly described in his <a href="https://www.youtube.com/watch?v=-v1huP4RBgI">Elevated Swift talk</a> I didn’t expect that the hardest part would be not the algorithm for the elevator (or lift, as we call it <a href="https://en.wikipedia.org/wiki/United_Kingdom">here</a>), but rather the one for generating the traffic.</p> <p>Let’s take a step back and describe what we are trying to achieve. We have an office building with a few lifts going up and down the floors. As a typical office building we want to simulate a realistic flow of people - more during the week, less on weekends; more arriving in the morning, less so at night. We also want to keep the randomness element. And let’s not forget the lunch break with lots of people going out and back in mumbling about the queue at the lifts.</p> <p>When we take all this into consideration, our next step is to decide how often we run the algorithms - both for the lifts and for the traffic flow. As we want to ideally animate the lifts at some point, we need a loop running at a set interval, ideally a short one. We also need fake in-game time or otherwise our game will only work in real time and everything will take forever. This will also help us implement controls to slow and fasten the in-game time. After some experimentation, I arrived the following:</p> <ul> <li>We have a timer that “ticks” at a set interval.</li> <li>Every “tick” equals 10 seconds in-game time.</li> <li>The initial “tick” interval is 0.5 seconds, with controls for changing it down to 0.01 (less is faster). I reached these timings with trial and error to what felt like a good pace - not too slow but also not too fast.</li> </ul> <p>Some sloppy SwiftUI designing later, and we have our first running version. We can see the three lifts going up and down. There are people arriving at the ground floor. The lifts seem to be working well.</p> <p><img src="/assets/img/lifts-moving.gif" style="width: 300px; border-width: 1px; border-color: #b20600; border-style: double;" alt="" /></p> <p>I showed it to a friend at the office and we decided to speed it up and see what happens. This would also allow me to show him a feature I’ve been building - a screen of charts for the enterpreneur who will manage the company maintaining the lifts. The more data we have, the more interesting and useful the charts will be.</p> <p>We let it run for a couple of hours and then opened the screen with the charts.</p> <p><img src="/assets/img/swift-charts-header.png" style="border-width: 1px; border-color: #b20600; border-style: double;" alt="" /></p> <p>Even though the lifts seemed to be working fine, we could easily spot a problem upon seeing the charts. The shape of the chart was the same regardless of the day of the week. The people generation algorithm should have prevented this from happening.</p> <p>Good thing that I decided to include charts in the game or otherwise I would have missed this. And it didn’t take much. All I needed to do is save the journey data and then display it. Moreover, Swift Charts allows you to show live charts which brings dynamisms to the game.</p> <p>Every time we update the building occupancy, we save a log entry.</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">newPeople</span> <span class="o">=</span> <span class="nf">peopleArriving</span><span class="p">(</span><span class="nv">hour</span><span class="p">:</span> <span class="n">hour</span><span class="p">,</span> <span class="nv">day</span><span class="p">:</span> <span class="n">day</span><span class="p">)</span> <span class="k">let</span> <span class="nv">peopleGoingOut</span> <span class="o">=</span> <span class="nf">goingOut</span><span class="p">(</span><span class="nv">hour</span><span class="p">:</span> <span class="n">hour</span><span class="p">,</span> <span class="nv">day</span><span class="p">:</span> <span class="n">day</span><span class="p">)</span> <span class="n">currentOccupancy</span> <span class="o">+=</span> <span class="n">newPeople</span> <span class="o">-</span> <span class="n">peopleGoingOut</span> <span class="nf">generateCalls</span><span class="p">(</span><span class="nv">newPeople</span><span class="p">:</span> <span class="n">newPeople</span><span class="p">,</span> <span class="nv">peopleGoingOut</span><span class="p">:</span> <span class="n">peopleGoingOut</span><span class="p">)</span> <span class="n">log</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span> <span class="kt">LogEntry</span><span class="p">(</span> <span class="nv">date</span><span class="p">:</span> <span class="n">date</span><span class="p">,</span> <span class="nv">occupancy</span><span class="p">:</span> <span class="n">currentOccupancy</span><span class="p">,</span> <span class="nv">groundFloorQueue</span><span class="p">:</span> <span class="n">hallCalls</span><span class="o">.</span><span class="n">filter</span> <span class="p">{</span> <span class="nv">$0</span><span class="o">.</span><span class="n">from</span> <span class="o">==</span> <span class="mi">0</span> <span class="p">}</span><span class="o">.</span><span class="n">count</span> <span class="p">)</span> <span class="p">)</span> </code></pre></div></div> <p>The code to show the charts is pretty straightforward.</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">Button</span><span class="p">(</span><span class="s">"Stats"</span><span class="p">)</span> <span class="p">{</span> <span class="n">showingStats</span><span class="o">.</span><span class="nf">toggle</span><span class="p">()</span> <span class="p">}</span> <span class="o">.</span><span class="nf">sheet</span><span class="p">(</span><span class="nv">isPresented</span><span class="p">:</span> <span class="err">$</span><span class="n">showingStats</span><span class="p">)</span> <span class="p">{</span> <span class="kt">GroupBox</span><span class="p">(</span><span class="s">"Building Occupancy"</span><span class="p">)</span> <span class="p">{</span> <span class="kt">Chart</span> <span class="p">{</span> <span class="kt">ForEach</span><span class="p">(</span><span class="n">dispatcher</span><span class="o">.</span><span class="n">building</span><span class="o">.</span><span class="n">log</span><span class="p">)</span> <span class="p">{</span> <span class="n">item</span> <span class="k">in</span> <span class="kt">BarMark</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="o">.</span><span class="nf">value</span><span class="p">(</span><span class="s">"Minute"</span><span class="p">,</span> <span class="n">item</span><span class="o">.</span><span class="n">date</span><span class="p">,</span> <span class="nv">unit</span><span class="p">:</span> <span class="o">.</span><span class="n">minute</span><span class="p">),</span> <span class="nv">y</span><span class="p">:</span> <span class="o">.</span><span class="nf">value</span><span class="p">(</span><span class="s">"People"</span><span class="p">,</span> <span class="n">item</span><span class="o">.</span><span class="n">occupancy</span><span class="p">))</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="o">.</span><span class="n">horizontal</span><span class="p">,</span> <span class="o">.</span><span class="n">top</span><span class="p">])</span> <span class="kt">GroupBox</span><span class="p">(</span><span class="s">"Ground Floor Queue"</span><span class="p">)</span> <span class="p">{</span> <span class="kt">Chart</span> <span class="p">{</span> <span class="kt">ForEach</span><span class="p">(</span><span class="n">dispatcher</span><span class="o">.</span><span class="n">building</span><span class="o">.</span><span class="n">log</span><span class="p">)</span> <span class="p">{</span> <span class="n">item</span> <span class="k">in</span> <span class="kt">BarMark</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="o">.</span><span class="nf">value</span><span class="p">(</span><span class="s">"Minute"</span><span class="p">,</span> <span class="n">item</span><span class="o">.</span><span class="n">date</span><span class="p">,</span> <span class="nv">unit</span><span class="p">:</span> <span class="o">.</span><span class="n">minute</span><span class="p">),</span> <span class="nv">y</span><span class="p">:</span> <span class="o">.</span><span class="nf">value</span><span class="p">(</span><span class="s">"Ground Floor Q"</span><span class="p">,</span> <span class="n">item</span><span class="o">.</span><span class="n">groundFloorQueue</span><span class="p">))</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="o">.</span><span class="nf">presentationDetents</span><span class="p">([</span><span class="o">.</span><span class="n">medium</span><span class="p">,</span> <span class="o">.</span><span class="n">large</span><span class="p">])</span> <span class="p">}</span> </code></pre></div></div> <p>After fixing the issue with the daily crowd generation, I realised I needed to wait until the game generates enough data to see the result. I decided to add a button to generate a day worth of data in a second. That way I could see the results immediately.</p> <p><img src="/assets/img/lift-stats-fixed.png" style="border-width: 1px; border-color: #b20600; border-style: double;" alt="" /></p> <p>The difference was immediately visible. The weekday patterns now showed the expected morning rush, lunch dip, and evening departure patterns, while weekends remained appropriately quiet. Without the visual representation, I might have spent hours stepping through code or worse, shipped a simulation that looked fine on the surface but behaved incorrectly underneath.</p> <p>This experience reinforced an important lesson: when building complex systems with time-based behaviour, visual feedback isn’t just nice to have; it’s essential. Swift Charts made it trivially easy to add this debugging capability, and the investment of a few dozen lines of code saved hours of blind debugging. The charts became not just a feature for the game’s entrepreneur character, but a development tool that continues to help me validate new features and catch regressions.</p> <p>If you’re working with any kind of time-series data or algorithmic behaviour, consider adding charts early in your development process. Future you will be grateful.</p> <p><span class="dropcap">W</span>hen I decided to implement the algorithm Soroush Khanlou so brilliantly described in his <a href="https://www.youtube.com/watch?v=-v1huP4RBgI">Elevated Swift talk</a> I didn’t expect that the hardest part would be not the algorithm for the elevator (or lift, as we call it <a href="https://en.wikipedia.org/wiki/United_Kingdom">here</a>), but rather the one for generating the traffic.</p> Sat, 08 Nov 2025 00:00:01 +0000 https://dchakarov.com/blog/visual-debugging-with-Swift-Charts/ https://dchakarov.com/blog/visual-debugging-with-Swift-Charts/ How to create a playful letter animation <p><span class="dropcap">I</span>have been working on a word game recently, and I wanted to have nice playful animations to make the UI more fun. I decided to improvise, and the result turned out well. In this post, I will explain how you can create this effect and engage your users.</p> <p>A journey of a great animation begins with a single letter. Or something. Let’s start our journey by designing a letter tile.</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">SingleLetter</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">letter</span><span class="p">:</span> <span class="kt">Character</span> <span class="k">let</span> <span class="nv">backgroundColor</span><span class="p">:</span> <span class="kt">Color</span> <span class="k">let</span> <span class="nv">letterColor</span><span class="p">:</span> <span class="kt">Color</span> <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">Text</span><span class="p">(</span><span class="kt">String</span><span class="p">(</span><span class="n">letter</span><span class="p">))</span> <span class="o">.</span><span class="nf">foregroundColor</span><span class="p">(</span><span class="n">letterColor</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">title3</span><span class="p">)</span> <span class="o">.</span><span class="nf">fontWeight</span><span class="p">(</span><span class="o">.</span><span class="n">heavy</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">44</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="mi">44</span><span class="p">)</span> <span class="o">.</span><span class="nf">background</span><span class="p">(</span><span class="n">backgroundColor</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="kd">struct</span> <span class="kt">SingleLetter_Previews</span><span class="p">:</span> <span class="kt">PreviewProvider</span> <span class="p">{</span> <span class="kd">static</span> <span class="k">var</span> <span class="nv">previews</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">SingleLetter</span><span class="p">(</span><span class="nv">letter</span><span class="p">:</span> <span class="s">"W"</span><span class="p">,</span> <span class="nv">backgroundColor</span><span class="p">:</span> <span class="o">.</span><span class="n">brown</span><span class="p">,</span> <span class="nv">letterColor</span><span class="p">:</span> <span class="o">.</span><span class="n">white</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p><img src="/assets/img/first-letter.png" style="width: 300px; border-width: 1px; border-color: #b20600; border-style: double;" alt="" /></p> <p>This is good enough for now. Using previews enables us to quickly test different colours and font sizes and land on an excellent combination. Expanding the preview will help us see the letter in context. That way, we can inform the code we will write next.</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">SingleLetter_Previews</span><span class="p">:</span> <span class="kt">PreviewProvider</span> <span class="p">{</span> <span class="kd">static</span> <span class="k">var</span> <span class="nv">previews</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">HStack</span> <span class="p">{</span> <span class="kt">SingleLetter</span><span class="p">(</span><span class="nv">letter</span><span class="p">:</span> <span class="s">"W"</span><span class="p">,</span> <span class="nv">backgroundColor</span><span class="p">:</span> <span class="o">.</span><span class="n">brown</span><span class="p">,</span> <span class="nv">letterColor</span><span class="p">:</span> <span class="o">.</span><span class="n">white</span><span class="p">)</span> <span class="o">.</span><span class="nf">border</span><span class="p">(</span><span class="o">.</span><span class="n">orange</span><span class="p">)</span> <span class="kt">SingleLetter</span><span class="p">(</span><span class="nv">letter</span><span class="p">:</span> <span class="s">"O"</span><span class="p">,</span> <span class="nv">backgroundColor</span><span class="p">:</span> <span class="o">.</span><span class="n">brown</span><span class="p">,</span> <span class="nv">letterColor</span><span class="p">:</span> <span class="o">.</span><span class="n">white</span><span class="p">)</span> <span class="o">.</span><span class="nf">border</span><span class="p">(</span><span class="o">.</span><span class="n">orange</span><span class="p">)</span> <span class="kt">SingleLetter</span><span class="p">(</span><span class="nv">letter</span><span class="p">:</span> <span class="s">"R"</span><span class="p">,</span> <span class="nv">backgroundColor</span><span class="p">:</span> <span class="o">.</span><span class="n">brown</span><span class="p">,</span> <span class="nv">letterColor</span><span class="p">:</span> <span class="o">.</span><span class="n">white</span><span class="p">)</span> <span class="o">.</span><span class="nf">border</span><span class="p">(</span><span class="o">.</span><span class="n">orange</span><span class="p">)</span> <span class="kt">SingleLetter</span><span class="p">(</span><span class="nv">letter</span><span class="p">:</span> <span class="s">"D"</span><span class="p">,</span> <span class="nv">backgroundColor</span><span class="p">:</span> <span class="o">.</span><span class="n">brown</span><span class="p">,</span> <span class="nv">letterColor</span><span class="p">:</span> <span class="o">.</span><span class="n">white</span><span class="p">)</span> <span class="o">.</span><span class="nf">border</span><span class="p">(</span><span class="o">.</span><span class="n">orange</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p><img src="/assets/img/previews-ftw.png" style="width: 300px; border-width: 1px; border-color: #b20600; border-style: double;" alt="" /></p> <p>My instinct tells me to move the border definitions into the <code class="language-plaintext highlighter-rouge">SingleLetter</code> struct. I will hold off for now as I want to animate the borders later, and I am still determining where I can do that better.</p> <p>Next, let’s create our <code class="language-plaintext highlighter-rouge">word</code> view, constructed by combining a bunch of letters. We already have the code in the preview above, so all we have to do is copy it to a new view.</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">SingleWord</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span> <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">HStack</span> <span class="p">{</span> <span class="kt">SingleLetter</span><span class="p">(</span><span class="nv">letter</span><span class="p">:</span> <span class="s">"W"</span><span class="p">,</span> <span class="nv">backgroundColor</span><span class="p">:</span> <span class="o">.</span><span class="n">brown</span><span class="p">,</span> <span class="nv">letterColor</span><span class="p">:</span> <span class="o">.</span><span class="n">white</span><span class="p">)</span> <span class="o">.</span><span class="nf">border</span><span class="p">(</span><span class="o">.</span><span class="n">orange</span><span class="p">)</span> <span class="kt">SingleLetter</span><span class="p">(</span><span class="nv">letter</span><span class="p">:</span> <span class="s">"O"</span><span class="p">,</span> <span class="nv">backgroundColor</span><span class="p">:</span> <span class="o">.</span><span class="n">brown</span><span class="p">,</span> <span class="nv">letterColor</span><span class="p">:</span> <span class="o">.</span><span class="n">white</span><span class="p">)</span> <span class="o">.</span><span class="nf">border</span><span class="p">(</span><span class="o">.</span><span class="n">orange</span><span class="p">)</span> <span class="kt">SingleLetter</span><span class="p">(</span><span class="nv">letter</span><span class="p">:</span> <span class="s">"R"</span><span class="p">,</span> <span class="nv">backgroundColor</span><span class="p">:</span> <span class="o">.</span><span class="n">brown</span><span class="p">,</span> <span class="nv">letterColor</span><span class="p">:</span> <span class="o">.</span><span class="n">white</span><span class="p">)</span> <span class="o">.</span><span class="nf">border</span><span class="p">(</span><span class="o">.</span><span class="n">orange</span><span class="p">)</span> <span class="kt">SingleLetter</span><span class="p">(</span><span class="nv">letter</span><span class="p">:</span> <span class="s">"D"</span><span class="p">,</span> <span class="nv">backgroundColor</span><span class="p">:</span> <span class="o">.</span><span class="n">brown</span><span class="p">,</span> <span class="nv">letterColor</span><span class="p">:</span> <span class="o">.</span><span class="n">white</span><span class="p">)</span> <span class="o">.</span><span class="nf">border</span><span class="p">(</span><span class="o">.</span><span class="n">orange</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">SingleWord_Previews</span><span class="p">:</span> <span class="kt">PreviewProvider</span> <span class="p">{</span> <span class="kd">static</span> <span class="k">var</span> <span class="nv">previews</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">SingleWord</span><span class="p">()</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>We already saw the result in the preview of the previous view, so let’s keep going. Next, let’s parametrise a little. We need to inject the word we want to show instead of hardcoding it.</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">SingleWord</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">letters</span><span class="p">:</span> <span class="p">[</span><span class="kt">Character</span><span class="p">]</span> <span class="nf">init</span><span class="p">(</span><span class="nv">word</span><span class="p">:</span> <span class="kt">String</span><span class="p">)</span> <span class="p">{</span> <span class="k">self</span><span class="o">.</span><span class="n">letters</span> <span class="o">=</span> <span class="kt">Array</span><span class="p">(</span><span class="n">word</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">HStack</span> <span class="p">{</span> <span class="kt">ForEach</span><span class="p">(</span><span class="n">letters</span><span class="p">,</span> <span class="nv">id</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="k">self</span><span class="p">)</span> <span class="p">{</span> <span class="n">letter</span> <span class="k">in</span> <span class="kt">SingleLetter</span><span class="p">(</span><span class="nv">letter</span><span class="p">:</span> <span class="n">letter</span><span class="p">,</span> <span class="nv">backgroundColor</span><span class="p">:</span> <span class="o">.</span><span class="n">brown</span><span class="p">,</span> <span class="nv">letterColor</span><span class="p">:</span> <span class="o">.</span><span class="n">white</span><span class="p">)</span> <span class="o">.</span><span class="nf">border</span><span class="p">(</span><span class="o">.</span><span class="n">orange</span><span class="p">)</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">SingleWord_Previews</span><span class="p">:</span> <span class="kt">PreviewProvider</span> <span class="p">{</span> <span class="kd">static</span> <span class="k">var</span> <span class="nv">previews</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">SingleWord</span><span class="p">(</span><span class="nv">word</span><span class="p">:</span> <span class="s">"WORD"</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>Again, the preview is the same.</p> <p>And we finally arrived at the interesting part. Let’s shake these letters! We can start by rotating them left and right continuously. Some trial and error help us determine the angle that will best serve our needs. Here you can see what happens when we try <code class="language-plaintext highlighter-rouge">.rotationEffect(Angle(degrees: -20))</code> and <code class="language-plaintext highlighter-rouge">.rotationEffect(Angle(degrees: +20))</code></p> <p><img src="/assets/img/rotate-20.png" style="border-width: 1px; border-color: #b20600; border-style: double;" alt="" /> <img src="/assets/img/rotate+20.png" style="border-width: 1px; border-color: #b20600; border-style: double;" alt="" /></p> <p>To animate the angle, we need to make a few changes. We need a state var to hold a value that can be animated. We will use a boolean in this case. We also need to use the powerful <code class="language-plaintext highlighter-rouge">.animation</code> view modifier. There are many animation settings you can play with. For our purposes, a simple <code class="language-plaintext highlighter-rouge">.linear</code> animation will suffice, with a duration of 1 second. We will set it to repeat forever and autoreverse as we don’t want the letters to suddenly <em>twitch</em>. Let’s see the code.</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">SingleWord</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span> <span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">isAnimating</span> <span class="o">=</span> <span class="kc">false</span> <span class="kd">private</span> <span class="k">let</span> <span class="nv">letters</span><span class="p">:</span> <span class="p">[</span><span class="kt">Character</span><span class="p">]</span> <span class="nf">init</span><span class="p">(</span><span class="nv">word</span><span class="p">:</span> <span class="kt">String</span><span class="p">)</span> <span class="p">{</span> <span class="k">self</span><span class="o">.</span><span class="n">letters</span> <span class="o">=</span> <span class="kt">Array</span><span class="p">(</span><span class="n">word</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">HStack</span> <span class="p">{</span> <span class="kt">ForEach</span><span class="p">(</span><span class="n">letters</span><span class="p">,</span> <span class="nv">id</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="k">self</span><span class="p">)</span> <span class="p">{</span> <span class="n">letter</span> <span class="k">in</span> <span class="kt">SingleLetter</span><span class="p">(</span><span class="nv">letter</span><span class="p">:</span> <span class="n">letter</span><span class="p">,</span> <span class="nv">backgroundColor</span><span class="p">:</span> <span class="o">.</span><span class="n">brown</span><span class="p">,</span> <span class="nv">letterColor</span><span class="p">:</span> <span class="o">.</span><span class="n">white</span><span class="p">)</span> <span class="o">.</span><span class="nf">border</span><span class="p">(</span><span class="o">.</span><span class="n">orange</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">isAnimating</span> <span class="p">?</span> <span class="o">-</span><span class="mi">20</span> <span class="p">:</span> <span class="mi">20</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">linear</span><span class="p">(</span><span class="nv">duration</span><span class="p">:</span> <span class="mi">1</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="nv">value</span><span class="p">:</span> <span class="n">isAnimating</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="o">.</span><span class="n">onAppear</span> <span class="p">{</span> <span class="n">isAnimating</span> <span class="o">=</span> <span class="kc">true</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p><img src="/assets/img/marching-letters.mov" style="width: 300px; border-width: 1px; border-color: #b20600; border-style: double;" alt="" /></p> <p>They are moving! Job done? No. I don’t know about you, but to me, that looks more like marching than dancing. It is too perfect and uniform. We can alternate rotating letters - while the first rotates from -20 to 20 degrees, the second rotates from 20 to -20 degrees, and so on. In addition, we will introduce explicit spacing for the <code class="language-plaintext highlighter-rouge">HStack</code> as we need the letters to be further apart and not overlap when they “dance”.</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <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">HStack</span><span class="p">(</span><span class="nv">spacing</span><span class="p">:</span> <span class="mi">20</span><span class="p">)</span> <span class="p">{</span> <span class="kt">ForEach</span><span class="p">(</span><span class="n">letters</span><span class="o">.</span><span class="n">indices</span><span class="p">,</span> <span class="nv">id</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="k">self</span><span class="p">)</span> <span class="p">{</span> <span class="n">index</span> <span class="k">in</span> <span class="k">let</span> <span class="nv">letter</span> <span class="o">=</span> <span class="n">letters</span><span class="p">[</span><span class="n">index</span><span class="p">]</span> <span class="kt">SingleLetter</span><span class="p">(</span><span class="nv">letter</span><span class="p">:</span> <span class="n">letter</span><span class="p">,</span> <span class="nv">backgroundColor</span><span class="p">:</span> <span class="o">.</span><span class="n">brown</span><span class="p">,</span> <span class="nv">letterColor</span><span class="p">:</span> <span class="o">.</span><span class="n">white</span><span class="p">)</span> <span class="o">.</span><span class="nf">border</span><span class="p">(</span><span class="o">.</span><span class="n">orange</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">isAnimating</span> <span class="p">?</span> <span class="nf">angles</span><span class="p">(</span><span class="nv">for</span><span class="p">:</span> <span class="n">index</span><span class="p">)</span><span class="o">.</span><span class="nv">from</span> <span class="p">:</span> <span class="nf">angles</span><span class="p">(</span><span class="nv">for</span><span class="p">:</span> <span class="n">index</span><span class="p">)</span><span class="o">.</span><span class="n">to</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">linear</span><span class="p">(</span><span class="nv">duration</span><span class="p">:</span> <span class="mi">1</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="nv">value</span><span class="p">:</span> <span class="n">isAnimating</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="o">.</span><span class="n">onAppear</span> <span class="p">{</span> <span class="n">isAnimating</span> <span class="o">=</span> <span class="kc">true</span> <span class="p">}</span> <span class="p">}</span> <span class="kd">private</span> <span class="kd">func</span> <span class="nf">angles</span><span class="p">(</span><span class="k">for</span> <span class="nv">index</span><span class="p">:</span> <span class="kt">Int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="p">(</span><span class="nv">from</span><span class="p">:</span> <span class="kt">Double</span><span class="p">,</span> <span class="nv">to</span><span class="p">:</span> <span class="kt">Double</span><span class="p">)</span> <span class="p">{</span> <span class="n">index</span><span class="o">.</span><span class="nf">isMultiple</span><span class="p">(</span><span class="nv">of</span><span class="p">:</span> <span class="mi">2</span><span class="p">)</span> <span class="p">?</span> <span class="p">(</span><span class="nv">from</span><span class="p">:</span> <span class="o">-</span><span class="mi">20</span><span class="p">,</span> <span class="nv">to</span><span class="p">:</span> <span class="mi">20</span><span class="p">)</span> <span class="p">:</span> <span class="p">(</span><span class="nv">from</span><span class="p">:</span> <span class="mi">20</span><span class="p">,</span> <span class="nv">to</span><span class="p">:</span> <span class="o">-</span><span class="mi">20</span><span class="p">)</span> <span class="p">}</span> </code></pre></div></div> <p><img src="/assets/img/rotating-letters.mov" style="width: 300px; border-width: 1px; border-color: #b20600; border-style: double;" alt="" /></p> <p>To make the movements even less <em>perfect</em> we can introduce randomness. Instead of returning <code class="language-plaintext highlighter-rouge">-20</code> or <code class="language-plaintext highlighter-rouge">20</code>, the <code class="language-plaintext highlighter-rouge">angles</code> method can return something like <code class="language-plaintext highlighter-rouge">Double.random(in: 18...22)</code>, for example.</p> <p>So far, we have managed to produce <em>rotating letters</em>. Our target is <em>dancing letters</em> so let’s keep going. The other two attributes we can animate are <code class="language-plaintext highlighter-rouge">scale</code> and <code class="language-plaintext highlighter-rouge">border width</code>. Changing the scale of the letters will introduce a pulsating effect, which, combined with a border thickness, will have the desired result. We want all these animations to happen in unison. That’s why we will use the same <code class="language-plaintext highlighter-rouge">isAnimating</code> variable to control all of them. Let’s start with <code class="language-plaintext highlighter-rouge">scale</code>. We don’t want the letters to get too big or too tiny, so the scale factor shouldn’t change more than 10-20%. You can play with it and see what values work for you. For my needs, 0.85 - 1.15 works best. Once again, feel free to add randomness for even better results.</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <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">HStack</span><span class="p">(</span><span class="nv">spacing</span><span class="p">:</span> <span class="mi">20</span><span class="p">)</span> <span class="p">{</span> <span class="kt">ForEach</span><span class="p">(</span><span class="n">letters</span><span class="o">.</span><span class="n">indices</span><span class="p">,</span> <span class="nv">id</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="k">self</span><span class="p">)</span> <span class="p">{</span> <span class="n">index</span> <span class="k">in</span> <span class="k">let</span> <span class="nv">letter</span> <span class="o">=</span> <span class="n">letters</span><span class="p">[</span><span class="n">index</span><span class="p">]</span> <span class="kt">SingleLetter</span><span class="p">(</span><span class="nv">letter</span><span class="p">:</span> <span class="n">letter</span><span class="p">,</span> <span class="nv">backgroundColor</span><span class="p">:</span> <span class="o">.</span><span class="n">brown</span><span class="p">,</span> <span class="nv">letterColor</span><span class="p">:</span> <span class="o">.</span><span class="n">white</span><span class="p">)</span> <span class="o">.</span><span class="nf">border</span><span class="p">(</span><span class="o">.</span><span class="n">orange</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">isAnimating</span> <span class="p">?</span> <span class="nf">angles</span><span class="p">(</span><span class="nv">for</span><span class="p">:</span> <span class="n">index</span><span class="p">)</span><span class="o">.</span><span class="nv">from</span> <span class="p">:</span> <span class="nf">angles</span><span class="p">(</span><span class="nv">for</span><span class="p">:</span> <span class="n">index</span><span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="p">))</span> <span class="o">.</span><span class="nf">scaleEffect</span><span class="p">(</span><span class="n">isAnimating</span> <span class="p">?</span> <span class="nf">scale</span><span class="p">(</span><span class="nv">for</span><span class="p">:</span> <span class="n">index</span><span class="p">)</span><span class="o">.</span><span class="nv">from</span> <span class="p">:</span> <span class="nf">scale</span><span class="p">(</span><span class="nv">for</span><span class="p">:</span> <span class="n">index</span><span class="p">)</span><span class="o">.</span><span class="n">to</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">linear</span><span class="p">(</span><span class="nv">duration</span><span class="p">:</span> <span class="mi">1</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="nv">value</span><span class="p">:</span> <span class="n">isAnimating</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="o">.</span><span class="n">onAppear</span> <span class="p">{</span> <span class="n">isAnimating</span> <span class="o">=</span> <span class="kc">true</span> <span class="p">}</span> <span class="p">}</span> <span class="kd">private</span> <span class="kd">func</span> <span class="nf">angles</span><span class="p">(</span><span class="k">for</span> <span class="nv">index</span><span class="p">:</span> <span class="kt">Int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="p">(</span><span class="nv">from</span><span class="p">:</span> <span class="kt">Double</span><span class="p">,</span> <span class="nv">to</span><span class="p">:</span> <span class="kt">Double</span><span class="p">)</span> <span class="p">{</span> <span class="n">index</span><span class="o">.</span><span class="nf">isMultiple</span><span class="p">(</span><span class="nv">of</span><span class="p">:</span> <span class="mi">2</span><span class="p">)</span> <span class="p">?</span> <span class="p">(</span><span class="nv">from</span><span class="p">:</span> <span class="o">-</span><span class="mi">20</span><span class="p">,</span> <span class="nv">to</span><span class="p">:</span> <span class="mi">20</span><span class="p">)</span> <span class="p">:</span> <span class="p">(</span><span class="nv">from</span><span class="p">:</span> <span class="mi">20</span><span class="p">,</span> <span class="nv">to</span><span class="p">:</span> <span class="o">-</span><span class="mi">20</span><span class="p">)</span> <span class="p">}</span> <span class="kd">private</span> <span class="kd">func</span> <span class="nf">scale</span><span class="p">(</span><span class="k">for</span> <span class="nv">index</span><span class="p">:</span> <span class="kt">Int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="p">(</span><span class="nv">from</span><span class="p">:</span> <span class="kt">Double</span><span class="p">,</span> <span class="nv">to</span><span class="p">:</span> <span class="kt">Double</span><span class="p">)</span> <span class="p">{</span> <span class="n">index</span><span class="o">.</span><span class="nf">isMultiple</span><span class="p">(</span><span class="nv">of</span><span class="p">:</span> <span class="mi">2</span><span class="p">)</span> <span class="p">?</span> <span class="p">(</span><span class="nv">from</span><span class="p">:</span> <span class="mf">0.85</span><span class="p">,</span> <span class="nv">to</span><span class="p">:</span> <span class="mf">1.15</span><span class="p">)</span> <span class="p">:</span> <span class="p">(</span><span class="nv">from</span><span class="p">:</span> <span class="mf">1.15</span><span class="p">,</span> <span class="nv">to</span><span class="p">:</span> <span class="mf">0.85</span><span class="p">)</span> <span class="p">}</span> </code></pre></div></div> <p><img src="/assets/img/almost-there.mov" style="width: 300px; border-width: 1px; border-color: #b20600; border-style: double;" alt="" /></p> <p>We are almost there. Next, let’s animate our borders too, and introduce that randomness I keep mentioning. Also, let’s change the preview background and see how our letters perform on a darker stage.</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">SingleWord</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span> <span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">isAnimating</span> <span class="o">=</span> <span class="kc">false</span> <span class="kd">private</span> <span class="k">let</span> <span class="nv">letters</span><span class="p">:</span> <span class="p">[</span><span class="kt">Character</span><span class="p">]</span> <span class="nf">init</span><span class="p">(</span><span class="nv">word</span><span class="p">:</span> <span class="kt">String</span><span class="p">)</span> <span class="p">{</span> <span class="k">self</span><span class="o">.</span><span class="n">letters</span> <span class="o">=</span> <span class="kt">Array</span><span class="p">(</span><span class="n">word</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">HStack</span><span class="p">(</span><span class="nv">spacing</span><span class="p">:</span> <span class="mi">20</span><span class="p">)</span> <span class="p">{</span> <span class="kt">ForEach</span><span class="p">(</span><span class="n">letters</span><span class="o">.</span><span class="n">indices</span><span class="p">,</span> <span class="nv">id</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="k">self</span><span class="p">)</span> <span class="p">{</span> <span class="n">index</span> <span class="k">in</span> <span class="k">let</span> <span class="nv">letter</span> <span class="o">=</span> <span class="n">letters</span><span class="p">[</span><span class="n">index</span><span class="p">]</span> <span class="kt">SingleLetter</span><span class="p">(</span><span class="nv">letter</span><span class="p">:</span> <span class="n">letter</span><span class="p">,</span> <span class="nv">backgroundColor</span><span class="p">:</span> <span class="o">.</span><span class="n">brown</span><span class="p">,</span> <span class="nv">letterColor</span><span class="p">:</span> <span class="o">.</span><span class="n">white</span><span class="p">)</span> <span class="o">.</span><span class="nf">border</span><span class="p">(</span><span class="o">.</span><span class="n">orange</span><span class="p">,</span> <span class="nv">width</span><span class="p">:</span> <span class="n">isAnimating</span> <span class="p">?</span> <span class="mi">5</span> <span class="p">:</span> <span class="mi">0</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">isAnimating</span> <span class="p">?</span> <span class="nf">angles</span><span class="p">(</span><span class="nv">for</span><span class="p">:</span> <span class="n">index</span><span class="p">)</span><span class="o">.</span><span class="nv">from</span> <span class="p">:</span> <span class="nf">angles</span><span class="p">(</span><span class="nv">for</span><span class="p">:</span> <span class="n">index</span><span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="p">))</span> <span class="o">.</span><span class="nf">scaleEffect</span><span class="p">(</span><span class="n">isAnimating</span> <span class="p">?</span> <span class="nf">scale</span><span class="p">(</span><span class="nv">for</span><span class="p">:</span> <span class="n">index</span><span class="p">)</span><span class="o">.</span><span class="nv">from</span> <span class="p">:</span> <span class="nf">scale</span><span class="p">(</span><span class="nv">for</span><span class="p">:</span> <span class="n">index</span><span class="p">)</span><span class="o">.</span><span class="n">to</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">linear</span><span class="p">(</span><span class="nv">duration</span><span class="p">:</span> <span class="mi">1</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="nv">value</span><span class="p">:</span> <span class="n">isAnimating</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="o">.</span><span class="n">onAppear</span> <span class="p">{</span> <span class="n">isAnimating</span> <span class="o">=</span> <span class="kc">true</span> <span class="p">}</span> <span class="p">}</span> <span class="kd">private</span> <span class="kd">func</span> <span class="nf">angles</span><span class="p">(</span><span class="k">for</span> <span class="nv">index</span><span class="p">:</span> <span class="kt">Int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="p">(</span><span class="nv">from</span><span class="p">:</span> <span class="kt">Double</span><span class="p">,</span> <span class="nv">to</span><span class="p">:</span> <span class="kt">Double</span><span class="p">)</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">firstAngle</span> <span class="o">=</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="o">-</span><span class="mi">22</span> <span class="o">...</span> <span class="o">-</span><span class="mi">18</span><span class="p">)</span> <span class="k">let</span> <span class="nv">secondAngle</span> <span class="o">=</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">18</span> <span class="o">...</span> <span class="mi">22</span><span class="p">)</span> <span class="k">return</span> <span class="n">index</span><span class="o">.</span><span class="nf">isMultiple</span><span class="p">(</span><span class="nv">of</span><span class="p">:</span> <span class="mi">2</span><span class="p">)</span> <span class="p">?</span> <span class="p">(</span><span class="nv">from</span><span class="p">:</span> <span class="n">firstAngle</span><span class="p">,</span> <span class="nv">to</span><span class="p">:</span> <span class="n">secondAngle</span><span class="p">)</span> <span class="p">:</span> <span class="p">(</span><span class="nv">from</span><span class="p">:</span> <span class="n">secondAngle</span><span class="p">,</span> <span class="nv">to</span><span class="p">:</span> <span class="n">firstAngle</span><span class="p">)</span> <span class="p">}</span> <span class="kd">private</span> <span class="kd">func</span> <span class="nf">scale</span><span class="p">(</span><span class="k">for</span> <span class="nv">index</span><span class="p">:</span> <span class="kt">Int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="p">(</span><span class="nv">from</span><span class="p">:</span> <span class="kt">Double</span><span class="p">,</span> <span class="nv">to</span><span class="p">:</span> <span class="kt">Double</span><span class="p">)</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">firstScale</span> <span class="o">=</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="mf">0.85</span> <span class="o">...</span> <span class="mf">0.9</span><span class="p">)</span> <span class="k">let</span> <span class="nv">secondScale</span> <span class="o">=</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="mf">1.1</span> <span class="o">...</span> <span class="mf">1.15</span><span class="p">)</span> <span class="k">return</span> <span class="n">index</span><span class="o">.</span><span class="nf">isMultiple</span><span class="p">(</span><span class="nv">of</span><span class="p">:</span> <span class="mi">2</span><span class="p">)</span> <span class="p">?</span> <span class="p">(</span><span class="nv">from</span><span class="p">:</span> <span class="n">firstScale</span><span class="p">,</span> <span class="nv">to</span><span class="p">:</span> <span class="n">secondScale</span><span class="p">)</span> <span class="p">:</span> <span class="p">(</span><span class="nv">from</span><span class="p">:</span> <span class="n">secondScale</span><span class="p">,</span> <span class="nv">to</span><span class="p">:</span> <span class="n">firstScale</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="kd">struct</span> <span class="kt">SingleWord_Previews</span><span class="p">:</span> <span class="kt">PreviewProvider</span> <span class="p">{</span> <span class="kd">static</span> <span class="k">var</span> <span class="nv">previews</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">ZStack</span> <span class="p">{</span> <span class="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.8</span><span class="p">)</span><span class="o">.</span><span class="nf">ignoresSafeArea</span><span class="p">()</span> <span class="kt">SingleWord</span><span class="p">(</span><span class="nv">word</span><span class="p">:</span> <span class="s">"PLAY"</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p><img src="/assets/img/dancing-letters.mov" style="width: 300px; border-width: 1px; border-color: #b20600; border-style: double;" alt="" /></p> <p>In summary, we created a cool dancing effect that can delight our users. We didn’t even have to build and run the app; we saw all the action in the live previews. Where to go from here? You can add more animations to your views. Another easily animatable property is the offset. I didn’t include it here as it would have been a bit too much animating all at once. Another thing you can do is extract the rest of the hardcoded values, such as the frame size, the spacing, and the border width, and play with them. <a href="https://dchakarov.com/contact/">Let me know</a> what you manage to achieve.</p> <p>You can download the code for this post from <a href="https://github.com/dchakarov/letter-play">https://github.com/dchakarov/letter-play</a>.</p> <p><span class="dropcap">I</span>have been working on a word game recently, and I wanted to have nice playful animations to make the UI more fun. I decided to improvise, and the result turned out well. In this post, I will explain how you can create this effect and engage your users.</p> Sat, 04 Mar 2023 00:00:01 +0000 https://dchakarov.com/blog/letter-play/ https://dchakarov.com/blog/letter-play/