<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://diamantidis.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://diamantidis.github.io/" rel="alternate" type="text/html" /><updated>2026-06-22T19:53:17+00:00</updated><id>https://diamantidis.github.io/feed.xml</id><title type="html">Ioannis Diamantidis</title><subtitle>A blog where I publish posts about my learnings on software development, my thoughts, my ideas and topics that I find interesting.</subtitle><author><name>Ioannis Diamantidis</name><email>diamantidis@outlook.com</email></author><entry><title type="html">One Version Manager to Rule Them All</title><link href="https://diamantidis.github.io/2026/06/07/one-version-manager-to-rule-them-all" rel="alternate" type="text/html" title="One Version Manager to Rule Them All" /><published>2026-06-07T10:00:00+00:00</published><updated>2026-06-07T10:00:00+00:00</updated><id>https://diamantidis.github.io/2026/06/07/one-version-manager-to-rule-them-all</id><content type="html" xml:base="https://diamantidis.github.io/2026/06/07/one-version-manager-to-rule-them-all"><![CDATA[<p>Managing versions is one of those things you don’t think about much. For a long time, I used <strong>rbenv</strong> and <strong>nvm</strong> to handle the versions of Ruby and Node.js on this project. Each worked fine on its own, but the friction was always there. Two separate tools, two config files, and two things to remember when getting back to the project. Something was feeling off.</p>

<p>And then I found <a href="https://mise.jdx.dev">mise</a>, and I had one of those aha moments where everything falls into place.</p>

<h2 id="the-problem-with-multiple-version-managers">The problem with multiple version managers</h2>

<p>If you work across different languages, you probably have a setup that looks something like this:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">~/.rbenv</code> + <code class="language-plaintext highlighter-rouge">rbenv</code> shell integration + <code class="language-plaintext highlighter-rouge">.ruby-version</code> per project</li>
  <li><code class="language-plaintext highlighter-rouge">~/.nvm</code> + <code class="language-plaintext highlighter-rouge">nvm</code> shell integration + <code class="language-plaintext highlighter-rouge">.nvmrc</code> per project</li>
  <li>Maybe <code class="language-plaintext highlighter-rouge">pyenv</code> for Python, or <code class="language-plaintext highlighter-rouge">goenv</code> for Go…</li>
</ul>

<p>Each version manager has its own installation method, its own version file format, and its own quirks. When you clone a new repo, you need to check which version files are present and install the right runtimes manually. It works, but it’s friction. And that’s one of the problems that <a href="https://mise.jdx.dev">mise</a> tries to solve.</p>

<h2 id="what-is-mise">What is mise</h2>

<p><a href="https://mise.jdx.dev">mise</a> (pronounced “meez”) is a polyglot version manager. It replaces rbenv, nvm, pyenv, goenv, and similar tools with a single binary. It reads a <code class="language-plaintext highlighter-rouge">.mise.toml</code> file in your project root and automatically activates the correct versions when you <code class="language-plaintext highlighter-rouge">cd</code> into the directory.</p>

<p>Key features:</p>

<ul>
  <li><strong>One config file</strong>: <code class="language-plaintext highlighter-rouge">.mise.toml</code> lists all your project’s runtimes and versions</li>
  <li><strong>Automatic switching</strong>: mise activates the right versions as you navigate between projects</li>
  <li><strong>Wide language support</strong>: Ruby, Node.js, Python, Go, Rust, and <a href="https://mise.jdx.dev/langs.html">many more</a></li>
  <li><strong>No shell plugins needed</strong>: uses a shell hook (<code class="language-plaintext highlighter-rouge">eval "$(mise activate bash/zsh)"</code>) instead of per-tool plugins</li>
</ul>

<h2 id="before-and-after">Before and after</h2>

<p>Here’s what my setup looked like before:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Before: .ruby-version
ruby 3.3.4

# Before: .nvmrc
22.2.0
</code></pre></div></div>

<p>Two files, two tools, two shell integrations.</p>

<p>After switching to mise, everything lives in one file:</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># After: .mise.toml</span>
<span class="nn">[tools]</span>
<span class="py">ruby</span> <span class="p">=</span> <span class="s">"3.3.4"</span>
<span class="py">node</span> <span class="p">=</span> <span class="s">"22.2.0"</span>
</code></pre></div></div>

<p>This is the actual <a href="https://github.com/diamantidis/diamantidis.github.io/blob/source/.mise.toml"><code class="language-plaintext highlighter-rouge">.mise.toml</code></a> file from this blog’s repository.</p>

<h2 id="migration">Migration</h2>

<p>Migrating was straightforward. Here’s what I did:</p>

<h3 id="1-install-mise">1. Install mise</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl https://mise.run | sh
</code></pre></div></div>

<p>Then add the shell hook to your <code class="language-plaintext highlighter-rouge">.zshrc</code> (or <code class="language-plaintext highlighter-rouge">.bashrc</code>) :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">eval</span> <span class="s2">"</span><span class="si">$(</span>~/.local/bin/mise activate zsh<span class="si">)</span><span class="s2">"</span>
</code></pre></div></div>

<h3 id="2-configure-your-tools">2. Configure your tools</h3>

<p>Create a <code class="language-plaintext highlighter-rouge">.mise.toml</code> file in your project root with the runtimes you need:</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># .mise.toml</span>
<span class="nn">[tools]</span>
<span class="py">ruby</span> <span class="p">=</span> <span class="s">"3.3.4"</span>
<span class="py">node</span> <span class="p">=</span> <span class="s">"22.2.0"</span>
</code></pre></div></div>

<p>Then run <code class="language-plaintext highlighter-rouge">mise install</code> to download and install the versions specified in the file. mise reads the config automatically when you <code class="language-plaintext highlighter-rouge">cd</code> into the project directory.</p>

<h3 id="3-remove-the-old-managers">3. Remove the old managers</h3>

<p>Once everything works with mise, the old <code class="language-plaintext highlighter-rouge">.ruby-version</code> and <code class="language-plaintext highlighter-rouge">.nvmrc</code> files from your projects. And maybe cleanup rbenv and nvm if they are not used on another project:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Remove rbenv</span>
<span class="nb">rm</span> <span class="nt">-rf</span> ~/.rbenv
<span class="c"># Remove rbenv from .zshrc / .bashrc</span>

<span class="c"># Remove nvm</span>
<span class="nb">rm</span> <span class="nt">-rf</span> ~/.nvm
<span class="c"># Remove nvm from .zshrc / .bashrc</span>
</code></pre></div></div>

<p>And that was it!</p>

<h2 id="conclusion">Conclusion</h2>

<p>Switching from rbenv and nvm to mise took only a few minutes. Now, whether I’m setting up an existing project or starting a new one, the process is the same: a single <code class="language-plaintext highlighter-rouge">.mise.toml</code> file that makes it clear which runtimes and versions a project needs. If you also feel that your project can benefit from this, give <a href="https://mise.jdx.dev">mise</a> a try.</p>

<p>Thanks for reading! If you found this post useful, consider <a href="https://www.buymeacoffee.com/diamantidis">buying me a coffee</a> to support the blog. For questions or comments, feel free to reach out on <a href="https://x.com/diamantidis_io">X</a>!</p>

<p>Until next time!</p>]]></content><author><name>Ioannis Diamantidis</name><email>diamantidis@outlook.com</email></author><category term="mise" /><category term="DevTools" /><category term="Productivity" /><summary type="html"><![CDATA[How I replaced rbenv and nvm with mise - a single polyglot version manager for Ruby, Node.js, and more.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://diamantidis.github.io/assets/social/one-version-manager-to-rule-them-all.png" /><media:content medium="image" url="https://diamantidis.github.io/assets/social/one-version-manager-to-rule-them-all.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Automating Lighthouse CI for a static Blog</title><link href="https://diamantidis.github.io/2026/05/22/automated-lighthouse-ci-static-blog" rel="alternate" type="text/html" title="Automating Lighthouse CI for a static Blog" /><published>2026-05-22T10:00:00+00:00</published><updated>2026-05-22T10:00:00+00:00</updated><id>https://diamantidis.github.io/2026/05/22/automated-lighthouse-ci-static-blog</id><content type="html" xml:base="https://diamantidis.github.io/2026/05/22/automated-lighthouse-ci-static-blog"><![CDATA[<p>Over time, performance, accessibility, and SEO metrics on a blog tend to drift downward. You add a new feature, embed a script, or restructure a page, and the scores quietly degrade. Manual Lighthouse checks are an option, but only if you remember to run them. From personal experience, it is easy to forget, so I decided to automate the process with Lighthouse CI.</p>

<h2 id="why-lighthouse-ci">Why Lighthouse CI</h2>

<p><a href="https://github.com/GoogleChrome/lighthouse-ci">Lighthouse CI (<code class="language-plaintext highlighter-rouge">@lhci/cli</code>)</a> lets you run Lighthouse audits from the command line and assert minimum scores across the four categories: Performance, Accessibility, Best Practices, and SEO. It uses a config file where you define which pages to test, how many runs per page, and what thresholds to enforce. The <code class="language-plaintext highlighter-rouge">collect</code> section controls what to audit, <code class="language-plaintext highlighter-rouge">assert</code> defines the pass/fail rules, and <code class="language-plaintext highlighter-rouge">upload</code> handles report storage.</p>

<p>The key advantage over running Lighthouse in Chrome DevTools is automation. You can integrate it into your development workflow and CI pipeline so it runs without you thinking about it.</p>

<h2 id="how-i-use-it">How I use it</h2>

<p>For this blog, I identified three different workflows that I would like to cover. Let’s explore them one by one.</p>

<h3 id="scenario-1-local-smoke-test">Scenario 1: Local Smoke Test</h3>

<p>Before pushing any change, I run a quick smoke test against the local build with a single page and a single run. It takes about 10 seconds and gives me an immediate sanity check.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"ci"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"collect"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"staticDistDir"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./_site"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"numberOfRuns"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
      </span><span class="nl">"url"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"/"</span><span class="p">]</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"assert"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"assertions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"categories:performance"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"warn"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"minScore"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.8</span><span class="w"> </span><span class="p">}],</span><span class="w">
        </span><span class="nl">"categories:accessibility"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"warn"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"minScore"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.9</span><span class="w"> </span><span class="p">}],</span><span class="w">
        </span><span class="nl">"categories:best-practices"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"warn"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"minScore"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.9</span><span class="w"> </span><span class="p">}],</span><span class="w">
        </span><span class="nl">"categories:seo"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"warn"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"minScore"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.9</span><span class="w"> </span><span class="p">}],</span><span class="w">
        </span><span class="nl">"is-on-https"</span><span class="p">:</span><span class="w"> </span><span class="s2">"off"</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Every assertion is set to <code class="language-plaintext highlighter-rouge">warn</code>, so it never blocks me. I can edit the <code class="language-plaintext highlighter-rouge">url</code> list to target a specific page I just added, for example a new blog post. The <code class="language-plaintext highlighter-rouge">is-on-https</code> check is disabled because the local static server doesn’t use HTTPS, which would trigger a false positive.</p>

<p>To run it, I use these commands:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm run build:css <span class="o">&amp;&amp;</span> <span class="nv">JEKYLL_ENV</span><span class="o">=</span>production bundle <span class="nb">exec </span>jekyll build
npm run lighthouse:smoke
</code></pre></div></div>

<h3 id="scenario-2-pr-guardrail">Scenario 2: PR Guardrail</h3>

<p>But the smoke test is a manual step that can easily get skipped when rushing to ship. So I added a second layer that runs automatically on every pull request via GitHub Actions. This one uses a fixed list of representative pages that are important for this blog (home page and one random post).</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"ci"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"collect"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"staticDistDir"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./_site"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"numberOfRuns"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
      </span><span class="nl">"url"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="s2">"/"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"/2026/05/10/how-to-create-dynamic-quick-actions-ios-swift.html"</span><span class="w">
      </span><span class="p">]</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"assert"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"assertions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"categories:performance"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"warn"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"minScore"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.8</span><span class="w"> </span><span class="p">}],</span><span class="w">
        </span><span class="nl">"categories:accessibility"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"error"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"minScore"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.85</span><span class="w"> </span><span class="p">}],</span><span class="w">
        </span><span class="nl">"categories:best-practices"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"error"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"minScore"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.8</span><span class="w"> </span><span class="p">}],</span><span class="w">
        </span><span class="nl">"categories:seo"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"error"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"minScore"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.9</span><span class="w"> </span><span class="p">}],</span><span class="w">
        </span><span class="nl">"is-on-https"</span><span class="p">:</span><span class="w"> </span><span class="s2">"off"</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"upload"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"target"</span><span class="p">:</span><span class="w"> </span><span class="s2">"temporary-public-storage"</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The CI workflow builds the site and runs Lighthouse before any code review happens:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build</span>
  <span class="na">env</span><span class="pi">:</span>
    <span class="na">JEKYLL_ENV</span><span class="pi">:</span> <span class="s">production</span>
  <span class="na">run</span><span class="pi">:</span> <span class="s">bundle exec jekyll build</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Run Lighthouse CI</span>
  <span class="na">run</span><span class="pi">:</span> <span class="s">npm run lighthouse</span>
</code></pre></div></div>

<p>If Accessibility drops below 0.85 or SEO below 0.9, the CI job fails and the PR cannot be merged. Performance is a warning because it tends to vary more between runs.</p>

<h3 id="scenario-3-production-monitoring">Scenario 3: Production Monitoring</h3>

<p>Local builds and production have differences: HTTPS, hosting behavior, real network conditions. To catch issues that only appear in production, I run audits against the live site in two ways: after every deployment and on a nightly schedule.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"ci"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"collect"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"numberOfRuns"</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span><span class="w">
      </span><span class="nl">"url"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="s2">"https://diamantidis.github.io/"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"https://diamantidis.github.io/about.html"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"https://diamantidis.github.io/archive.html"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"https://diamantidis.github.io/tags.html"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"https://diamantidis.github.io/tips/"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"https://diamantidis.github.io/2026/05/10/how-to-create-dynamic-quick-actions-ios-swift.html"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"https://diamantidis.github.io/2021/10/17/two-way-communication-between-flutter-webview-and-a-web-page.html"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"https://diamantidis.github.io/tips/2026/05/09/loop-a-video-on-ios.html"</span><span class="w">
      </span><span class="p">]</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"assert"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"assertions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"categories:performance"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"warn"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"minScore"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.8</span><span class="w"> </span><span class="p">}],</span><span class="w">
        </span><span class="nl">"categories:accessibility"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"warn"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"minScore"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.85</span><span class="w"> </span><span class="p">}],</span><span class="w">
        </span><span class="nl">"categories:best-practices"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"error"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"minScore"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.9</span><span class="w"> </span><span class="p">}],</span><span class="w">
        </span><span class="nl">"categories:seo"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"error"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"minScore"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.9</span><span class="w"> </span><span class="p">}]</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"upload"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"target"</span><span class="p">:</span><span class="w"> </span><span class="s2">"temporary-public-storage"</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>This config uses full URLs instead of a <code class="language-plaintext highlighter-rouge">staticDistDir</code> and runs 3 times per page for more stable averages. The assertions focus on the four category-level scores, keeping the checks simple.</p>

<p>The post-deploy job uses <code class="language-plaintext highlighter-rouge">continue-on-error: true</code>, so it reports results without blocking the deployment:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">lighthouse</span><span class="pi">:</span>
  <span class="na">needs</span><span class="pi">:</span> <span class="s">deploy</span>
  <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
  <span class="na">continue-on-error</span><span class="pi">:</span> <span class="no">true</span>
  <span class="na">steps</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Run Lighthouse CI</span>
      <span class="na">run</span><span class="pi">:</span> <span class="s">npx lhci autorun --config=lighthouserc.live.json</span>
</code></pre></div></div>

<p>The nightly cron runs the same config on a schedule (<code class="language-plaintext highlighter-rouge">0 3 * * *</code>), giving me a daily report I can review at my own pace.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Automating Lighthouse audits has made quality metrics a consistent part of my workflow instead of an afterthought. What matters most is having the numbers easily available and part of the process, so my recommendation is to start with soft limits that would allow you to continue posting and plan to gradually tighten them as the blog evolves. If you maintain a blog or static site, I think this setup is straightforward to adapt and well worth the initial effort.</p>

<p>Thanks for reading! If you found this post useful, consider <a href="https://www.buymeacoffee.com/diamantidis">buying me a coffee</a> to support the blog. For questions or comments, feel free to reach out on <a href="https://x.com/diamantidis_io">X</a>!</p>

<p>Until next time!</p>]]></content><author><name>Ioannis Diamantidis</name><email>diamantidis@outlook.com</email></author><category term="Lighthouse" /><category term="CI" /><category term="Jekyll" /><summary type="html"><![CDATA[How to automate Lighthouse CI with local smoke tests, PR guardrails, and production monitoring to catch regressions before they ship.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://diamantidis.github.io/assets/social/automated-lighthouse-ci-static-blog.png" /><media:content medium="image" url="https://diamantidis.github.io/assets/social/automated-lighthouse-ci-static-blog.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">How to Create Dynamic Quick Actions in iOS</title><link href="https://diamantidis.github.io/2026/05/10/how-to-create-dynamic-quick-actions-ios-swift" rel="alternate" type="text/html" title="How to Create Dynamic Quick Actions in iOS" /><published>2026-05-10T09:00:00+00:00</published><updated>2026-05-10T09:00:00+00:00</updated><id>https://diamantidis.github.io/2026/05/10/how-to-create-dynamic-quick-actions-ios-swift</id><content type="html" xml:base="https://diamantidis.github.io/2026/05/10/how-to-create-dynamic-quick-actions-ios-swift"><![CDATA[<p>Recently I was thinking about gathering user feedback, and I wanted to focus more on the segment of users who aren’t happy with the app, the ones who want to delete it. I tried to find a way to get some comments from those users and learn more about their experience. Then I had an aha moment: Duolingo’s fun quick action on hard press that says something like “You can’t hide from me?”. What if I added a quick action like that to my app? Even if one unhappy user gives me feedback, it’s a win.</p>

<p>So here’s how to set up dynamic quick actions in your own iOS app.</p>

<h2 id="static-vs-dynamic-quick-actions">Static vs Dynamic Quick Actions</h2>

<p>There are two ways to add quick actions: static and dynamic. Both appear when the user long-presses your app icon, but they are configured differently and serve different purposes.</p>

<p>Static quick actions are defined in your app’s <code class="language-plaintext highlighter-rouge">Info.plist</code>. They are loaded by the system automatically and remain the same as long as the app is installed. They are very easy to set up and require no code, but they cannot be changed at runtime, cannot adapt to user state, and are harder to reuse across multiple apps.</p>

<p>On the other hand, dynamic quick actions are created programmatically using <code class="language-plaintext highlighter-rouge">UIApplicationShortcutItem</code>. They are configured at runtime, which means your app can update them whenever necessary. This allows you to adapt actions based on user preferences, remote config, or app state, and makes it easy to reuse the same logic across multiple apps. And that’s exactly what we are going to focus on in this post.</p>

<h2 id="dynamic-quick-actions-with-uiapplicationshortcutitem">Dynamic Quick Actions with UIApplicationShortcutItem</h2>

<p>To create a dynamic quick action, instantiate <code class="language-plaintext highlighter-rouge">UIApplicationShortcutItem</code> and assign it to <code class="language-plaintext highlighter-rouge">UIApplication.shared.shortcutItems</code>:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">shortcut</span> <span class="o">=</span> <span class="kt">UIApplicationShortcutItem</span><span class="p">(</span>
            <span class="nv">type</span><span class="p">:</span> <span class="s">"com.feedback.action"</span><span class="p">,</span>
    <span class="nv">localizedTitle</span><span class="p">:</span> <span class="s">"Send Feedback"</span><span class="p">,</span>
    <span class="nv">localizedSubtitle</span><span class="p">:</span> <span class="s">"Help us improve"</span><span class="p">,</span>
    <span class="nv">icon</span><span class="p">:</span> <span class="kt">UIApplicationShortcutIcon</span><span class="p">(</span><span class="nv">systemImageName</span><span class="p">:</span> <span class="s">"envelope"</span><span class="p">),</span>
    <span class="nv">userInfo</span><span class="p">:</span> <span class="p">[</span>
        <span class="s">"email"</span><span class="p">:</span> <span class="s">"support@example.com"</span> <span class="k">as</span> <span class="kt">NSString</span><span class="p">,</span>
        <span class="s">"subject"</span><span class="p">:</span> <span class="s">"App Feedback"</span> <span class="k">as</span> <span class="kt">NSString</span>
    <span class="p">]</span>
<span class="p">)</span>

<span class="kt">UIApplication</span><span class="o">.</span><span class="n">shared</span><span class="o">.</span><span class="n">shortcutItems</span> <span class="o">=</span> <span class="p">[</span><span class="n">shortcut</span><span class="p">]</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">type</code> is a string you define to identify the action when it’s triggered. <code class="language-plaintext highlighter-rouge">localizedTitle</code> and <code class="language-plaintext highlighter-rouge">localizedSubtitle</code> are what the user sees. The <code class="language-plaintext highlighter-rouge">icon</code> uses an SF Symbol, and <code class="language-plaintext highlighter-rouge">userInfo</code> carries any extra data you need when handling the action.</p>

<h3 id="where-to-register-the-shortcut">Where to register the shortcut</h3>

<p>To register the shortcut when the app launches, use a <code class="language-plaintext highlighter-rouge">UIApplicationDelegateAdaptor</code> and set the shortcuts in <code class="language-plaintext highlighter-rouge">application(_:didFinishLaunchingWithOptions:)</code>:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="kt">AppDelegate</span><span class="p">:</span> <span class="kt">NSObject</span><span class="p">,</span> <span class="kt">UIApplicationDelegate</span> <span class="p">{</span>
    <span class="kd">func</span> <span class="nf">application</span><span class="p">(</span>
        <span class="n">_</span> <span class="nv">application</span><span class="p">:</span> <span class="kt">UIApplication</span><span class="p">,</span>
        <span class="n">didFinishLaunchingWithOptions</span> <span class="nv">launchOptions</span><span class="p">:</span> <span class="p">[</span><span class="kt">UIApplication</span><span class="o">.</span><span class="kt">LaunchOptionsKey</span><span class="p">:</span> <span class="kt">Any</span><span class="p">]?</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">shortcut</span> <span class="o">=</span> <span class="kt">UIApplicationShortcutItem</span><span class="p">(</span>
            <span class="nv">type</span><span class="p">:</span> <span class="s">"com.feedback.action"</span><span class="p">,</span>
            <span class="nv">localizedTitle</span><span class="p">:</span> <span class="s">"Send Feedback"</span><span class="p">,</span>
            <span class="nv">localizedSubtitle</span><span class="p">:</span> <span class="s">"Help us improve"</span><span class="p">,</span>
            <span class="nv">icon</span><span class="p">:</span> <span class="kt">UIApplicationShortcutIcon</span><span class="p">(</span><span class="nv">systemImageName</span><span class="p">:</span> <span class="s">"envelope"</span><span class="p">),</span>
            <span class="nv">userInfo</span><span class="p">:</span> <span class="p">[</span>
                <span class="s">"email"</span><span class="p">:</span> <span class="s">"support@example.com"</span> <span class="k">as</span> <span class="kt">NSString</span><span class="p">,</span>
                <span class="s">"subject"</span><span class="p">:</span> <span class="s">"App Feedback"</span> <span class="k">as</span> <span class="kt">NSString</span>
            <span class="p">]</span>
        <span class="p">)</span>
        <span class="n">application</span><span class="o">.</span><span class="n">shortcutItems</span> <span class="o">=</span> <span class="p">[</span><span class="n">shortcut</span><span class="p">]</span>
        <span class="k">return</span> <span class="kc">true</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">@main</span>
<span class="kd">struct</span> <span class="kt">MyApp</span><span class="p">:</span> <span class="kt">App</span> <span class="p">{</span>
    <span class="kd">@UIApplicationDelegateAdaptor</span><span class="p">(</span><span class="kt">AppDelegate</span><span class="o">.</span><span class="k">self</span><span class="p">)</span> <span class="k">var</span> <span class="nv">appDelegate</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">Scene</span> <span class="p">{</span>
        <span class="kt">WindowGroup</span> <span class="p">{</span>
            <span class="kt">ContentView</span><span class="p">()</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>If you build and run the app now, long-pressing the app icon will show the “Send Feedback” quick action. But if you tap it, the app will just launch as if you tapped the icon normally. That’s because we haven’t handled the quick action yet.</p>

<h2 id="handling-quick-actions">Handling Quick Actions</h2>

<p>There are two states to consider when handling a quick action tap: the app can be launched from a terminated state, or it can already be running. Each state is handled by a different method in the <code class="language-plaintext highlighter-rouge">SceneDelegate</code>.</p>

<p>First, let’s extract the logic for opening the mail client into a helper function so we can reuse it in both cases:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@discardableResult</span>
<span class="kd">func</span> <span class="nf">handleShortcut</span><span class="p">(</span><span class="n">_</span> <span class="nv">shortcutItem</span><span class="p">:</span> <span class="kt">UIApplicationShortcutItem</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Bool</span> <span class="p">{</span>
    <span class="k">guard</span> <span class="n">shortcutItem</span><span class="o">.</span><span class="n">type</span> <span class="o">==</span> <span class="s">"com.feedback.action"</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">false</span> <span class="p">}</span>

    <span class="k">guard</span> <span class="k">let</span> <span class="nv">userInfo</span> <span class="o">=</span> <span class="n">shortcutItem</span><span class="o">.</span><span class="n">userInfo</span><span class="p">,</span>
          <span class="k">let</span> <span class="nv">email</span> <span class="o">=</span> <span class="n">userInfo</span><span class="p">[</span><span class="s">"email"</span><span class="p">]</span> <span class="k">as?</span> <span class="kt">String</span><span class="p">,</span>
          <span class="k">let</span> <span class="nv">subject</span> <span class="o">=</span> <span class="n">userInfo</span><span class="p">[</span><span class="s">"subject"</span><span class="p">]</span> <span class="k">as?</span> <span class="kt">String</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">false</span> <span class="p">}</span>

    <span class="k">let</span> <span class="nv">mailtoURL</span> <span class="o">=</span> <span class="kt">URL</span><span class="p">(</span><span class="nv">string</span><span class="p">:</span> <span class="s">"mailto:</span><span class="se">\(</span><span class="n">email</span><span class="se">)</span><span class="s">?subject=</span><span class="se">\(</span><span class="n">subject</span><span class="o">.</span><span class="nf">addingPercentEncoding</span><span class="p">(</span><span class="nv">withAllowedCharacters</span><span class="p">:</span> <span class="o">.</span><span class="n">urlQueryAllowed</span><span class="p">)</span> <span class="p">??</span> <span class="n">subject</span><span class="se">)</span><span class="s">"</span><span class="p">)</span><span class="o">!</span>

    <span class="k">guard</span> <span class="kt">UIApplication</span><span class="o">.</span><span class="n">shared</span><span class="o">.</span><span class="nf">canOpenURL</span><span class="p">(</span><span class="n">mailtoURL</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">false</span> <span class="p">}</span>
    <span class="kt">UIApplication</span><span class="o">.</span><span class="n">shared</span><span class="o">.</span><span class="nf">open</span><span class="p">(</span><span class="n">mailtoURL</span><span class="p">)</span>
    <span class="k">return</span> <span class="kc">true</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now we can use this function from a <code class="language-plaintext highlighter-rouge">SceneDelegate</code>. For the first case, when the app is launched from a terminated state, we check <code class="language-plaintext highlighter-rouge">connectionOptions.shortcutItem</code> in <code class="language-plaintext highlighter-rouge">scene(_:willConnectTo:options:)</code>. For the second case, when the app is already running, we use <code class="language-plaintext highlighter-rouge">windowScene(_:performActionFor:completionHandler:)</code>. The final code looks like this:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">final</span> <span class="kd">class</span> <span class="kt">SceneDelegate</span><span class="p">:</span> <span class="kt">NSObject</span><span class="p">,</span> <span class="kt">UIWindowSceneDelegate</span> <span class="p">{</span>
    <span class="kd">func</span> <span class="nf">scene</span><span class="p">(</span>
        <span class="n">_</span> <span class="nv">scene</span><span class="p">:</span> <span class="kt">UIScene</span><span class="p">,</span>
        <span class="n">willConnectTo</span> <span class="nv">session</span><span class="p">:</span> <span class="kt">UISceneSession</span><span class="p">,</span>
        <span class="n">options</span> <span class="nv">connectionOptions</span><span class="p">:</span> <span class="kt">UIScene</span><span class="o">.</span><span class="kt">ConnectionOptions</span>
    <span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="k">let</span> <span class="nv">shortcutItem</span> <span class="o">=</span> <span class="n">connectionOptions</span><span class="o">.</span><span class="n">shortcutItem</span> <span class="p">{</span>
            <span class="nf">handleShortcut</span><span class="p">(</span><span class="n">shortcutItem</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="kd">func</span> <span class="nf">windowScene</span><span class="p">(</span>
        <span class="n">_</span> <span class="nv">windowScene</span><span class="p">:</span> <span class="kt">UIWindowScene</span><span class="p">,</span>
        <span class="n">performActionFor</span> <span class="nv">shortcutItem</span><span class="p">:</span> <span class="kt">UIApplicationShortcutItem</span><span class="p">,</span>
        <span class="nv">completionHandler</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="kt">Bool</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span>
    <span class="p">)</span> <span class="p">{</span>
        <span class="nf">completionHandler</span><span class="p">(</span><span class="nf">handleShortcut</span><span class="p">(</span><span class="n">shortcutItem</span><span class="p">))</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And that’s it! Now if you try to press the quick action again, the default mail client of your iPhone will be launched.</p>

<h2 id="conclusion">Conclusion</h2>

<p>To sum up, dynamic quick actions give you the flexibility that static <code class="language-plaintext highlighter-rouge">Info.plist</code> entries can’t, without much more effort! In this post, we used them to ask for feedback from unhappy users right before they uninstall the app, but the same approach works for any action you want to surface on the home screen. And the best part is that setting up the shortcut and the handlers is just plain Swift, which means we can export it into a Swift Package and reuse it across multiple apps. Thanks for reading! If you found this post useful, consider <a href="https://www.buymeacoffee.com/diamantidis">buying me a coffee</a> to support the blog. For questions or comments, feel free to reach out on <a href="https://x.com/diamantidis_io">X</a>!</p>

<p>Until next time!</p>]]></content><author><name>Ioannis Diamantidis</name><email>diamantidis@outlook.com</email></author><category term="Swift" /><category term="SwiftUI" /><category term="iOS" /><summary type="html"><![CDATA[Learn how to create dynamic home screen quick actions in iOS using UIApplicationShortcutItem - with a practical example of adding a feedback shortcut that opens the mail client when users long-press your app icon]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://diamantidis.github.io/assets/social/dynamic-quick-actions.png" /><media:content medium="image" url="https://diamantidis.github.io/assets/social/dynamic-quick-actions.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Improve In App Video Guides with Picture-in-Picture in SwiftUI</title><link href="https://diamantidis.github.io/2026/04/18/improve-in-app-guides-with-picture-in-picture" rel="alternate" type="text/html" title="Improve In App Video Guides with Picture-in-Picture in SwiftUI" /><published>2026-04-18T09:00:00+00:00</published><updated>2026-04-18T09:00:00+00:00</updated><id>https://diamantidis.github.io/2026/04/18/improve-in-app-guides-with-picture-in-picture</id><content type="html" xml:base="https://diamantidis.github.io/2026/04/18/improve-in-app-guides-with-picture-in-picture"><![CDATA[<p>When we build features like widgets or Safari extensions in our app, short video guides can help our users follow the setup steps that must be performed outside of the app.</p>

<h3 id="the-swiftui-videoplayer">The SwiftUI VideoPlayer</h3>
<p>For this kind of content, SwiftUI provides a built-in <code class="language-plaintext highlighter-rouge">VideoPlayer</code> that makes it straightforward to play videos. With just a few lines of code, we can add a fully functional video player in a SwiftUI view.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">SwiftUI</span>
<span class="kd">import</span> <span class="kt">AVKit</span>

<span class="kd">struct</span> <span class="kt">TutorialVideoView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">player</span><span class="p">:</span> <span class="kt">AVPlayer</span> <span class="o">=</span> <span class="p">{</span>
        <span class="k">guard</span> <span class="k">let</span> <span class="nv">url</span> <span class="o">=</span> <span class="kt">Bundle</span><span class="o">.</span><span class="n">module</span><span class="o">.</span><span class="nf">url</span><span class="p">(</span><span class="nv">forResource</span><span class="p">:</span> <span class="s">"widget"</span><span class="p">,</span> <span class="nv">withExtension</span><span class="p">:</span> <span class="s">"mp4"</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span>
            <span class="nf">fatalError</span><span class="p">(</span><span class="s">"widget.mp4 not found"</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="kt">AVPlayer</span><span class="p">(</span><span class="nv">url</span><span class="p">:</span> <span class="n">url</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">VideoPlayer</span><span class="p">(</span><span class="nv">player</span><span class="p">:</span> <span class="n">player</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">height</span><span class="p">:</span> <span class="mi">200</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This works well for basic playback, but it falls short for guided onboarding flows. As soon as users leave the app to follow the instructions, the video goes into the background as well. That forces them to return to the app, resume the video, watch the next step, and repeat the same cycle.</p>

<p>Thankfully, there is a solution to this problem: Picture-In-Picture.</p>

<h2 id="the-solution-avplayerviewcontroller-with-picture-in-picture">The Solution: AVPlayerViewController with Picture-in-Picture</h2>

<p>To enable Picture-in-Picture (PiP) support, we’ll use an <code class="language-plaintext highlighter-rouge">AVPlayerViewController</code>, and we’ll wrap it in a <code class="language-plaintext highlighter-rouge">UIViewControllerRepresentable</code> so that we can use it from SwiftUI.</p>

<p>Here is the implementation:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">SwiftUI</span>
<span class="kd">import</span> <span class="kt">AVKit</span>

<span class="kd">struct</span> <span class="kt">PiPVideoPlayer</span><span class="p">:</span> <span class="kt">UIViewControllerRepresentable</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">player</span><span class="p">:</span> <span class="kt">AVPlayer</span>

    <span class="kd">func</span> <span class="nf">makeUIViewController</span><span class="p">(</span><span class="nv">context</span><span class="p">:</span> <span class="kt">Context</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">AVPlayerViewController</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">controller</span> <span class="o">=</span> <span class="kt">AVPlayerViewController</span><span class="p">()</span>
        <span class="n">controller</span><span class="o">.</span><span class="n">player</span> <span class="o">=</span> <span class="n">player</span>
        <span class="n">controller</span><span class="o">.</span><span class="n">allowsPictureInPicturePlayback</span> <span class="o">=</span> <span class="kc">true</span>
        <span class="n">controller</span><span class="o">.</span><span class="n">canStartPictureInPictureAutomaticallyFromInline</span> <span class="o">=</span> <span class="kc">true</span>
        <span class="k">return</span> <span class="n">controller</span>
    <span class="p">}</span>

    <span class="kd">func</span> <span class="nf">updateUIViewController</span><span class="p">(</span><span class="n">_</span> <span class="nv">uiViewController</span><span class="p">:</span> <span class="kt">AVPlayerViewController</span><span class="p">,</span> <span class="nv">context</span><span class="p">:</span> <span class="kt">Context</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">uiViewController</span><span class="o">.</span><span class="n">player</span> <span class="o">=</span> <span class="n">player</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>In this snippet, we configure two key properties on the <code class="language-plaintext highlighter-rouge">AVPlayerViewController</code>:</p>

<ul>
  <li><strong>allowsPictureInPicturePlayback</strong>: Enables PiP support on the player. Without this, the PiP button won’t appear at all.</li>
  <li><strong>canStartPictureInPictureAutomaticallyFromInline</strong>: Allows PiP to activate automatically when the user navigates away from the app while the video is playing inline (not fullscreen). This is key for our use case — users don’t need to manually enable PiP.</li>
</ul>

<p>Then in our view, we create the player and pass it in:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">TutorialVideoView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">player</span><span class="p">:</span> <span class="kt">AVPlayer</span> <span class="o">=</span> <span class="p">{</span>
        <span class="k">guard</span> <span class="k">let</span> <span class="nv">url</span> <span class="o">=</span> <span class="kt">Bundle</span><span class="o">.</span><span class="n">module</span><span class="o">.</span><span class="nf">url</span><span class="p">(</span><span class="nv">forResource</span><span class="p">:</span> <span class="s">"widget"</span><span class="p">,</span> <span class="nv">withExtension</span><span class="p">:</span> <span class="s">"mp4"</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span>
            <span class="nf">fatalError</span><span class="p">(</span><span class="s">"widget.mp4 not found"</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="kt">AVPlayer</span><span class="p">(</span><span class="nv">url</span><span class="p">:</span> <span class="n">url</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">PiPVideoPlayer</span><span class="p">(</span><span class="nv">player</span><span class="p">:</span> <span class="n">player</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">height</span><span class="p">:</span> <span class="mi">200</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>With this implementation, the system provides a PiP button in the playback controls. Once activated, users can continue watching the video while navigating to system settings or other apps.</p>

<h3 id="prerequisites">Prerequisites</h3>

<p>Before using this implementation, ensure your project meets these requirements:</p>

<ol>
  <li><strong>Minimum iOS version</strong>: iOS 15 or later</li>
  <li><strong>Background Modes</strong>: Enable “Audio, AirPlay, and Picture in Picture” in your app’s capabilities.</li>
</ol>

<h2 id="tracking-picture-in-picture-events">Tracking Picture-in-Picture Events</h2>

<p>To take it a step further, you can implement the delegate methods provided by <code class="language-plaintext highlighter-rouge">AVPlayerViewController</code> to understand how users interact with your onboarding videos. Just create a coordinator that conforms to <code class="language-plaintext highlighter-rouge">AVPlayerViewControllerDelegate</code>:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">AVKit</span>
<span class="kd">import</span> <span class="n">os</span><span class="o">.</span><span class="n">log</span>

<span class="kd">class</span> <span class="kt">VideoPlayerCoordinator</span><span class="p">:</span> <span class="kt">NSObject</span><span class="p">,</span> <span class="kt">AVPlayerViewControllerDelegate</span> <span class="p">{</span>
    <span class="kd">private</span> <span class="k">let</span> <span class="nv">logger</span> <span class="o">=</span> <span class="kt">Logger</span><span class="p">(</span><span class="nv">subsystem</span><span class="p">:</span> <span class="s">"com.yourapp.video"</span><span class="p">,</span> <span class="nv">category</span><span class="p">:</span> <span class="s">"PiP"</span><span class="p">)</span>

    <span class="kd">func</span> <span class="nf">playerViewControllerWillStartPictureInPicture</span><span class="p">(</span><span class="n">_</span> <span class="nv">playerViewController</span><span class="p">:</span> <span class="kt">AVPlayerViewController</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">logger</span><span class="o">.</span><span class="nf">info</span><span class="p">(</span><span class="s">"PiP will start"</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="kd">func</span> <span class="nf">playerViewControllerDidStartPictureInPicture</span><span class="p">(</span><span class="n">_</span> <span class="nv">playerViewController</span><span class="p">:</span> <span class="kt">AVPlayerViewController</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">logger</span><span class="o">.</span><span class="nf">info</span><span class="p">(</span><span class="s">"PiP started"</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="kd">func</span> <span class="nf">playerViewController</span><span class="p">(</span><span class="n">_</span> <span class="nv">playerViewController</span><span class="p">:</span> <span class="kt">AVPlayerViewController</span><span class="p">,</span> <span class="n">failedToStartPictureInPictureWithError</span> <span class="nv">error</span><span class="p">:</span> <span class="kt">Error</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">logger</span><span class="o">.</span><span class="nf">error</span><span class="p">(</span><span class="s">"PiP failed to start: </span><span class="se">\(</span><span class="n">error</span><span class="o">.</span><span class="n">localizedDescription</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="kd">func</span> <span class="nf">playerViewControllerWillStopPictureInPicture</span><span class="p">(</span><span class="n">_</span> <span class="nv">playerViewController</span><span class="p">:</span> <span class="kt">AVPlayerViewController</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">logger</span><span class="o">.</span><span class="nf">info</span><span class="p">(</span><span class="s">"PiP will stop"</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="kd">func</span> <span class="nf">playerViewControllerDidStopPictureInPicture</span><span class="p">(</span><span class="n">_</span> <span class="nv">playerViewController</span><span class="p">:</span> <span class="kt">AVPlayerViewController</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">logger</span><span class="o">.</span><span class="nf">info</span><span class="p">(</span><span class="s">"PiP stopped"</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="kd">func</span> <span class="nf">playerViewController</span><span class="p">(</span><span class="n">_</span> <span class="nv">playerViewController</span><span class="p">:</span> <span class="kt">AVPlayerViewController</span><span class="p">,</span> <span class="n">restoreUserInterfaceForPictureInPictureStopWithCompletionHandler</span> <span class="nv">completionHandler</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="kt">Bool</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">logger</span><span class="o">.</span><span class="nf">info</span><span class="p">(</span><span class="s">"Restoring UI for PiP stop"</span><span class="p">)</span>
        <span class="nf">completionHandler</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>To use this coordinator, update the <code class="language-plaintext highlighter-rouge">PiPVideoPlayer</code> to include it:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">PiPVideoPlayer</span><span class="p">:</span> <span class="kt">UIViewControllerRepresentable</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">player</span><span class="p">:</span> <span class="kt">AVPlayer</span>

    <span class="kd">func</span> <span class="nf">makeCoordinator</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">VideoPlayerCoordinator</span> <span class="p">{</span>
        <span class="kt">VideoPlayerCoordinator</span><span class="p">()</span>
    <span class="p">}</span>

    <span class="kd">func</span> <span class="nf">makeUIViewController</span><span class="p">(</span><span class="nv">context</span><span class="p">:</span> <span class="kt">Context</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">AVPlayerViewController</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">controller</span> <span class="o">=</span> <span class="kt">AVPlayerViewController</span><span class="p">()</span>
        <span class="n">controller</span><span class="o">.</span><span class="n">player</span> <span class="o">=</span> <span class="n">player</span>
        <span class="n">controller</span><span class="o">.</span><span class="n">delegate</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">coordinator</span>
        <span class="n">controller</span><span class="o">.</span><span class="n">allowsPictureInPicturePlayback</span> <span class="o">=</span> <span class="kc">true</span>
        <span class="n">controller</span><span class="o">.</span><span class="n">canStartPictureInPictureAutomaticallyFromInline</span> <span class="o">=</span> <span class="kc">true</span>
        <span class="k">return</span> <span class="n">controller</span>
    <span class="p">}</span>

    <span class="kd">func</span> <span class="nf">updateUIViewController</span><span class="p">(</span><span class="n">_</span> <span class="nv">uiViewController</span><span class="p">:</span> <span class="kt">AVPlayerViewController</span><span class="p">,</span> <span class="nv">context</span><span class="p">:</span> <span class="kt">Context</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">uiViewController</span><span class="o">.</span><span class="n">player</span> <span class="o">=</span> <span class="n">player</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>With these delegate methods in place, you can now track when users start or stop PiP and identify failures.</p>

<h2 id="conclusion">Conclusion</h2>

<p>To sum up, the built-in SwiftUI <code class="language-plaintext highlighter-rouge">VideoPlayer</code> is convenient for simple use cases, but it’s insufficient for onboarding flows where users must leave the app to complete setup. With Picture-in-Picture, they can keep the tutorial visible while performing the actual setup steps, resulting in a much smoother onboarding experience.</p>

<p>Thanks for reading! If you found this post useful, consider <a href="https://www.buymeacoffee.com/diamantidis">buying me a coffee</a> to support the blog. For questions or comments, feel free to reach out on <a href="https://x.com/diamantidis_io">X</a>!</p>

<p>Until next time!</p>]]></content><author><name>Ioannis Diamantidis</name><email>diamantidis@outlook.com</email></author><category term="Swift" /><category term="SwiftUI" /><category term="iOS" /><category term="AVKit" /><category term="AVFoundation" /><summary type="html"><![CDATA[A post exploring how to add Picture-in-Picture support to video players in SwiftUI for better user experience in onboarding flows]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://diamantidis.github.io/assets/social/pip-swiftui.png" /><media:content medium="image" url="https://diamantidis.github.io/assets/social/pip-swiftui.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Flutter: Two-way communication between a WebView and a web page</title><link href="https://diamantidis.github.io/2021/10/17/two-way-communication-between-flutter-webview-and-a-web-page" rel="alternate" type="text/html" title="Flutter: Two-way communication between a WebView and a web page" /><published>2021-10-17T08:00:00+00:00</published><updated>2021-10-17T08:00:00+00:00</updated><id>https://diamantidis.github.io/2021/10/17/two-way-communication-between-flutter-webview-and-a-web-page</id><content type="html" xml:base="https://diamantidis.github.io/2021/10/17/two-way-communication-between-flutter-webview-and-a-web-page"><![CDATA[<p>Displaying web content in an app is quite easy and straightforward with WebViews but what happens when we want to open a communication channel between the app and the web page we load on a WebView?</p>

<p>Let’s say, for example, you want to “listen” to a button click event happening on the web page and take some action in the app. Or you want to update a label of a web page with some data from the app.</p>

<p>In this post, I will show you how you can communicate between the app and the web page of a WebView.</p>

<p>In order to do so, I am going to use an example to demonstrate how to achieve this communication in both directions; both from the web page to the app and from the app to the web page.</p>

<p>Let’s get started!</p>

<blockquote>
  <p>This post is based on <code class="language-plaintext highlighter-rouge">Flutter 2.5.2</code> and <code class="language-plaintext highlighter-rouge">Dart SDK 2.14.3</code></p>
</blockquote>

<h2 id="implementation">Implementation</h2>

<h3 id="the-package">The package</h3>

<p>Flutter doesn’t have built in support for WebViews.
As a result, we are going to use a package with name <a href="https://pub.dev/packages/webview_flutter">webview_flutter</a>.</p>

<p>So first thing first is to edit <code class="language-plaintext highlighter-rouge">pubspec.yaml</code> and add the dependency:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">dependencies</span><span class="pi">:</span>
  <span class="na">flutter</span><span class="pi">:</span>
    <span class="na">sdk</span><span class="pi">:</span> <span class="s">flutter</span>
  <span class="na">webview_flutter</span><span class="pi">:</span> <span class="s">^2.1.1</span>
</code></pre></div></div>

<h3 id="the-html-file">The HTML file</h3>

<p>Next, we are going to create the web page that we will later load in the WebView.</p>

<p>For the sake of this article and for ease, we are going to create an <code class="language-plaintext highlighter-rouge">index.html</code> file which we are going to load in the webview instead of loading a webpage from a URL.</p>

<p>Let’s create an HTML file named <code class="language-plaintext highlighter-rouge">index.html</code> inside the <code class="language-plaintext highlighter-rouge">assets</code> folder (if the folder doesn’t exist, feel free to create one).</p>

<p>Then, we will open this file and add the following content:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;!DOCTYPE html&gt;</span>
<span class="nt">&lt;html&gt;</span>
    <span class="nt">&lt;head&gt;</span>
        <span class="nt">&lt;meta</span> <span class="na">name=</span><span class="s">"viewport"</span> <span class="na">content=</span><span class="s">"width=device-width, initial-scale=1"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;style&gt;</span>
            <span class="nc">.switch</span> <span class="p">{</span> <span class="nl">position</span><span class="p">:</span> <span class="nb">relative</span><span class="p">;</span> <span class="nl">display</span><span class="p">:</span> <span class="n">inline-block</span><span class="p">;</span> <span class="nl">width</span><span class="p">:</span> <span class="m">60px</span><span class="p">;</span> <span class="nl">height</span><span class="p">:</span> <span class="m">34px</span><span class="p">;</span> <span class="p">}</span>
            <span class="nc">.switch</span> <span class="nt">input</span> <span class="p">{</span> <span class="nl">opacity</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span> <span class="nl">width</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span> <span class="nl">height</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span> <span class="p">}</span>
            <span class="nt">label</span><span class="nc">.switch</span> <span class="p">{</span> <span class="nl">outline</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span> <span class="p">}</span>
            <span class="nc">.slider</span> <span class="p">{</span> <span class="nl">position</span><span class="p">:</span> <span class="nb">absolute</span><span class="p">;</span> <span class="nl">cursor</span><span class="p">:</span> <span class="nb">pointer</span><span class="p">;</span> <span class="nl">top</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span> <span class="nl">left</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span> <span class="nl">right</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span> <span class="nl">bottom</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span> <span class="nl">background-color</span><span class="p">:</span> <span class="m">#ccc</span><span class="p">;</span> <span class="nl">-webkit-transition</span><span class="p">:</span> <span class="m">.4s</span><span class="p">;</span> <span class="nl">transition</span><span class="p">:</span> <span class="m">.4s</span><span class="p">;</span> <span class="nl">outline</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span> <span class="p">}</span>
            <span class="nc">.slider</span><span class="nd">:before</span> <span class="p">{</span> <span class="nl">position</span><span class="p">:</span> <span class="nb">absolute</span><span class="p">;</span> <span class="nl">content</span><span class="p">:</span> <span class="s1">""</span><span class="p">;</span> <span class="nl">height</span><span class="p">:</span> <span class="m">26px</span><span class="p">;</span> <span class="nl">width</span><span class="p">:</span> <span class="m">26px</span><span class="p">;</span> <span class="nl">left</span><span class="p">:</span> <span class="m">4px</span><span class="p">;</span> <span class="nl">bottom</span><span class="p">:</span> <span class="m">4px</span><span class="p">;</span> <span class="nl">background-color</span><span class="p">:</span> <span class="no">white</span><span class="p">;</span> <span class="nl">-webkit-transition</span><span class="p">:</span> <span class="m">.4s</span><span class="p">;</span> <span class="nl">transition</span><span class="p">:</span> <span class="m">.4s</span><span class="p">;</span> <span class="p">}</span>
            <span class="nt">input</span><span class="nd">:checked</span> <span class="o">+</span> <span class="nc">.slider</span> <span class="p">{</span> <span class="nl">background-color</span><span class="p">:</span> <span class="m">#2196F3</span><span class="p">;</span> <span class="p">}</span>
            <span class="nt">input</span><span class="nd">:focus</span> <span class="o">+</span> <span class="nc">.slider</span> <span class="p">{</span> <span class="nl">box-shadow</span><span class="p">:</span> <span class="m">0</span> <span class="m">0</span> <span class="m">1px</span> <span class="m">#2196F3</span><span class="p">;</span> <span class="p">}</span>
            <span class="nt">input</span><span class="nd">:checked</span> <span class="o">+</span> <span class="nc">.slider</span><span class="nd">:before</span> <span class="p">{</span> <span class="nl">-webkit-transform</span><span class="p">:</span> <span class="n">translateX</span><span class="p">(</span><span class="m">26px</span><span class="p">);</span> <span class="nl">-ms-transform</span><span class="p">:</span> <span class="n">translateX</span><span class="p">(</span><span class="m">26px</span><span class="p">);</span> <span class="nl">transform</span><span class="p">:</span> <span class="n">translateX</span><span class="p">(</span><span class="m">26px</span><span class="p">);</span> <span class="p">}</span>
            <span class="nc">.slider.round</span> <span class="p">{</span> <span class="nl">border-radius</span><span class="p">:</span> <span class="m">34px</span><span class="p">;</span> <span class="p">}</span>
            <span class="nc">.slider.round</span><span class="nd">:before</span> <span class="p">{</span> <span class="nl">border-radius</span><span class="p">:</span> <span class="m">50%</span><span class="p">;</span> <span class="p">}</span>
        <span class="nt">&lt;/style&gt;</span>
    <span class="nt">&lt;/head&gt;</span>
    <span class="nt">&lt;body&gt;</span>
        <span class="nt">&lt;h2</span> <span class="na">id=</span><span class="s">"value"</span><span class="nt">&gt;</span>Toggle Switch is off<span class="nt">&lt;/h2&gt;</span>
        <span class="nt">&lt;label</span> <span class="na">class=</span><span class="s">"switch"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;input</span> <span class="na">type=</span><span class="s">"checkbox"</span> <span class="na">name=</span><span class="s">"myCheckbox"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;span</span> <span class="na">class=</span><span class="s">"slider round"</span><span class="nt">&gt;&lt;/span&gt;</span>
        <span class="nt">&lt;/label&gt;</span>
    <span class="nt">&lt;/body&gt;</span>
<span class="nt">&lt;/html&gt;</span>
</code></pre></div></div>

<blockquote>
  <p>For brevity reasons, the CSS part is minimized.</p>
</blockquote>

<p>This HTML file produces a web page with a label and a toggle switch. Our goal for this article is to listen to the toggle events from the app and update the text on the label to represent the corresponding state of the toggle switch.</p>

<p>To be able to access this <code class="language-plaintext highlighter-rouge">html</code> file from our application, we have to edit <code class="language-plaintext highlighter-rouge">pubspec.yaml</code> and add the following under the <code class="language-plaintext highlighter-rouge">flutter</code> section:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">flutter</span><span class="pi">:</span>

  <span class="na">assets</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">assets/index.html</span>
</code></pre></div></div>

<h3 id="the-app">The app</h3>

<p>Now, with all the preparations done, we can focus on the app!</p>

<p>First of all, we will import the package:</p>
<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="s">'package:webview_flutter/webview_flutter.dart'</span><span class="o">;</span>
</code></pre></div></div>

<p>Then, we can create our webview like this:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">WebViewController</span><span class="o">?</span> <span class="n">_webViewController</span><span class="p">;</span>

<span class="n">Widget</span> <span class="nf">buildWebView</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">WebView</span><span class="p">(</span>
        <span class="nl">initialUrl:</span> <span class="s">'about:blank'</span><span class="p">,</span>
        <span class="nl">javascriptMode:</span> <span class="n">JavascriptMode</span><span class="o">.</span><span class="na">unrestricted</span><span class="p">,</span>
        <span class="nl">onWebViewCreated:</span> <span class="p">(</span><span class="n">WebViewController</span> <span class="n">webViewController</span><span class="p">)</span> <span class="kd">async</span> <span class="p">{</span>
            <span class="n">_webViewController</span> <span class="o">=</span> <span class="n">webViewController</span><span class="p">;</span>
            <span class="kt">String</span> <span class="n">fileContent</span> <span class="o">=</span> <span class="k">await</span> <span class="n">rootBundle</span><span class="o">.</span><span class="na">loadString</span><span class="p">(</span><span class="s">'assets/index.html'</span><span class="p">);</span>
            <span class="n">_webViewController</span><span class="o">?.</span><span class="na">loadUrl</span><span class="p">(</span><span class="kt">Uri</span><span class="o">.</span><span class="na">dataFromString</span><span class="p">(</span><span class="n">fileContent</span><span class="p">,</span> <span class="nl">mimeType:</span> <span class="s">'text/html'</span><span class="p">,</span> <span class="nl">encoding:</span> <span class="n">Encoding</span><span class="o">.</span><span class="na">getByName</span><span class="p">(</span><span class="s">'utf-8'</span><span class="p">))</span><span class="o">.</span><span class="na">toString</span><span class="p">());</span>
        <span class="p">},</span>
    <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>In this snippet, we added a function that will return a new <code class="language-plaintext highlighter-rouge">WebView</code> and we pass some parameters.</p>

<p>Firstly, we will use <code class="language-plaintext highlighter-rouge">about:black</code> as the initial URL to load an empty page.
Then, we will set <code class="language-plaintext highlighter-rouge">javascriptMode</code> to <code class="language-plaintext highlighter-rouge">unrestricted</code> to allow the execution of Javascript code, and lastly, when the web view is created, we will load the content of the <code class="language-plaintext highlighter-rouge">index.html</code> and load it to the web view.</p>

<p>The only thing left is to call this function from the <code class="language-plaintext highlighter-rouge">Widget build(BuildContext context)</code>, add the required imports and run the app. If you do so, you will be able to see the page with the label and the toggle!</p>

<p><img src="https://diamantidis.github.io/assets/fl_webview/toggle_off.jpeg" alt="WKWebView with a toggle screenshot" loading="lazy" width="320" height="692" /></p>

<h2 id="adding-a-message-handler">Adding a message handler</h2>

<p>The next step is to enable our <code class="language-plaintext highlighter-rouge">WebView</code> to receive messages from the web page. 
For this, we will update the <code class="language-plaintext highlighter-rouge">WebView</code> we created before and add the <code class="language-plaintext highlighter-rouge">javascriptChannels:</code> parameter, like in the following snippet:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Widget</span> <span class="nf">buildWebView</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">WebView</span><span class="p">(</span>
        <span class="p">.</span>
        <span class="p">.</span>
        <span class="p">.</span>
        <span class="nl">javascriptChannels:</span> <span class="p">&lt;</span><span class="n">JavascriptChannel</span><span class="p">&gt;{</span>
            <span class="n">JavascriptChannel</span><span class="p">(</span>
                <span class="nl">name:</span> <span class="s">'messageHandler'</span><span class="p">,</span>
                <span class="nl">onMessageReceived:</span> <span class="p">(</span><span class="n">JavascriptMessage</span> <span class="n">message</span><span class="p">)</span> <span class="p">{</span>
                    <span class="n">print</span><span class="p">(</span><span class="s">"message from the web view=</span><span class="se">\"</span><span class="si">${message.message}</span><span class="se">\"</span><span class="s">"</span><span class="p">);</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>Here, we create a new <code class="language-plaintext highlighter-rouge">JavascriptChannel</code> with name <code class="language-plaintext highlighter-rouge">messageHandler</code> and provide a callback for when a message is received. For now, we just print the message.</p>

<p>We are now ready to send our messages from the web page.</p>

<p>Let’s see how!</p>

<h3 id="sending-message-from-the-webpage">Sending message from the webpage</h3>

<p>For this part, we are going to use some <code class="language-plaintext highlighter-rouge">JavaScript</code> to listen to the toggle events and then send a message to the app. This message will be different if the toggle is selected or not. Let’s edit our HTML file and before the closing <code class="language-plaintext highlighter-rouge">&lt;/body&gt;</code> tag, add the following content:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script </span><span class="na">type=</span><span class="s">"text/javascript"</span><span class="nt">&gt;</span>
    <span class="kd">var</span> <span class="nx">_selector</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">input[name=myCheckbox]</span><span class="dl">'</span><span class="p">);</span>
    <span class="nx">_selector</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">change</span><span class="dl">'</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">var</span> <span class="nx">message</span> <span class="o">=</span> <span class="p">(</span><span class="nx">_selector</span><span class="p">.</span><span class="nx">checked</span><span class="p">)</span> <span class="p">?</span> <span class="dl">"</span><span class="s2">Toggle Switch is on</span><span class="dl">"</span> <span class="p">:</span> <span class="dl">"</span><span class="s2">Toggle Switch is off</span><span class="dl">"</span><span class="p">;</span>

        <span class="k">if</span> <span class="p">(</span><span class="nx">messageHandler</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">messageHandler</span><span class="p">.</span><span class="nx">postMessage</span><span class="p">(</span><span class="nx">message</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="p">});</span>
<span class="nt">&lt;/script&gt;</span>
</code></pre></div></div>

<p>In this script, we are adding an <code class="language-plaintext highlighter-rouge">eventListener</code> for the toggle change event, and depending on whether it is selected or not, we are using the <code class="language-plaintext highlighter-rouge">messageHandler</code> that we created previously, to call the function <code class="language-plaintext highlighter-rouge">postMessage</code>, which in turn will trigger the <code class="language-plaintext highlighter-rouge">onMessageReceived</code> of the <code class="language-plaintext highlighter-rouge">JavascriptChannel</code> with the name <code class="language-plaintext highlighter-rouge">messageHandler</code>.</p>

<p>If you run the app and turn the toggle on and off, you will be able to see some messages on the console like <code class="language-plaintext highlighter-rouge">message from the web view="Toggle Switch is on"</code>.</p>

<p>The last thing to achieve our goal is to update the label of the web page when we change the value of the toggle.</p>

<h3 id="evaluating-javascript-to-update-webpages-ui">Evaluating JavaScript to update webpage’s UI</h3>

<p><code class="language-plaintext highlighter-rouge">WebViewController</code> has a function with name <code class="language-plaintext highlighter-rouge">evaluateJavascript</code> and this is what we are going to use to update the UI.</p>

<p>Head over to the definition of the <code class="language-plaintext highlighter-rouge">JavascriptChannel</code> and instead of the <code class="language-plaintext highlighter-rouge">print</code> command on the <code class="language-plaintext highlighter-rouge">onMessageReceived</code> callback, add the following snippet:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">final</span> <span class="n">script</span> <span class="o">=</span> <span class="s">"document.getElementById('value').innerText=</span><span class="se">\"</span><span class="si">${message.message}</span><span class="se">\"</span><span class="s">"</span><span class="p">;</span>
<span class="n">_webViewController</span><span class="o">?.</span><span class="na">evaluateJavascript</span><span class="p">(</span><span class="n">script</span><span class="p">);</span>
</code></pre></div></div>

<p>In this snippet, we are creating a variable with the JavaScript code to change the label on the web page, and then use the <code class="language-plaintext highlighter-rouge">evaluateJavaScript</code> to execute this code.</p>

<p>And that’s about it, if you now run the app, the text of the label will change when the toggle is switched on or off.</p>

<video autoplay="" loop="" muted="" playsinline="" width="320" height="692">
  <source src="https://diamantidis.github.io/assets/fl_webview/toggle_animation.mp4" type="video/mp4" />
  <img src="https://diamantidis.github.io/assets/fl_webview/toggle_animation.gif" alt="WebView with toggle on and off screenshot" loading="lazy" width="320" height="692" />
</video>

<h2 id="conclusion">Conclusion</h2>

<p>In this post, we have seen how to achieve bidirectional communication between a WebView and a web page. Using this, we can use events from the website to trigger actions on the app and similarly adjust the content of the loaded website based on the information we have on the app.</p>

<p>Thanks for reading! If you found this post useful, consider <a href="https://www.buymeacoffee.com/diamantidis">buying me a coffee</a> to support the blog. For questions or comments, feel free to reach out on <a href="https://x.com/diamantidis_io">X</a>!</p>

<p>Until next time!</p>]]></content><author><name>Ioannis Diamantidis</name><email>diamantidis@outlook.com</email></author><category term="Flutter" /><summary type="html"><![CDATA[A post about how to communicate between a WebView in Flutter and the website]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://diamantidis.github.io/assets/social/flutter_webview.png" /><media:content medium="image" url="https://diamantidis.github.io/assets/social/flutter_webview.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Exploring Flutter’s Scrollable.ensureVisible</title><link href="https://diamantidis.github.io/2021/10/10/exploring-flutter-scrollable-ensurevisible" rel="alternate" type="text/html" title="Exploring Flutter’s Scrollable.ensureVisible" /><published>2021-10-10T04:00:00+00:00</published><updated>2021-10-10T04:00:00+00:00</updated><id>https://diamantidis.github.io/2021/10/10/exploring-flutter-scrollable-ensurevisible</id><content type="html" xml:base="https://diamantidis.github.io/2021/10/10/exploring-flutter-scrollable-ensurevisible"><![CDATA[<p>Have you ever had to build an app with a scroll view of multiple widgets with different heights, and you wanted to add a link so that the user can automatically scroll from one widget to another? One such example could be the table of contents for an article or a menu.</p>

<p>In this post, I will showcase how we can implement this and in order to do so, I am going to use a screen with a few sections and a table of contents at the top of the screen with links to the corresponding sections.</p>

<p>Let’s see how!</p>

<blockquote>
  <p>This post is based on <code class="language-plaintext highlighter-rouge">Flutter 2.5.2</code> and <code class="language-plaintext highlighter-rouge">Dart SDK 2.14.3</code></p>
</blockquote>

<h2 id="solution">Solution</h2>

<p>To do so, we are going to use <a href="https://api.flutter.dev/flutter/widgets/Scrollable/ensureVisible.html"><code class="language-plaintext highlighter-rouge">Scrollable.ensureVisible</code></a>  within a <code class="language-plaintext highlighter-rouge">SingleChildScrollView</code> widget with a <code class="language-plaintext highlighter-rouge">Column</code> child.</p>

<p>Briefly, we will create a <code class="language-plaintext highlighter-rouge">GlobalKey()</code> for each section. Then we will use this key as the key of the widget that the link will be targeting. Finally, we are going to use this key when we press the link from the table of contents like in the following snippet:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">final</span> <span class="n">targetContext</span> <span class="o">=</span> <span class="n">targetKey</span><span class="o">.</span><span class="na">currentContext</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="n">targetContext</span> <span class="o">!=</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">Scrollable</span><span class="o">.</span><span class="na">ensureVisible</span><span class="p">(</span>
        <span class="n">targetContext</span><span class="p">,</span>
        <span class="nl">duration:</span> <span class="kd">const</span> <span class="n">Duration</span><span class="p">(</span><span class="nl">milliseconds:</span> <span class="mi">400</span><span class="p">),</span>
        <span class="nl">curve:</span> <span class="n">Curves</span><span class="o">.</span><span class="na">easeInOut</span><span class="p">,</span>
    <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>As a result, the target widget will get visible after 400 milliseconds.</p>

<p>In the next section, I will try to give a more detailed presentation of the implementation!</p>

<h2 id="implementation">Implementation</h2>

<h3 id="prep-work">Prep work</h3>

<p>First of all, let’s create a data structure that will represent the section. It will have a key, a title and a body.</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">Section</span> <span class="p">{</span>
  <span class="kd">final</span> <span class="n">GlobalKey</span> <span class="n">key</span><span class="p">;</span>
  <span class="kd">final</span> <span class="kt">String</span> <span class="n">title</span><span class="p">;</span>
  <span class="kd">final</span> <span class="kt">String</span> <span class="n">body</span><span class="p">;</span>

  <span class="kd">const</span> <span class="n">Section</span><span class="p">(</span><span class="k">this</span><span class="o">.</span><span class="na">key</span><span class="p">,</span> <span class="k">this</span><span class="o">.</span><span class="na">title</span><span class="p">,</span> <span class="k">this</span><span class="o">.</span><span class="na">body</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The title will be used in the table of contents and the key will be used to scroll to the target section.</p>

<p>Then, we will use this class to generate some dummy data for our example. We can use some lorem ipsum to represent some long text.</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="n">reallyLongBody</span> <span class="o">=</span>
    <span class="s">'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce venenatis pharetra dui, ac semper nulla dapibus ultrices.'</span>
    <span class="s">' Pellentesque sed erat accumsan lorem rhoncus mattis eu eget nulla. Phasellus sagittis vehicula dapibus. Nulla dolor nunc, '</span>
    <span class="s">'feugiat ac ullamcorper vel, commodo sed lacus. Nunc volutpat rutrum euismod. Nullam venenatis imperdiet odio, non porta leo '</span>
    <span class="s">'ullamcorper ac. Aliquam fringilla mauris ut ante faucibus, non tempus elit placerat. Donec sed porttitor tellus. Donec lobortis '</span>
    <span class="s">'arcu id lectus commodo varius. Fusce tincidunt ante in faucibus suscipit. Nulla facilisi. Nunc at nibh dictum sem aliquet '</span>
    <span class="s">'consectetur eu nec neque. Nullam ullamcorper vulputate nisl quis pharetra. Etiam dapibus ullamcorper magna, a iaculis libero '</span>
    <span class="s">'dignissim in. Vestibulum dictum, justo posuere consectetur eleifend, augue mi dictum dui, eu sollicitudin elit mauris vel lacus. '</span>
    <span class="s">'Donec dui felis, dapibus vel urna at, commodo facilisis felis.</span><span class="se">\n</span><span class="s">Curabitur faucibus leo ipsum, in vehicula risus rhoncus id. Donec '</span>
    <span class="s">'ac velit quis nulla suscipit efficitur. Nulla non euismod neque. Sed blandit urna sed ex tempor sagittis. Curabitur condimentum nec '</span>
    <span class="s">'dui quis sollicitudin. Proin consectetur, metus sed rutrum varius, mi augue placerat est, sed posuere risus nunc ac urna. Nam leo '</span>
    <span class="s">'erat, bibendum non nibh sed, sollicitudin aliquet metus. Aliquam finibus turpis vitae leo laoreet molestie.'</span><span class="p">;</span>

<span class="kd">final</span> <span class="n">sections</span> <span class="o">=</span> <span class="p">[</span>
  <span class="n">Section</span><span class="p">(</span><span class="n">GlobalKey</span><span class="p">(),</span> <span class="s">'1. Section'</span><span class="p">,</span> <span class="n">reallyLongBody</span><span class="p">),</span>
  <span class="n">Section</span><span class="p">(</span><span class="n">GlobalKey</span><span class="p">(),</span> <span class="s">'2. Section'</span><span class="p">,</span> <span class="n">reallyLongBody</span><span class="p">),</span>
  <span class="n">Section</span><span class="p">(</span><span class="n">GlobalKey</span><span class="p">(),</span> <span class="s">'3. Section'</span><span class="p">,</span> <span class="n">reallyLongBody</span><span class="p">),</span>
  <span class="n">Section</span><span class="p">(</span><span class="n">GlobalKey</span><span class="p">(),</span> <span class="s">'4. Section'</span><span class="p">,</span> <span class="n">reallyLongBody</span><span class="p">),</span>
  <span class="n">Section</span><span class="p">(</span><span class="n">GlobalKey</span><span class="p">(),</span> <span class="s">'5. Section'</span><span class="p">,</span> <span class="n">reallyLongBody</span><span class="p">),</span>
  <span class="n">Section</span><span class="p">(</span><span class="n">GlobalKey</span><span class="p">(),</span> <span class="s">'6. Section'</span><span class="p">,</span> <span class="n">reallyLongBody</span><span class="p">),</span>
<span class="p">];</span>
</code></pre></div></div>

<h3 id="widgets">Widgets</h3>

<p>Next, let’s create a widget for the <code class="language-plaintext highlighter-rouge">Section</code>, where we are going to show the title and the body of each section</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">SectionWidget</span> <span class="kd">extends</span> <span class="n">StatelessWidget</span> <span class="p">{</span>
  <span class="kd">final</span> <span class="n">Section</span> <span class="n">section</span><span class="p">;</span>

  <span class="kd">const</span> <span class="n">SectionWidget</span><span class="p">({</span><span class="n">Key</span><span class="o">?</span> <span class="n">key</span><span class="p">,</span> <span class="kd">required</span> <span class="k">this</span><span class="o">.</span><span class="na">section</span><span class="p">})</span> <span class="o">:</span> <span class="k">super</span><span class="p">(</span><span class="nl">key:</span> <span class="n">key</span><span class="p">);</span>

  <span class="nd">@override</span>
  <span class="n">Widget</span> <span class="n">build</span><span class="p">(</span><span class="n">BuildContext</span> <span class="n">context</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">Container</span><span class="p">(</span>
        <span class="nl">decoration:</span> <span class="kd">const</span> <span class="n">BoxDecoration</span><span class="p">(</span>
          <span class="nl">color:</span> <span class="n">Color</span><span class="p">(</span><span class="mh">0xfffff8e2</span><span class="p">),</span>
          <span class="nl">borderRadius:</span> <span class="n">BorderRadius</span><span class="o">.</span><span class="na">all</span><span class="p">(</span><span class="n">Radius</span><span class="o">.</span><span class="na">circular</span><span class="p">(</span><span class="mf">8.0</span><span class="p">)),</span>
        <span class="p">),</span>
        <span class="nl">margin:</span> <span class="kd">const</span> <span class="n">EdgeInsets</span><span class="o">.</span><span class="na">symmetric</span><span class="p">(</span><span class="nl">horizontal:</span> <span class="mi">16</span><span class="p">,</span> <span class="nl">vertical:</span> <span class="mi">8</span><span class="p">),</span>
        <span class="nl">padding:</span> <span class="kd">const</span> <span class="n">EdgeInsets</span><span class="o">.</span><span class="na">symmetric</span><span class="p">(</span><span class="nl">horizontal:</span> <span class="mi">16</span><span class="p">,</span> <span class="nl">vertical:</span> <span class="mi">8</span><span class="p">),</span>
        <span class="nl">child:</span> <span class="n">Column</span><span class="p">(</span>
          <span class="nl">crossAxisAlignment:</span> <span class="n">CrossAxisAlignment</span><span class="o">.</span><span class="na">stretch</span><span class="p">,</span>
          <span class="nl">children:</span> <span class="p">[</span>
            <span class="n">Text</span><span class="p">(</span>
              <span class="n">section</span><span class="o">.</span><span class="na">title</span><span class="p">,</span>
              <span class="nl">textAlign:</span> <span class="n">TextAlign</span><span class="o">.</span><span class="na">center</span><span class="p">,</span>
              <span class="nl">style:</span> <span class="n">Theme</span><span class="o">.</span><span class="na">of</span><span class="p">(</span><span class="n">context</span><span class="p">)</span>
                  <span class="o">.</span><span class="na">textTheme</span>
                  <span class="o">.</span><span class="na">headline2</span>
                  <span class="o">?.</span><span class="na">copyWith</span><span class="p">(</span><span class="nl">color:</span> <span class="n">Colors</span><span class="o">.</span><span class="na">black</span><span class="p">),</span>
            <span class="p">),</span>
            <span class="kd">const</span> <span class="n">SizedBox</span><span class="p">(</span>
              <span class="nl">height:</span> <span class="mi">36</span><span class="p">,</span>
            <span class="p">),</span>
            <span class="n">Text</span><span class="p">(</span>
              <span class="n">section</span><span class="o">.</span><span class="na">body</span><span class="p">,</span>
              <span class="nl">style:</span> <span class="n">Theme</span><span class="o">.</span><span class="na">of</span><span class="p">(</span><span class="n">context</span><span class="p">)</span>
                  <span class="o">.</span><span class="na">textTheme</span>
                  <span class="o">.</span><span class="na">bodyText1</span>
                  <span class="o">?.</span><span class="na">copyWith</span><span class="p">(</span><span class="nl">color:</span> <span class="n">Colors</span><span class="o">.</span><span class="na">black54</span><span class="p">,</span> <span class="nl">height:</span> <span class="mf">1.3</span><span class="p">),</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>After that, let’s create a widget for the link to a section. We will name it <code class="language-plaintext highlighter-rouge">SectionLink</code> and we will pass a section and the callback for the <code class="language-plaintext highlighter-rouge">onTap</code> event of the <code class="language-plaintext highlighter-rouge">InkWell</code>.</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">SectionLink</span> <span class="kd">extends</span> <span class="n">StatelessWidget</span> <span class="p">{</span>
  <span class="kd">final</span> <span class="n">Section</span> <span class="n">section</span><span class="p">;</span>
  <span class="kd">final</span> <span class="kt">void</span> <span class="kt">Function</span><span class="p">(</span><span class="n">Section</span><span class="p">)</span> <span class="n">onTap</span><span class="p">;</span>

  <span class="kd">const</span> <span class="n">SectionLink</span><span class="p">({</span><span class="n">Key</span><span class="o">?</span> <span class="n">key</span><span class="p">,</span> <span class="kd">required</span> <span class="k">this</span><span class="o">.</span><span class="na">section</span><span class="p">,</span> <span class="kd">required</span> <span class="k">this</span><span class="o">.</span><span class="na">onTap</span><span class="p">})</span>
      <span class="o">:</span> <span class="k">super</span><span class="p">(</span><span class="nl">key:</span> <span class="n">key</span><span class="p">);</span>

  <span class="nd">@override</span>
  <span class="n">Widget</span> <span class="n">build</span><span class="p">(</span><span class="n">BuildContext</span> <span class="n">context</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">InkWell</span><span class="p">(</span>
      <span class="nl">onTap:</span> <span class="p">()</span> <span class="o">=</span><span class="p">&gt;</span> <span class="n">onTap</span><span class="p">(</span><span class="n">section</span><span class="p">),</span>
      <span class="nl">child:</span> <span class="n">Padding</span><span class="p">(</span>
        <span class="nl">padding:</span> <span class="kd">const</span> <span class="n">EdgeInsets</span><span class="o">.</span><span class="na">all</span><span class="p">(</span><span class="mf">8.0</span><span class="p">),</span>
        <span class="nl">child:</span> <span class="n">Text</span><span class="p">(</span>
          <span class="n">section</span><span class="o">.</span><span class="na">title</span><span class="p">,</span>
          <span class="nl">style:</span> <span class="n">Theme</span><span class="o">.</span><span class="na">of</span><span class="p">(</span><span class="n">context</span><span class="p">)</span>
              <span class="o">.</span><span class="na">textTheme</span>
              <span class="o">.</span><span class="na">headline3</span>
              <span class="o">?.</span><span class="na">copyWith</span><span class="p">(</span><span class="nl">color:</span> <span class="n">Colors</span><span class="o">.</span><span class="na">black87</span><span class="p">,</span> <span class="nl">fontWeight:</span> <span class="n">FontWeight</span><span class="o">.</span><span class="na">bold</span><span class="p">),</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>Next, we are going to add a <code class="language-plaintext highlighter-rouge">TableOfContents</code> widget, where we basically iterate on the sections and for each section, we create a <code class="language-plaintext highlighter-rouge">SectionLink</code>.</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">TableOfContents</span> <span class="kd">extends</span> <span class="n">StatelessWidget</span> <span class="p">{</span>
  <span class="kd">final</span> <span class="kt">List</span><span class="p">&lt;</span><span class="n">Section</span><span class="p">&gt;</span> <span class="n">sections</span><span class="p">;</span>
  <span class="kd">final</span> <span class="kt">void</span> <span class="kt">Function</span><span class="p">(</span><span class="n">Section</span><span class="p">)</span> <span class="n">onItemTap</span><span class="p">;</span>

  <span class="kd">const</span> <span class="n">TableOfContents</span><span class="p">({</span>
    <span class="n">Key</span><span class="o">?</span> <span class="n">key</span><span class="p">,</span>
    <span class="k">this</span><span class="o">.</span><span class="na">sections</span> <span class="o">=</span> <span class="kd">const</span> <span class="p">&lt;</span><span class="n">Section</span><span class="p">&gt;[],</span>
    <span class="kd">required</span> <span class="k">this</span><span class="o">.</span><span class="na">onItemTap</span><span class="p">,</span>
  <span class="p">})</span> <span class="o">:</span> <span class="k">super</span><span class="p">(</span><span class="nl">key:</span> <span class="n">key</span><span class="p">);</span>

  <span class="nd">@override</span>
  <span class="n">Widget</span> <span class="n">build</span><span class="p">(</span><span class="n">BuildContext</span> <span class="n">context</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">Container</span><span class="p">(</span>
      <span class="nl">margin:</span> <span class="kd">const</span> <span class="n">EdgeInsets</span><span class="o">.</span><span class="na">all</span><span class="p">(</span><span class="mi">20</span><span class="p">),</span>
      <span class="nl">padding:</span> <span class="kd">const</span> <span class="n">EdgeInsets</span><span class="o">.</span><span class="na">only</span><span class="p">(</span><span class="nl">left:</span> <span class="mi">16</span><span class="p">,</span> <span class="nl">top:</span> <span class="mi">24</span><span class="p">,</span> <span class="nl">right:</span> <span class="mi">16</span><span class="p">,</span> <span class="nl">bottom:</span> <span class="mi">10</span><span class="p">),</span>
      <span class="nl">decoration:</span> <span class="n">BoxDecoration</span><span class="p">(</span>
        <span class="nl">color:</span> <span class="n">Colors</span><span class="o">.</span><span class="na">white12</span><span class="o">.</span><span class="na">withOpacity</span><span class="p">(</span><span class="mf">0.3</span><span class="p">),</span>
        <span class="nl">borderRadius:</span> <span class="kd">const</span> <span class="n">BorderRadius</span><span class="o">.</span><span class="na">all</span><span class="p">(</span><span class="n">Radius</span><span class="o">.</span><span class="na">circular</span><span class="p">(</span><span class="mf">8.0</span><span class="p">)),</span>
        <span class="nl">border:</span> <span class="n">Border</span><span class="o">.</span><span class="na">all</span><span class="p">(</span>
          <span class="nl">width:</span> <span class="mi">2</span><span class="p">,</span>
          <span class="nl">color:</span> <span class="n">Colors</span><span class="o">.</span><span class="na">grey</span><span class="p">,</span>
        <span class="p">),</span>
      <span class="p">),</span>
      <span class="nl">child:</span> <span class="n">Column</span><span class="p">(</span>
        <span class="nl">crossAxisAlignment:</span> <span class="n">CrossAxisAlignment</span><span class="o">.</span><span class="na">stretch</span><span class="p">,</span>
        <span class="nl">children:</span> <span class="n">sections</span>
            <span class="o">.</span><span class="na">map</span><span class="p">((</span><span class="n">e</span><span class="p">)</span> <span class="o">=</span><span class="p">&gt;</span> <span class="n">SectionLink</span><span class="p">(</span><span class="nl">section:</span> <span class="n">e</span><span class="p">,</span> <span class="nl">onTap:</span> <span class="n">onItemTap</span><span class="p">))</span>
            <span class="o">.</span><span class="na">toList</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>Finally, let’s create <code class="language-plaintext highlighter-rouge">ArticlePage</code> where we will tie everything together. In this widget, we will create a <code class="language-plaintext highlighter-rouge">SingleChildScrollView</code> with a <code class="language-plaintext highlighter-rouge">Column</code> widget containing the <code class="language-plaintext highlighter-rouge">TableOfContents</code> and a <code class="language-plaintext highlighter-rouge">ListView</code> with the sections.</p>

<p>For the <code class="language-plaintext highlighter-rouge">TableOfContents</code>, we are going to pass as parameters the section and the callback for when a section is tapped. This callback contains the logic to scroll to the target widget. First, we verify that there is a widget with this key in the tree, by ensuring that the value of the <code class="language-plaintext highlighter-rouge">currentContext</code> is not null. And then, we will pass this context to <code class="language-plaintext highlighter-rouge">Scrollable.ensureVisible</code> to scroll to the target widget.</p>

<p>For the <code class="language-plaintext highlighter-rouge">ListView</code>, we iterate over the sections and for each one, we create a new <code class="language-plaintext highlighter-rouge">SectionWidget</code> using the key from the section as the key for the widget.</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">ArticlePage</span> <span class="kd">extends</span> <span class="n">StatelessWidget</span> <span class="p">{</span>
  <span class="kd">final</span> <span class="kt">List</span><span class="p">&lt;</span><span class="n">Section</span><span class="p">&gt;</span> <span class="n">sections</span><span class="p">;</span>
  <span class="kd">const</span> <span class="n">ArticlePage</span><span class="p">({</span><span class="n">Key</span><span class="o">?</span> <span class="n">key</span><span class="p">,</span> <span class="kd">required</span> <span class="k">this</span><span class="o">.</span><span class="na">sections</span><span class="p">})</span> <span class="o">:</span> <span class="k">super</span><span class="p">(</span><span class="nl">key:</span> <span class="n">key</span><span class="p">);</span>

  <span class="nd">@override</span>
  <span class="n">Widget</span> <span class="n">build</span><span class="p">(</span><span class="n">BuildContext</span> <span class="n">context</span><span class="p">)</span> <span class="p">{</span>

    <span class="kd">final</span> <span class="n">tableOfContents</span> <span class="o">=</span> <span class="n">TableOfContents</span><span class="p">(</span>
      <span class="nl">sections:</span> <span class="n">sections</span><span class="p">,</span>
      <span class="nl">onItemTap:</span> <span class="p">(</span><span class="n">section</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">final</span> <span class="n">targetContext</span> <span class="o">=</span> <span class="n">section</span><span class="o">.</span><span class="na">key</span><span class="o">.</span><span class="na">currentContext</span><span class="p">;</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">targetContext</span> <span class="o">!=</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
          <span class="n">Scrollable</span><span class="o">.</span><span class="na">ensureVisible</span><span class="p">(</span>
            <span class="n">targetContext</span><span class="p">,</span>
            <span class="nl">duration:</span> <span class="kd">const</span> <span class="n">Duration</span><span class="p">(</span><span class="nl">milliseconds:</span> <span class="mi">400</span><span class="p">),</span>
            <span class="nl">curve:</span> <span class="n">Curves</span><span class="o">.</span><span class="na">easeInOut</span><span class="p">,</span>
          <span class="p">);</span>
        <span class="p">}</span>
      <span class="p">},</span>
    <span class="p">);</span>

    <span class="kd">final</span> <span class="n">listView</span> <span class="o">=</span> <span class="n">ListView</span><span class="o">.</span><span class="na">builder</span><span class="p">(</span>
      <span class="nl">shrinkWrap:</span> <span class="kc">true</span><span class="p">,</span>
      <span class="nl">physics:</span> <span class="kd">const</span> <span class="n">NeverScrollableScrollPhysics</span><span class="p">(),</span>
      <span class="nl">itemCount:</span> <span class="n">sections</span><span class="o">.</span><span class="na">length</span><span class="p">,</span>
      <span class="nl">itemBuilder:</span> <span class="p">(</span><span class="n">BuildContext</span> <span class="n">context</span><span class="p">,</span> <span class="kt">int</span> <span class="n">index</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">final</span> <span class="n">section</span> <span class="o">=</span> <span class="n">sections</span><span class="p">[</span><span class="n">index</span><span class="p">];</span>

        <span class="k">return</span> <span class="n">SectionWidget</span><span class="p">(</span>
          <span class="nl">key:</span> <span class="n">section</span><span class="o">.</span><span class="na">key</span><span class="p">,</span>
          <span class="nl">section:</span> <span class="n">section</span><span class="p">,</span>
        <span class="p">);</span>
      <span class="p">},</span>
    <span class="p">);</span>

    <span class="k">return</span> <span class="n">Scaffold</span><span class="p">(</span>
      <span class="nl">appBar:</span> <span class="n">AppBar</span><span class="p">(</span>
        <span class="nl">title:</span> <span class="kd">const</span> <span class="n">Text</span><span class="p">(</span><span class="s">'Home Screen'</span><span class="p">),</span>
      <span class="p">),</span>
      <span class="nl">body:</span> <span class="n">SafeArea</span><span class="p">(</span>
        <span class="nl">child:</span> <span class="n">SingleChildScrollView</span><span class="p">(</span>
          <span class="nl">child:</span> <span class="n">Padding</span><span class="p">(</span>
            <span class="nl">padding:</span> <span class="kd">const</span> <span class="n">EdgeInsets</span><span class="o">.</span><span class="na">all</span><span class="p">(</span><span class="mf">8.0</span><span class="p">),</span>
            <span class="nl">child:</span> <span class="n">Column</span><span class="p">(</span>
              <span class="nl">crossAxisAlignment:</span> <span class="n">CrossAxisAlignment</span><span class="o">.</span><span class="na">stretch</span><span class="p">,</span>
              <span class="nl">children:</span> <span class="p">[</span>
                <span class="n">tableOfContents</span><span class="p">,</span>
                <span class="n">listView</span><span class="p">,</span>
              <span class="p">],</span>
            <span class="p">),</span>
          <span class="p">),</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>If you now build and run the app, when you press on a link from the table of contents, you will have the same with the following video behavior.</p>

<p><img src="https://diamantidis.github.io/assets/scrollable/scrollable_ensureVisible.gif" alt="Demo app gif" /></p>

<blockquote>
  <p>You can also use <a href="https://dartpad.dev/?id=33c44d2f905cf23d4e0f825b45d79d91&amp;null_safety=true">dartpad.dev</a> to find and run the code of this post.</p>
</blockquote>

<h2 id="more-options">More options</h2>

<p>Now that we have seen how we can use <code class="language-plaintext highlighter-rouge">Scrollable.ensureVisible</code>, let’s explore some more options that we can use to customize the transition to the target widget.</p>

<p>Two of those, which we have seen before in the previous example, are <code class="language-plaintext highlighter-rouge">duration</code> and <code class="language-plaintext highlighter-rouge">curve</code>. <code class="language-plaintext highlighter-rouge">duration</code> can be used to set the desired duration that we want the animation from the link to the target widget to take.</p>

<p>With the <code class="language-plaintext highlighter-rouge">curve</code> parameter, we can define the animation curve that the transition will follow. Basically, instead of making the transition at a constant rate, we can set a value to the <code class="language-plaintext highlighter-rouge">curve</code> parameter to change the animation over time, either by speeding it up or slowing it down at specific time frames. It can take values like <code class="language-plaintext highlighter-rouge">Curves.bounceInOut</code>, <code class="language-plaintext highlighter-rouge">Curves.easeInOut</code>, etc. For example, with <code class="language-plaintext highlighter-rouge">Curves.easeInOut</code> the animation will start slowly, then speed up and will then end slowly.</p>

<blockquote>
  <p>Note: A visualization of the different options can be found on <a href="https://api.flutter.dev/flutter/animation/Curves-class.html">api.flutter.dev</a>.</p>
</blockquote>

<p>Another parameter of <code class="language-plaintext highlighter-rouge">Scrollable.ensureVisible</code> is <code class="language-plaintext highlighter-rouge">alignment</code>, and it can be used to set the position of the target widget. If the value is 0.0, the child will be positioned as close to the leading edge of the viewport, 0.5 as close to the center, and 1.0 as close to the trailing edge.</p>

<p>Finally, the last parameter is <code class="language-plaintext highlighter-rouge">alignmentPolicy</code> and can be used to decide the policy when applying the <code class="language-plaintext highlighter-rouge">alignment</code> parameter. This parameter is of type <code class="language-plaintext highlighter-rouge">ScrollPositionAlignmentPolicy</code>, which is an enum with the following options: <code class="language-plaintext highlighter-rouge">explicit</code>, <code class="language-plaintext highlighter-rouge">keepVisibleAtEnd</code> or <code class="language-plaintext highlighter-rouge">keepVisibleAtStart</code>.</p>

<p>When it is set to <code class="language-plaintext highlighter-rouge">explicit</code>, it will use the <code class="language-plaintext highlighter-rouge">alignment</code> property to decide where to align the target object. If it is set to <code class="language-plaintext highlighter-rouge">keepVisibleAtEnd</code>, it will make sure that the bottom of the target item is just visible if the bottom edge of the target item is below the bottom edge of the scroll container. Contrary, <code class="language-plaintext highlighter-rouge">keepVisibleAtStart</code>, will make sure that the top of the target object is just visible if the top edge of the target object is above the top edge of the scroll container</p>

<h2 id="conclusion">Conclusion</h2>

<p>And this is it! I hope that you find this post useful and it has given you some insights into how to use <code class="language-plaintext highlighter-rouge">Scrollable.ensureVisible</code> and all its options to scroll to a specific widget on a scroll view.</p>

<p>Thanks for reading! If you found this post useful, consider <a href="https://www.buymeacoffee.com/diamantidis">buying me a coffee</a> to support the blog. For questions or comments, feel free to reach out on <a href="https://x.com/diamantidis_io">X</a>!</p>

<p>Until next time!</p>]]></content><author><name>Ioannis Diamantidis</name><email>diamantidis@outlook.com</email></author><category term="Flutter" /><summary type="html"><![CDATA[A post to showcase how to use `Scrollable.ensureVisible` to scroll from one widget to another in a `SingleChildScrollView`]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://diamantidis.github.io/assets/social/scrollable_ensure_visible.png" /><media:content medium="image" url="https://diamantidis.github.io/assets/social/scrollable_ensure_visible.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Quick and simple load testing with Apache Bench</title><link href="https://diamantidis.github.io/2020/07/15/load-testing-with-apache-bench" rel="alternate" type="text/html" title="Quick and simple load testing with Apache Bench" /><published>2020-07-15T04:00:00+00:00</published><updated>2020-07-15T04:00:00+00:00</updated><id>https://diamantidis.github.io/2020/07/15/load-testing-with-apache-bench</id><content type="html" xml:base="https://diamantidis.github.io/2020/07/15/load-testing-with-apache-bench"><![CDATA[<p><a href="https://httpd.apache.org/docs/2.4/programs/ab.html">Apache Bench</a> or ab for short, is a command-line tool to perform simple load tests on an HTTP server, be it a website or an API.</p>

<p>By running the following command, you will get an overview of how the server is performing under load:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ab <span class="nt">-n</span> 100 <span class="nt">-c</span> 10 &lt;url&gt;
</code></pre></div></div>

<p>So, in this post, I will try to explain how we can use Apache Bench. I will start with how to install it, then proceed on how to use it and the available options, and finally, I explain how to interpret the results.</p>

<p>Let’s get started!</p>

<h2 id="installation">Installation</h2>
<p>One of the advantages of Apache Bench is that you may already have <code class="language-plaintext highlighter-rouge">ab</code> installed, depending on the OS you are using.
For macOS users, it comes pre-installed by default, and if you are using a Linux Distribution, there are a lot of chances that it is also installed as it comes with the <code class="language-plaintext highlighter-rouge">httpd</code> package. Try to run <code class="language-plaintext highlighter-rouge">ab -help</code> to verify if that’s the case.</p>

<p>In case it is not installed, you will need to install the <code class="language-plaintext highlighter-rouge">apache2-utils</code> package.
For example, if you are using <code class="language-plaintext highlighter-rouge">Ubuntu</code> you will have to run:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apt-get update
apt-get <span class="nb">install </span>apache2-utils
</code></pre></div></div>

<p>So, now, that we have Apache Bench installed, let’s see how we can use it and some of the available options.</p>

<h2 id="usage">Usage</h2>

<p>The simplest possible way to use Apache Bench is by running <code class="language-plaintext highlighter-rouge">ab &lt;url&gt;</code>. This command will perform a single network request, but this is not exactly what we would call “load”. For this reason, Apache Bench comes with a plethora of options you can use to define more complex use cases.</p>

<p>Some of the most useful ones are the following:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">-n</code>: Number of requests</li>
  <li><code class="language-plaintext highlighter-rouge">-c</code>: Number of concurrent requests</li>
  <li><code class="language-plaintext highlighter-rouge">-H</code>: Add header</li>
  <li><code class="language-plaintext highlighter-rouge">—r</code>: flag to not exit on socket receive errors</li>
  <li><code class="language-plaintext highlighter-rouge">-k</code>: Use HTTP KeepAlive feature</li>
  <li><code class="language-plaintext highlighter-rouge">-p</code>: File containing data to POST</li>
  <li><code class="language-plaintext highlighter-rouge">-T</code>: Content-type header to use for POST/PUT data,</li>
</ul>

<p>Now you can use those options like so:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ab <span class="nt">-n</span> 100 <span class="nt">-c</span> 10 <span class="nt">-H</span> <span class="s2">"Accept-Encoding: gzip, deflate"</span> <span class="nt">-rk</span> https://0.0.0.0:4000/
</code></pre></div></div>

<blockquote>
  <p>:warning: Please note that you need the trailing <code class="language-plaintext highlighter-rouge">/</code> on the URL, or else you will get the error message <code class="language-plaintext highlighter-rouge">ab: invalid URL</code>.</p>
</blockquote>

<p>If you want to perform a benchmark test for a POST request, you can run the following command:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ab <span class="nt">-n</span> 100 <span class="nt">-c</span> 10 <span class="nt">-p</span> data.json <span class="nt">-T</span> application/json <span class="nt">-rk</span> https://0.0.0.0:4000/
</code></pre></div></div>

<blockquote>
  <p>:bulb: TIP: For a full list of options, you can run <code class="language-plaintext highlighter-rouge">ab -help</code>, refer the man page by running <code class="language-plaintext highlighter-rouge">man ab</code>, or visit the <a href="https://httpd.apache.org/docs/2.4/programs/ab.html">documentation</a> online.</p>
</blockquote>

<p>Let’s now see how the output would look like and how to interpret it!</p>

<h2 id="response-interpretation">Response interpretation</h2>

<p>Once <code class="language-plaintext highlighter-rouge">ab</code> completes the HTTP requests, it will generate an output that will look like the following snippet:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Concurrency Level:      10
Time taken for tests:   0.791 seconds
Complete requests:      1000
Failed requests:        0
Keep-Alive requests:    1000
Total transferred:      4649081 bytes
HTML transferred:       3934000 bytes
Requests per second:    1264.17 [#/sec] (mean)
Time per request:       7.910 [ms] (mean)
Time per request:       0.791 [ms] (mean, across all concurrent requests)
Transfer rate:          5739.47 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    1   9.4      0     105
Processing:     3    6   7.1      5      77
Waiting:        3    6   6.8      5      65
Total:          3    7  11.9      5     111

Percentage of the requests served within a certain time (ms)
  50%      5
  66%      6
  75%      6
  80%      6
  90%      7
  95%      8
  98%     62
  99%     77
 100%    111 (longest request)
</code></pre></div></div>

<p>In this first section, you can find some useful information like, for example, that the number of <code class="language-plaintext highlighter-rouge">Complete requests</code> was 1000 and the <code class="language-plaintext highlighter-rouge">Concurrency Level</code> was 10. Furthermore, you can see that it performed 1264.17 <code class="language-plaintext highlighter-rouge">Requests per second</code>.</p>

<p>In the <code class="language-plaintext highlighter-rouge">Connection Times</code> section, you can see that the fastest request took 3 ms (<code class="language-plaintext highlighter-rouge">Total</code> row and <code class="language-plaintext highlighter-rouge">min</code> column), the slowest took 111 ms (<code class="language-plaintext highlighter-rouge">Total</code> row and <code class="language-plaintext highlighter-rouge">max</code> column), while the mean was 7 ms (<code class="language-plaintext highlighter-rouge">Total</code> row &amp; <code class="language-plaintext highlighter-rouge">mean</code> column).</p>

<p>In the last section, you get an overview of the response times in a cumulative distribution. In this example, we can see that 95% of requests took 8 ms or less to complete and that total response times of more than 100 ms is an outlier as it covers less than 1% of the sample.</p>

<h2 id="conclusion">Conclusion</h2>

<p>And that’s about for this intro on Apache Bench! By now, you should be able to use the <code class="language-plaintext highlighter-rouge">ab</code> command to perform load tests on an HTTP server and get some insights from the results.</p>

<p>In the end, I would say that Apache Bench is an ideal solution if you want to perform a quick load test since it is probably already installed on your machine and it is really simple to use. In case you want to cover more advanced use cases like flows and random URL entries, then I think that there are other more modern and feature-complete tools. A few examples are <a href="https://jmeter.apache.org/">JMeter</a>, <a href="https://k6.io/">K6</a> and <a href="https://gatling.io/">Gatling</a>.</p>

<p>Thanks for reading! If you found this post useful, consider <a href="https://www.buymeacoffee.com/diamantidis">buying me a coffee</a> to support the blog. For questions or comments, feel free to reach out on <a href="https://x.com/diamantidis_io">X</a>!</p>

<p>Until next time!</p>]]></content><author><name>Ioannis Diamantidis</name><email>diamantidis@outlook.com</email></author><category term="Performance" /><summary type="html"><![CDATA[A brief into to Apache Bench, how to use, available option and result interpretation]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://diamantidis.github.io/assets/social/load-testing-with-apache-bench.png" /><media:content medium="image" url="https://diamantidis.github.io/assets/social/load-testing-with-apache-bench.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Dart extensions to flatten Flutter’s deep nested widget trees</title><link href="https://diamantidis.github.io/2020/07/07/dart-extensions-flatten-flutter-nested-widget-trees" rel="alternate" type="text/html" title="Dart extensions to flatten Flutter’s deep nested widget trees" /><published>2020-07-07T04:00:00+00:00</published><updated>2020-07-07T04:00:00+00:00</updated><id>https://diamantidis.github.io/2020/07/07/dart-extensions-flatten-flutter-nested-widget-trees</id><content type="html" xml:base="https://diamantidis.github.io/2020/07/07/dart-extensions-flatten-flutter-nested-widget-trees"><![CDATA[<p>In version 2.7, <code class="language-plaintext highlighter-rouge">Dart</code> introduced extensions to allow developers to add further functionality to an existing component. Extensions can be a great tool in our toolkit when we write business logic, but they can also be as useful in other areas! One such example can be the way we construct the widgets.</p>

<p>Coming from an iOS background and inspired by SwiftUI’s ViewModifiers, I was intrigued to use the Dart extensions in a similar fashion to reduce the visual clutter that comes with the deep tree of many nested widgets.</p>

<p>Let’s see an example!</p>

<h2 id="with-nested-widgets">With nested widgets</h2>

<p>Here we have the default widget created on <a href="https://dartpad.dev/flutter">dartpad.dev</a>, where we also wrapped the text inside a <code class="language-plaintext highlighter-rouge">Padding</code> widget.</p>
<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">MyWidget</span> <span class="kd">extends</span> <span class="n">StatelessWidget</span> <span class="p">{</span>
  <span class="nd">@override</span>
  <span class="n">Widget</span> <span class="n">build</span><span class="p">(</span><span class="n">BuildContext</span> <span class="n">context</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span>
      <span class="n">Padding</span><span class="p">(</span>
        <span class="nl">padding:</span> <span class="kd">const</span> <span class="n">EdgeInsets</span><span class="o">.</span><span class="na">all</span><span class="p">(</span><span class="mi">16</span><span class="p">),</span>
        <span class="nl">child:</span> <span class="n">Text</span><span class="p">(</span><span class="s">'Hello, World!'</span><span class="p">,</span> <span class="nl">style:</span> <span class="n">Theme</span><span class="o">.</span><span class="na">of</span><span class="p">(</span><span class="n">context</span><span class="p">)</span><span class="o">.</span><span class="na">textTheme</span><span class="o">.</span><span class="na">headline4</span><span class="p">)</span>
      <span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Now, let’s see how we can accomplish the same with the help of Dart extensions.</p>

<h2 id="with-extensions-methods">With extensions methods</h2>

<p>First, we will introduce an extension on <code class="language-plaintext highlighter-rouge">Widget</code> that will wrap the caller with a padding <code class="language-plaintext highlighter-rouge">Widget</code>:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">extension</span> <span class="n">WidgetModifier</span> <span class="kd">on</span> <span class="n">Widget</span> <span class="p">{</span>
  <span class="n">Widget</span> <span class="n">padding</span><span class="p">([</span><span class="n">EdgeInsetsGeometry</span> <span class="n">value</span> <span class="o">=</span> <span class="kd">const</span> <span class="n">EdgeInsets</span><span class="o">.</span><span class="na">all</span><span class="p">(</span><span class="mi">16</span><span class="p">)])</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">Padding</span><span class="p">(</span>
      <span class="nl">padding:</span> <span class="n">value</span><span class="p">,</span>
      <span class="nl">child:</span> <span class="k">this</span><span class="p">,</span>
    <span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>With this extension in place, we could turn our <code class="language-plaintext highlighter-rouge">Widget</code> to the following:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">MyWidget</span> <span class="kd">extends</span> <span class="n">StatelessWidget</span> <span class="p">{</span>
  <span class="nd">@override</span>
  <span class="n">Widget</span> <span class="n">build</span><span class="p">(</span><span class="n">BuildContext</span> <span class="n">context</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">Text</span><span class="p">(</span><span class="s">'Hello, World!'</span><span class="p">,</span> <span class="nl">style:</span> <span class="n">Theme</span><span class="o">.</span><span class="na">of</span><span class="p">(</span><span class="n">context</span><span class="p">)</span><span class="o">.</span><span class="na">textTheme</span><span class="o">.</span><span class="na">headline4</span><span class="p">)</span>
            <span class="o">.</span><span class="na">padding</span><span class="p">();</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This example is just a small one, but I hope you get the picture!!</p>

<p>We are using the extension method as a syntactic sugar to structure our layout, instead of wrapping a widget inside another one.</p>

<p>Similarly, we can add more functions to our extension and create more complex user interfaces:</p>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">extension</span> <span class="n">WidgetModifier</span> <span class="kd">on</span> <span class="n">Widget</span> <span class="p">{</span>

  <span class="c1">// ...</span>

  <span class="n">Widget</span> <span class="n">background</span><span class="p">(</span><span class="n">Color</span> <span class="n">color</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">DecoratedBox</span><span class="p">(</span>
      <span class="nl">decoration:</span> <span class="n">BoxDecoration</span><span class="p">(</span>
        <span class="nl">color:</span> <span class="n">color</span><span class="p">,</span>
      <span class="p">),</span>
      <span class="nl">child:</span> <span class="k">this</span><span class="p">,</span>
    <span class="p">);</span>
  <span class="p">}</span>

  <span class="n">Widget</span> <span class="n">cornerRadius</span><span class="p">(</span><span class="n">BorderRadiusGeometry</span> <span class="n">radius</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">ClipRRect</span><span class="p">(</span>
      <span class="nl">borderRadius:</span> <span class="n">radius</span><span class="p">,</span>
      <span class="nl">child:</span> <span class="k">this</span><span class="p">,</span>
    <span class="p">);</span>
  <span class="p">}</span>

  <span class="n">Widget</span> <span class="n">align</span><span class="p">([</span><span class="n">AlignmentGeometry</span> <span class="n">alignment</span> <span class="o">=</span> <span class="n">Alignment</span><span class="o">.</span><span class="na">center</span><span class="p">])</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">Align</span><span class="p">(</span>
      <span class="nl">alignment:</span> <span class="n">alignment</span><span class="p">,</span>
      <span class="nl">child:</span> <span class="k">this</span><span class="p">,</span>
    <span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="language-dart highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">MyWidget</span> <span class="kd">extends</span> <span class="n">StatelessWidget</span> <span class="p">{</span>
  <span class="nd">@override</span>
  <span class="n">Widget</span> <span class="n">build</span><span class="p">(</span><span class="n">BuildContext</span> <span class="n">context</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">Text</span><span class="p">(</span><span class="s">'Hello, World!'</span><span class="p">,</span> <span class="nl">style:</span> <span class="n">Theme</span><span class="o">.</span><span class="na">of</span><span class="p">(</span><span class="n">context</span><span class="p">)</span><span class="o">.</span><span class="na">textTheme</span><span class="o">.</span><span class="na">headline4</span><span class="p">)</span>
            <span class="o">.</span><span class="na">padding</span><span class="p">()</span>
            <span class="o">.</span><span class="na">background</span><span class="p">(</span><span class="n">Colors</span><span class="o">.</span><span class="na">lightBlue</span><span class="p">)</span>
            <span class="o">.</span><span class="na">cornerRadius</span><span class="p">(</span><span class="n">BorderRadius</span><span class="o">.</span><span class="na">all</span><span class="p">(</span><span class="n">Radius</span><span class="o">.</span><span class="na">circular</span><span class="p">(</span><span class="mf">8.0</span><span class="p">)))</span>
            <span class="o">.</span><span class="na">padding</span><span class="p">(</span><span class="n">EdgeInsets</span><span class="o">.</span><span class="na">symmetric</span><span class="p">(</span><span class="nl">horizontal:</span> <span class="mi">8</span><span class="p">,</span> <span class="nl">vertical:</span> <span class="mi">16</span><span class="p">))</span>
            <span class="o">.</span><span class="na">background</span><span class="p">(</span><span class="n">Colors</span><span class="o">.</span><span class="na">purple</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><img src="https://diamantidis.github.io/assets/dart_extensions/dart_widget_extensions_example.png" alt="Dart widget extensions example screenshot" /></p>

<p>Isn’t that great? We now have a clean and elegant widget that we can easily understand! Imagine how many levels of nested widgets you would have to use to recreate this layout without the help of extensions.</p>

<h2 id="wrap-up">Wrap-up</h2>

<p>To wrap up, in this post, we have followed an alternative approach on how to structure a widget tree in a Flutter project, by applying a concept similar to SwiftUI’s ViewModifiers.</p>

<p>As a result, this could help us flatten the widget hierarchy and reduce nesting. I have showcased a few examples of such extensions, but you can use the same concept in many other cases as you see fit.</p>

<p>Thanks for reading! If you found this post useful, consider <a href="https://www.buymeacoffee.com/diamantidis">buying me a coffee</a> to support the blog. For questions or comments, feel free to reach out on <a href="https://x.com/diamantidis_io">X</a>!</p>

<p>Until next time!</p>]]></content><author><name>Ioannis Diamantidis</name><email>diamantidis@outlook.com</email></author><category term="Flutter" /><category term="Dart" /><summary type="html"><![CDATA[How to use Dart extensions instead of nested widgets to flatten the widget tree in a Flutter project]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://diamantidis.github.io/assets/social/dart-extension-flutter-widget.png" /><media:content medium="image" url="https://diamantidis.github.io/assets/social/dart-extension-flutter-widget.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Use AppleScript to generate an enum for SF Symbols</title><link href="https://diamantidis.github.io/2020/06/28/applescript-to-generate-enum-for-sf-symbols" rel="alternate" type="text/html" title="Use AppleScript to generate an enum for SF Symbols" /><published>2020-06-28T04:00:00+00:00</published><updated>2020-06-28T04:00:00+00:00</updated><id>https://diamantidis.github.io/2020/06/28/applescript-to-generate-enum-for-sf-symbols</id><content type="html" xml:base="https://diamantidis.github.io/2020/06/28/applescript-to-generate-enum-for-sf-symbols"><![CDATA[<p><a href="https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/">SF Symbols</a> is a great way to add symbols in apps. Introduced during WWDC 2019, and with a new updated version this year, the <a href="https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/">SF Symbols</a> app now provides more than 2300 symbols, supports iOS, Mac Catalyst, tvOS, and watchOS and now macOS, and offers many more features, like multicolor support, RTL, etc.</p>

<p>But despite its greatness, it comes with some shortcomings when we have to use those symbols from an app.
The provided APIs expect a hard-coded string literal for the name of the <code class="language-plaintext highlighter-rouge">SF Symbol</code>. This makes their usage susceptible to errors since we will not get notified by the compiler if we accidentally mistype the name.</p>

<p>Wouldn’t it be much better if we were to have a type-safe solution? Maybe an enum?</p>

<p>That’s the problem we will try to solve in this post. We will create a script using <code class="language-plaintext highlighter-rouge">AppleScript</code> which will open the <code class="language-plaintext highlighter-rouge">SF Symbols</code> app, traverse the list of symbols and generate an enum with a case for each symbol.</p>

<h2 id="script-overview">Script Overview</h2>

<p>To start with, we will use the <code class="language-plaintext highlighter-rouge">Script Editor</code> app to write the <code class="language-plaintext highlighter-rouge">AppleScript</code>. Before we start writing the script, let’s try to figure out the steps we will take on the <code class="language-plaintext highlighter-rouge">SF Symbols</code> app. First, we will open the app. Then, we will select the list layout. Choosing the list layout instead of the grid will make it much easier to traverse the list of symbols.</p>

<p>After that, we will select the option <code class="language-plaintext highlighter-rouge">All</code> from the <code class="language-plaintext highlighter-rouge">Categories</code> menu to make sure that the output will contain the full list of options. Then, we will loop through the symbols, and for each one, we will create an enum case of the format <code class="language-plaintext highlighter-rouge">case &lt;name&gt; = &lt;sf symbol name&gt;</code>.</p>

<p>Let’s see the script!</p>

<h2 id="preparation">Preparation</h2>

<p>We will first define some helper functions to compute the identifier that we will use for the case.</p>

<p>This identifier should be a valid one, so we have to cater for some edge cases. In some cases, the names of some SF Symbols are reserved keywords in Swift, like <code class="language-plaintext highlighter-rouge">return</code> and <code class="language-plaintext highlighter-rouge">repeat</code>. Since we cannot use those words for the case identifier, we will escape them with the <code class="language-plaintext highlighter-rouge">backtick</code> symbol (<code class="language-plaintext highlighter-rouge">`</code>).</p>

<div class="language-applescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">on</span> <span class="nv">convertReservedKeywords</span><span class="p">(</span><span class="nv">theText</span><span class="p">)</span><span class="w">
    </span><span class="k">set</span><span class="w"> </span><span class="nv">keywords</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="p">{</span><span class="s2">"repeat"</span><span class="p">,</span><span class="w"> </span><span class="s2">"return"</span><span class="p">,</span><span class="w"> </span><span class="s2">"case"</span><span class="p">}</span><span class="w">
    </span><span class="k">set</span><span class="w"> </span><span class="nv">theNewText</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="nv">theText</span><span class="w">

    </span><span class="k">if</span><span class="w"> </span><span class="nv">keywords</span><span class="w"> </span><span class="ow">contains</span><span class="w"> </span><span class="p">(</span><span class="nv">theText</span><span class="w"> </span><span class="k">as </span><span class="nc">string</span><span class="p">)</span><span class="w"> </span><span class="k">then</span><span class="w">
        </span><span class="k">set</span><span class="w"> </span><span class="nv">theNewText</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="s2">"`"</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="nv">theText</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="s2">"`"</span><span class="w">
    </span><span class="k">end</span><span class="w"> </span><span class="k">if</span><span class="w">

    </span><span class="nb">return</span><span class="w"> </span><span class="nv">theNewText</span><span class="w">
</span><span class="k">end</span><span class="w"> </span><span class="nv">convertReservedKeywords</span><span class="w">
</span></code></pre></div></div>

<p>Another such case is the <code class="language-plaintext highlighter-rouge">SF Symbols</code> starting with a number literal, like <code class="language-plaintext highlighter-rouge">0.square</code>. In that scenario, we will prepend the name with the string literal <code class="language-plaintext highlighter-rouge">number</code>.</p>

<div class="language-applescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">on</span> <span class="nv">handleNameRestrictions</span><span class="p">(</span><span class="nv">theText</span><span class="p">)</span><span class="w">
    </span><span class="k">set</span><span class="w"> </span><span class="nv">theNewText</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="nv">theText</span><span class="w">
    </span><span class="k">set</span><span class="w"> </span><span class="nv">firstCharacter</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="nb">text</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">theText</span><span class="w">
    </span><span class="k">if</span><span class="w"> </span><span class="nv">isNumber</span><span class="p">(</span><span class="nv">firstCharacter</span><span class="p">)</span><span class="w"> </span><span class="k">then</span><span class="w">
        </span><span class="k">set</span><span class="w"> </span><span class="nv">theNewText</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="s2">"number"</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="nv">theText</span><span class="w">
    </span><span class="k">end</span><span class="w"> </span><span class="k">if</span><span class="w">

    </span><span class="nb">return</span><span class="w"> </span><span class="nv">convertReservedKeywords</span><span class="p">(</span><span class="nv">theNewText</span><span class="p">)</span><span class="w">
</span><span class="k">end</span><span class="w"> </span><span class="nv">handleNameRestrictions</span><span class="w">

</span><span class="k">on</span> <span class="nv">isNumber</span><span class="p">(</span><span class="nv">theString</span><span class="p">)</span><span class="w">
    </span><span class="k">try</span><span class="w">
        </span><span class="k">set</span><span class="w"> </span><span class="nv">theString</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="nv">theString</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="nv">number</span><span class="w">
        </span><span class="nb">return</span><span class="w"> </span><span class="nb">true</span><span class="w">
    </span><span class="nb">on</span><span class="w"> </span><span class="k">error</span><span class="w">
        </span><span class="nb">return</span><span class="w"> </span><span class="nb">false</span><span class="w">
    </span><span class="k">end</span><span class="w"> </span><span class="k">try</span><span class="w">
</span><span class="k">end</span><span class="w"> </span><span class="nv">isNumber</span><span class="w">
</span></code></pre></div></div>

<p>Also, as you may have noticed, a lot of the <code class="language-plaintext highlighter-rouge">SF Symbols</code> are using the dot (<code class="language-plaintext highlighter-rouge">.</code>) on their name, which is another invalid character for the case identifier. To solve this issue, we will convert the name of the symbol to a camelCase string that we will use as the identifier. To do so, we will remove the dot and instead capitalize the first letter after the dot.</p>

<div class="language-applescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">on</span> <span class="nv">toCamelCase</span><span class="p">(</span><span class="nv">theText</span><span class="p">)</span><span class="w">
    </span><span class="k">set</span><span class="w"> </span><span class="nv">theNewText</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="s2">""</span><span class="w">
    </span><span class="k">set</span><span class="w"> </span><span class="nv">dotIsFound</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="nb">false</span><span class="w">
    </span><span class="k">repeat</span><span class="w"> </span><span class="nv">with</span><span class="w"> </span><span class="nv">aCharacter</span><span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="nv">theText</span><span class="w">
        </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nv">aCharacter</span><span class="w"> </span><span class="k">as </span><span class="nc">string</span><span class="p">)</span><span class="w"> </span><span class="ow">is equal</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="s2">"."</span><span class="w"> </span><span class="k">then</span><span class="w">
            </span><span class="k">set</span><span class="w"> </span><span class="nv">dotIsFound</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="nb">true</span><span class="w">
        </span><span class="k">else</span><span class="w">
            </span><span class="k">if</span><span class="w"> </span><span class="nv">dotIsFound</span><span class="w"> </span><span class="k">then</span><span class="w">
                </span><span class="k">set</span><span class="w"> </span><span class="nv">theNewText</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="p">(</span><span class="nv">theNewText</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="nv">toCapitalized</span><span class="p">(</span><span class="nv">aCharacter</span><span class="p">))</span><span class="w"> </span><span class="k">as </span><span class="nc">string</span><span class="w">
            </span><span class="k">else</span><span class="w">
                </span><span class="k">set</span><span class="w"> </span><span class="nv">theNewText</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="nv">theNewText</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="nv">aCharacter</span><span class="w">
            </span><span class="k">end</span><span class="w"> </span><span class="k">if</span><span class="w">
            </span><span class="k">set</span><span class="w"> </span><span class="nv">dotIsFound</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="nb">false</span><span class="w">
        </span><span class="k">end</span><span class="w"> </span><span class="k">if</span><span class="w">

    </span><span class="k">end</span><span class="w"> </span><span class="k">repeat</span><span class="w">

    </span><span class="nb">return</span><span class="w"> </span><span class="nv">theNewText</span><span class="w">
</span><span class="k">end</span><span class="w"> </span><span class="nv">toCamelCase</span><span class="w">

</span><span class="k">on</span> <span class="nv">toCapitalized</span><span class="p">(</span><span class="nv">theText</span><span class="p">)</span><span class="w">
    </span><span class="k">set</span><span class="w"> </span><span class="nv">theNewText</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="s2">""</span><span class="w">

    </span><span class="k">set</span><span class="w"> </span><span class="nv">theComparisonCharacters</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="s2">"abcdefghijklmnopqrstuvwxyz"</span><span class="w">
    </span><span class="k">set</span><span class="w"> </span><span class="nv">theSourceCharacters</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="s2">"ABCDEFGHIJKLMNOPQRSTUVWXYZ"</span><span class="w">
    </span><span class="k">set</span><span class="w"> </span><span class="nv">firstCharacter</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="nb">text</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">theText</span><span class="w">

    </span><span class="k">set</span><span class="w"> </span><span class="nv">stringLength</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="nb">the</span><span class="w"> </span><span class="nv">length</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">theText</span><span class="w">
    </span><span class="k">set</span><span class="w"> </span><span class="nv">restOfText</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="s2">""</span><span class="w">
    </span><span class="k">if</span><span class="w"> </span><span class="nv">stringLength</span><span class="w"> </span><span class="ow">is greater than</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">then</span><span class="w">
        </span><span class="k">set</span><span class="w"> </span><span class="nv">restOfText</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="nb">text</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="nb">thru</span><span class="w"> </span><span class="o">-</span><span class="mi">1</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">theText</span><span class="w">
    </span><span class="k">end</span><span class="w"> </span><span class="k">if</span><span class="w">

    </span><span class="k">set</span><span class="w"> </span><span class="nv">theOffset</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="nb">offset</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">firstCharacter</span><span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="nv">theComparisonCharacters</span><span class="w">
    </span><span class="k">if</span><span class="w"> </span><span class="nv">theOffset</span><span class="w"> </span><span class="ow">is not</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="k">then</span><span class="w">
        </span><span class="k">set</span><span class="w"> </span><span class="nv">theNewText</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="p">(</span><span class="nv">theNewText</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="nb">character</span><span class="w"> </span><span class="nv">theOffset</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">theSourceCharacters</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="nv">restOfText</span><span class="p">)</span><span class="w"> </span><span class="k">as </span><span class="nc">string</span><span class="w">
    </span><span class="k">else</span><span class="w">
        </span><span class="k">set</span><span class="w"> </span><span class="nv">theNewText</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="p">(</span><span class="nv">theNewText</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="nv">firstCharacter</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="nv">restOfText</span><span class="p">)</span><span class="w"> </span><span class="k">as </span><span class="nc">string</span><span class="w">
    </span><span class="k">end</span><span class="w"> </span><span class="k">if</span><span class="w">
    </span><span class="nb">return</span><span class="w"> </span><span class="nv">theNewText</span><span class="w">
</span><span class="k">end</span><span class="w"> </span><span class="nv">toCapitalized</span><span class="w">
</span></code></pre></div></div>

<h2 id="implementation">Implementation</h2>

<p>With all those functions in place, we can now shift our focus on the part of our program that will interact with the <code class="language-plaintext highlighter-rouge">SF Symbols</code> app.</p>

<div class="language-applescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">activate</span><span class="w"> </span><span class="nb">application</span><span class="w"> </span><span class="s2">"SF Symbols"</span><span class="w">

</span><span class="k">tell</span><span class="w"> </span><span class="nb">application</span><span class="w"> </span><span class="s2">"System Events"</span><span class="w">
    </span><span class="k">tell</span><span class="w"> </span><span class="nv">process</span><span class="w"> </span><span class="s2">"SF Symbols"</span><span class="w">

        </span><span class="c1">-- Click the “list” radio button.</span><span class="w">
        </span><span class="nv">click</span><span class="w"> </span><span class="nv">radio</span><span class="w"> </span><span class="nb">button</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">radio</span><span class="w"> </span><span class="nv">group</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">group</span><span class="w"> </span><span class="mi">3</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="na">toolbar</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="na">window</span><span class="w"> </span><span class="mi">0</span><span class="w">

        </span><span class="k">tell</span><span class="w"> </span><span class="nv">outline</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nb">scroll</span><span class="w"> </span><span class="nv">area</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">splitter</span><span class="w"> </span><span class="nv">group</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="na">window</span><span class="w"> </span><span class="mi">0</span><span class="w">
            </span><span class="nb">select</span><span class="w"> </span><span class="p">(</span><span class="nv">row</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="nb">where</span><span class="w"> </span><span class="nv">value</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">static</span><span class="w"> </span><span class="nb">text</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">UI</span><span class="w"> </span><span class="nv">element</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="ow">starts with</span><span class="w"> </span><span class="s2">"All"</span><span class="p">)</span><span class="w">
        </span><span class="k">end</span><span class="w"> </span><span class="k">tell</span><span class="w">

        </span><span class="k">set</span><span class="w"> </span><span class="nv">enumCases</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="s2">""</span><span class="w">

        </span><span class="k">repeat</span><span class="w"> </span><span class="nv">with</span><span class="w"> </span><span class="nv">sfSymbolRow</span><span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="nv">rows</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">table</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nb">scroll</span><span class="w"> </span><span class="nv">area</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">group</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">splitter</span><span class="w"> </span><span class="nv">group</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="na">window</span><span class="w"> </span><span class="mi">0</span><span class="w">

            </span><span class="k">set</span><span class="w"> </span><span class="nv">sfSymbolName</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="nv">value</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">static</span><span class="w"> </span><span class="nb">text</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">UI</span><span class="w"> </span><span class="nv">element</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nv">sfSymbolRow</span><span class="w">

            </span><span class="k">set</span><span class="w"> </span><span class="nv">caseIdentifier</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="k">my</span><span class="w"> </span><span class="nv">toCamelCase</span><span class="p">(</span><span class="k">my</span><span class="w"> </span><span class="nv">handleNameRestrictions</span><span class="p">(</span><span class="nv">sfSymbolName</span><span class="p">))</span><span class="w">
            </span><span class="k">set</span><span class="w"> </span><span class="nv">enumCases</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="nv">enumCases</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="s2">"	case "</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="nv">caseIdentifier</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="s2">" = \""</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="nv">sfSymbolName</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="s2">"\"
"</span><span class="w">
        </span><span class="k">end</span><span class="w"> </span><span class="k">repeat</span><span class="w">

        </span><span class="k">set</span><span class="w"> </span><span class="nv">startOfEnum</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="s2">"public enum SFSymbol: String, CaseIterable {
"</span><span class="w">
        </span><span class="k">set</span><span class="w"> </span><span class="nv">endOfEnum</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="s2">"}"</span><span class="w">
        </span><span class="k">set</span><span class="w"> </span><span class="nv">enum</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="nv">startOfEnum</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="nv">enumCases</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="nv">endOfEnum</span><span class="w">

        </span><span class="nb">set the clipboard to</span><span class="w"> </span><span class="p">{</span><span class="nb">text</span><span class="p">:(</span><span class="nv">enum</span><span class="w"> </span><span class="k">as </span><span class="nc">string</span><span class="p">),</span><span class="w"> </span><span class="nv">Unicode</span><span class="w"> </span><span class="nb">text</span><span class="p">:</span><span class="nv">enum</span><span class="p">}</span><span class="w">
        </span><span class="nv">enum</span><span class="w">
    </span><span class="k">end</span><span class="w"> </span><span class="k">tell</span><span class="w">
</span><span class="k">end</span><span class="w"> </span><span class="k">tell</span><span class="w">
</span></code></pre></div></div>

<p>In this snippet, we initially select the list layout and the option <code class="language-plaintext highlighter-rouge">All</code> from the <code class="language-plaintext highlighter-rouge">Categories</code> as we explained earlier. Then, we create a string variable that will gather all the enum cases.
After that, we loop through the <code class="language-plaintext highlighter-rouge">SF Symbols</code>, and for each one of them, we use the functions we defined earlier to compute the identifier of the case. Then, we append a new case to the string we defined outside of the loop.</p>

<p>After the loop, we will define two more variables that will contain the definition and the trailing bracket of the enum respectively. We will then, merge those three variables to construct a variable with the final version of the enum.</p>

<p>Finally, we will add this to the clipboard to make it easier to paste on the project and use it as the output of the script.</p>

<p>And that’s about it when it comes to the script!</p>

<h2 id="but-what-about-sf-symbols-2">But what about SF Symbols 2?</h2>

<p>This script is also compatible with the new version of the <code class="language-plaintext highlighter-rouge">SF Symbols</code> app with only a minor change. Just replace the two occurrences of <code class="language-plaintext highlighter-rouge">"SF Symbols"</code> to <code class="language-plaintext highlighter-rouge">"SF Symbols beta"</code>.</p>

<p>A word of caution, though: be extra careful because there are some new <code class="language-plaintext highlighter-rouge">SF Symbols</code> that are only available on the latest OS versions while some others like the <code class="language-plaintext highlighter-rouge">bin.xmark</code> are deprecated in favor of new ones (<code class="language-plaintext highlighter-rouge">xmark.bin</code>).</p>

<blockquote>
  <p>You can find the script as well as the enums generated from both the <code class="language-plaintext highlighter-rouge">SF Symbols</code> and <code class="language-plaintext highlighter-rouge">SF Symbols 2</code> app on <a href="https://gist.github.com/diamantidis/7dfa8de52aa2f36a3c64ff30a16dd22a">this GitHub Gist</a>.</p>
</blockquote>

<p>Let’s now see how we can run it!</p>

<h2 id="how-to-run">How to run</h2>

<p>You can use either the <code class="language-plaintext highlighter-rouge">Script Editor</code> app or the <code class="language-plaintext highlighter-rouge">Terminal</code> app. If you use the <code class="language-plaintext highlighter-rouge">Script Editor</code> app, press the play button or use the shortcut Command (<code class="language-plaintext highlighter-rouge">⌘</code>) + R.</p>

<p>If you decide to run the script from the <code class="language-plaintext highlighter-rouge">Terminal</code>, you can use the command <code class="language-plaintext highlighter-rouge">osascript &lt;name of the script&gt;.scpt</code>. The output of this command will be the enum, which you can redirect to a Swift file on your project: <code class="language-plaintext highlighter-rouge">osascript &lt;name of the script&gt;.scpt &gt; GeneratedSFSymbols.swift</code>.</p>

<blockquote>
  <p>:bulb: <strong>TIP</strong>: You can place the script on a specific folder and then use the absolute path of this folder to create an alias on your <code class="language-plaintext highlighter-rouge">~/.bash_profile</code> or <code class="language-plaintext highlighter-rouge">~/.zshrc</code> file. <br /><br /> <code class="language-plaintext highlighter-rouge">alias sfsymbols='&lt;path to the script&gt;/&lt;name of the script&gt;.scpt'</code>. <br /><br /> This will allow you to run this script with ease from any project: <code class="language-plaintext highlighter-rouge">sfsymbols &gt; GeneratedSFSymbols.swift</code></p>
</blockquote>

<blockquote>
  <p>:grey_exclamation: <strong>INFO</strong>: Regardless of how you run the script, you will probably get a prompt to grant Accessibility Access like in the following screenshot. To grant the required access, go to <code class="language-plaintext highlighter-rouge">System Preferences</code> &gt; <code class="language-plaintext highlighter-rouge">Security &amp; Privacy</code> &gt; <code class="language-plaintext highlighter-rouge">Privacy</code> &gt; <code class="language-plaintext highlighter-rouge">Accessibility</code> and select the program that you are using to run the script.  <img src="https://diamantidis.github.io/assets/sf_symbols_applescript/apple_script_warning.png" alt="AppleScript warning screenshot" /></p>
</blockquote>

<p>Now that we have added the enum in our project, it’s time to find out how we can use it!</p>

<h2 id="how-to-use">How to use</h2>

<p>You can simply add an extension for the component you are interested in.</p>

<p>For example, if you are using SwiftUI, you can add an extension to the <code class="language-plaintext highlighter-rouge">Image</code> struct:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">SwiftUI</span>

<span class="kd">@available</span><span class="p">(</span><span class="n">iOS</span> <span class="mf">13.0</span><span class="p">,</span> <span class="n">macOS</span> <span class="mf">10.15</span><span class="p">,</span> <span class="n">tvOS</span> <span class="mf">13.0</span><span class="p">,</span> <span class="n">watchOS</span> <span class="mf">6.0</span><span class="p">,</span> <span class="o">*</span><span class="p">)</span>
<span class="kd">public</span> <span class="kd">extension</span> <span class="kt">Image</span> <span class="p">{</span>
    <span class="c1">/// Creates an image object containing a system symbol image.</span>
    <span class="c1">///</span>
    <span class="c1">/// - Parameter sfSymbol: The name of the `SFSymbol`</span>
    <span class="c1">/// - Usage</span>
    <span class="c1">///   ```</span>
    <span class="c1">///   Image(sfSymbol: .checkmarkCircleFill)</span>
    <span class="c1">///   ```</span>
    <span class="kd">@available</span><span class="p">(</span><span class="kt">OSX</span><span class="p">,</span> <span class="n">unavailable</span><span class="p">)</span>
    <span class="nf">init</span><span class="p">(</span><span class="nv">sfSymbol</span><span class="p">:</span> <span class="kt">SFSymbol</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">self</span><span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="nv">systemName</span><span class="p">:</span> <span class="n">sfSymbol</span><span class="o">.</span><span class="n">rawValue</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Similarly, if you are using UIKit’s UIImage, you can use the following extension:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">UIKit</span>

<span class="kd">@available</span><span class="p">(</span><span class="n">iOS</span> <span class="mf">13.0</span><span class="p">,</span> <span class="o">*</span><span class="p">)</span>
<span class="kd">public</span> <span class="kd">extension</span> <span class="kt">UIImage</span> <span class="p">{</span>
    <span class="c1">/// Creates an image object containing a system symbol image.</span>
    <span class="c1">///</span>
    <span class="c1">/// - Parameter sfSymbol: The name of the `SFSymbol`</span>
    <span class="c1">/// - Usage</span>
    <span class="c1">///   ```</span>
    <span class="c1">///   UIImage(sfSymbol: .checkmarkCircleFill)</span>
    <span class="c1">///   ```</span>
    <span class="kd">convenience</span> <span class="nf">init</span><span class="p">?(</span><span class="nv">sfSymbol</span><span class="p">:</span> <span class="kt">SFSymbol</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">self</span><span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="nv">systemName</span><span class="p">:</span> <span class="n">sfSymbol</span><span class="o">.</span><span class="n">rawValue</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="conclusion">Conclusion</h2>

<p>And that’s about it! In this post, we have seen how we can utilize the ability of AppleScript to interact with other macOS apps to generate an enum for all the symbols on the <code class="language-plaintext highlighter-rouge">SF Symbols</code> app.
This will alleviate all the pains that come with the use of an error-prone string literal when we create an Image based on an <code class="language-plaintext highlighter-rouge">SF Symbol</code>. Instead, with this enum we will be able to create those Images in a type-safe manner.</p>

<p>Thanks for reading! If you found this post useful, consider <a href="https://www.buymeacoffee.com/diamantidis">buying me a coffee</a> to support the blog. For questions or comments, feel free to reach out on <a href="https://x.com/diamantidis_io">X</a>!</p>

<p>Until next time!</p>]]></content><author><name>Ioannis Diamantidis</name><email>diamantidis@outlook.com</email></author><category term="AppleScript" /><category term="SF Symbols" /><summary type="html"><![CDATA[Use SF Symbols in a type-safe manner with an enum generated with AppleScript]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://diamantidis.github.io/assets/social/applescript-to-generate-enum-for-sf-symbols.png" /><media:content medium="image" url="https://diamantidis.github.io/assets/social/applescript-to-generate-enum-for-sf-symbols.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Keyboards options for SwiftUI fields</title><link href="https://diamantidis.github.io/2020/06/21/keyboard-options-for-swiftui-fields" rel="alternate" type="text/html" title="Keyboards options for SwiftUI fields" /><published>2020-06-21T04:00:00+00:00</published><updated>2021-10-30T04:00:00+00:00</updated><id>https://diamantidis.github.io/2020/06/21/keyboard-options-for-swiftui-fields</id><content type="html" xml:base="https://diamantidis.github.io/2020/06/21/keyboard-options-for-swiftui-fields"><![CDATA[<p>One of the ways to improve the user experience when they fill a form on iOS apps is the use of keyboard types. Keyboard types can be used to show different keyboards based on the context of the field. If it’s an email field, for example, you would probably prefer to present a keyboard with easy access to characters like the at (<code class="language-plaintext highlighter-rouge">@</code>) and the dot (<code class="language-plaintext highlighter-rouge">.</code>).</p>

<p>In this post, we are going to see how different keyboard options work with SwiftUI. First, we will cover the “common” scenarios, which include keyboards for fields like name, email, number, etc. Then, we will take a look at how we can add more custom options like a keyboard with a picker view.</p>

<h2 id="the-common-cases">The common cases</h2>

<p>For the most common cases, SwiftUI provides the function <code class="language-plaintext highlighter-rouge">keyboardType</code> in a <code class="language-plaintext highlighter-rouge">View</code> extension. This function has a parameter of type <code class="language-plaintext highlighter-rouge">UIKeyboardType</code>, which is an enum with cases like <code class="language-plaintext highlighter-rouge">emailAddress</code>, <code class="language-plaintext highlighter-rouge">numberPad</code>, <code class="language-plaintext highlighter-rouge">URL</code>, etc.</p>

<blockquote>
  <p>For the full set of cases, you can refer to the <a href="https://developer.apple.com/documentation/uikit/uikeyboardtype">documentation page</a>.</p>
</blockquote>

<p>You can use this extension from any SwiftUI View in the following way:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">TextField</span><span class="p">(</span><span class="s">"Type the email..."</span><span class="p">,</span> <span class="nv">text</span><span class="p">:</span> <span class="err">$</span><span class="n">email</span><span class="p">)</span>
    <span class="o">.</span><span class="nf">keyboardType</span><span class="p">(</span><span class="o">.</span><span class="n">emailAddress</span><span class="p">)</span>
</code></pre></div></div>

<p><img src="https://diamantidis.github.io/assets/swiftui_textfield_keyboards/email-keyboard.png" alt="Email keyboard screenshot" /></p>

<p>But what if we want to add a custom view? For example, a picker view, just like we can do using UIKit?</p>

<h2 id="keyboard-with-picker-view">Keyboard with picker view</h2>

<p>As you may have guessed, the answer lies exactly there.</p>

<p>We will have to use UIKit’s <code class="language-plaintext highlighter-rouge">UITextField</code> and make it available to SwiftUI by creating a struct with conformance to the <code class="language-plaintext highlighter-rouge">UIViewRepresentable</code> protocol.</p>

<p>But let’s take it step by step and see how we can create <code class="language-plaintext highlighter-rouge">PickerField</code>; a field that will show a keyboard with a picker, just like in the following screenshot.</p>

<p><img src="https://diamantidis.github.io/assets/swiftui_textfield_keyboards/picker-view-keyboard.png" alt="Picker view keyboard screenshot" /></p>

<p>To keep things separated, let’s create a subclass of <code class="language-plaintext highlighter-rouge">UITextField</code> where we will implement the logic to show a keyboard with a picker.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">SwiftUI</span>

<span class="kd">class</span> <span class="kt">PickerTextField</span><span class="p">:</span> <span class="kt">UITextField</span> <span class="p">{</span>
    <span class="c1">// MARK: - Public properties</span>
    <span class="k">var</span> <span class="nv">data</span><span class="p">:</span> <span class="p">[</span><span class="kt">String</span><span class="p">]</span>
    <span class="kd">@Binding</span> <span class="k">var</span> <span class="nv">selectionIndex</span><span class="p">:</span> <span class="kt">Int</span><span class="p">?</span>

    <span class="c1">// MARK: - Initializers</span>
    <span class="nf">init</span><span class="p">(</span><span class="nv">data</span><span class="p">:</span> <span class="p">[</span><span class="kt">String</span><span class="p">],</span> <span class="nv">selectionIndex</span><span class="p">:</span> <span class="kt">Binding</span><span class="o">&lt;</span><span class="kt">Int</span><span class="p">?</span><span class="o">&gt;</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">self</span><span class="o">.</span><span class="n">data</span> <span class="o">=</span> <span class="n">data</span>
        <span class="k">self</span><span class="o">.</span><span class="n">_selectionIndex</span> <span class="o">=</span> <span class="n">selectionIndex</span>
        <span class="k">super</span><span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="nv">frame</span><span class="p">:</span> <span class="o">.</span><span class="n">zero</span><span class="p">)</span>

        <span class="k">self</span><span class="o">.</span><span class="n">inputView</span> <span class="o">=</span> <span class="n">pickerView</span>
        <span class="k">self</span><span class="o">.</span><span class="n">inputAccessoryView</span> <span class="o">=</span> <span class="n">toolbar</span>
        <span class="k">self</span><span class="o">.</span><span class="n">tintColor</span> <span class="o">=</span> <span class="o">.</span><span class="n">clear</span>

        <span class="k">guard</span> <span class="k">let</span> <span class="nv">selectionIndex</span> <span class="o">=</span> <span class="n">selectionIndex</span><span class="o">.</span><span class="n">wrappedValue</span> <span class="k">else</span> <span class="p">{</span>
            <span class="k">return</span>
        <span class="p">}</span>

        <span class="k">self</span><span class="o">.</span><span class="n">pickerView</span><span class="o">.</span><span class="nf">selectRow</span><span class="p">(</span><span class="n">selectionIndex</span><span class="p">,</span> <span class="nv">inComponent</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="nv">animated</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="kd">@available</span><span class="p">(</span><span class="o">*</span><span class="p">,</span> <span class="n">unavailable</span><span class="p">)</span>
    <span class="kd">required</span> <span class="nf">init</span><span class="p">?(</span><span class="nv">coder</span><span class="p">:</span> <span class="kt">NSCoder</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">fatalError</span><span class="p">(</span><span class="s">"init(coder:) has not been implemented"</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="c1">// MARK: - Private properties</span>
    <span class="kd">private</span> <span class="kd">lazy</span> <span class="k">var</span> <span class="nv">pickerView</span><span class="p">:</span> <span class="kt">UIPickerView</span> <span class="o">=</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">pickerView</span> <span class="o">=</span> <span class="kt">UIPickerView</span><span class="p">()</span>
        <span class="n">pickerView</span><span class="o">.</span><span class="n">delegate</span> <span class="o">=</span> <span class="k">self</span>
        <span class="n">pickerView</span><span class="o">.</span><span class="n">dataSource</span> <span class="o">=</span> <span class="k">self</span>
        <span class="k">return</span> <span class="n">pickerView</span>
    <span class="p">}()</span>

    <span class="kd">private</span> <span class="kd">lazy</span> <span class="k">var</span> <span class="nv">toolbar</span><span class="p">:</span> <span class="kt">UIToolbar</span> <span class="o">=</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">toolbar</span> <span class="o">=</span> <span class="kt">UIToolbar</span><span class="p">()</span>

        <span class="k">let</span> <span class="nv">flexibleSpace</span> <span class="o">=</span> <span class="kt">UIBarButtonItem</span><span class="p">(</span><span class="nv">barButtonSystemItem</span><span class="p">:</span> <span class="o">.</span><span class="n">flexibleSpace</span><span class="p">,</span> <span class="nv">target</span><span class="p">:</span> <span class="kc">nil</span><span class="p">,</span> <span class="nv">action</span><span class="p">:</span> <span class="kc">nil</span><span class="p">)</span>

        <span class="k">let</span> <span class="nv">doneButton</span> <span class="o">=</span> <span class="kt">UIBarButtonItem</span><span class="p">(</span>
            <span class="nv">title</span><span class="p">:</span> <span class="s">"Done"</span><span class="p">,</span>
            <span class="nv">style</span><span class="p">:</span> <span class="o">.</span><span class="n">done</span><span class="p">,</span>
            <span class="nv">target</span><span class="p">:</span> <span class="k">self</span><span class="p">,</span>
            <span class="nv">action</span><span class="p">:</span> <span class="kd">#selector(</span><span class="nf">donePressed</span><span class="kd">)</span>
        <span class="p">)</span>

        <span class="n">toolbar</span><span class="o">.</span><span class="nf">setItems</span><span class="p">([</span><span class="n">flexibleSpace</span><span class="p">,</span> <span class="n">doneButton</span><span class="p">],</span> <span class="nv">animated</span><span class="p">:</span> <span class="kc">false</span><span class="p">)</span>
        <span class="n">toolbar</span><span class="o">.</span><span class="nf">sizeToFit</span><span class="p">()</span>
        <span class="k">return</span> <span class="n">toolbar</span>
    <span class="p">}()</span>

    <span class="c1">// MARK: - Private methods</span>
    <span class="kd">@objc</span>
    <span class="kd">private</span> <span class="kd">func</span> <span class="nf">donePressed</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">self</span><span class="o">.</span><span class="n">selectionIndex</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="n">pickerView</span><span class="o">.</span><span class="nf">selectedRow</span><span class="p">(</span><span class="nv">inComponent</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span>
        <span class="k">self</span><span class="o">.</span><span class="nf">endEditing</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// MARK: - UIPickerViewDataSource &amp; UIPickerViewDelegate extension</span>
<span class="kd">extension</span> <span class="kt">PickerTextField</span><span class="p">:</span> <span class="kt">UIPickerViewDataSource</span><span class="p">,</span> <span class="kt">UIPickerViewDelegate</span> <span class="p">{</span>
    <span class="kd">func</span> <span class="nf">numberOfComponents</span><span class="p">(</span><span class="k">in</span> <span class="nv">pickerView</span><span class="p">:</span> <span class="kt">UIPickerView</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Int</span> <span class="p">{</span>
        <span class="k">return</span> <span class="mi">1</span>
    <span class="p">}</span>

    <span class="kd">func</span> <span class="nf">pickerView</span><span class="p">(</span><span class="n">_</span> <span class="nv">pickerView</span><span class="p">:</span> <span class="kt">UIPickerView</span><span class="p">,</span> <span class="n">numberOfRowsInComponent</span> <span class="nv">component</span><span class="p">:</span> <span class="kt">Int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Int</span> <span class="p">{</span>
        <span class="k">return</span> <span class="k">self</span><span class="o">.</span><span class="n">data</span><span class="o">.</span><span class="n">count</span>
    <span class="p">}</span>

    <span class="kd">func</span> <span class="nf">pickerView</span><span class="p">(</span><span class="n">_</span> <span class="nv">pickerView</span><span class="p">:</span> <span class="kt">UIPickerView</span><span class="p">,</span> <span class="n">titleForRow</span> <span class="nv">row</span><span class="p">:</span> <span class="kt">Int</span><span class="p">,</span> <span class="n">forComponent</span> <span class="nv">component</span><span class="p">:</span> <span class="kt">Int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">String</span><span class="p">?</span> <span class="p">{</span>
        <span class="k">return</span> <span class="k">self</span><span class="o">.</span><span class="n">data</span><span class="p">[</span><span class="n">row</span><span class="p">]</span>
    <span class="p">}</span>

    <span class="kd">func</span> <span class="nf">pickerView</span><span class="p">(</span><span class="n">_</span> <span class="nv">pickerView</span><span class="p">:</span> <span class="kt">UIPickerView</span><span class="p">,</span> <span class="n">didSelectRow</span> <span class="nv">row</span><span class="p">:</span> <span class="kt">Int</span><span class="p">,</span> <span class="n">inComponent</span> <span class="nv">component</span><span class="p">:</span> <span class="kt">Int</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">self</span><span class="o">.</span><span class="n">selectionIndex</span> <span class="o">=</span> <span class="n">row</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>In this snippet, first, we declare two properties; one with the options of the picker and a <code class="language-plaintext highlighter-rouge">Binding</code> property which acts both as an input in case the field is pre-filled and as an output for when the user changes the value of the picker.</p>

<p>Then, in the initializer, we are expecting two arguments that we will use to instantiate the two properties. After that, we set the property <code class="language-plaintext highlighter-rouge">inputView</code> to the instance of the <code class="language-plaintext highlighter-rouge">UIPickerView</code>, and use the <code class="language-plaintext highlighter-rouge">inputAccessoryView</code> to set a <code class="language-plaintext highlighter-rouge">UIToolbar</code> with a “Done” button to help users dismiss the keyboard.</p>

<p>Finally, if there is a pre-selected value, we select this option from the <code class="language-plaintext highlighter-rouge">UIPickerView</code>.</p>

<p>In this file, we also add an extension to provide the implementation for the functions to conform to <code class="language-plaintext highlighter-rouge">UIPickerViewDataSource</code> and <code class="language-plaintext highlighter-rouge">UIPickerViewDelegate</code> and set the <code class="language-plaintext highlighter-rouge">dataSource</code> and the <code class="language-plaintext highlighter-rouge">delegate</code> for the <code class="language-plaintext highlighter-rouge">UIPickerView</code> to self.</p>

<p>Worth noting is the implementation of <code class="language-plaintext highlighter-rouge">didSelectRow</code>, where we set the selected row to the <code class="language-plaintext highlighter-rouge">Binding</code> property <code class="language-plaintext highlighter-rouge">selectionIndex</code> to pass this info to the parent view.</p>

<p>With that in place, we will create a new struct that we will use to communicate between the UIKit view and the SwiftUI world.</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">SwiftUI</span>

<span class="kd">struct</span> <span class="kt">PickerField</span><span class="p">:</span> <span class="kt">UIViewRepresentable</span> <span class="p">{</span>
    <span class="c1">// MARK: - Public properties</span>
    <span class="kd">@Binding</span> <span class="k">var</span> <span class="nv">selectionIndex</span><span class="p">:</span> <span class="kt">Int</span><span class="p">?</span>

    <span class="c1">// MARK: - Initializers</span>
    <span class="kd">init</span><span class="o">&lt;</span><span class="kt">S</span><span class="o">&gt;</span><span class="p">(</span><span class="n">_</span> <span class="nv">title</span><span class="p">:</span> <span class="kt">S</span><span class="p">,</span> <span class="nv">data</span><span class="p">:</span> <span class="p">[</span><span class="kt">String</span><span class="p">],</span> <span class="nv">selectionIndex</span><span class="p">:</span> <span class="kt">Binding</span><span class="o">&lt;</span><span class="kt">Int</span><span class="p">?</span><span class="o">&gt;</span><span class="p">)</span> <span class="k">where</span> <span class="kt">S</span><span class="p">:</span> <span class="kt">StringProtocol</span> <span class="p">{</span>
        <span class="k">self</span><span class="o">.</span><span class="n">placeholder</span> <span class="o">=</span> <span class="kt">String</span><span class="p">(</span><span class="n">title</span><span class="p">)</span>
        <span class="k">self</span><span class="o">.</span><span class="n">data</span> <span class="o">=</span> <span class="n">data</span>
        <span class="k">self</span><span class="o">.</span><span class="n">_selectionIndex</span> <span class="o">=</span> <span class="n">selectionIndex</span>

        <span class="n">textField</span> <span class="o">=</span> <span class="kt">PickerTextField</span><span class="p">(</span><span class="nv">data</span><span class="p">:</span> <span class="n">data</span><span class="p">,</span> <span class="nv">selectionIndex</span><span class="p">:</span> <span class="n">selectionIndex</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="c1">// MARK: - Public methods</span>
    <span class="kd">func</span> <span class="nf">makeUIView</span><span class="p">(</span><span class="nv">context</span><span class="p">:</span> <span class="kt">UIViewRepresentableContext</span><span class="o">&lt;</span><span class="kt">PickerField</span><span class="o">&gt;</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">UITextField</span> <span class="p">{</span>
        <span class="n">textField</span><span class="o">.</span><span class="n">placeholder</span> <span class="o">=</span> <span class="n">placeholder</span>
        <span class="k">return</span> <span class="n">textField</span>
    <span class="p">}</span>

    <span class="kd">func</span> <span class="nf">updateUIView</span><span class="p">(</span><span class="n">_</span> <span class="nv">uiView</span><span class="p">:</span> <span class="kt">UITextField</span><span class="p">,</span> <span class="nv">context</span><span class="p">:</span> <span class="kt">UIViewRepresentableContext</span><span class="o">&lt;</span><span class="kt">PickerField</span><span class="o">&gt;</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="k">let</span> <span class="nv">index</span> <span class="o">=</span> <span class="n">selectionIndex</span> <span class="p">{</span>
            <span class="n">uiView</span><span class="o">.</span><span class="n">text</span> <span class="o">=</span> <span class="n">data</span><span class="p">[</span><span class="n">index</span><span class="p">]</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="n">uiView</span><span class="o">.</span><span class="n">text</span> <span class="o">=</span> <span class="s">""</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="c1">// MARK: - Private properties</span>
    <span class="kd">private</span> <span class="k">var</span> <span class="nv">placeholder</span><span class="p">:</span> <span class="kt">String</span>
    <span class="kd">private</span> <span class="k">var</span> <span class="nv">data</span><span class="p">:</span> <span class="p">[</span><span class="kt">String</span><span class="p">]</span>
    <span class="kd">private</span> <span class="k">let</span> <span class="nv">textField</span><span class="p">:</span> <span class="kt">PickerTextField</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Same as the <code class="language-plaintext highlighter-rouge">PickerTextField</code> class, this struct will also have two properties; a list with the options and a binding property for the value of the field.</p>

<p>Once again, we will use the initializer to pass the values of these properties via its arguments. The initializer takes one more argument for the placeholder of the <code class="language-plaintext highlighter-rouge">UITextField</code> if the field has no value.</p>

<p>Then, we provide the implementations for the <code class="language-plaintext highlighter-rouge">makeUIView</code> and <code class="language-plaintext highlighter-rouge">updateUIView</code> requirements of the <code class="language-plaintext highlighter-rouge">UIViewRepresentable</code> protocol.</p>

<p>In the first one, we set the initial state for the text field by setting the value for the placeholder. In the second, we update the value of the <code class="language-plaintext highlighter-rouge">PickerField</code> with any new information we might get from SwiftUI.</p>

<p>Finally, from any SwiftUI view, we can use this struct in the following way:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@State</span> <span class="k">var</span> <span class="nv">selectedIndex</span><span class="p">:</span> <span class="kt">Int</span><span class="p">?</span> <span class="o">=</span> <span class="kc">nil</span>
<span class="k">let</span> <span class="nv">options</span><span class="p">:</span> <span class="p">[</span><span class="kt">String</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="s">"GraphQL"</span><span class="p">,</span> <span class="s">"Swift"</span><span class="p">,</span> <span class="s">"Vapor"</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="c1">// ...</span>
    <span class="kt">PickerField</span><span class="p">(</span><span class="s">"Select an option"</span><span class="p">,</span> <span class="nv">data</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">options</span><span class="p">,</span> <span class="nv">selectionIndex</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="err">$</span><span class="n">selectedIndex</span><span class="p">)</span>
    <span class="c1">// ...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>You can find the code for this <a href="https://gist.github.com/diamantidis/061d101853f6400f76780345614b2c90#file-pickertextfield-swift"><code class="language-plaintext highlighter-rouge">PickerTextField</code></a>, as well as <a href="https://gist.github.com/diamantidis/061d101853f6400f76780345614b2c90#file-pickerfield-swift">the rest of the code</a> and <a href="https://gist.github.com/diamantidis/061d101853f6400f76780345614b2c90#file-contentview-swift">an example of how to use it from a SwiftUI View</a> on this <a href="https://gist.github.com/diamantidis/061d101853f6400f76780345614b2c90">Gist</a>.</p>

<h2 id="conclusion">Conclusion</h2>
<p>And that’s about it! In this post, we have first seen what is the current level of support for keyboard types on SwiftUI’s <code class="language-plaintext highlighter-rouge">TextField</code>. Then, we investigated how we can provide support for more options by relying on the UIKit’s UITextField, and its property <code class="language-plaintext highlighter-rouge">inputView</code>. This way, we can present a keyboard with a <code class="language-plaintext highlighter-rouge">UIPickerView</code>, or any other view we may want.</p>

<p>Thanks for reading! If you found this post useful, consider <a href="https://www.buymeacoffee.com/diamantidis">buying me a coffee</a> to support the blog. For questions or comments, feel free to reach out on <a href="https://x.com/diamantidis_io">X</a>!</p>

<p>Until next time!</p>]]></content><author><name>Ioannis Diamantidis</name><email>diamantidis@outlook.com</email></author><category term="Swift" /><category term="SwiftUI" /><category term="iOS" /><summary type="html"><![CDATA[A post exploring the keyboard options on SwiftUI. From the keyboardType function to using custom views like a picker view]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://diamantidis.github.io/assets/social/keyboard-options-for-swiftui-fields.png" /><media:content medium="image" url="https://diamantidis.github.io/assets/social/keyboard-options-for-swiftui-fields.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>