<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Marmalade, of course</title><link href="https://marmaladeofcourse.com/" rel="alternate"></link><link href="https://marmaladeofcourse.com/feed.xml" rel="self"></link><id>https://marmaladeofcourse.com/</id><updated>2025-11-26T00:00:00+00:00</updated><entry><title>Brickset LEGO Gift Guide - 2025</title><link href="https://marmaladeofcourse.com/2025/11/26/brickset-lego-gift-guide-2025/" rel="alternate"></link><published>2025-11-26T00:00:00+00:00</published><updated>2025-11-26T00:00:00+00:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2025-11-26:/2025/11/26/brickset-lego-gift-guide-2025/</id><summary type="html">&lt;p&gt;It&amp;rsquo;s that time of the year again (where on earth has 2025 gone?!) where Brickset publish their annual holiday gift guide, in five parts split by price category. Along with the other Brickset contributors, I provided my opinion, and there&amp;rsquo;s a nice diverse range of sets across the …&lt;/p&gt;</summary><content type="html">&lt;p&gt;It&amp;rsquo;s that time of the year again (where on earth has 2025 gone?!) where Brickset publish their annual holiday gift guide, in five parts split by price category. Along with the other Brickset contributors, I provided my opinion, and there&amp;rsquo;s a nice diverse range of sets across the price ranges! You can view each of the articles below, along with my pick from the available choices:&lt;/p&gt;
&lt;h4 id="under-25"&gt;&lt;a href="https://brickset.com/article/126965/holiday-gift-guide-under-25"&gt;Under $25&lt;/a&gt;&lt;a class="headerlink" href="#under-25" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://brickset.com/sets/10349-1"&gt;10349&lt;/a&gt; Happy Plants&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="25-50"&gt;&lt;a href="https://brickset.com/article/126964/holiday-gift-guide-25-50"&gt;$25-$50&lt;/a&gt;&lt;a class="headerlink" href="#25-50" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://brickset.com/sets/31165-1"&gt;31165&lt;/a&gt; Panda Family&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="50-100"&gt;&lt;a href="https://brickset.com/article/126963/holiday-gift-guide-50-100"&gt;$50-$100&lt;/a&gt;&lt;a class="headerlink" href="#50-100" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://brickset.com/sets/72046-1"&gt;72046&lt;/a&gt; Game Boy&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="100-200"&gt;&lt;a href="https://brickset.com/article/126962"&gt;$100-$200&lt;/a&gt;&lt;a class="headerlink" href="#100-200" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://brickset.com/sets/31216-1"&gt;31216 &lt;/a&gt;Keith Haring - Dancing Figures&lt;/li&gt;
&lt;li&gt;&lt;small&gt;honorary mention of &lt;a href="https://brickset.com/sets/72037-1"&gt;72037&lt;/a&gt; Mario Kart - Mario &amp;amp; Standard Kart&lt;/small&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="200"&gt;&lt;a href="https://brickset.com/article/126961"&gt;$200+&lt;/a&gt;&lt;a class="headerlink" href="#200" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://brickset.com/sets/76457-1"&gt;76457 &lt;/a&gt;Hogsmeade Village - Collectors&amp;rsquo; Edition&lt;/li&gt;
&lt;li&gt;&lt;small&gt;honorary mention of &lt;a href="https://brickset.com/sets/76968-1"&gt;76968&lt;/a&gt; Dinosaur Fossils: Tyrannosaurus rex&lt;/small&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What would your choices be?&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://images.brickset.com/news/126963_hgg25.jpg" /&gt;&lt;/p&gt;</content><category term="LEGO"></category></entry><entry><title>Pendulum 1.15 Released</title><link href="https://marmaladeofcourse.com/2025/11/06/pendulum-115-released/" rel="alternate"></link><published>2025-11-06T00:00:00+00:00</published><updated>2025-11-06T00:00:00+00:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2025-11-06:/2025/11/06/pendulum-115-released/</id><summary type="html">&lt;p&gt;After quite some time without any updates, I have finally released a new version of &lt;a href="/pendulum"&gt;Pendulum&lt;/a&gt;—version 1.15 is now available in the App Store!&lt;/p&gt;
&lt;p&gt;&lt;img alt="Pendulum 1.15" src="/assets/pendulum-1-15.png" /&gt;&lt;/p&gt;
&lt;p&gt;With the release of iOS 26 and it&amp;rsquo;s new Liquid Glass design language, I wanted to update Pendulum to feel more at home …&lt;/p&gt;</summary><content type="html">&lt;p&gt;After quite some time without any updates, I have finally released a new version of &lt;a href="/pendulum"&gt;Pendulum&lt;/a&gt;—version 1.15 is now available in the App Store!&lt;/p&gt;
&lt;p&gt;&lt;img alt="Pendulum 1.15" src="/assets/pendulum-1-15.png" /&gt;&lt;/p&gt;
&lt;p&gt;With the release of iOS 26 and it&amp;rsquo;s new Liquid Glass design language, I wanted to update Pendulum to feel more at home on the OS, and took the opportunity to change the design somewhat more drastically—at least for the main Pen Pals list. The biggest structural change was to drop the tab bar—it makes little sense when there are only two tabs—and move Settings to a top bar button. This meant I could take advantage of the new navigation transition available in iOS 26 to &lt;a href="https://nilcoalescing.com/blog/PresentingLiquidGlassSheetsInSwiftUI/"&gt;morph between the button and the presented sheet&lt;/a&gt; in a very pleasing manner.&lt;/p&gt;
&lt;p&gt;Visually, the Pen Pal list has had a huge overhaul, with the standout change being the map in the background. This will centre itself on the location you most recently sent a letter to or received one from, and I love the way it turned out. I hope it&amp;rsquo;ll be fun for Pendulum&amp;rsquo;s users to see it pan around the world as they send and receiving their correspondence. A further update may pull the map out into its own feature, with pins for your pen pals&amp;rsquo; locations in a more interactive manner.&lt;/p&gt;
&lt;p&gt;Given the design now focuses rather heavily on your pen pals&amp;rsquo; addresses, it was about time I addressed (pun intended) the inability for you to store an address against a pen pal if you&amp;rsquo;ve disabled syncing with Contacts. It&amp;rsquo;s a fairly commonly-requested feature, and this was the impetus I needed to finally get it done. Both types of Pen Pals can live happily together in Pendulum—those synced with a device contact will require you to use Contacts to update their address, and those added manually can be edited directly within the app.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Storing addresses locally in Pendulum 1.15" src="/assets/pendulum-1-15-addresses.png" /&gt;&lt;/p&gt;
&lt;p&gt;The final feature in this release can be seen in the screenshot above, in Amelia&amp;rsquo;s contact details page, Pendulum will pull in any nicknames you have against your linked Contacts, with an option to prefer nicknames over full names in most of Pendulum&amp;rsquo;s UI. Amelia will be shown as Millie in the Pen Pal list and her correspondence screen, for example.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;It&amp;rsquo;s been fun to have the motivation and impetus to work on Pendulum again, and I&amp;rsquo;m excited to keep adding features and iterating as I go.&lt;/p&gt;</content><category term="Development"></category><category term="Swift"></category><category term="SwiftUI"></category><category term="Apps"></category></entry><entry><title>Django Forms and CSV Processing</title><link href="https://marmaladeofcourse.com/2025/09/01/django-forms-and-csv-processing/" rel="alternate"></link><published>2025-09-01T00:00:00+01:00</published><updated>2025-09-01T00:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2025-09-01:/2025/09/01/django-forms-and-csv-processing/</id><summary type="html">&lt;p&gt;Recently, I&amp;rsquo;ve found myself building a number of tools that accept input data in the form of a CSV from the user, and parse and validate it before executing whatever processing is necessary for the given tool. Django&amp;rsquo;s forms provide an easy way to accept file uploads, and …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Recently, I&amp;rsquo;ve found myself building a number of tools that accept input data in the form of a CSV from the user, and parse and validate it before executing whatever processing is necessary for the given tool. Django&amp;rsquo;s forms provide an easy way to accept file uploads, and model forms make it trivial to store those files on disk alongside a model instance.&lt;/p&gt;
&lt;p&gt;What I was struggling with was where to do the validation of the contents of the CSV. Forms provide some level of validation, and give the user feedback via field- or form-level errors, but in order to provide &lt;em&gt;useful&lt;/em&gt; feedback via this mechanism the form needs to parse the CSV to validate each of its rows. This is easily doable in the field-specific clean method, such as the following, which validates that every row has an &lt;code&gt;identifier&lt;/code&gt; field and a &lt;code&gt;date&lt;/code&gt; field, with the date in the future (using &lt;a href="https://arrow.readthedocs.io/en/latest/"&gt;&lt;code&gt;arrow&lt;/code&gt;&lt;/a&gt; for date parsing):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;csv&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;arrow&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;django&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;forms&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;django.db&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;django.utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CSVUpload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;uploaded_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DateTimeField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auto_now_add&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;csv_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FileField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;upload_to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;csv_uploads&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CSVUploadForm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;forms&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModelForm&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CSVUpload&lt;/span&gt;
        &lt;span class="n"&gt;fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;csv_file&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;clean_csv_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;csv_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cleaned_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;csv_file&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DictReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csv_file&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;identifier&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Missing identifier&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;arrow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;date&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Date not in the future&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;forms&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ValidationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Could not read CSV file; please ensure it is in the correct format (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;csv_file&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This works fine—the form won&amp;rsquo;t pass the &lt;code&gt;is_valid()&lt;/code&gt; check unless the data within the CSV is valid. So what&amp;rsquo;s the problem?&lt;/p&gt;
&lt;p&gt;Well, there&amp;rsquo;s a reason we&amp;rsquo;re reading the CSV and validating the data—we want to &lt;em&gt;do&lt;/em&gt; something with the data. The form has parsed it, but it&amp;rsquo;s thrown away any results of that parsing, leaving the view to have to do it all over again, which is less than ideal. We can&amp;rsquo;t simply return the parsed data from the &lt;code&gt;clean&lt;/code&gt; method, because that would break Django&amp;rsquo;s &lt;code&gt;FileField&lt;/code&gt; handling within the model form. Instead, we can take advantage of the fact that a &lt;code&gt;Form&lt;/code&gt; instance is just that—a standard Python object, nothing special—and set an attribute on the instance with the parsed data:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CSVUploadForm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;forms&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModelForm&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CSVUpload&lt;/span&gt;
        &lt;span class="n"&gt;fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;csv_file&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;clean_csv_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;csv_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cleaned_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;csv_file&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="hll"&gt;        &lt;span class="n"&gt;parsed_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;/span&gt;        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DictReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csv_file&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="hll"&gt;                &lt;span class="n"&gt;row_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                    &lt;span class="s2"&gt;&amp;quot;identifier&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;identifier&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                    &lt;span class="s2"&gt;&amp;quot;date&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;arrow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;date&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;row_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;identifier&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Missing identifier&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;date&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Date not in the future&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="hll"&gt;                &lt;span class="n"&gt;parsed_data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;forms&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ValidationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Could not read CSV file; please ensure it is in the correct format (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="hll"&gt;        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parsed_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parsed_data&lt;/span&gt;
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;csv_file&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now, within the view, we can let Django handle the model form as it should, and read the form&amp;rsquo;s &lt;code&gt;parsed_data&lt;/code&gt; attribute to use the data from within the CSV as necessary:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CSVUploadView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FormView&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;form_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CSVUploadForm&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_valid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;instance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;save&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parsed_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Use the data from the CSV, already parsed&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;HttpResponseRedirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_success_url&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Simple!&lt;/p&gt;</content><category term="Development"></category><category term="Python"></category><category term="Django"></category></entry><entry><title>A Swift API Client</title><link href="https://marmaladeofcourse.com/2025/07/15/a-swift-api-client/" rel="alternate"></link><published>2025-07-15T00:00:00+01:00</published><updated>2025-07-15T00:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2025-07-15:/2025/07/15/a-swift-api-client/</id><summary type="html">&lt;p&gt;In a new app I&amp;rsquo;ve been toying with the idea of developing, much of the data comes from a third-party API. This isn&amp;rsquo;t uncommon nowadays, and there are multiple Swift packages out there to make interacting with a REST API easier, such as &lt;a href="https://github.com/Alamofire/Alamofire"&gt;Alamofire&lt;/a&gt;. However, I wanted to …&lt;/p&gt;</summary><content type="html">&lt;p&gt;In a new app I&amp;rsquo;ve been toying with the idea of developing, much of the data comes from a third-party API. This isn&amp;rsquo;t uncommon nowadays, and there are multiple Swift packages out there to make interacting with a REST API easier, such as &lt;a href="https://github.com/Alamofire/Alamofire"&gt;Alamofire&lt;/a&gt;. However, I wanted to build a minimal API client that I could use without relying on a third-party dependency, code that is under my control, and that I hopefully understand!&lt;/p&gt;
&lt;p&gt;Using &lt;code&gt;URLSession&lt;/code&gt; and &lt;code&gt;URLRequest&lt;/code&gt; is relatively simple, but without some form of abstraction you&amp;rsquo;ll end up with a bunch of boilerplate code for each different request you need make. My goal was to build a simple, generic API client protocol that I could use for this particular API, but would also work for other use cases in the future.&lt;/p&gt;
&lt;p&gt;So let&amp;rsquo;s get started!&lt;/p&gt;
&lt;h2 id="the-apiclient-protocol"&gt;The APIClient protocol&lt;a class="headerlink" href="#the-apiclient-protocol" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The fundamental job of an API client is to send HTTP requests to the API, and return the response. We can start by building a protocol for such a client, using the power of Swift&amp;rsquo;s &lt;a href="https://docs.swift.org/swift-book/documentation/the-swift-programming-language/generics/"&gt;Generics&lt;/a&gt; to accept a variety of different request objects and return a variety of different responses:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;protocol&lt;/span&gt; &lt;span class="nc"&gt;APIClient&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;URL&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kr"&gt;get&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;APIRequest&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="kc"&gt;_&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kr"&gt;throws&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I&amp;rsquo;ve also added a &lt;code&gt;baseUrl&lt;/code&gt; parameter, to allow the client to specify a single base URL for the API calls.&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;ll note that I&amp;rsquo;ve used a type I haven&amp;rsquo;t yet defined, &lt;code&gt;APIRequest&lt;/code&gt;, so let&amp;rsquo;s do that now. A request needs a handful of properties:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the resource the request relates to;&lt;/li&gt;
&lt;li&gt;the HTTP method to use;&lt;/li&gt;
&lt;li&gt;any querystring parameters;&lt;/li&gt;
&lt;li&gt;any request body;&lt;/li&gt;
&lt;li&gt;any custom headers.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We can define a protocol to handle these requirements:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;protocol&lt;/span&gt; &lt;span class="nc"&gt;APIRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Encodable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;associatedtype&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decodable&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;resourceName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kr"&gt;get&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kr"&gt;get&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;URLQueryItem&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kr"&gt;get&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kr"&gt;get&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kr"&gt;get&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;extension&lt;/span&gt; &lt;span class="nc"&gt;APIRequest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;URLQueryItem&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;GET&amp;quot;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[:]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There are sensible defaults for most of these properties (such as defaulting to a &lt;code&gt;GET&lt;/code&gt; request with no body, no parameters, and no custom headers), so an extension to the protocol can define these.&lt;/p&gt;
&lt;p&gt;Here we also define an &lt;strong&gt;associated type&lt;/strong&gt; called &lt;code&gt;Response&lt;/code&gt;, which must be &lt;code&gt;Decodable&lt;/code&gt;. This allows us to tie an &lt;code&gt;APIRequest&lt;/code&gt; to a struct representing the response it expects, and is used as the return type in the function signature of &lt;code&gt;send&lt;/code&gt; in the &lt;code&gt;APIClient&lt;/code&gt; protocol above.&lt;/p&gt;
&lt;p&gt;So what&amp;rsquo;s missing? The actual functionality of the &lt;code&gt;send&lt;/code&gt; method, of course!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;extension&lt;/span&gt; &lt;span class="nc"&gt;APIClient&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;APIRequest&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="kc"&gt;_&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kr"&gt;throws&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;endpointRequest&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;endpointRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;URLSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;endpointRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;JSONDecoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Just three simple lines:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Call another method, &lt;code&gt;endpointRequest(for:)&lt;/code&gt;, to generate a &lt;code&gt;URLRequest&lt;/code&gt; object (more on this below).&lt;/li&gt;
&lt;li&gt;Execute the request and await the response.&lt;/li&gt;
&lt;li&gt;Decode that response from JSON into the request&amp;rsquo;s &lt;code&gt;Response&lt;/code&gt; struct.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is a nice short method for a couple of reasons. First, it doesn&amp;rsquo;t do any error handling—that&amp;rsquo;s left as an exercise for the reader to decide how to handle the various possible network request errors or response decoding errors. Secondly, the conversion of the &lt;code&gt;APIRequest&lt;/code&gt; object into a &lt;code&gt;URLRequest&lt;/code&gt; object is handed off to another method, so let&amp;rsquo;s write that now:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;extension&lt;/span&gt; &lt;span class="nc"&gt;APIClient&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;endpointRequest&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;APIRequest&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;URLRequest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;baseUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resourceName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;relativeTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="bp"&gt;fatalError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Invalid URL for resource &lt;/span&gt;&lt;span class="si"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resourceName&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;components&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;URLComponents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resolvingAgainstBaseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
        &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;queryItems&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parameters&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;urlRequest&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;URLRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;!)&lt;/span&gt;
        &lt;span class="n"&gt;urlRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;httpMethod&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;urlRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;httpBody&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;urlRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;forHTTPHeaderField&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;urlRequest&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;   
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This does a couple of things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Adds the request&amp;rsquo;s &lt;code&gt;resourceName&lt;/code&gt; to the API client&amp;rsquo;s &lt;code&gt;baseUrl&lt;/code&gt; to generate the full URL for the request.&lt;/li&gt;
&lt;li&gt;Adds any parameters from the request a &lt;code&gt;URLComponents&lt;/code&gt; object based on the generated URL.&lt;/li&gt;
&lt;li&gt;Creates a &lt;code&gt;URLRequest&lt;/code&gt; object with the full URL (that will now include any querystring parameters).&lt;/li&gt;
&lt;li&gt;Sets the HTTP method, body, and headers from the &lt;code&gt;APIRequest&lt;/code&gt; object.&lt;/li&gt;
&lt;li&gt;Returns the fully configured &lt;code&gt;URLRequest&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now we&amp;rsquo;re ready to actually use the API client!&lt;/p&gt;
&lt;h2 id="using-the-apiclient-protocol"&gt;Using the APIClient protocol&lt;a class="headerlink" href="#using-the-apiclient-protocol" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;First, we need to create a concrete class from the protocol, and define our API&amp;rsquo;s base URL:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyAPIClient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;APIClient&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;baseUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;https://example.com/api/v3/&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;shared&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MyAPIClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In this example API, there are two endpoints:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/api/v3/checkKey&lt;/code&gt; - returns the status of the API key provided.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/api/v3/getKeyUsageStats&lt;/code&gt; - returns the usage stats of the API key provided.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In both cases, the API key must be provided as a querystring parameter called &lt;code&gt;apiKey&lt;/code&gt;—for example, &lt;code&gt;/api/v3/checkKey?apiKey=12345&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We can write the &lt;code&gt;APIRequest&lt;/code&gt; structs to represent both of these calls:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;API_KEY_PARAMETER&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;URLQueryItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;apiKey&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;MY_API_KEY&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;ExampleCheckKeyRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;APIRequest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;typealias&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ExampleCheckKeyResponse&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;resourceName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;checkKey&amp;quot;&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;URLQueryItem&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;API_KEY_PARAMETER&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;ExampleGetKeyUsageRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;APIRequest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;typealias&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ExampleGetKeyUsageResponse&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;resourceName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;getKeyUsageStats&amp;quot;&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;URLQueryItem&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;API_KEY_PARAMETER&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Note that they both specify their associated &lt;code&gt;Response&lt;/code&gt; types—remember, these need to be &lt;code&gt;Decodable&lt;/code&gt; structs that can be used as the destination for the returned JSON from each API call. We can write these as follows:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;// checkKey returns a JSON object with a `status` string and optional `message`&lt;/span&gt;
&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;ExampleCheckKeyResponse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decodable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// getKeyUsage returns a JSON object with a `status` string, an optional `message`,&lt;/span&gt;
&lt;span class="c1"&gt;// and a `matches` integer with the number of times the key has been used recently&lt;/span&gt;
&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;ExampleGetKeyUsageResponse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ExampleAPIResponse&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With the request and response types set up, all that&amp;rsquo;s left is to add helper methods to our actual client, so that the rest of our code doesn&amp;rsquo;t need to know or understand the requests themselves:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;extension&lt;/span&gt; &lt;span class="nc"&gt;ExampleAPIClient&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;checkKey&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kr"&gt;throws&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ExampleCheckKeyResponse&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ExampleCheckKeyRequest&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;getKeyUsage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kr"&gt;throws&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ExampleGetKeyUsageResponse&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ExampleGetKeyUsageRequest&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And finally, call them!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;checkKeyResult&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ExampleAPIClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;checkKey&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="bp"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;checkKeyResult&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;getKeyUsageResult&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ExampleAPIClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getKeyUsage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="bp"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;getKeyUsageResult&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="bp"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Error: &lt;/span&gt;&lt;span class="si"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;localizedDescription&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Prints:&lt;/span&gt;
&lt;span class="c1"&gt;// ExampleCheckKeyResponse(status: &amp;quot;success&amp;quot;, message: nil)&lt;/span&gt;
&lt;span class="c1"&gt;// ExampleGetKeyUsageResponse(status: &amp;quot;success&amp;quot;, message: nil, matches: Optional(2))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Simples, no?&lt;/p&gt;
&lt;p&gt;So far, these are very simple requests, and I haven&amp;rsquo;t included much (if any!) error handling—but it feels like a good start to a simple API client interface I can use throughout my apps, with little code, and code that actually feels maintainable.&lt;/p&gt;</content><category term="Development"></category><category term="Swift"></category></entry><entry><title>Birds and Angles: Dabbling in Django Components</title><link href="https://marmaladeofcourse.com/2025/03/24/birds-and-angles-dabbling-in-django-components/" rel="alternate"></link><published>2025-03-24T00:00:00+00:00</published><updated>2025-03-24T00:00:00+00:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2025-03-24:/2025/03/24/birds-and-angles-dabbling-in-django-components/</id><summary type="html">&lt;p&gt;The &lt;a href="https://docs.djangoproject.com/en/5.2/ref/templates/language/"&gt;Django template language&lt;/a&gt; is great. It&amp;rsquo;s been one of the core pillars of Django&amp;rsquo;s popularity since the beginning—a simple, easy-to-use templating language that tries to give you just enough power to do what you need without having to think too hard.&lt;/p&gt;
&lt;p&gt;However, there are more modern …&lt;/p&gt;</summary><content type="html">&lt;p&gt;The &lt;a href="https://docs.djangoproject.com/en/5.2/ref/templates/language/"&gt;Django template language&lt;/a&gt; is great. It&amp;rsquo;s been one of the core pillars of Django&amp;rsquo;s popularity since the beginning—a simple, easy-to-use templating language that tries to give you just enough power to do what you need without having to think too hard.&lt;/p&gt;
&lt;p&gt;However, there are more modern ways of thinking about template rendering that the DTL lacks: notably, support for components—splitting templates into smaller reusable chunks, that can each take their own contexts and render just what they need to. There&amp;rsquo;s always been the &lt;a href="https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#include"&gt;&lt;code&gt;include&lt;/code&gt;&lt;/a&gt; tag, but it&amp;rsquo;s rather limited.&lt;/p&gt;
&lt;p&gt;A number of third-party libraries have sprung up, such as &lt;a href="https://django-bird.readthedocs.io/"&gt;django-bird&lt;/a&gt;, &lt;a href="https://django-cotton.com/"&gt;django-cotton&lt;/a&gt;, and &lt;a href="https://django-components.github.io/django-components/latest/"&gt;django-components&lt;/a&gt;, to name just a few. Until now, I&amp;rsquo;ve never used any of them, and made do with the &lt;code&gt;include&lt;/code&gt; tag wherever I needed to reuse a snippet of a template—however, I decided to take a look and see what all the fuss was about on a small project at work.&lt;/p&gt;
&lt;p&gt;The design of a page I was building called for some repeated boxes displaying a couple of pieces of data for different time periods, shown below:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Statistics boxes" src="/assets/request-stats.png" /&gt;&lt;/p&gt;
&lt;p&gt;While this could relatively easily be done with an &lt;code&gt;include&lt;/code&gt;, this seemed like the perfect opportunity to try one of the component libraries. I chose &lt;code&gt;django-bird&lt;/code&gt;, as the way it functions seems to gel best with the way my brain thinks about components. I ended up with the following component:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c"&gt;{# templates/bird/request-stats-summary-button.html #}&lt;/span&gt;
&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;load&lt;/span&gt; &lt;span class="nv"&gt;humanize&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;

&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;bird&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;prop&lt;/span&gt; &lt;span class="nv"&gt;period&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;bird&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;prop&lt;/span&gt; &lt;span class="nv"&gt;this_period&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;bird&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;prop&lt;/span&gt; &lt;span class="nv"&gt;active_users&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;999&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;bird&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;prop&lt;/span&gt; &lt;span class="nv"&gt;totals&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;999&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;

&lt;span class="x"&gt;&amp;lt;div&lt;/span&gt;
&lt;span class="x"&gt;  class=&amp;quot;card card-hover me-sm-3 rounded border &lt;/span&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;props.period&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nv"&gt;props.this_period&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;&lt;span class="x"&gt;bg-primary text-light&lt;/span&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;&lt;span class="x"&gt;card-hover-bg-primary&lt;/span&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;endif&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;&lt;span class="x"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="x"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="x"&gt;  &amp;lt;div class=&amp;quot;card-body pb-2 pt-2 shadow-sm&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="x"&gt;    &amp;lt;h6 class=&amp;quot;card-title &lt;/span&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;props.period&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nv"&gt;props.this_period&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;&lt;span class="x"&gt;text-muted&lt;/span&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;endif&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;&lt;span class="x"&gt; text-uppercase fw-normal&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="x"&gt;      &lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;slot&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;span class="x"&gt;    &amp;lt;/h6&amp;gt;&lt;/span&gt;
&lt;span class="x"&gt;    &amp;lt;p class=&amp;quot;mb-0 d-flex align-items-center&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="x"&gt;      &amp;lt;i class=&amp;quot;fas fa-user &lt;/span&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;props.period&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nv"&gt;props.this_period&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;&lt;span class="x"&gt;text-light&lt;/span&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;&lt;span class="x"&gt;text-primary&lt;/span&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;endif&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;&lt;span class="x"&gt; fs-5&amp;quot;&amp;gt;&amp;lt;/i&amp;gt;&lt;/span&gt;
&lt;span class="x"&gt;      &amp;lt;span class=&amp;quot;fs-3 ms-1&amp;quot;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;props.active_users&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nf"&gt;intcomma&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="x"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="x"&gt;      &amp;lt;i class=&amp;quot;fas fa-mouse-pointer ms-3 &lt;/span&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;props.period&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nv"&gt;props.this_period&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;&lt;span class="x"&gt;text-light&lt;/span&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;&lt;span class="x"&gt;text-primary&lt;/span&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;endif&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;&lt;span class="x"&gt; fs-5&amp;quot;&amp;gt;&amp;lt;/i&amp;gt;&lt;/span&gt;
&lt;span class="x"&gt;      &amp;lt;span class=&amp;quot;fs-3 ms-1&amp;quot;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;props.totals&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nf"&gt;intcomma&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="x"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="x"&gt;    &amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="x"&gt;  &amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="x"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You&amp;rsquo;ll notice the use of four &amp;ldquo;props&amp;rdquo; to pass data through, as well as the &lt;code&gt;{{ slot }}&lt;/code&gt; variable to capture the contents of the component. I updated the main template to use the component:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;bird&lt;/span&gt; &lt;span class="nv"&gt;request_stats_summary_button&lt;/span&gt; &lt;span class="nv"&gt;period&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;period&lt;/span&gt; &lt;span class="nv"&gt;this_period&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;today&amp;quot;&lt;/span&gt; &lt;span class="nv"&gt;active_users&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;active_users.today&lt;/span&gt; &lt;span class="nv"&gt;totals&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;totals.today&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;span class="x"&gt;  Today&lt;/span&gt;
&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;endbird&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;bird&lt;/span&gt; &lt;span class="nv"&gt;request_stats_summary_button&lt;/span&gt; &lt;span class="nv"&gt;period&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;period&lt;/span&gt; &lt;span class="nv"&gt;this_period&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;this_week&amp;quot;&lt;/span&gt; &lt;span class="nv"&gt;active_users&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;active_users.week&lt;/span&gt; &lt;span class="nv"&gt;totals&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;totals.week&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;span class="x"&gt;  This Week&lt;/span&gt;
&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;endbird&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;bird&lt;/span&gt; &lt;span class="nv"&gt;request_stats_summary_button&lt;/span&gt; &lt;span class="nv"&gt;period&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;period&lt;/span&gt; &lt;span class="nv"&gt;this_period&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;this_month&amp;quot;&lt;/span&gt; &lt;span class="nv"&gt;active_users&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;active_users.month&lt;/span&gt; &lt;span class="nv"&gt;totals&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;totals.month&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;span class="x"&gt;  This Month&lt;/span&gt;
&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;endbird&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;bird&lt;/span&gt; &lt;span class="nv"&gt;request_stats_summary_button&lt;/span&gt; &lt;span class="nv"&gt;period&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;period&lt;/span&gt; &lt;span class="nv"&gt;this_period&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;this_year&amp;quot;&lt;/span&gt; &lt;span class="nv"&gt;active_users&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;active_users.year&lt;/span&gt; &lt;span class="nv"&gt;totals&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;totals.year&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;span class="x"&gt;  This Year&lt;/span&gt;
&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;endbird&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And it worked great!&lt;/p&gt;
&lt;p&gt;I originally picked &lt;code&gt;django-bird&lt;/code&gt; because I liked how it used standard DTL tags, and didn&amp;rsquo;t require any custom template parsers. However, in actual use, I don&amp;rsquo;t like that I can&amp;rsquo;t wrap DTL tags to multiple lines, which ends up with the &lt;code&gt;bird&lt;/code&gt; lines quickly becoming unwieldy. There&amp;rsquo;s a &lt;a href="https://forum.djangoproject.com/t/allow-newlines-inside-tags/36040/29"&gt;forum post&lt;/a&gt; and &lt;a href="https://code.djangoproject.com/ticket/35899"&gt;ticket&lt;/a&gt; about supporting new lines in DTL tags, but that won&amp;rsquo;t happen any time soon.&lt;/p&gt;
&lt;p&gt;This is where another new-to-me package comes in—&lt;a href="https://dj-angles.adamghill.com/en/stable/"&gt;&lt;code&gt;dj-angles&lt;/code&gt;&lt;/a&gt;! The main purpose of this package is to provide a web-component-style template tag interface to the built-in DTL template tags, as demonstrated quite neatly in their docs. This is done by adding a new template loader that parses the alternative syntax. However, what&amp;rsquo;s of interest to me is that they also provide native integration with &lt;code&gt;django-bird&lt;/code&gt;, allowing us to use the more compact (and new-line-compatible!) style tags with bird components.&lt;/p&gt;
&lt;p&gt;A couple of settings later, and we can reference the above components in a way that really seems to click for me:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;dj-request-stats-summary-button&lt;/span&gt;
  &lt;span class="na"&gt;period&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;period&lt;/span&gt;
  &lt;span class="na"&gt;this_period&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;today&amp;quot;&lt;/span&gt;
  &lt;span class="na"&gt;active_users&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;active_users.today&lt;/span&gt;
  &lt;span class="na"&gt;totals&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;totals.today&lt;/span&gt;
&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  Today
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;dj-request-stats-summary-button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;dj-request-stats-summary-button&lt;/span&gt;
  &lt;span class="na"&gt;period&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;period&lt;/span&gt;
  &lt;span class="na"&gt;this_period&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;this_week&amp;quot;&lt;/span&gt;
  &lt;span class="na"&gt;active_users&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;active_users.week&lt;/span&gt;
  &lt;span class="na"&gt;totals&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;totals.week&lt;/span&gt;
&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  This Week
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;dj-request-stats-summary-button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;dj-request-stats-summary-button&lt;/span&gt;
  &lt;span class="na"&gt;period&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;period&lt;/span&gt;
  &lt;span class="na"&gt;this_period&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;this_month&amp;quot;&lt;/span&gt;
  &lt;span class="na"&gt;active_users&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;active_users.month&lt;/span&gt;
  &lt;span class="na"&gt;totals&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;totals.month&lt;/span&gt;
&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  This Month
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;dj-request-stats-summary-button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;dj-request-stats-summary-button&lt;/span&gt;
  &lt;span class="na"&gt;period&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;period&lt;/span&gt;
  &lt;span class="na"&gt;this_period&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;this_year&amp;quot;&lt;/span&gt;
  &lt;span class="na"&gt;active_users&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;active_users.year&lt;/span&gt;
  &lt;span class="na"&gt;totals&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;totals.year&lt;/span&gt;
&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  This Year
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;dj-request-stats-summary-button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It&amp;rsquo;s more verbose in number of lines, but much more readable!&lt;/p&gt;
&lt;p&gt;Yes, I&amp;rsquo;m aware that this is the sort of thing that &lt;code&gt;django-cotton&lt;/code&gt; and other template libraries provide automatically, but I&amp;rsquo;ve come to quite like &lt;code&gt;django-bird&lt;/code&gt;. Perhaps I&amp;rsquo;ll swap to one of the others one day, but for now, I&amp;rsquo;m enjoying what the combination of birds and angles can give me.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;There&amp;rsquo;s only one main issue I have with &lt;code&gt;django-bird&lt;/code&gt;, and that&amp;rsquo;s how it doesn&amp;rsquo;t parse DTL filters in values passed as properties or attributes. For example, the following component reference:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;bird&lt;/span&gt; &lt;span class="nv"&gt;button&lt;/span&gt; &lt;span class="nv"&gt;badge_count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;users&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nf"&gt;length&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;&lt;span class="x"&gt;Users&lt;/span&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;endbird&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;will result in the &lt;code&gt;badge_count&lt;/code&gt; prop inside the &lt;code&gt;button&lt;/code&gt; component being set to the string &lt;code&gt;"users|length"&lt;/code&gt;, which is not the desired result at all. I have an &lt;a href="https://github.com/joshuadavidthomas/django-bird/issues/181"&gt;open issue&lt;/a&gt; and &lt;a href="https://github.com/joshuadavidthomas/django-bird/pull/229"&gt;associated draft PR&lt;/a&gt; on the repo, so hopefully we&amp;rsquo;ll be able to get the feature into the library before too long.&lt;/p&gt;</content><category term="Development"></category><category term="Python"></category><category term="Django"></category></entry><entry><title>Brickset LEGO Gift Guide - 2024</title><link href="https://marmaladeofcourse.com/2024/11/27/brickset-lego-gift-guide-2024/" rel="alternate"></link><published>2024-11-27T00:00:00+00:00</published><updated>2024-11-27T00:00:00+00:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2024-11-27:/2024/11/27/brickset-lego-gift-guide-2024/</id><summary type="html">&lt;p&gt;Once again Brickset have published their annual holiday gift guide, in five parts split by price category. Huw asked for my opinion, along with the other Brickset contributors, and I think most of us made some pretty good choices! You can view each of the articles below, along with my …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Once again Brickset have published their annual holiday gift guide, in five parts split by price category. Huw asked for my opinion, along with the other Brickset contributors, and I think most of us made some pretty good choices! You can view each of the articles below, along with my pick from the available choices:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://brickset.com/article/116043/holiday-gift-guide-under-25"&gt;Less than $25&lt;/a&gt; - &lt;a href="https://brickset.com/sets/31147-1"&gt;31147&lt;/a&gt; Retro Camera&lt;/li&gt;
&lt;li&gt;&lt;a href="https://brickset.com/article/116042/holiday-gift-guide-25-50"&gt;$25-$50&lt;/a&gt; - &lt;a href="https://brickset.com/sets/31154-1"&gt;31154&lt;/a&gt; Forest Animals: Red Fox&lt;/li&gt;
&lt;li&gt;&lt;a href="https://brickset.com/article/116041/holiday-gift-guide-50-100"&gt;$50-$100&lt;/a&gt; - &lt;a href="https://brickset.com/sets/43249-1"&gt;43249&lt;/a&gt; Stitch&lt;/li&gt;
&lt;li&gt;&lt;a href="https://brickset.com/article/116040/holiday-gift-guide-100-200"&gt;$100-$200&lt;/a&gt; - &lt;a href="https://brickset.com/sets/31212-1"&gt;31212&lt;/a&gt; The Milky Way Galaxy&lt;/li&gt;
&lt;li&gt;&lt;a href="https://brickset.com/article/116039/holiday-gift-guide-over-200"&gt;$200+&lt;/a&gt; - &lt;a href="https://brickset.com/sets/10326-1/"&gt;10326&lt;/a&gt; Natural History Museum&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What would your choices be?&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://images.brickset.com/news/116039_hgg2024.jpg" /&gt;&lt;/p&gt;</content><category term="LEGO"></category></entry><entry><title>Static Files in Django: An Introduction</title><link href="https://marmaladeofcourse.com/2024/04/03/static-files-in-django-an-introduction/" rel="alternate"></link><published>2024-04-03T00:00:00+01:00</published><updated>2024-04-03T00:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2024-04-03:/2024/04/03/static-files-in-django-an-introduction/</id><summary type="html">&lt;p&gt;Some of the most common recurring questions in the &lt;a href="https://www.djangoproject.com/community/"&gt;Django Discord&lt;/a&gt; revolve around static files, why they&amp;rsquo;re not loading when they should, or how to configure them and what should be responsible for serving them. I thought I&amp;rsquo;d write up a bit of an introduction to static files …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Some of the most common recurring questions in the &lt;a href="https://www.djangoproject.com/community/"&gt;Django Discord&lt;/a&gt; revolve around static files, why they&amp;rsquo;re not loading when they should, or how to configure them and what should be responsible for serving them. I thought I&amp;rsquo;d write up a bit of an introduction to static files in Django, which will hopefully be a little more approachable to beginners than the official docs (as excellent as they are), and serve as a starting point for diagnosing problems with them.&lt;/p&gt;
&lt;h2 id="what-are-static-files"&gt;What are static files?&lt;a class="headerlink" href="#what-are-static-files" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Static files are files served as part of your Django application whose contents do not change from user to user or request to request—they are not &lt;em&gt;dynamic&lt;/em&gt;. By far the most common examples are the stylesheets (CSS), Javascript, and images that are required for the application to look and function correctly in the browser, but they could just as easily be other things—JSON files of data that doesn&amp;rsquo;t change, for example. &lt;/p&gt;
&lt;p&gt;Static files are &lt;em&gt;not&lt;/em&gt; any files that your users upload, such as profile pictures, or other content. Those are referred to by Django as &lt;em&gt;media&lt;/em&gt; files, and are handled differently. &lt;/p&gt;
&lt;h2 id="static-files-terminology"&gt;Static files terminology&lt;a class="headerlink" href="#static-files-terminology" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;There are three key pieces of information to know about Django&amp;rsquo;s static files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the directories you store them in during development of the project,&lt;/li&gt;
&lt;li&gt;the URL they are served under when requested by the browser, and&lt;/li&gt;
&lt;li&gt;the directory (note: singular) they are collecting into as part of deploying the app.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are three distinct, but interrelated, pieces of the puzzle, and they each have different settings to adjust or define their behaviour in &lt;code&gt;settings.py&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="where-do-i-put-my-static-files"&gt;Where do I put my static files?&lt;a class="headerlink" href="#where-do-i-put-my-static-files" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;By default, Django will look for your static files in a directory named &lt;code&gt;static&lt;/code&gt; inside each app directory in &lt;code&gt;INSTALLED_APPS&lt;/code&gt;, and without any additional configuration this is the &lt;em&gt;only&lt;/em&gt; place it will search. For many, that&amp;rsquo;s enough.&lt;/p&gt;
&lt;p&gt;Others may wish to store some static files outside any one particular app, because they may be relevant globally to the project and there isn&amp;rsquo;t a single app that fits them best. This can be done by creating a new directory somewhere (often, named &lt;code&gt;static&lt;/code&gt; at the same level as &lt;code&gt;manage.py&lt;/code&gt;), and adding a new setting to &lt;code&gt;settings.py&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;STATICFILES_DIRS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BASE_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;static&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;STATICFILES_DIRS&lt;/code&gt; setting tells Django to look for static files in those additional directories, as well as within each individual app directory.&lt;/p&gt;
&lt;h2 id="what-url-do-i-use-to-request-my-static-files-from-the-browser"&gt;What URL do I use to request my static files from the browser?&lt;a class="headerlink" href="#what-url-do-i-use-to-request-my-static-files-from-the-browser" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The single setting, &lt;code&gt;STATIC_URL&lt;/code&gt;, defines the URL prefix that Django expects the static files to be available under when the browser requests them. This is entirely independent from their actual location on the file system as discussed above.&lt;/p&gt;
&lt;p&gt;Django suggests a perfectly reasonable default for this: &lt;code&gt;"static/"&lt;/code&gt;. It must end in a slash.&lt;/p&gt;
&lt;p&gt;When running your application, Django will expect the static files to be available under that URL. For example, a file in a directory &lt;code&gt;my_app/static/css/styles.css&lt;/code&gt; would correspond to a URL of &lt;code&gt;/static/css/styles.css&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;When referencing static files in templates, such as using a &lt;code&gt;link&lt;/code&gt; or &lt;code&gt;script&lt;/code&gt; tag to include CSS or JavaScript in HTML, Django provides a template tag in the &lt;code&gt;static&lt;/code&gt; library to automatically convert a relative static file name into its full URL path. For example, to include the above CSS file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;load&lt;/span&gt; &lt;span class="nv"&gt;static&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;span class="x"&gt;…&lt;/span&gt;
&lt;span class="x"&gt;&amp;lt;link href=&amp;quot;&lt;/span&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;css/main.css&amp;quot;&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;&lt;span class="x"&gt;&amp;quot; rel=&amp;quot;stylesheet&amp;quot;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This would result in the following HTML output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;link&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;/static/css/main.css&amp;quot;&lt;/span&gt; &lt;span class="na"&gt;rel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;stylesheet&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="so-how-does-django-map-the-url-to-the-actual-file-on-the-file-system"&gt;So, how does Django map the URL to the actual file on the file system?&lt;a class="headerlink" href="#so-how-does-django-map-the-url-to-the-actual-file-on-the-file-system" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;And what&amp;rsquo;s this third key point I mentioned above, the single static directory?&lt;/p&gt;
&lt;p&gt;The answer to this differs depending on how you&amp;rsquo;re running Django; in development or production. &lt;/p&gt;
&lt;h3 id="serving-static-files-in-development"&gt;Serving static files in development&lt;a class="headerlink" href="#serving-static-files-in-development" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;During development, it&amp;rsquo;s acceptable for Django to serve the static files itself. Normally, this job would be handed off to a web server or proxy, as there is no benefit to spinning up the full Django application process just to chuck some static CSS back to the browser, but during development it is really not worth having to set that all up. &lt;/p&gt;
&lt;p&gt;When running Django with the development server (&lt;code&gt;manage.py runserver&lt;/code&gt;) and &lt;code&gt;DEBUG=True&lt;/code&gt;, Django will automatically add a new URL path to your URL patterns that matches the value of &lt;code&gt;STATIC_URL&lt;/code&gt;, and search the various directories mentioned above for matching files to serve whenever a request comes in. This means that by only setting a &lt;code&gt;STATIC_URL&lt;/code&gt; to something sensible, Django will automatically be able to serve the CSS, JavaScript, and images stored in any &lt;code&gt;static&lt;/code&gt; folder inside each &lt;code&gt;INSTALLED_APPS&lt;/code&gt; directory, and any folders pointed to by &lt;code&gt;STATICFILES_DIRS&lt;/code&gt;. That&amp;rsquo;s usually enough for development purposes. &lt;/p&gt;
&lt;h3 id="serving-static-files-in-a-production-environment"&gt;Serving static files in a production environment&lt;a class="headerlink" href="#serving-static-files-in-a-production-environment" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;However, as mentioned above, it&amp;rsquo;s inefficient to have Django serve these files itself when deployed in a production environment. Instead, that responsibility is usually handed off to a web server or proxy that sits in front of Django, such as nginx. So how does this proxy know where to find the static files?&lt;/p&gt;
&lt;p&gt;The answer is in the &lt;code&gt;STATIC_ROOT&lt;/code&gt; setting. This should be pointed to a directory on the file system that, once deployed, both the Django application and the proxy can access. &lt;/p&gt;
&lt;p&gt;What should be in that directory? Nothing, to start with. Part of the deployment process should be to run &lt;code&gt;manage.py collectstatic&lt;/code&gt;, a built-in management command which runs round all the various folders the static files live in (see the first point above) and copies them into the &lt;code&gt;STATIC_ROOT&lt;/code&gt; directory, ready for the proxy to efficiently serve them.&lt;/p&gt;
&lt;p&gt;The final part of the puzzle is to correctly configure the proxy so that requests for any path matching the prefix defined in &lt;code&gt;STATIC_URL&lt;/code&gt; map onto the folder defined in &lt;code&gt;STATIC_ROOT&lt;/code&gt;. This way, once &lt;code&gt;collectstatic&lt;/code&gt; has been run, all the discrete parts of the application point together at a single, efficiently-served directory of static files.&lt;/p&gt;
&lt;h2 id="troubleshooting-missing-static-files"&gt;Troubleshooting missing static files&lt;a class="headerlink" href="#troubleshooting-missing-static-files" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Follow these steps to help diagnose why styles may not be loading, images not showing up, or any other static files not appearing correctly in the browser.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Ensure the file is in either a directory named &lt;code&gt;static&lt;/code&gt; under one of your &lt;code&gt;INSTALLED_APPS&lt;/code&gt;, or in a directory pointed at by &lt;code&gt;STATICFILES_DIRS&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Make sure &lt;code&gt;STATIC_URL&lt;/code&gt; is configured and ends in a &lt;code&gt;/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Ensure you are using the &lt;code&gt;{% static "relative/path/to/file" %}&lt;/code&gt; template tag wherever you reference static files in your templates. &lt;/li&gt;
&lt;li&gt;If you are developing and using &lt;code&gt;runserver&lt;/code&gt;, make sure &lt;code&gt;DEBUG=True&lt;/code&gt;. Restart the &lt;code&gt;runserver&lt;/code&gt; to be sure it picks up any changes you may have made. &lt;/li&gt;
&lt;li&gt;If the app is deployed, make sure your &lt;code&gt;STATIC_ROOT&lt;/code&gt; points to a directory on the file system, you have run &lt;code&gt;collectstatic&lt;/code&gt;, and whatever proxy that fronts Django is properly configured to point &lt;code&gt;STATIC_URL&lt;/code&gt; at the &lt;code&gt;STATIC_ROOT&lt;/code&gt; directory. &lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Note also that browsers will cache static files, so a forced refresh can help debug. This is usually done with &lt;code&gt;Ctrl-F5&lt;/code&gt;, but varies from browser to browser. &lt;/p&gt;
&lt;h2 id="further-reading"&gt;Further reading&lt;a class="headerlink" href="#further-reading" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Hopefully this should provide a basic introduction to static files in Django, with enough information to get you up and running and help diagnose when static files aren&amp;rsquo;t working as you expect. For further reading, I suggest looking at the following resources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Django&amp;rsquo;s documentation on:&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.djangoproject.com/en/5.0/howto/static-files/"&gt;Managing static files&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.djangoproject.com/en/5.0/ref/settings/#static-files"&gt;Static files settings.&lt;/a&gt; Take note of the additional options we didn&amp;rsquo;t discuss here, such as &lt;code&gt;STATICFILES_FINDERS&lt;/code&gt;. &lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.djangoproject.com/en/5.0/ref/contrib/staticfiles/#module-django.contrib.staticfiles"&gt;The &lt;code&gt;staticfiles&lt;/code&gt; app.&lt;/a&gt;. If this app is not included in &lt;code&gt;INSTALLED_APPS&lt;/code&gt;, none of the above will work.&lt;/li&gt;
&lt;li&gt;&lt;a href="[https://docs.djangoproject.com/en/5.0/howto/static-files/deployment/](https://docs.djangoproject.com/en/5.0/howto/static-files/deployment/#staticfiles-from-cdn)"&gt;Deploying static files&lt;/a&gt;, including how to deploy to a CDN rather than a local directory. &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Libraries such as &lt;a href="https://whitenoise.readthedocs.io/en/stable/index.html"&gt;Whitenoise&lt;/a&gt; for situations where configuring a proxy in front of Django isn&amp;rsquo;t possible. &lt;/li&gt;
&lt;/ul&gt;</content><category term="Development"></category><category term="Python"></category><category term="Django"></category></entry><entry><title>Review: 31154 Forest Animals: Red Fox</title><link href="https://marmaladeofcourse.com/2024/03/12/review-31154-forest-animals-red-fox/" rel="alternate"></link><published>2024-03-12T00:00:00+00:00</published><updated>2024-03-12T00:00:00+00:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2024-03-12:/2024/03/12/review-31154-forest-animals-red-fox/</id><summary type="html">&lt;p&gt;LEGO&amp;rsquo;s got some really strong Creator 3-in-1 sets out this year, and &lt;a href="https://brickset.com/sets/31154-1"&gt;31154 Forest Animals: Red Fox&lt;/a&gt; is one of the best. You can build a fox, an owl, or a squirrel, and all three models are truly excellent.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://brickset.com/sets/31154-1"&gt;31154&lt;/a&gt; Forest Animals: Red Fox doesn&amp;rsquo;t disappoint; all three …&lt;/p&gt;&lt;/blockquote&gt;</summary><content type="html">&lt;p&gt;LEGO&amp;rsquo;s got some really strong Creator 3-in-1 sets out this year, and &lt;a href="https://brickset.com/sets/31154-1"&gt;31154 Forest Animals: Red Fox&lt;/a&gt; is one of the best. You can build a fox, an owl, or a squirrel, and all three models are truly excellent.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://brickset.com/sets/31154-1"&gt;31154&lt;/a&gt; Forest Animals: Red Fox doesn&amp;rsquo;t disappoint; all three models are fantastic, were a joy to build, and look great on display. Each manages to capture the unique characteristics of the woodland creatures, and the articulation makes them quite satisfying to pose in the way that you want.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img alt="31154 Forest Animals: Red Fox LEGO set" src="https://images.brickset.com/news/106724_31156-2528.jpg" /&gt;&lt;/p&gt;</content><category term="LEGO Reviews"></category></entry><entry><title>Review: 31148 Retro Roller Skate</title><link href="https://marmaladeofcourse.com/2023/12/15/review-31148-retro-roller-skate/" rel="alternate"></link><published>2023-12-15T00:00:00+00:00</published><updated>2023-12-15T00:00:00+00:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2023-12-15:/2023/12/15/review-31148-retro-roller-skate/</id><summary type="html">&lt;p&gt;Once again LEGO are coming up with some very creative 3-in-1 sets for their first wave of 2024:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I&amp;rsquo;m a big fan of the 3-in-1 range. Lately LEGO have been knocking it out of the park with so many of these sets, and this one held great promise. I …&lt;/p&gt;&lt;/blockquote&gt;</summary><content type="html">&lt;p&gt;Once again LEGO are coming up with some very creative 3-in-1 sets for their first wave of 2024:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I&amp;rsquo;m a big fan of the 3-in-1 range. Lately LEGO have been knocking it out of the park with so many of these sets, and this one held great promise. I love the colour scheme, and the roller skate is a brilliant build. It&amp;rsquo;s a decent size, feels solid in the hand, and although it&amp;rsquo;s not a vehicle, pushing it around is quite satisfying. It&amp;rsquo;s just a shame there&amp;rsquo;s only one—a pair would have been fantastic!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img alt="LEGO 31148 Retro Roller Skate" src="https://images.brickset.com/news/102585_31148--4_0834.jpg" /&gt;&lt;/p&gt;</content><category term="LEGO Reviews"></category></entry><entry><title>Anatomy of a Configurable Widget</title><link href="https://marmaladeofcourse.com/2023/10/07/anatomy-of-a-configurable-widget/" rel="alternate"></link><published>2023-10-07T00:00:00+01:00</published><updated>2023-10-07T00:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2023-10-07:/2023/10/07/anatomy-of-a-configurable-widget/</id><summary type="html">&lt;p&gt;In my previous post, &lt;a href="https://marmaladeofcourse.com/2023/08/12/anatomy-of-a-widget/"&gt;Anatomy of a Widget&lt;/a&gt;, I outlined my basic understanding of building a simple widget in Xcode. These were the most trivial widgets possible: they provided no options, and certainly no interactivity as introduced with iOS 17. In this post, I&amp;rsquo;m going to write up my …&lt;/p&gt;</summary><content type="html">&lt;p&gt;In my previous post, &lt;a href="https://marmaladeofcourse.com/2023/08/12/anatomy-of-a-widget/"&gt;Anatomy of a Widget&lt;/a&gt;, I outlined my basic understanding of building a simple widget in Xcode. These were the most trivial widgets possible: they provided no options, and certainly no interactivity as introduced with iOS 17. In this post, I&amp;rsquo;m going to write up my (also limited) understanding of the parts that need to be added to provide configurable options within the widget—in other words, those the user sees when they long press on a widget and hit &amp;ldquo;Edit&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;The previous post introduced the concept of the the timeline entry, the timeline provider, the widget&amp;rsquo;s view,  the widget definition, and the widget bundle. We&amp;rsquo;ll be adding one more, the &lt;em&gt;configuration intent&lt;/em&gt;, and tying it in to the rest.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#the-configuration-intent"&gt;The Configuration Intent&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#updating-the-timeline-entry"&gt;Updating the Timeline Entry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#updating-the-timeline-provider"&gt;Updating the Timeline Provider&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#updating-the-widgets-view"&gt;Updating the Widget&amp;rsquo;s View&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#configuration-intent-parameters"&gt;Configuration Intent Parameters&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The same major caveat as before continues to apply: I still do not fully understand widgets, or really know how to build them properly. Nor do I understand interactive widgets that come in with iOS 17. But, hopefully, my knowledge will increase and I can update these posts as I learn more! Please do not hesitate to let me know about anything I write here that&amp;rsquo;s misleading or factually incorrect.&lt;/p&gt;
&lt;h2 id="the-configuration-intent"&gt;The Configuration Intent&lt;a class="headerlink" href="#the-configuration-intent" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;iOS apps use what Apple refers to as &amp;ldquo;intents&amp;rdquo; to tell other parts of the system what the app can do—such as Siri, or Shortcuts. The same mechanism is used by widgets to define what options are available, and we do this using a struct conforming to &lt;a href="https://developer.apple.com/documentation/AppIntents/WidgetConfigurationIntent"&gt;&lt;code&gt;WidgetConfigurationIntent&lt;/code&gt;&lt;/a&gt;. This struct needs to hold all the parameters available to the user in the widget&amp;rsquo;s edit menu. For example, a very simple widget intent could look like the following:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;WidgetKit&lt;/span&gt;
&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;AppIntents&lt;/span&gt;

&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;MyConfigurationIntent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WidgetConfigurationIntent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LocalizedStringResource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Configuration&amp;quot;&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;IntentDescription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;This is an example widget.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Favourite Emoji&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;😃&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;favouriteEmoji&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is about as basic as it gets, providing a single string parameter that will be exposed in the edit menu via a text field, with a sensible default. I&amp;rsquo;ll go through some other parameter types available later.&lt;/p&gt;
&lt;p&gt;For now, we need to propagate the configuration intent throughout the rest of the widget&amp;rsquo;s stack.&lt;/p&gt;
&lt;h2 id="updating-the-timeline-entry"&gt;Updating the Timeline Entry&lt;a class="headerlink" href="#updating-the-timeline-entry" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The timeline entry is responsible for holding all the information the widget&amp;rsquo;s view needs to render for a given point in time. We need to update this to also hold the configuration intent. I will be using the example  code from the previous post, and adding/amending it as necessary:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;MyWidgetEntry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TimelineEntry&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Date&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MyConfigurationIntent&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="updating-the-timeline-provider"&gt;Updating the Timeline Provider&lt;a class="headerlink" href="#updating-the-timeline-provider" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Because the timeline entries are created by the timeline provider, we need to update this to include the configuration intent too. This time, we also need to change from conforming to the basic &lt;code&gt;TimelineProvider&lt;/code&gt; protocol to the mode advanced &lt;code&gt;AppIntentTimelineProvider&lt;/code&gt; protocol, which also necessitates changing the method signatures of the three methods, &lt;code&gt;placeholder&lt;/code&gt;, &lt;code&gt;getSnapshot&lt;/code&gt;, and &lt;code&gt;getTimeline&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;MyTimelineProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AppIntentTimelineProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MyWidgetEntry&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;MyWidgetEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MyConfigurationIntent&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MyConfigurationIntent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MyWidgetEntry&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;MyWidgetEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;timeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MyConfigurationIntent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Timeline&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MyWidgetEntry&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;MyWidgetEntry&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

        &lt;span class="c1"&gt;// Generate a timeline consisting of five entries an hour apart, starting from the current date.&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;currentDate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;hourOffset&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;entryDate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Calendar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;byAdding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;hourOffset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;currentDate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
            &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;entry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MyWidgetEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;entryDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Timeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;atEnd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For the &lt;code&gt;placeholder&lt;/code&gt; method, the timeline provider isn&amp;rsquo;t handed a configuration, so it has to create one to pass to the timeline entry. For the other two methods, however, the first argument passed is a &lt;code&gt;MyConfigurationIntent&lt;/code&gt; struct representing the options the user has selected in the widget edit view, and we can pass this directly to the timeline entry.&lt;/p&gt;
&lt;h2 id="updating-the-widgets-view"&gt;Updating the Widget&amp;rsquo;s View&lt;a class="headerlink" href="#updating-the-widgets-view" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The only thing left to do is update the widget&amp;rsquo;s View to make use of the options provided by the configuration intent. In this case, we can use the &lt;code&gt;favouriteEmoji&lt;/code&gt; parameter, as it&amp;rsquo;s the only one provided by our very simple intent:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;MyWidgetView&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MyWidgetEntry&lt;/span&gt;

    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;some&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;VStack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Favourite Emoji:&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;favouriteEmoji&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And that&amp;rsquo;s it! The widget now allows the user to customise its view by presenting an edit menu with a bunch of parameters. The user-chosen values for these parameters are passed into the View as a Configuration Intent parameter via the timeline entry, and the View can make use of them as it wishes.&lt;/p&gt;
&lt;h2 id="configuration-intent-parameters"&gt;Configuration Intent Parameters&lt;a class="headerlink" href="#configuration-intent-parameters" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Above, we saw a very simple configuration intent parameter of a string:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Favourite Emoji&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;😃&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;favouriteEmoji&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;title&lt;/code&gt; is the name of the parameter as shown to the user in the edit view of the widget, and the default you provide is what the parameter is set to when the user hasn&amp;rsquo;t edited the widget and entered something else. You can view the &lt;a href="https://developer.apple.com/documentation/AppIntents/WidgetConfigurationIntent"&gt;&lt;code&gt;WidgetConfigurationIntent&lt;/code&gt; documentation&lt;/a&gt; for more options that are available, such as how to control the order the parameters appear in the widget edit view or define those which depend on others.&lt;/p&gt;
&lt;p&gt;Adding other type of data is easy, such as asking for an integer:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Age&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Int&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Or a boolean:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Show Background&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;showBackground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Bool&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You can also present the user with a choice of options by conforming to &lt;code&gt;DynamicOptionsProvider&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;IntegerOptionsProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DynamicOptionsProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Int&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;defaultInteger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Int&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;results&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kr"&gt;throws&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="bp"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;defaultResult&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;defaultInteger&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;...&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Hour&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;optionsProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IntegerOptionsProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;defaultInteger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Int&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;More complicated data types can be represented by conforming to various protocols, such as &lt;code&gt;AppEnum&lt;/code&gt; to provide users with a choice based on an enum:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;Weekday&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AppEnum&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;typeDisplayRepresentation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TypeDisplayRepresentation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Weekday&amp;quot;&lt;/span&gt;

    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;Sunday&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;Monday&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;Tuesday&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;Wednesday&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;Thursday&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;Friday&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;Saturday&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;

    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;caseDisplayRepresentations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Weekday&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DisplayRepresentation&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sunday&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Sunday&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Monday&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Monday&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tuesday&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Tuesday&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wednesday&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Wednesday&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Thursday&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Thursday&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Friday&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Friday&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Saturday&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Saturday&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;...&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Weekday&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Friday&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;weekday&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Weekday&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Or by conforming an struct to &lt;a href="https://developer.apple.com/documentation/appintents/appentity"&gt;&lt;code&gt;AppEntity&lt;/code&gt;&lt;/a&gt; and the associated &lt;code&gt;EntityQuery&lt;/code&gt;, you can add support for arbitrary data types, which is the most powerful but complicated option, such as this example for adding a time zone choice to the widget:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;TimeZoneQuery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EntityStringQuery&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;convertToWidgetTimeZone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;identifiers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;WidgetTimeZone&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;identifiers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compactMap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;TimeZone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="bp"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;WidgetTimeZone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;entities&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;matching&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kr"&gt;throws&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;WidgetTimeZone&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;convertToWidgetTimeZone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;identifiers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TimeZone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;knownTimeZoneIdentifiers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="bp"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;localizedStandardContains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;entities&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;identifiers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kr"&gt;throws&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;WidgetTimeZone&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;convertToWidgetTimeZone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;identifiers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TimeZone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;knownTimeZoneIdentifiers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="bp"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;identifiers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="bp"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;suggestedEntities&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kr"&gt;throws&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;WidgetTimeZone&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;convertToWidgetTimeZone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;identifiers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TimeZone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;knownTimeZoneIdentifiers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;WidgetTimeZone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Equatable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Hashable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AppEntity&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;typealias&lt;/span&gt; &lt;span class="n"&gt;DefaultQuery&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeZoneQuery&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;defaultQuery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TimeZoneQuery&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeZoneQuery&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;typeDisplayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LocalizedStringResource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LocalizedStringResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;TimeZone&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;TimeZone&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;typeDisplayRepresentation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TypeDisplayRepresentation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;TypeDisplayRepresentation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stringLiteral&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;TimeZone&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;displayRepresentation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DisplayRepresentation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;DisplayRepresentation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stringLiteral&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;identifier&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TimeZone&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;...&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Time Zone&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;timeZone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WidgetTimeZone&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;img alt="Anatomy of a configurable widget" src="/assets/configurable-widgets.png" /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;An extension of the Configuration Intent protocol is also what powers the interactive widgets available in iOS 17. Hopefully, once I&amp;rsquo;ve figured those out a little more, a future post will cover the basics of them too!&lt;/p&gt;</content><category term="Development"></category><category term="Swift"></category><category term="SwiftUI"></category></entry><entry><title>Anatomy of a Widget</title><link href="https://marmaladeofcourse.com/2023/08/12/anatomy-of-a-widget/" rel="alternate"></link><published>2023-08-12T00:00:00+01:00</published><updated>2023-08-12T00:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2023-08-12:/2023/08/12/anatomy-of-a-widget/</id><summary type="html">&lt;p&gt;I have long been a little confused by how widgets work, from a development perspective, in iOS apps. There are a number of moving parts that all have to work together &lt;em&gt;just so&lt;/em&gt; to make the widget appear how you want, with the data you want, when you want. This …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I have long been a little confused by how widgets work, from a development perspective, in iOS apps. There are a number of moving parts that all have to work together &lt;em&gt;just so&lt;/em&gt; to make the widget appear how you want, with the data you want, when you want. This post is my attempt to break it down into each part, in the order they need to be defined so the app still compiles after each step, with my understanding of what they&amp;rsquo;re for and what they do.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Anatomy of a widget" src="/assets/widgets.png" /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#the-timeline-entry"&gt;The Timeline Entry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-timeline-provider"&gt;The Timeline Provider&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-widgets-view"&gt;The Widget&amp;rsquo;s View&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-widget-itself"&gt;The Widget Itself&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-widget-bundle"&gt;The Widget Bundle&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The major caveat here is: I still do not understand widgets, or really know how to build them properly. Nor do I understand interactive widgets that come in with iOS 17. But, hopefully, my knowledge will increase and I can update this post as I learn more! Please do not hesitate to let me know about anything I write here that&amp;rsquo;s misleading or factually incorrect.&lt;/p&gt;
&lt;h2 id="the-timeline-entry"&gt;The Timeline Entry&lt;a class="headerlink" href="#the-timeline-entry" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Widgets are a series of static SwiftUI views, rendered on a timeline into the future. When the system reaches the end of the timeline, or at some point determined by your app or widget configuration, the app extension is asked for another timeline to render.&lt;/p&gt;
&lt;p&gt;Each item in this timeline is a &lt;em&gt;timeline entry&lt;/em&gt;, which is simply a struct conforming to &lt;a href="https://developer.apple.com/documentation/widgetkit/timelineentry"&gt;&lt;code&gt;TimelineEntry&lt;/code&gt;&lt;/a&gt;. &lt;strong&gt;This struct needs to hold all the data your widget needs to know in order to render correctly.&lt;/strong&gt; The &lt;code&gt;date&lt;/code&gt; property is mandatory (specified by the &lt;code&gt;TimelineEntry&lt;/code&gt; protocol), but all other properties are up to you. For example, a widget that simply renders some text may need a timeline entry such as the following:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;MyWidgetEntry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TimelineEntry&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Date&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="the-timeline-provider"&gt;The Timeline Provider&lt;a class="headerlink" href="#the-timeline-provider" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This is the part of the widget that is responsible for providing each timeline when iOS asks for one, and needs to be a struct conforming to &lt;code&gt;TimelineProvider&lt;/code&gt;. &lt;a href="https://developer.apple.com/documentation/widgetkit/timelineprovider"&gt;Apple&amp;rsquo;s documentation&lt;/a&gt; is pretty good here. There are three required methods that need to be implemented:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;placeholder&lt;/code&gt; method must return, as quickly as possible, a single timeline entry for use in placeholder views (such as when the user taps the your app in the Add Widget gallery).&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;getSnapshot&lt;/code&gt; method also needs to provide a single timeline entry, but gets a bit more time to fetch real data, and can be used to make the widget previews in the gallery more representative of the actual widget once added.&lt;/li&gt;
&lt;li&gt;Finally, the &lt;code&gt;getTimeline&lt;/code&gt; method must provide a &lt;code&gt;Timeline&lt;/code&gt; object with a list of dated entries stretching into the future, and a policy of when the timeline should be refreshed.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;MyTimelineProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TimelineProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MyWidgetEntry&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;WidgetEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Placeholder&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;getSnapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;escaping&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MyWidgetEntry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;entry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MyWidgetEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Snapshot&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;getTimeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;escaping&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Timeline&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MyWidgetEntry&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;MyWidgetEntry&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

        &lt;span class="c1"&gt;// Generate a timeline consisting of five entries an hour apart, starting from the current date.&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;currentDate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;hourOffset&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;entryDate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Calendar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;byAdding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;hourOffset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;currentDate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
            &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;entry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MyWidgetEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;entryDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;emoji&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;In a timeline! &lt;/span&gt;&lt;span class="si"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;hourOffset&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;timeline&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Timeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;atEnd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The most common refresh policy is &lt;code&gt;.atEnd&lt;/code&gt;, which will instruct iOS to ask for a new timeline once this one is complete. The widget will be rendered with each timeline entry at its specified date.&lt;/p&gt;
&lt;h2 id="the-widgets-view"&gt;The Widget&amp;rsquo;s View&lt;a class="headerlink" href="#the-widgets-view" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This is the core of the display of the widget, a SwiftUI view that takes the timeline entry as a parameter and renders the data as necessary. It doesn&amp;rsquo;t &lt;em&gt;have&lt;/em&gt; to be a separate &lt;code&gt;View&lt;/code&gt; (it could be rendered as part of the widget itself, see below), but it&amp;rsquo;s much neater this way.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;MyWidgetView&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MyWidgetEntry&lt;/span&gt;

    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;some&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;VStack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There&amp;rsquo;s nothing magic here.&lt;/p&gt;
&lt;h2 id="the-widget-itself"&gt;The Widget Itself&lt;a class="headerlink" href="#the-widget-itself" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Each widget is a struct that conforms to &lt;a href="https://developer.apple.com/documentation/swiftui/widget"&gt;&lt;code&gt;Widget&lt;/code&gt;&lt;/a&gt;, which looks similar to a SwiftUI &lt;code&gt;View&lt;/code&gt; with a couple of extra options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You must provide a &lt;code&gt;kind&lt;/code&gt; String constant, with a unique (to the app) identifier for the type of widget.&lt;/li&gt;
&lt;li&gt;You need to return a &lt;code&gt;WidgetConfiguration&lt;/code&gt; object from &lt;code&gt;body&lt;/code&gt;, and provide the &lt;code&gt;configurationDisplayName(_)&lt;/code&gt; and &lt;code&gt;description(_)&lt;/code&gt; view modifiers. The simplest option here is a &lt;code&gt;StaticConfiguration&lt;/code&gt; (well, I haven&amp;rsquo;t learned about any others yet).&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;MyWidget&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;MyWidget&amp;quot;&lt;/span&gt;

    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;some&lt;/span&gt; &lt;span class="n"&gt;WidgetConfiguration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;StaticConfiguration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MyTimelineProvider&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="cp"&gt;#available&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cp"&gt;iOS&lt;/span&gt; &lt;span class="mf"&gt;17.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;MyWidgetView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;containerBackground&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tertiary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;widget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;MyWidgetView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;background&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;configurationDisplayName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;My Widget&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;This is an example widget.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;supportedFamilies&lt;/span&gt;&lt;span class="p"&gt;([.&lt;/span&gt;&lt;span class="n"&gt;systemSmall&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;systemMedium&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;systemLarge&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;StaticConfiguration&lt;/code&gt; struct takes the widget&amp;rsquo;s &lt;code&gt;kind&lt;/code&gt; string, an instance of your timeline provider, and a closure to call with each entry in the timeline. The closure should return the SwiftUI view configured/rendered for that particular entry.&lt;/p&gt;
&lt;p&gt;You can also provide the &lt;code&gt;supportedFamilies&lt;/code&gt; view modifier with a list of the different types of widget sizes that this widget supports, including Lock Screen widgets. You can use the environment variable &lt;code&gt;.widgetFamily&lt;/code&gt; inside the view to change the layout of the view based on &lt;a href="https://developer.apple.com/documentation/WidgetKit/WidgetFamily"&gt;what size widget&lt;/a&gt; is currently displayed:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;widgetFamily&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;widgetFamily&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Apps built using the iOS 17 SDK require all widgets to use the new &lt;code&gt;containerBackground&lt;/code&gt; modifier, which automatically handles padding.&lt;/p&gt;
&lt;h3 id="previewing-widgets"&gt;Previewing Widgets&lt;a class="headerlink" href="#previewing-widgets" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Widgets are simple to use with SwiftUI previews: you can either preview the widget &lt;code&gt;View&lt;/code&gt; by itself, passing a static timeline entry, such as using the pre-Xcode 15 preview provider:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;MyWidgetView_Previews&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PreviewProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;previews&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;some&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;MyWidgetView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MyWidgetEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Text&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;previewContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WidgetPreviewContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;systemSmall&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Or you can use Xcode 15&amp;rsquo;s new &lt;code&gt;#Preview&lt;/code&gt; macro, with the version specifically designed for widgets, that accepts a timeline of entries. This time you pass it the widget itself, not the view the widget renders:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="n"&gt;Preview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;systemSmall&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;MyWidget&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;timeline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;MyWidgetEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Text 1&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;MyWidgetEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Text 2&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;hr /&gt;
&lt;p&gt;If you&amp;rsquo;ve reached this far, then you&amp;rsquo;ve done enough to design the widget and how it populates its timeline into the future, but we still need to tell iOS about it. This is done with one last struct.&lt;/p&gt;
&lt;h2 id="the-widget-bundle"&gt;The Widget Bundle&lt;a class="headerlink" href="#the-widget-bundle" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To tell iOS about the available widgets in your app, you need a single widget bundle defined in the widget extension, which is a struct conforming to &lt;a href="https://developer.apple.com/documentation/swiftui/widgetbundle"&gt;&lt;code&gt;WidgetBundle&lt;/code&gt;&lt;/a&gt;, and marked with the &lt;code&gt;@main&lt;/code&gt; wrapper. Similar to SwiftUI views, this requires one computed parameter, &lt;code&gt;body&lt;/code&gt;, but this time is of type &lt;code&gt;some Widget&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;
&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;MyWidgets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WidgetBundle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;some&lt;/span&gt; &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;MyWidget&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Multiple different widgets can be returned, just put each on a new line within the &lt;code&gt;body&lt;/code&gt;. You can also do some logic here, such as &lt;code&gt;if #available&lt;/code&gt; checks to limit certain widgets to particular iOS versions, etc.&lt;/p&gt;
&lt;p&gt;If you have multiple widgets that need the same data, you can reuse the same Timeline Entry and Timeline Provider in multiple &lt;code&gt;Widget&lt;/code&gt; structs.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;With that, your app should be able to provide one or more widgets to the user, and control what sizes they are available in. However, you can&amp;rsquo;t yet provide options for the user to pick from, allowing them to &amp;ldquo;edit&amp;rdquo; the widget. I&amp;rsquo;ll write up what I know about that in another post, soon!&lt;/p&gt;</content><category term="Development"></category><category term="Swift"></category><category term="SwiftUI"></category></entry><entry><title>Review: 43215 The Enchanted Treehouse</title><link href="https://marmaladeofcourse.com/2023/06/18/review-43215-the-enchanted-treehouse/" rel="alternate"></link><published>2023-06-18T00:00:00+01:00</published><updated>2023-06-18T00:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2023-06-18:/2023/06/18/review-43215-the-enchanted-treehouse/</id><summary type="html">&lt;p&gt;My latest LEGO review is up on &lt;a href="https://brickset.com/article/97503/review-43215-the-enchanted-treehouse"&gt;Brickset&lt;/a&gt;, of the Disney 100 Enchanted Treehouse.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The star of the set is clearly the impressive selection of minidolls. Their shape definitely suits the Disney Princesses more than a minifigure would; it is just a shame their articulation is significantly less.&lt;/p&gt;
&lt;p&gt;The two …&lt;/p&gt;&lt;/blockquote&gt;</summary><content type="html">&lt;p&gt;My latest LEGO review is up on &lt;a href="https://brickset.com/article/97503/review-43215-the-enchanted-treehouse"&gt;Brickset&lt;/a&gt;, of the Disney 100 Enchanted Treehouse.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The star of the set is clearly the impressive selection of minidolls. Their shape definitely suits the Disney Princesses more than a minifigure would; it is just a shame their articulation is significantly less.&lt;/p&gt;
&lt;p&gt;The two halves of the treehouse look fantastic together, and there&amp;rsquo;s plenty of play value with the slide, stairs, zip wire, canoe, and various other smaller builds and interactive sections. It looks good both on display and during play.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img alt="LEGO 43215 The Enchanted Treehouse" src="https://images.brickset.com/news/97503_IMG_2051.jpg" /&gt;&lt;/p&gt;</content><category term="LEGO Reviews"></category></entry><entry><title>Matching the List background colour in SwiftUI</title><link href="https://marmaladeofcourse.com/2023/03/26/matching-the-list-background-colour-in-swiftui/" rel="alternate"></link><published>2023-03-26T20:00:00+01:00</published><updated>2023-03-26T20:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2023-03-26:/2023/03/26/matching-the-list-background-colour-in-swiftui/</id><summary type="html">&lt;p&gt;I recently came across a situation where I wanted to match the background colour of a a header above a SwiftUI &lt;code&gt;List&lt;/code&gt; (using the default &lt;code&gt;.insetGrouped&lt;/code&gt; List style) to that used by the List itself. I had done no styling to the List itself, so was relying on the system-provided …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I recently came across a situation where I wanted to match the background colour of a a header above a SwiftUI &lt;code&gt;List&lt;/code&gt; (using the default &lt;code&gt;.insetGrouped&lt;/code&gt; List style) to that used by the List itself. I had done no styling to the List itself, so was relying on the system-provided background colour—this is what I wanted to match.&lt;/p&gt;
&lt;p&gt;I tried a couple of the standard constants provided by &lt;code&gt;UIColor&lt;/code&gt;, and landed on &lt;a href="https://developer.apple.com/documentation/uikit/uicolor/3173137-secondarysystembackground"&gt;&lt;code&gt;.secondarySystemBackground&lt;/code&gt;&lt;/a&gt;. It wasn&amp;rsquo;t until I had the build running on my phone and I was using the app later in the day that I noticed something was off slightly:&lt;/p&gt;
&lt;p&gt;&lt;img alt="SwiftUI List background using .secondarySystemBackground in light and dark mode" src="/assets/swiftui-secondary-background.png" /&gt;&lt;/p&gt;
&lt;p&gt;In light mode, everything was fine; but in dark mode, the background of the header and navigation toolbar wasn&amp;rsquo;t dark enough! It turns out that what I &lt;em&gt;actually&lt;/em&gt; wanted was &lt;a href="https://developer.apple.com/documentation/uikit/uicolor/3173145-systemgroupedbackground"&gt;&lt;code&gt;.systemGroupedBackground&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img alt="SwiftUI List background using .systemGroupedBackground in light and dark mode" src="/assets/swiftui-grouped-background.png" /&gt;&lt;/p&gt;
&lt;p&gt;Now they match up as intended. Let this be a lesson to myself to test in both light mode and dark mode when developing anything that relies on colour!&lt;/p&gt;</content><category term="Development"></category><category term="Swift"></category><category term="SwiftUI"></category></entry><entry><title>Review: 31138 Beach Camper Van</title><link href="https://marmaladeofcourse.com/2023/03/26/review-31138-beach-camper-van/" rel="alternate"></link><published>2023-03-26T14:00:00+01:00</published><updated>2023-03-26T14:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2023-03-26:/2023/03/26/review-31138-beach-camper-van/</id><summary type="html">&lt;p&gt;I am just loving the Creator 3-in-1 sets that LEGO are &lt;a href="https://brickset.com/article/92284/review-31138-beach-camper-van"&gt;churning out nowadays&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://brickset.com/sets/31138-1"&gt;31138&lt;/a&gt; Beach Camper Van is a perfect example of this: an excellent camper van, some lovely little beach huts, an adorable crab—and that&amp;rsquo;s all just in the primary build! Let&amp;rsquo;s take a look …&lt;/p&gt;&lt;/blockquote&gt;</summary><content type="html">&lt;p&gt;I am just loving the Creator 3-in-1 sets that LEGO are &lt;a href="https://brickset.com/article/92284/review-31138-beach-camper-van"&gt;churning out nowadays&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://brickset.com/sets/31138-1"&gt;31138&lt;/a&gt; Beach Camper Van is a perfect example of this: an excellent camper van, some lovely little beach huts, an adorable crab—and that&amp;rsquo;s all just in the primary build! Let&amp;rsquo;s take a look at the set in detail, including the two alternative models that often make the 3-in-1 sets the success they are.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img alt="LEGO 31138 Beach Camper Van" src="https://images.brickset.com/news/92284_IMG_2001-Edit.jpg" /&gt;&lt;/p&gt;</content><category term="LEGO Reviews"></category></entry><entry><title>Review: 31139 Cozy House</title><link href="https://marmaladeofcourse.com/2023/03/16/review-31139-cozy-house/" rel="alternate"></link><published>2023-03-16T00:00:00+00:00</published><updated>2023-03-16T00:00:00+00:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2023-03-16:/2023/03/16/review-31139-cozy-house/</id><summary type="html">&lt;p&gt;I love LEGO&amp;rsquo;s Creator 3-in-1 range, where every set can be built into three different designs. &lt;a href="https://brickset.com/sets/31139-1"&gt;31139&lt;/a&gt; Cozy House is no exception, with three excellent builds, and a handful of adorable little bugs to boot!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The primary model of the set is the titular Cozy House (or &amp;ldquo;cosy&amp;rdquo;, as …&lt;/p&gt;&lt;/blockquote&gt;</summary><content type="html">&lt;p&gt;I love LEGO&amp;rsquo;s Creator 3-in-1 range, where every set can be built into three different designs. &lt;a href="https://brickset.com/sets/31139-1"&gt;31139&lt;/a&gt; Cozy House is no exception, with three excellent builds, and a handful of adorable little bugs to boot!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The primary model of the set is the titular Cozy House (or &amp;ldquo;cosy&amp;rdquo;, as we&amp;rsquo;d be more likely to write here in the UK). It&amp;rsquo;s a fantastic little two-story building with pitched roofs, a delightful garden, and is full of excellent details!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img alt="Cozy House LEGO set" src="https://images.brickset.com/news/92285_IMG_1979.jpg" /&gt;&lt;/p&gt;</content><category term="LEGO Reviews"></category></entry><entry><title>Review: 60359 Dunk Stunt Ramp Challenge</title><link href="https://marmaladeofcourse.com/2023/03/07/review-60359-dunk-stunt-ramp-challenge/" rel="alternate"></link><published>2023-03-07T00:00:00+00:00</published><updated>2023-03-07T00:00:00+00:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2023-03-07:/2023/03/07/review-60359-dunk-stunt-ramp-challenge/</id><summary type="html">&lt;p&gt;The latest batch of LEGO I&amp;rsquo;ve been provided to review included three City Stuntz sets, a range that I&amp;rsquo;ve not built or played with before. They come with flywheel-powered bikes and stunt arenas, and are all pretty creative and fun! The first review went up today, for &lt;a href="https://brickset.com/article/92281/review-60359-dunk-stunt-ramp-challenge"&gt;60359 …&lt;/a&gt;&lt;/p&gt;</summary><content type="html">&lt;p&gt;The latest batch of LEGO I&amp;rsquo;ve been provided to review included three City Stuntz sets, a range that I&amp;rsquo;ve not built or played with before. They come with flywheel-powered bikes and stunt arenas, and are all pretty creative and fun! The first review went up today, for &lt;a href="https://brickset.com/article/92281/review-60359-dunk-stunt-ramp-challenge"&gt;60359&lt;/a&gt; Dunk Stunt Ramp Challenge:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Despite the theme entering its third year, this was my first experience of the line, and I was quite impressed! The flywheel-powered bike is a lot of fun, and it kept both my children (aged four and six) entertained for some considerable time (once they stopped fighting over whose turn it was).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img alt="Dunk Stunt Ramp Challenge LEGO set" src="https://images.brickset.com/news/92281_IMG_1957_202303011355.jpg" /&gt;&lt;/p&gt;</content><category term="LEGO Reviews"></category></entry><entry><title>Review: 71419 Peach's Garden Balloon Ride</title><link href="https://marmaladeofcourse.com/2023/02/25/review-71419-peachs-garden-balloon-ride/" rel="alternate"></link><published>2023-02-25T00:00:00+00:00</published><updated>2023-02-25T00:00:00+00:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2023-02-25:/2023/02/25/review-71419-peachs-garden-balloon-ride/</id><summary type="html">&lt;p&gt;Princess Peach is back (in LEGO form) with another expansion set to the Super Mario theme. Huw over at &lt;a href="https://brickset.com"&gt;Brickset.com&lt;/a&gt; asked me to take a look and see how it stacks up.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It is typical LEGO Super Mario fare: bright and colourful, with standard game mechanics, introducing a new …&lt;/p&gt;&lt;/blockquote&gt;</summary><content type="html">&lt;p&gt;Princess Peach is back (in LEGO form) with another expansion set to the Super Mario theme. Huw over at &lt;a href="https://brickset.com"&gt;Brickset.com&lt;/a&gt; asked me to take a look and see how it stacks up.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It is typical LEGO Super Mario fare: bright and colourful, with standard game mechanics, introducing a new character and recycling some old favourites.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Read the full review on &lt;a href="https://brickset.com/article/90800/review-71419-peach-s-garden-balloon-ride"&gt;Brickset.com&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Princess Peach Garden Balloon Ride LEGO set" src="https://images.brickset.com/news/90800_IMG_1930_202302231422.jpg" /&gt;&lt;/p&gt;</content><category term="LEGO Reviews"></category></entry><entry><title>Implementing a Tip Jar with Swift and SwiftUI</title><link href="https://marmaladeofcourse.com/2023/02/17/implementing-a-tip-jar-with-swift-and-swiftui/" rel="alternate"></link><published>2023-02-17T00:00:00+00:00</published><updated>2023-02-17T00:00:00+00:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2023-02-17:/2023/02/17/implementing-a-tip-jar-with-swift-and-swiftui/</id><summary type="html">&lt;p&gt;Pressured by friends, I recently added a tip jar to &lt;a href="/pendulum/"&gt;Pendulum&lt;/a&gt;, the pen pal tracking app I develop with my friend &lt;a href="https://418teapot.net"&gt;Alex&lt;/a&gt;. It&amp;rsquo;s implemented (like the rest of the app) in pure SwiftUI, and uses the newer &lt;a href="https://developer.apple.com/storekit/"&gt;StoreKit 2&lt;/a&gt; APIs to communicate with Apple to fetch the IAP information …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Pressured by friends, I recently added a tip jar to &lt;a href="/pendulum/"&gt;Pendulum&lt;/a&gt;, the pen pal tracking app I develop with my friend &lt;a href="https://418teapot.net"&gt;Alex&lt;/a&gt;. It&amp;rsquo;s implemented (like the rest of the app) in pure SwiftUI, and uses the newer &lt;a href="https://developer.apple.com/storekit/"&gt;StoreKit 2&lt;/a&gt; APIs to communicate with Apple to fetch the IAP information and make purchases. This is a write of how I muddled through the process, from start to finish.&lt;/p&gt;
&lt;h2 id="defining-the-tip-iaps"&gt;Defining the tip IAPs&lt;a class="headerlink" href="#defining-the-tip-iaps" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The first step is to head into &lt;a href="https://appstoreconnect.apple.com"&gt;App Store Connect&lt;/a&gt; and define the IAPs for each of the tips you want to offer. In my case, I knew what I wanted the tips to be called, in ascending order of price, but not exactly what price each would be. That doesn&amp;rsquo;t matter for now, though. I had the following in mind, amusingly named after fountain pen nib sizes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Extra Fine Tip&lt;/li&gt;
&lt;li&gt;Fine Tip&lt;/li&gt;
&lt;li&gt;Medium Tip&lt;/li&gt;
&lt;li&gt;Broad Tip&lt;/li&gt;
&lt;li&gt;Stub Tip&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To create these in App Store Connect, I headed to the &lt;strong&gt;In-App Purchases&lt;/strong&gt; section under &lt;strong&gt;Features&lt;/strong&gt; on the app&amp;rsquo;s &lt;strong&gt;App Store&lt;/strong&gt; tab. There, I could create each tip using the plus button. The initial form has three fields:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Type&lt;/strong&gt;: either &lt;em&gt;Consumable&lt;/em&gt; or &lt;em&gt;Non-Consumable&lt;/em&gt;. I didn&amp;rsquo;t know what these are, and had to look them up: consumable IAPs are those that can be purchased multiple times by the user (for things such as in-game currency), and non-consumable IAPs can only be purchased once (for features such as unlocking a premium mode of the app). For a tip jar, I wanted the former.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reference Name&lt;/strong&gt;: a name for the IAP, solely for your own use. It doesn&amp;rsquo;t appear anywhere public; for ease, I entered the names I&amp;rsquo;d chosen for the tips above.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Product ID&lt;/strong&gt;: a unique identifier for the IAP. I wasn&amp;rsquo;t sure &lt;em&gt;how&lt;/em&gt; unique this was meant to be, so to be on the safe side I went for the usual Apple-style of defining them as reversed-DNS bundle identifiers. For example, &lt;code&gt;uk.co.bencardy.Pendulum.ExtraFineTip&lt;/code&gt;, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once created, to complete the IAP you need to define a few extra fields that aren&amp;rsquo;t present on the initial form, such as the Price Schedule (where you set the cost of the IAP, using Apple&amp;rsquo;s price tiers), and the App Store Localization, where you define how the tip appears (its name and description) in the App Store for each language. I defined only &amp;ldquo;English (U.K.)&amp;rdquo; which is all the app is offered in.&lt;/p&gt;
&lt;h2 id="defining-the-tips-in-swift"&gt;Defining the tips in Swift&lt;a class="headerlink" href="#defining-the-tips-in-swift" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;StoreKit2 doesn&amp;rsquo;t have an API to fetch all the IAPs associated with an app; instead, you need to request specific Product IDs known by the app ahead of time. To this end, I decided it would be best to represent the available tips in the app with an &lt;code&gt;enum&lt;/code&gt;, based off their unique IDs:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;TipJar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CaseIterable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;extraFine&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;uk.co.bencardy.Pendulum.ExtraFineTip&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;fine&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;uk.co.bencardy.Pendulum.FineTip&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;medium&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;uk.co.bencardy.Pendulum.MediumTip&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;broad&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;uk.co.bencardy.Pendulum.BroadTip&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;stub&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;uk.co.bencardy.Pendulum.StubTip&amp;quot;&lt;/span&gt;

    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="kc"&gt;self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;extraFine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Extra Fine&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Fine&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;medium&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Medium&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;broad&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Broad&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Stub&amp;quot;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I made the &lt;code&gt;enum&lt;/code&gt; conform to &lt;code&gt;CaseIterable&lt;/code&gt;, meaning I can iterate over &lt;code&gt;TipJar.allCases&lt;/code&gt; to display all available tips in the SwiftUI view. This I did inside my &lt;code&gt;TipJarView&lt;/code&gt;, wrapping each tip in a button and displaying some placeholder information about each one:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;TipJarView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;some&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;List&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ForEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TipJar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allCases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;tip&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
                &lt;span class="n"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;HStack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foregroundColor&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="n"&gt;Spacer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                        &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;£??&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foregroundColor&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;accentColor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;navigationTitle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Support Pendulum&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This presented a list of the available tips, with a place for me to put their prices (once known), and a button to purchase the tip (functionality yet to be completed):&lt;/p&gt;
&lt;p&gt;&lt;img alt="Tip jar mockup" src="/assets/swiftui-tip-jar/list.png" /&gt;&lt;/p&gt;
&lt;h2 id="fetching-iap-information-with-storekit-2"&gt;Fetching IAP information with StoreKit 2&lt;a class="headerlink" href="#fetching-iap-information-with-storekit-2" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The next step was to actually fetch the prices I had defined in App Store Connect within the app, and display them. For this, I needed to use Apple&amp;rsquo;s &lt;a href="https://developer.apple.com/storekit/"&gt;StoreKit 2&lt;/a&gt; APIs. The particular one I&amp;rsquo;m interested in here is &lt;a href="https://developer.apple.com/documentation/storekit/product/3851116-products"&gt;&lt;code&gt;Product.products(for:)&lt;/code&gt;&lt;/a&gt;, which returns an array of &lt;code&gt;Product&lt;/code&gt; objects for each ID passed in. I decided to add a static method to the &lt;code&gt;TipJar&lt;/code&gt; enum to call this with all my IAP IDs, and return a mapping of &lt;code&gt;[TipJar: Product]&lt;/code&gt; that the view could use. The new StoreKit 2 APIs are all asyncronous, so my function needed to be to:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;StoreKit&lt;/span&gt;

&lt;span class="kd"&gt;extension&lt;/span&gt; &lt;span class="nc"&gt;TipJar&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;fetchProducts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kc"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;products&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allCases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="bp"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kc"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[:]&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TipJar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;storeLogger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Could not fetch products: &lt;/span&gt;&lt;span class="si"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;localizedDescription&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[:]&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;(I am sure there is a more concise way to compile the dictionary, but those are the kinds of Swift tricks I am not yet proficient enough in the language to be able to come up with when I need them, so a simple for loop had to suffice here.)&lt;/p&gt;
&lt;p&gt;With this extension in place, I can add a new &lt;code&gt;State&lt;/code&gt; variable to my view, and fetch the product information when the view is loaded:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;TipJarView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="hll"&gt;    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;tipJarPrices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TipJar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[:]&lt;/span&gt;
&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;some&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;List&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="p"&gt;...&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="hll"&gt;        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;            &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;products&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;TipJar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fetchProducts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;            &lt;span class="n"&gt;DispatchQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                &lt;span class="n"&gt;withAnimation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                    &lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tipJarPrices&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;            &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;        &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I can now use this information in the loop around the products. The &lt;a href="https://developer.apple.com/documentation/storekit/product"&gt;&lt;code&gt;Product&lt;/code&gt;&lt;/a&gt; object provides a &lt;code&gt;displayPrice&lt;/code&gt; property, which handily returns the price of the tip in the user&amp;rsquo;s local currency, with the currency symbol:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;ForEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TipJar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allCases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;tip&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="n"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;HStack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foregroundColor&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;Spacer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="hll"&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;product&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tipJarPrices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tip&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;displayPrice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foregroundColor&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;accentColor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;            &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;After a brief moment with no prices available, they suddenly all fade in:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Tip jar with prices" src="/assets/swiftui-tip-jar/prices.png" /&gt;&lt;/p&gt;
&lt;p&gt;We can do better than that, though. Using another state variable, we can notify the view when the product information has been loaded, and display a progress spinner until that point. We also need to handle the case that, for some reason, the products have been fetched but a particular tip isn&amp;rsquo;t present. I chose to do so with a simple warning triangle.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;TipJarView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;tipJarPrices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TipJar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[:]&lt;/span&gt;
&lt;span class="hll"&gt;    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;productsFetched&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;some&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;List&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ForEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TipJar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allCases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;tip&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
                &lt;span class="n"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;HStack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foregroundColor&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="n"&gt;Spacer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;product&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tipJarPrices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tip&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                            &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;displayPrice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foregroundColor&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;accentColor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="hll"&gt;                        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;productsFetched&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                                &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;systemName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;exclamationmark.triangle&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                                &lt;span class="n"&gt;ProgressView&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                            &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;                        &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;products&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;TipJar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fetchProducts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;DispatchQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;withAnimation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tipJarPrices&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;
&lt;span class="hll"&gt;                    &lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;productsFetched&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/span&gt;                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;img alt="Tip jar loading" src="/assets/swiftui-tip-jar/loading.gif" /&gt;&lt;/p&gt;
&lt;h2 id="making-a-purchase"&gt;Making a purchase&lt;a class="headerlink" href="#making-a-purchase" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To initiate the actual purchase of the IAP, we need to call the &lt;code&gt;Product&lt;/code&gt;&amp;lsquo;s &lt;code&gt;.purchase()&lt;/code&gt; method. This async method returns a result indicating whether the purchase was successful or not, and a few other bits of information. As is my way, I chose to wrap this up in a method on the &lt;code&gt;TipJar&lt;/code&gt; enum:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;extension&lt;/span&gt; &lt;span class="nc"&gt;TipJar&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;purchase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;_&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;storeLogger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Attempting to purchase &lt;/span&gt;&lt;span class="si"&gt;\(&lt;/span&gt;&lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;purchaseResult&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;purchase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;purchaseResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;verificationResult&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;storeLogger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Purchase result: success&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;verificationResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="n"&gt;storeLogger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Purchase success result: verified&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
                &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;storeLogger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Purchase success result: unverified&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;storeLogger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Could not purchase &lt;/span&gt;&lt;span class="si"&gt;\(&lt;/span&gt;&lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;localizedDescription&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For ease of use in the view, I convert the result into a simple &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt; for whether the purchase went through successfully. In the view, I can fire this off inside a &lt;code&gt;Task&lt;/code&gt; in the button&amp;rsquo;s &lt;code&gt;action&lt;/code&gt; method, and handle the response appropriately. In this case, I want to display an alert saying thank you on a successful purchase, and do nothing if it was cancelled:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;TipJarView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="hll"&gt;    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;showingSuccessAlert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/span&gt;    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;some&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;List&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ForEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TipJar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allCases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;tip&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
                &lt;span class="n"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="hll"&gt;                    &lt;span class="n"&gt;storeLogger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;tip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s"&gt; tapped&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;product&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tipJarPrices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tip&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                        &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;tip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;purchase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                                &lt;span class="n"&gt;DispatchQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                                    &lt;span class="n"&gt;withAnimation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                                        &lt;span class="n"&gt;showingSuccessAlert&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;successful&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                                    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                                &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                            &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                        &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;                &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="p"&gt;...&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="hll"&gt;        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isPresented&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;showingSuccessAlert&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;            &lt;span class="n"&gt;Alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Purchase Successful&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Thank you for supporting Pendulum!&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;dismissButton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;🧡&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;        &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;        &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;img alt="Tip jar success alert" src="/assets/swiftui-tip-jar/alert.png" /&gt;&lt;/p&gt;
&lt;p&gt;We now have a fully-functional tip jar! Users can view the list of tips, complete with pricing information in their own local currency, and tap on a tip to purchase it.&lt;/p&gt;
&lt;h2 id="preventing-user-interaction"&gt;Preventing user interaction&lt;a class="headerlink" href="#preventing-user-interaction" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The final niggle I wanted to fix was preventing user interaction with the tip buttons in three situations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;while the tip information is still loading,&lt;/li&gt;
&lt;li&gt;if there was an error loading a particular tip, and&lt;/li&gt;
&lt;li&gt;while an IAP purchase is in progress.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The first two situations can be handled together: in both cases, the &lt;code&gt;tipJarPrices&lt;/code&gt; dict has no entry for the given tip. A simple &lt;code&gt;disabled&lt;/code&gt; modifier on the &lt;code&gt;Button&lt;/code&gt; will prevent the user from tapping it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;TipJarView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;some&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;List&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ForEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TipJar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allCases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;tip&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
                &lt;span class="n"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="p"&gt;...&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="hll"&gt;                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tipJarPrices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tip&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The latter requires us to store some information about whether a purchase is in progress or not. Again, a &lt;code&gt;State&lt;/code&gt; variable is perfect here. We can set it when the tip is tapped, and check it later:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;TipJarView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="hll"&gt;    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;pendingPurchase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TipJar&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
&lt;/span&gt;    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;some&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;List&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ForEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TipJar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allCases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;tip&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
                &lt;span class="n"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="hll"&gt;                    &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="n"&gt;pendingPurchase&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;                    &lt;span class="n"&gt;storeLogger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;tip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s"&gt; tapped&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;product&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tipJarPrices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tip&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="hll"&gt;                        &lt;span class="n"&gt;withAnimation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                            &lt;span class="n"&gt;pendingPurchase&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tip&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                        &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;                        &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                            &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;successful&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;tip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;purchase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                            &lt;span class="n"&gt;storeLogger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Successful? &lt;/span&gt;&lt;span class="si"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;successful&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                            &lt;span class="n"&gt;DispatchQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                &lt;span class="n"&gt;withAnimation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                    &lt;span class="n"&gt;showingSuccessAlert&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;successful&lt;/span&gt;
&lt;span class="hll"&gt;                                    &lt;span class="n"&gt;pendingPurchase&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
&lt;/span&gt;                                &lt;span class="p"&gt;}&lt;/span&gt;
                            &lt;span class="p"&gt;}&lt;/span&gt;
                        &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;GroupBox&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="n"&gt;HStack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                            &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;tip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s"&gt; Tip&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fullWidth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;product&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tipJarPrices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tip&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="hll"&gt;                                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pendingPurchase&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;tip&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                                    &lt;span class="n"&gt;ProgressView&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                                &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;                                    &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;displayPrice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                                        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foregroundColor&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;accentColor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="hll"&gt;                                &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;                            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;productsFetched&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                    &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;systemName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;exclamationmark.triangle&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                                &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                    &lt;span class="n"&gt;ProgressView&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                                &lt;span class="p"&gt;}&lt;/span&gt;
                            &lt;span class="p"&gt;}&lt;/span&gt;
                        &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foregroundColor&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="hll"&gt;                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tipJarPrices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tip&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;pendingPurchase&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There&amp;rsquo;s a few parts to this code, so I&amp;rsquo;ll highlight them here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;State&lt;/code&gt; variable stores whether or not a purchase is in progress, and if so, which tip is being purchased.&lt;/li&gt;
&lt;li&gt;When a tip is tapped, we guard against making multiple concurrent purchases by immediately returning if &lt;code&gt;pendingPurchase&lt;/code&gt; isn&amp;rsquo;t &lt;code&gt;nil&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If there&amp;rsquo;s no purchase in progress, we update &lt;code&gt;pendingPurchase&lt;/code&gt; when a tip is tapped, and set it back to &lt;code&gt;nil&lt;/code&gt; once the purchase has completed.&lt;/li&gt;
&lt;li&gt;If a purchase is pending for a given tip (&lt;code&gt;purchasePending == tip&lt;/code&gt;), we display a progress spinner instead of the price, so the user knows that something is still happening.&lt;/li&gt;
&lt;li&gt;Finally, we disable the button for each tip while a purchase is in progress.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="Tip jar Apple confirmation" src="/assets/swiftui-tip-jar/confirm.png" /&gt;&lt;/p&gt;
&lt;h2 id="what-i-learned"&gt;What I learned&lt;a class="headerlink" href="#what-i-learned" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In putting this tip jar together, I learned a number of things, not least:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;how to define IAPs in App Store Connect;&lt;/li&gt;
&lt;li&gt;how to fetch that IAP information using StoreKit 2, and display it in the app; and&lt;/li&gt;
&lt;li&gt;how to initiate IAP purchases when the user taps a button, and handle the various possible responses.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are a number of ways we could improve upon this basic tip jar, but it&amp;rsquo;s a pretty decent start. In my own implementation in the app, I also added support for storing a history of how many of each tip the user has purchased, in order to show them the size of their &amp;ldquo;tip collection&amp;rdquo; for a little bit of whimsy (and to hopefully encourage more tips!). That&amp;rsquo;s left as an exercise for the reader; but I&amp;rsquo;m storing the information in &lt;code&gt;NSUbiquitousKeyValueStore&lt;/code&gt;, a useful little class that automatically syncs its using iCloud.&lt;/p&gt;
&lt;p&gt;Hopefully you&amp;rsquo;ve found some useful information in this post to inspire you to add a tip jar to your own indie apps! I&amp;rsquo;d love to hear about them: let me know on &lt;a href="https://snailedit.social/@benbacardi"&gt;Mastodon&lt;/a&gt;.&lt;/p&gt;</content><category term="Development"></category><category term="Swift"></category><category term="SwiftUI"></category></entry><entry><title>SwiftUI: Equal Width Icons</title><link href="https://marmaladeofcourse.com/2023/02/02/swiftui-equal-width-icons/" rel="alternate"></link><published>2023-02-02T00:00:00+00:00</published><updated>2023-02-02T00:00:00+00:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2023-02-02:/2023/02/02/swiftui-equal-width-icons/</id><summary type="html">&lt;p&gt;Following on from my previous post on &lt;a href="https://marmaladeofcourse.com/2023/01/25/swiftui-text-views-and-alignment/"&gt;SwiftUI Text alignment&lt;/a&gt;, I thought I&amp;rsquo;d post about another common issue I run into and how to solve it relatively simply: equal width icons. This logic applies to any series of Views you want to display equally in either height or width …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Following on from my previous post on &lt;a href="https://marmaladeofcourse.com/2023/01/25/swiftui-text-views-and-alignment/"&gt;SwiftUI Text alignment&lt;/a&gt;, I thought I&amp;rsquo;d post about another common issue I run into and how to solve it relatively simply: equal width icons. This logic applies to any series of Views you want to display equally in either height or width, but the most common place it occurs in my own code is when using SF Symbols. Each symbol has its own width, so when using them as bullets or in other situations where you want them to line up it can be infuriating.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s set the stage with some code. The example I&amp;rsquo;m using is the ubiquitous &amp;ldquo;What&amp;rsquo;s New&amp;rdquo; sheet, found in many of Apple&amp;rsquo;s own apps. I&amp;rsquo;ve borrowed the text and icons from the latest update to &lt;a href="https://www.penedex.com/"&gt;Penedex&lt;/a&gt;, a pen-tracking app developed by Connor Rose. Here&amp;rsquo;s the sample View:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;EqualWidthIcons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;some&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;VStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

            &lt;span class="n"&gt;VStack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;What&amp;#39;s New!&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;largeTitle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Version 2023.01&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;HStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;systemName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;star.circle.fill&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foregroundColor&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;yellow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;VStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Star Ratings Toggle&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;headline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;If you believe all your pens are your favourite, you can now turn off star ratings via Settings.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fullWidth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="n"&gt;HStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;systemName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;square.and.arrow.up.fill&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foregroundColor&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;green&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;VStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Share Sheet Fix&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;headline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Fixed an issue where the date in your Currently Ink&amp;#39;d shared image would not display correctly.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fullWidth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="n"&gt;HStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;systemName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;scroll.fill&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foregroundColor&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;blue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;VStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Brand List Fix&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;headline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Fixed issues with duplicate brands populating your Brand List.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fullWidth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="n"&gt;HStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;systemName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;ladybug.fill&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foregroundColor&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;red&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;VStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Misc. Bug Fixes&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;headline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Plenty of other minor improvements.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fullWidth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="n"&gt;Spacer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;A series of repeated sections (don&amp;rsquo;t worry, it&amp;rsquo;ll be much neater by the end of the post), each with an icon, a title and a short summary. It makes use of the &lt;code&gt;fullWidth()&lt;/code&gt; modifier from my &lt;a href="https://marmaladeofcourse.com/2023/01/25/swiftui-text-views-and-alignment/"&gt;previous post&lt;/a&gt;. This is how iOS renders it:&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="/assets/swiftui-equal/initial.png" /&gt;&lt;/p&gt;
&lt;p&gt;As a starter for ten, this is pretty good! But the scroll icon is wider than the previous two, and the ladybird icon even wider still. This pushes the text out to the right and it no longer lines up. We could manually define a width for the icon:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;HStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;systemName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;square.and.arrow.up.fill&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foregroundColor&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;green&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;VStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Share Sheet Fix&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;headline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Fixed an issue where the date in your Currently Ink&amp;#39;d shared image would not display correctly.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fullWidth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;(From now on, I&amp;rsquo;m only going to show the code for one of the four sections. The others are identical in all but the content.)&lt;/p&gt;
&lt;p&gt;Yay, that works!&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="/assets/swiftui-equal/manual-width.png" /&gt;&lt;/p&gt;
&lt;p&gt;But it&amp;rsquo;s a bit of a &amp;ldquo;magic number&amp;rdquo;, and one that would likely need to be tweaked should you change the icons at a later date. Not to mention that it just won&amp;rsquo;t scale with the icon if the user adjusts the text size on their iOS device. We can do better than that.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s three parts to the solution. We need to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;read the width of each icon,&lt;/li&gt;
&lt;li&gt;store the maximum of those widths somewhere, and&lt;/li&gt;
&lt;li&gt;set the width of each icon to that maximum.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let&amp;rsquo;s take these one at a time.&lt;/p&gt;
&lt;h4 id="read-the-width-of-each-icon"&gt;Read the width of each icon&lt;a class="headerlink" href="#read-the-width-of-each-icon" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;This is easily achieved using a &lt;code&gt;GeometryReader&lt;/code&gt;. I have a bit of a love/hate relationship with this SwiftUI utility, but in this case it works very well. Appyling it as a background to the icon means it will grow to match the size of the icon&amp;rsquo;s view, and we can read the frame&amp;rsquo;s size:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;systemName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;square.and.arrow.up.fill&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foregroundColor&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;green&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;background&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;GeometryReader&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;geo&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="c1"&gt;// geo.size.width is the width of the icon&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;But what we can do with that value?&lt;/p&gt;
&lt;h4 id="store-the-maximum-width"&gt;Store the maximum width&lt;a class="headerlink" href="#store-the-maximum-width" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;In order to calculate the maximum, we need a couple of things. We need a State variable for the max icon width, and let&amp;rsquo;s give it a sensible default:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;iconWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CGFloat&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And we need a way to accumulate the values read by each &lt;code&gt;GeometryReader&lt;/code&gt; and take the maximum for our &lt;code&gt;iconWidth&lt;/code&gt; variable. SwiftUI provides us with just the thing: a &lt;a href="https://developer.apple.com/documentation/swiftui/preferencekey"&gt;&lt;code&gt;PreferenceKey&lt;/code&gt;&lt;/a&gt;. This is a strange bit of SwiftUI that allows us to combine a number of values into a single one, and store it somewhere. First, we need to define a custom &lt;code&gt;PreferenceKey&lt;/code&gt;, with a &lt;code&gt;reduce&lt;/code&gt; function that returns the maximum of the values it is passed. I like to do this on an extension of the main view:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;extension&lt;/span&gt; &lt;span class="nc"&gt;EqualWidthIcons&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;IconWidthPreferenceKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PreferenceKey&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CGFloat&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;inout&lt;/span&gt; &lt;span class="n"&gt;CGFloat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nextValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;CGFloat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nextValue&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The code here is a little odd, but the important part is the call to &lt;code&gt;max&lt;/code&gt;, setting the &lt;code&gt;value&lt;/code&gt; variable to the maximum of either what it was before, or the value it has just been passed (the result of the &lt;code&gt;nextValue()&lt;/code&gt; call).&lt;/p&gt;
&lt;p&gt;Now we need to use this &lt;code&gt;PreferenceKey&lt;/code&gt; in our &lt;code&gt;GeometryReader&lt;/code&gt;. To do so, we have to call &lt;code&gt;.preference(key:value:)&lt;/code&gt; on a View. We can place an invisible view in the &lt;code&gt;GeometryReader&lt;/code&gt; and use it there:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;systemName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;square.and.arrow.up.fill&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foregroundColor&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;green&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;background&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;GeometryReader&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;geo&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;preference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EqualWidthIcons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IconWidthPreferenceKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;geo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Applying this to each icon will propagate the maxiumum size up into our &lt;code&gt;EqualWidthIcons.IconWidthPreferenceKey&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id="set-the-width-of-each-icon"&gt;Set the width of each icon&lt;a class="headerlink" href="#set-the-width-of-each-icon" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;Now all that&amp;rsquo;s left is to set the width of each icon to that maximum. Remember the State variable we created for it previously? We can watch for changes to the &lt;code&gt;PreferenceKey&lt;/code&gt; and update it accordingly. I like to do this on the highest view in the hierarchy, the immediate one returned by &lt;code&gt;body&lt;/code&gt; (in this case, that&amp;rsquo;s the outer &lt;code&gt;VStack&lt;/code&gt;):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;some&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;VStack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Rest of view...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;onPreferenceChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EqualWidthIcons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IconWidthPreferenceKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
        &lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iconWidth&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Finally, update each icon to use this value as its width. It&amp;rsquo;s important that we set the frame &lt;em&gt;after&lt;/em&gt; the &lt;code&gt;GeometryReader&lt;/code&gt; background.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;systemName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;square.and.arrow.up.fill&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foregroundColor&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;green&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;background&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;GeometryReader&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;geo&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;preference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EqualWidthIcons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IconWidthPreferenceKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;geo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;iconWidth&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And voila! Each icon has the same width, and will scale along with dynamic type as specified by the user.&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="/assets/swiftui-equal/fixed.png" /&gt;&lt;/p&gt;
&lt;h4 id="cleaning-up"&gt;Cleaning up&lt;a class="headerlink" href="#cleaning-up" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;This works great, but we have a fair bit of duplicated code. We did already, but since we&amp;rsquo;ve added the &lt;code&gt;background&lt;/code&gt;, &lt;code&gt;GeometryReader&lt;/code&gt;, and &lt;code&gt;frame&lt;/code&gt; definitions, the sections have become fairly unwieldy. It&amp;rsquo;s probably time we split it out into its own view:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;WhatsNewSection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;iconColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;

    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;some&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;HStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;systemName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foregroundColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iconColor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;background&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;GeometryReader&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;geo&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
                        &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;preference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EqualWidthIcons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IconWidthPreferenceKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;geo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;//              How do we read the iconWidth here?&lt;/span&gt;
&lt;span class="c1"&gt;//              .frame(width: iconWidth)&lt;/span&gt;
            &lt;span class="n"&gt;VStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;headline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fullWidth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This dramatically reduces the size of the original view:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;EqualWidthIcons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;iconWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CGFloat&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;

    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;some&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;VStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

            &lt;span class="n"&gt;VStack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;What&amp;#39;s New!&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;largeTitle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Version 2023.01&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;WhatsNewSection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;star.circle.fill&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iconColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;yellow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Star Ratings Toggle&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;If you believe all your pens are your favourite, you can now turn off star ratings via Settings.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;WhatsNewSection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;square.and.arrow.up.fill&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iconColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;green&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Share Sheet Fix&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Fixed an issue where the date in your Currently Ink&amp;#39;d shared image would not display correctly.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;WhatsNewSection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;scroll.fill&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iconColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;blue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Brand List Fix&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Fixed issues with duplicate brands populating your Brand List.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;WhatsNewSection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;ladybug.fill&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iconColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;red&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Misc. Bug Fixes&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Plenty of other minor improvements.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;Spacer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;onPreferenceChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EqualWidthIcons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IconWidthPreferenceKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iconWidth&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;But you may have noticed the question in the comments in the &lt;code&gt;WhatsNewSection&lt;/code&gt; code: where do we read &lt;code&gt;iconWidth&lt;/code&gt; from now?&lt;/p&gt;
&lt;p&gt;We have to pass it down as a binding from the parent view:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;WhatsNewSection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;iconColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;Binding&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;iconWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CGFloat&lt;/span&gt;
    &lt;span class="c1"&gt;// Rest of view... &lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Read it as usual to set the icon&amp;rsquo;s frame:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;systemName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foregroundColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iconColor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;background&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;GeometryReader&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;geo&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;preference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EqualWidthIcons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IconWidthPreferenceKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;geo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;iconWidth&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And finally, pass the binding through from the main view:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;WhatsNewSection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;star.circle.fill&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iconColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;yellow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Star Ratings Toggle&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;If you believe all your pens are your favourite, you can now turn off star ratings via Settings.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iconWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;iconWidth&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="a-better-alternative"&gt;A better alternative&lt;a class="headerlink" href="#a-better-alternative" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;For this particular situation, we can actually do away with the &lt;code&gt;PreferenceKey&lt;/code&gt; entirely, if we switch our layout to using a &lt;code&gt;Grid&lt;/code&gt;. Grids automatically size the width of their columns based on the widest cell within the column, which is exactly what we want. Here&amp;rsquo;s a verison of the code using &lt;code&gt;Grid&lt;/code&gt; instead:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;WhatsNewGridRow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;iconColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;

    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;some&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;GridRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;systemName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foregroundColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iconColor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;VStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;headline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fullWidth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;GridWidthIcons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;some&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;VStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;VStack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;What&amp;#39;s New!&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;largeTitle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Version 2023.01&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;Grid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;horizontalSpacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verticalSpacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;WhatsNewGridRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;star.circle.fill&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iconColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;yellow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Star Ratings Toggle&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;If you believe all your pens are your favourite, you can now turn off star ratings via Settings.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;WhatsNewGridRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;square.and.arrow.up.fill&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iconColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;green&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Share Sheet Fix&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Fixed an issue where the date in your Currently Ink&amp;#39;d shared image would not display correctly.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;WhatsNewGridRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;scroll.fill&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iconColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;blue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Brand List Fix&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Fixed issues with duplicate brands populating your Brand List.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;WhatsNewGridRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;ladybug.fill&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iconColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;red&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Misc. Bug Fixes&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Plenty of other minor improvements.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="n"&gt;Spacer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The result is identical, with a little less code a lot less complexity. However, there are still some situations where you want icons or other views to match widths or heights but a grid isn&amp;rsquo;t appropriate—there may be other content between the views, for example, that you don&amp;rsquo;t want conforming to a grid—so the &lt;code&gt;PreferenceKey&lt;/code&gt; method is still valuable to know.&lt;/p&gt;
&lt;p&gt;The full code for both solutions can be found &lt;a href="https://gist.github.com/benbacardi/4df235736f03cd4cda5cc32d828a9298"&gt;on Github&lt;/a&gt;.&lt;/p&gt;</content><category term="Development"></category><category term="Swift"></category><category term="SwiftUI"></category></entry><entry><title>SwiftUI Text Views and Alignment</title><link href="https://marmaladeofcourse.com/2023/01/25/swiftui-text-views-and-alignment/" rel="alternate"></link><published>2023-01-25T00:00:00+00:00</published><updated>2023-01-25T00:00:00+00:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2023-01-25:/2023/01/25/swiftui-text-views-and-alignment/</id><summary type="html">&lt;p&gt;There&amp;rsquo;s no doubting that SwiftUI makes app development fast and easy—I certainly wouldn&amp;rsquo;t have two apps on the store by now without it—but it&amp;rsquo;s not without its sharp edges and unexpected behaviours.&lt;/p&gt;
&lt;p&gt;One of these that I ran into pretty early on is how &lt;code&gt;Text …&lt;/code&gt;&lt;/p&gt;</summary><content type="html">&lt;p&gt;There&amp;rsquo;s no doubting that SwiftUI makes app development fast and easy—I certainly wouldn&amp;rsquo;t have two apps on the store by now without it—but it&amp;rsquo;s not without its sharp edges and unexpected behaviours.&lt;/p&gt;
&lt;p&gt;One of these that I ran into pretty early on is how &lt;code&gt;Text&lt;/code&gt; views behave, particularly with regard to alignment and how it lays itself out when text spills over more than one line. &lt;/p&gt;
&lt;h2 id="simple-text-views"&gt;Simple &lt;code&gt;Text&lt;/code&gt; views&lt;a class="headerlink" href="#simple-text-views" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Putting a bare &lt;code&gt;Text&lt;/code&gt; view on the screen, and it&amp;rsquo;ll be centered by default:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Hello, World!&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;img alt="" src="/assets/swiftui-text/single.png" /&gt;&lt;/p&gt;
&lt;p&gt;Putting a border around the &lt;code&gt;Text&lt;/code&gt; view shows us what the boundaries of the view&amp;rsquo;s frame are, and they hug the text as tightly as possible. The &lt;code&gt;Text&lt;/code&gt;&amp;lsquo;s frame doesn&amp;rsquo;t expand to fill all the available space; only what is necessary.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Hello, World!&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;border&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;red&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;img alt="" src="/assets/swiftui-text/single-border.png" /&gt;&lt;/p&gt;
&lt;p&gt;If the text flows onto multiple lines, it will be left-aligned, and expand to fill the width available before wrapping the text:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;border&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;red&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;img alt="" src="/assets/swiftui-text/single-full-plus-border.png" /&gt;&lt;/p&gt;
&lt;p&gt;In this particular case, the frame has expanded to fill the entire width of the screen—but this is only because the second line happens to fit exactly. The frame still &lt;strong&gt;wants&lt;/strong&gt; to be centered, as you can see by adjusting the paragraph slightly:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do abore et dolore magna aliqua.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;border&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;red&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;img alt="" src="/assets/swiftui-text/single-almost-full-plus-border.png" /&gt;&lt;/p&gt;
&lt;p&gt;This is often &lt;strong&gt;not&lt;/strong&gt; the behaviour we want from our views! If we had multiple paragraphs, or different sections as part of a stack, they wouldn&amp;rsquo;t necessarily be aligned with each other, and it&amp;rsquo;s entirely dependent on exactly what words are in each and where the line breaks fall.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;VStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do abore et dolore magna aliqua.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Lorem ipsum dolor sit amet, sed do abore dolore magna aliqua.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;img alt="" src="/assets/swiftui-text/multiple.png" /&gt;&lt;/p&gt;
&lt;p&gt;With the borders on, you can clearly see what&amp;rsquo;s going on:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;VStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do abore et dolore magna aliqua.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;border&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;red&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Lorem ipsum dolor sit amet, sed do abore dolore magna aliqua.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;border&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;green&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;img alt="" src="/assets/swiftui-text/multiple-plus-borders.png" /&gt;&lt;/p&gt;
&lt;h2 id="a-real-world-example"&gt;A real world example&lt;a class="headerlink" href="#a-real-world-example" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Let&amp;rsquo;s use a less contrived example: a settings page, with multiple different settings each with explanatory text. For lack of imagination, I&amp;rsquo;ve borrowed the settings and text from &lt;a href="http://david-smith.org/"&gt;_DavidSmith&lt;/a&gt;&amp;lsquo;s Pedometer++ app (inspired by a &lt;a href="https://david-smith.org/blog/2023/01/20/design-notes-17/"&gt;recent post&lt;/a&gt; in his excellent &lt;a href="https://david-smith.org/dnd/"&gt;Design Notes Diary&lt;/a&gt; series).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;VStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;GroupBox&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;HStack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Allow Rest Days&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;headline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;Spacer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;When enabled, activity streaks will not be broken by a single day missed after six consecutive days of reaching your goal.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;Picker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Allow Rest Days&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;allowRestDays&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Rest Days&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Unbroken Streaks&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pickerStyle&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;segmented&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;GroupBox&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;HStack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Wheelchair Mode&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;headline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;Spacer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Have Pedometer++ use your Apple Watch to measure your daily wheelchair push counts rather than steps.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;Picker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Wheelchair Mode&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;allowRestDays&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Steps&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Pushes&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pickerStyle&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;segmented&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;img alt="" src="/assets/swiftui-text/settings-page.png" /&gt;&lt;/p&gt;
&lt;p&gt;In order to left-align the settings headers, I&amp;rsquo;ve wrapped them in an &lt;code&gt;HStack&lt;/code&gt; and followed them by a &lt;code&gt;Spacer&lt;/code&gt;. There is a better way that we&amp;rsquo;ll come to later, but for now you can clearly see that the two explanatory paragraphs don&amp;rsquo;t align with each other, or with their surrounding content!&lt;/p&gt;
&lt;p&gt;Adding borders in, it&amp;rsquo;s obvious why:&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="/assets/swiftui-text/settings-page-plus-borders.png" /&gt;&lt;/p&gt;
&lt;h2 id="fixing-the-alignment-problems"&gt;Fixing the alignment problems&lt;a class="headerlink" href="#fixing-the-alignment-problems" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;We &lt;em&gt;could&lt;/em&gt; fix this in the same way that we pushed the titles to the left - but a better way would be to use the &lt;code&gt;frame&lt;/code&gt; modifier on the &lt;code&gt;Text&lt;/code&gt; views to tell them to expand to take all available space horizontally, rather than just what they require. This is a technique we can also use on the headings.&lt;/p&gt;
&lt;p&gt;We can do this by using &lt;code&gt;.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)&lt;/code&gt;, telling SwiftUI that we want the frame to fill the entire width of its container, and align the text inside it to the left. In the code below, I&amp;rsquo;ve added it to four places: the two headers, and the two paragraphs.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;VStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;GroupBox&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Allow Rest Days&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;headline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;infinity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;When enabled, activity streaks will not be broken by a single day missed after six consecutive days of reaching your goal.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;infinity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;Picker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Allow Rest Days&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;allowRestDays&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Rest Days&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Unbroken Streaks&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pickerStyle&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;segmented&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;GroupBox&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Wheelchair Mode&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;headline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;infinity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Have Pedometer++ use your Apple Watch to measure your daily wheelchair push counts rather than steps.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;infinity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;Picker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Wheelchair Mode&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;allowRestDays&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Steps&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Pushes&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pickerStyle&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;segmented&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;img alt="" src="/assets/swiftui-text/final.png" /&gt;&lt;/p&gt;
&lt;p&gt;Once again, putting the borders back in, it&amp;rsquo;s clear what&amp;rsquo;s now going on:&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="/assets/swiftui-text/final-plus-borders.png" /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;A brief note about the way &lt;code&gt;alignment:&lt;/code&gt; works within the &lt;code&gt;frame&lt;/code&gt; modifier: it is not for aligning the text, it is for aligning &lt;strong&gt;the view within the frame&lt;/strong&gt;. When we say &lt;code&gt;.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)&lt;/code&gt;, despite what it looks like, we&amp;rsquo;re not actually asking SwiftUI to change the size of the &lt;code&gt;Text&lt;/code&gt; view—instead, we&amp;rsquo;re asking SwiftUI to place that view within a frame that takes up the specified space, and place it at the left of the space. If we put a border around the &lt;code&gt;Text&lt;/code&gt; view &lt;strong&gt;before&lt;/strong&gt; the frame modifier (green), and another &lt;strong&gt;after&lt;/strong&gt; (red), we can see what SwiftUI is doing under the hood:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Allow Rest Days&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;headline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;border&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;green&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;infinity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;border&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;red&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;When enabled, activity streaks will not be broken by a single day missed after six consecutive days of reaching your goal.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;border&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;green&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;infinity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;border&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;red&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;img alt="" src="/assets/swiftui-text/frame-border.png" /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2 id="a-neat-solution"&gt;A neat solution&lt;a class="headerlink" href="#a-neat-solution" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In fact, this is such a common thing I want to do to &lt;code&gt;Text&lt;/code&gt; views within my apps, that I&amp;rsquo;ve written a small view modifier to handle it. Typing &lt;code&gt;.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)&lt;/code&gt; in so many places is a pain in the backside.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;FullWidthText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ViewModifier&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TextAlignment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leading&lt;/span&gt;

    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;frameAlignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Alignment&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;alignment&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leading&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;trailing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;trailing&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;center&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;center&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;some&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;content&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;multilineTextAlignment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;infinity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;frameAlignment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;extension&lt;/span&gt; &lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;fullWidth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TextAlignment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;some&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;modifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FullWidthText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now we can clean up our previous code, producing the same result but with a much neater and easier to remember view modifier:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;VStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;GroupBox&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Allow Rest Days&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;headline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fullWidth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;When enabled, activity streaks will not be broken by a single day missed after six consecutive days of reaching your goal.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fullWidth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;Picker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Allow Rest Days&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;allowRestDays&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Rest Days&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Unbroken Streaks&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pickerStyle&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;segmented&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;GroupBox&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Wheelchair Mode&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;headline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fullWidth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Have Pedometer++ use your Apple Watch to measure your daily wheelchair push counts rather than steps.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fullWidth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;Picker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Wheelchair Mode&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;wheelchairMode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Steps&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Pushes&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pickerStyle&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;segmented&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;As you may have noticed, it also supports providing the other &lt;code&gt;TextAlignment&lt;/code&gt; options for multiline text, passed to the &lt;code&gt;.fullWidth(alignment:)&lt;/code&gt; videw modifier: &lt;code&gt;.leading&lt;/code&gt; (the default), &lt;code&gt;.centered&lt;/code&gt;, and &lt;code&gt;.trailing&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Hopefully this can be of some use to you and help clear up some of the oddities surrounding the layout of text in SwiftUI! If you do find it helpful, I&amp;rsquo;d love for you to &lt;a href="https://snailedit.social/@benbacardi"&gt;let me know&lt;/a&gt;.&lt;/p&gt;</content><category term="Development"></category><category term="Swift"></category><category term="SwiftUI"></category></entry><entry><title>Diamine Inkvent 2022</title><link href="https://marmaladeofcourse.com/2023/01/21/diamine-inkvent-2022/" rel="alternate"></link><published>2023-01-21T00:00:00+00:00</published><updated>2023-01-21T00:00:00+00:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2023-01-21:/2023/01/21/diamine-inkvent-2022/</id><summary type="html">&lt;p&gt;The Diamine Inkvent advent calendars are something I never imagined existed—24 small 12ml bottles of unique ink, and one 30ml bottle on Christmas Day. 2022 was the second year they produced one, and although I didn&amp;rsquo;t buy one for advent itself, I did pick one up at half …&lt;/p&gt;</summary><content type="html">&lt;p&gt;The Diamine Inkvent advent calendars are something I never imagined existed—24 small 12ml bottles of unique ink, and one 30ml bottle on Christmas Day. 2022 was the second year they produced one, and although I didn&amp;rsquo;t buy one for advent itself, I did pick one up at half price a couple of days after Christmas from &lt;a href="https://www.cultpens.com"&gt;Cult Pens&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The little 12ml bottles are adorably cute, and all the inks are somewhat Christmas- or winter-themed in name. I opened the calendar all in one go, and spent half an hour one afternoon swatching every ink. I can recommend a dip pen if you&amp;rsquo;re going to do this—I used a Lamy Safari, dipping the nib into each bottle in turn, but thanks to the feed it was a bit more of a pain to clean between inks than a proper dip pen would have been.&lt;/p&gt;
&lt;p&gt;&lt;a href="/assets/diamine-inkvent-2022.png"&gt;&lt;img alt="Diamine Inkvent 2022 swatches" src="/assets/diamine-inkvent-2022.png" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I swabbed three to a swatch for my homemade &lt;a href="https://wellappointeddesk.bigcartel.com/product/col-o-ring-ink-testing-book"&gt;col-o-ring&lt;/a&gt;, and gave the large 30ml bottle its own. These are cards I&amp;rsquo;ve cut from 350gsm watercolour postcards, rounded the corners using a &lt;a href="https://www.amazon.co.uk/dp/B09LHVFGVP"&gt;corner punch&lt;/a&gt;, and punched a hole in to thread them through a keyring. The result is a fantastic way to flick through my available inks and make a choice, and I love having it as something to play with sitting on my desk:&lt;/p&gt;
&lt;p&gt;&lt;a href="/assets/col-o-ring.png"&gt;&lt;img alt="My homemade col-o-ring ink swatches" src="/assets/col-o-ring.png" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Speaking of which, how beautiful is that Van Diemen&amp;rsquo;s &lt;a href="https://www.vandiemansink.com.au/products/van-diemans-the-wilderness-series-azzure-kingfisher-30ml-shimmer-ink?shpxid=c58d90cd-9c27-4430-a56c-7f74146176c1"&gt;Azure Kingfisher&lt;/a&gt; from the &lt;a href="https://www.vandiemansink.com.au/collections/the-wilderness-series"&gt;Wilderness Series&lt;/a&gt;? It has such a beautiful golden shimmer and a slight red sheen. It was a birthday gift from a couple of wonderful friends, along with a custom pen from &lt;a href="https://www.instagram.com/justturnings/"&gt;Just Turnings&lt;/a&gt; that matches it perfectly, and shows off its properties fantastically with a broad nib.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not sure I&amp;rsquo;m likely to buy the advent calendar full price next Christmas—it is a fair amount of money, and I have many inks I still haven&amp;rsquo;t used yet from it—but I think they&amp;rsquo;re a fantastic idea from Diamine. Judging by last year, they&amp;rsquo;ll be selling each of the colours individually later in the year in larger bottles, so if there&amp;rsquo;s any that really take my fancy I don&amp;rsquo;t need to worry about 12ml not going particularly far.&lt;/p&gt;
&lt;p&gt;So far, my favourite is Flame, but I am particularly partial to a good orange. What&amp;rsquo;s yours?&lt;/p&gt;</content><category term="Pens &amp; Ink"></category></entry><entry><title>Swift Charts &amp; Calendar Weekdays</title><link href="https://marmaladeofcourse.com/2023/01/20/swift-charts-calendar-weekdays/" rel="alternate"></link><published>2023-01-20T16:30:00+00:00</published><updated>2023-01-20T16:30:00+00:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2023-01-20:/2023/01/20/swift-charts-calendar-weekdays/</id><summary type="html">&lt;p&gt;I&amp;rsquo;ve recently been working on adding a statistics section to &lt;a href="/pendulum/"&gt;Pendulum&lt;/a&gt;, the pen pal tracking app I develop with my friend &lt;a href="https://418teapot.net"&gt;Alex&lt;/a&gt;. This seemed like the perfect opportunity to use &lt;a href="https://developer.apple.com/documentation/charts"&gt;Swift Charts&lt;/a&gt;, Apple&amp;rsquo;s new charting framework.&lt;/p&gt;
&lt;p&gt;I ultimately wanted to end up with a graph like the following …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I&amp;rsquo;ve recently been working on adding a statistics section to &lt;a href="/pendulum/"&gt;Pendulum&lt;/a&gt;, the pen pal tracking app I develop with my friend &lt;a href="https://418teapot.net"&gt;Alex&lt;/a&gt;. This seemed like the perfect opportunity to use &lt;a href="https://developer.apple.com/documentation/charts"&gt;Swift Charts&lt;/a&gt;, Apple&amp;rsquo;s new charting framework.&lt;/p&gt;
&lt;p&gt;I ultimately wanted to end up with a graph like the following:&lt;/p&gt;
&lt;p&gt;&lt;a href="/assets/pendulum-weekday-chart.jpeg"&gt;&lt;img alt="Bar chart showing the number of letters written and sent per day of the week" src="/assets/pendulum-weekday-chart.jpeg" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Swift Charts can perfectly handle multiple datasets on one graph, but the problem I ran into was that it doesn&amp;rsquo;t seem to have a way to natively aggregate data per &lt;em&gt;day of the week&lt;/em&gt;. If I only had seven days worth of data, it would be fine—I could display just the day name on the axis, and no days would be repeated because I wouldn&amp;rsquo;t be displaying more than a week of data. However, as I want to aggregate every event, this wasn&amp;rsquo;t going to work. I decided to do the grouping of the data myself, and just pass Swift Charts a pre-binned dataset for it to present, where it wouldn&amp;rsquo;t have to worry about dates at all.&lt;/p&gt;
&lt;p&gt;I had one other problem I wanted to solve: I wanted the graph to start on whatever the current locale thinks the start of the week is&lt;sup id="fnref:1"&gt;&lt;a class="footnote-ref" href="#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt;. For us in the UK and Europe, we generally consider Monday the beginning of the week, as reflected in the graph above. But for the US, it should probably start with Sunday.&lt;/p&gt;
&lt;p&gt;In order to generate the seven &amp;ldquo;bins&amp;rdquo; for the chart to show, I could use the handy &lt;code&gt;Calendar.current.shortWeekdaySymbols&lt;/code&gt; property, which produces an array of the shortened names of the week, properly localised to the user&amp;rsquo;s current locale. However, regardless of locale, this array always starts with Sunday and ends with Saturday. There&amp;rsquo;s another property of the calendar, &lt;code&gt;.firstWeekday&lt;/code&gt;, that returns a number between 1 (for Sunday) and 7 (for Saturday) representing what the locale considers to be the first day of the week. Using this, I can shift the array from &lt;code&gt;shortWeekdaySymbols&lt;/code&gt; to produce the output in the right order. I decided to wrap both these pieces of information up in an enum to represent each day of the week:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;Weekday&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CaseIterable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;sun&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;mon&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;tue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;wed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;thu&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;fri&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;sat&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;

    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;shortName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Calendar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shortWeekdaySymbols&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;orderedCases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Weekday&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kc"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allCases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shiftRight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Calendar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;firstWeekday&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You&amp;rsquo;ll also notice I&amp;rsquo;m using an extension to &lt;code&gt;Array&lt;/code&gt; that allows me to shift an array, wrapping the values around to the end as they get popped off the front:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;extension&lt;/span&gt; &lt;span class="nc"&gt;Array&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;shiftRight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Int&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Element&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="bp"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;self&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;amount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;by&lt;/span&gt;
        &lt;span class="bp"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="bp"&gt;count&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="bp"&gt;count&lt;/span&gt; &lt;span class="o"&gt;~=&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Shift amount out of bounds&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="bp"&gt;count&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="bp"&gt;count&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now that I have an enum I can use to represent the days of the week correctly, and order them as defined  by the user&amp;rsquo;s locale, I needed to use this somehow to generate data to pass to the chart. I started off by defining a struct to hold a single datapoint:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;StatusCountByDay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Identifiable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EventType&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Weekday&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Int&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="si"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Here, &lt;code&gt;EventType&lt;/code&gt; is an internal enum used by Pendulum to mark whether the event was a letter being sent, written, received, etc. What makes each data point unique in the chart is the combination of the day of the week, and the event type, so I combine those two together as the &lt;code&gt;id&lt;/code&gt; for the struct.&lt;/p&gt;
&lt;p&gt;Next, I needed to fetch the data and group it into buckets:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Weekday&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[:]&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;withStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[.&lt;/span&gt;&lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sent&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="bp"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wrappedDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;weekday&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wrappedDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;weekday&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
     &lt;span class="p"&gt;}&lt;/span&gt;
     &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wrappedDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;weekday&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I start by defining a dictionary mapping weekdays to an array of events, and then looping over the events I&amp;rsquo;m interested in and adding them to the corresponding weekday key in the dictionary. This necessitated another extension to a &lt;code&gt;Foundation&lt;/code&gt; object, this time on &lt;code&gt;Date&lt;/code&gt;&lt;sup id="fnref:2"&gt;&lt;a class="footnote-ref" href="#fn:2"&gt;2&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;extension&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;dayNumberOfWeek&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Calendar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dateComponents&lt;/span&gt;&lt;span class="p"&gt;([.&lt;/span&gt;&lt;span class="n"&gt;weekday&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;weekday&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;weekday&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Weekday&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Weekday&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dayNumberOfWeek&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sun&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This uses the &lt;code&gt;.weekday&lt;/code&gt; date component from the user&amp;rsquo;s current calendar, which returns the same 1–7 index as used by &lt;code&gt;.firstWeekday&lt;/code&gt;, and returns the corresponding &lt;code&gt;Weekday&lt;/code&gt; object.&lt;/p&gt;
&lt;p&gt;With the data correctly bucketed, it was time to sum up the series and create the datapoints for the chart. When the data provided is not sequential (such as a series of dates) but is instead discrete (such as list of names, for example) Swift Charts will draw the bars in the order in which it first encounters them. You may think that weekdays are sequential—and you&amp;rsquo;d be right—but in this case, they&amp;rsquo;re not an object that Swift Charts understands in that way. So to draw the chart as intended, we need to create a &lt;code&gt;StatusCountByDay&lt;/code&gt; instance for each weekday in the order we want. We also need to include one even when the count for that day is zero, because we don&amp;rsquo;t want the chart to just skip a day. I do this by looping over the weekdays ordered according to the locale, inside that looping over each event type, and calculating the sum for each:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;StatusCountByDay&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;eventType&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EventType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EventType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sent&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Weekday&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orderedCases&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="bp"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;eventType&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="bp"&gt;count&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StatusCountByDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;count&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Ultimately, we end up with a series of data like the following:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;StatusCountByDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sun&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;StatusCountByDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;StatusCountByDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;StatusCountByDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;StatusCountByDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;StatusCountByDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;StatusCountByDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;StatusCountByDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sun&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;StatusCountByDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;StatusCountByDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;StatusCountByDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;StatusCountByDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;StatusCountByDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;StatusCountByDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;All that&amp;rsquo;s left is to pass that to Swift Charts, for which I&amp;rsquo;ll break down each section after I show the code:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;Chart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="n"&gt;BarMark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Day&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shortName&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Count&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="bp"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;annotation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="bp"&gt;count&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="bp"&gt;count&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;footnote&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foregroundColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foregroundStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;event&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actionableTextShort&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;event&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actionableTextShort&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chartForegroundStyleScale&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="n"&gt;EventType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actionableTextShort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EventType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;EventType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actionableTextShort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EventType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Firstly, we want a bar chart, so the correct type of mark to use is a &lt;code&gt;BarMark&lt;/code&gt;. The &lt;code&gt;x&lt;/code&gt; axis is the short name of the weekday (&amp;ldquo;Mon&amp;rdquo;, &amp;ldquo;Tue&amp;rdquo;, etc), and the &lt;code&gt;y&lt;/code&gt; axis is the count.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;.annotation&lt;/code&gt; section puts the little figures above each bar, and isn&amp;rsquo;t particularly necessary but I liked the way it looked.&lt;/p&gt;
&lt;p&gt;The two &lt;code&gt;BarMark&lt;/code&gt; modifiers &lt;code&gt;.foregroundStyle(by:)&lt;/code&gt; and &lt;code&gt;.position(by:)&lt;/code&gt; both tell Swift Charts how to define and handle each series independently; otherwise, they&amp;rsquo;d be a single bar, stacked on top of each other within each day. Grouping them by event type, the first modifier tells them to be different colours, and the second puts them as independent bars side by side instead of on top of each other. I use &lt;code&gt;data.status.actionableTextShort&lt;/code&gt; as the value to distinguish the data by, because that is what I want shown in the legend beneath the chart (&amp;ldquo;Sent&amp;rdquo; vs &amp;ldquo;Written&amp;rdquo;, etc).&lt;/p&gt;
&lt;p&gt;You can see below the results of the chart without the &lt;code&gt;.position(by:)&lt;/code&gt; modifier, and without the &lt;code&gt;.foregroundStyle(by:)&lt;/code&gt; modifier, respectively.&lt;/p&gt;
&lt;p&gt;&lt;img alt="The chart without each modifier" src="/assets/pendulum-chart-modifiers.png" /&gt;&lt;/p&gt;
&lt;p&gt;Finally, the &lt;code&gt;.chartForegroundStyleScale&lt;/code&gt; modifier defines the colours to be used for each series, which is a dictionary mapping the name of the series to its colour. In this case, I use want them using the colour defined for the event type, to keep it consistent with the rest of the app.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;I&amp;rsquo;m quite impressed with Swift Charts and how easy it makes drawing a good looking chart, but there are definitely some things that could be more obvious about it. The lack of decent documentation &lt;em&gt;with plenty of examples and screenshots&lt;/em&gt; being a very clear area for improvement!&lt;/p&gt;
&lt;hr /&gt;
&lt;div class="footnote"&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Yes, I realise none of the rest of the app is localised. Baby steps, though!&amp;#160;&lt;a class="footnote-backref" href="#fnref:1" title="Jump back to footnote 1 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;As you may have realised by now, I&amp;rsquo;m quite a fan of writing extensions to standard types for common functions that could end up being performed regularly. They make the rest of the code a lot cleaner. It&amp;rsquo;s one of my favourite features of Swift.&amp;#160;&lt;a class="footnote-backref" href="#fnref:2" title="Jump back to footnote 2 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content><category term="Development"></category><category term="Swift"></category></entry><entry><title>Review: LEGO Monster Jam Trucks</title><link href="https://marmaladeofcourse.com/2023/01/20/review-lego-monster-jam-trucks/" rel="alternate"></link><published>2023-01-20T00:00:00+00:00</published><updated>2023-01-20T00:00:00+00:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2023-01-20:/2023/01/20/review-lego-monster-jam-trucks/</id><summary type="html">&lt;p&gt;Recently, two more reviews of mine went up on Brickset: one for &lt;a href="https://brickset.com/article/88534/review-42149-monster-jam-dragon"&gt;42149 Monster Jam Dragon&lt;/a&gt; and the other for its counterpart, &lt;a href="https://brickset.com/article/88535/review-42150-monster-jam-monster-mutt-dalmation"&gt;42150 Monster Jam Monster Mutt Dalmation&lt;/a&gt;. They&amp;rsquo;re part of a small series that LEGO has released over the past few years, with two pull-back-and-go Technic monster trucks …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Recently, two more reviews of mine went up on Brickset: one for &lt;a href="https://brickset.com/article/88534/review-42149-monster-jam-dragon"&gt;42149 Monster Jam Dragon&lt;/a&gt; and the other for its counterpart, &lt;a href="https://brickset.com/article/88535/review-42150-monster-jam-monster-mutt-dalmation"&gt;42150 Monster Jam Monster Mutt Dalmation&lt;/a&gt;. They&amp;rsquo;re part of a small series that LEGO has released over the past few years, with two pull-back-and-go Technic monster trucks from the &lt;a href="https://www.monsterjam.com"&gt;Monster Jam&lt;/a&gt; sport. Spoilers for the reviews, but I think they&amp;rsquo;re great little &amp;ldquo;intro to Technic&amp;rdquo; sets, and you can&amp;rsquo;t go wrong at less than £18.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Monster Jam LEGO Sets" src="https://images.brickset.com/news/88534_IMG_1897.jpg" /&gt;&lt;/p&gt;</content><category term="LEGO Reviews"></category></entry><entry><title>Review: 40651 Australia Postcard</title><link href="https://marmaladeofcourse.com/2023/01/03/review-40651-australia-postcard/" rel="alternate"></link><published>2023-01-03T00:00:00+00:00</published><updated>2023-01-03T00:00:00+00:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2023-01-03:/2023/01/03/review-40651-australia-postcard/</id><summary type="html">&lt;p&gt;I&amp;rsquo;ve quite liked the previous sets in the LEGO Postcard series, so it was good to get a chance to review the latest, &lt;a href="https://brickset.com/sets/40651-1"&gt;40651&lt;/a&gt; Australia, even if it&amp;rsquo;s a &lt;a href="https://brickset.com/article/88236/review-40651-australia-postcard"&gt;slight departure from the rest&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The designers have chosen to represent the entirety of Australia with a small shack …&lt;/p&gt;&lt;/blockquote&gt;</summary><content type="html">&lt;p&gt;I&amp;rsquo;ve quite liked the previous sets in the LEGO Postcard series, so it was good to get a chance to review the latest, &lt;a href="https://brickset.com/sets/40651-1"&gt;40651&lt;/a&gt; Australia, even if it&amp;rsquo;s a &lt;a href="https://brickset.com/article/88236/review-40651-australia-postcard"&gt;slight departure from the rest&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The designers have chosen to represent the entirety of Australia with a small shack, a windpump, an outdoor toilet, a large tree with a cockatoo, and a sign warning of kangaroos. There&amp;rsquo;s also a Qantas plane flying through the bright blue sky, and small colourful plants growing around the ground.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img alt="Australia Postcard LEGO set" src="https://images.brickset.com/news/88236_IMG_1873.jpg" /&gt;&lt;/p&gt;</content><category term="LEGO Reviews"></category></entry><entry><title>Brickset LEGO Gift Guide - $50 and up</title><link href="https://marmaladeofcourse.com/2022/11/23/brickset-lego-gift-guide-50-and-up/" rel="alternate"></link><published>2022-11-23T00:00:00+00:00</published><updated>2022-11-23T00:00:00+00:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2022-11-23:/2022/11/23/brickset-lego-gift-guide-50-and-up/</id><content type="html">&lt;p&gt;The last two holiday gift guides have been published at Brickset, where we&amp;rsquo;ve chosen a number of sets in a variety of price brackets. Go and check out the sets between &lt;a href="https://brickset.com/article/84508/holiday-gift-guide-50-100"&gt;$50 to $100&lt;/a&gt;, &lt;a href="https://brickset.com/article/84509/holiday-gift-guide-100-200"&gt;$100 to $200&lt;/a&gt;, and &lt;a href="https://brickset.com/article/84510/holiday-gift-guide-over-200"&gt;$200 and up!&lt;/a&gt;&lt;/p&gt;</content><category term="LEGO"></category></entry><entry><title>Brickset LEGO Gift Guide</title><link href="https://marmaladeofcourse.com/2022/11/20/brickset-lego-gift-guide/" rel="alternate"></link><published>2022-11-20T00:00:00+00:00</published><updated>2022-11-20T00:00:00+00:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2022-11-20:/2022/11/20/brickset-lego-gift-guide/</id><content type="html">&lt;p&gt;Brickset have started publishing their annual Gift Guide, and this year Huw asked for my thoughts to be included. So far, guides for the first two price ranges have been released, for sets &lt;a href="https://brickset.com/article/84506/holiday-gift-guide-under-25"&gt;under $25&lt;/a&gt; and sets priced &lt;a href="https://brickset.com/article/84507/holiday-gift-guide-25-50"&gt;between $25 and $50&lt;/a&gt;. Click through to see my recommendations!&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://images.brickset.com/news/84506_hgg2.jpg" /&gt;&lt;/p&gt;</content><category term="LEGO"></category></entry><entry><title>Review: 71409 Big Spike's Cloudtop Challenge</title><link href="https://marmaladeofcourse.com/2022/10/28/review-71409-big-spikes-cloudtop-challenge/" rel="alternate"></link><published>2022-10-28T00:00:00+01:00</published><updated>2022-10-28T00:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2022-10-28:/2022/10/28/review-71409-big-spikes-cloudtop-challenge/</id><summary type="html">&lt;p&gt;The last of this round of Super Mario LEGO sets to review, &lt;a href="https://brickset.com/sets/71409-1"&gt;71409&lt;/a&gt; Big Spike&amp;rsquo;s Cloudtop Challenge:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;LEGO keep on producing Super Mario expansion sets to add to what is clearly a popular theme for them—this year they have released &lt;a href="https://brickset.com/sets/subtheme-Expansion-Set/year-2022"&gt;14 distinct sets&lt;/a&gt; in the range so far …&lt;/p&gt;&lt;/blockquote&gt;</summary><content type="html">&lt;p&gt;The last of this round of Super Mario LEGO sets to review, &lt;a href="https://brickset.com/sets/71409-1"&gt;71409&lt;/a&gt; Big Spike&amp;rsquo;s Cloudtop Challenge:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;LEGO keep on producing Super Mario expansion sets to add to what is clearly a popular theme for them—this year they have released &lt;a href="https://brickset.com/sets/subtheme-Expansion-Set/year-2022"&gt;14 distinct sets&lt;/a&gt; in the range so far. &lt;a href="https://brickset.com/sets/71409-1"&gt;71409&lt;/a&gt; Big Spike&amp;rsquo;s Cloudtop Challenge is one of the largest, with 540 parts, and packs a decent punch: three opponents, two of which are new, and some fun takes on the interactivity and game play we&amp;rsquo;ve come to expect with these sets.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img alt="Big Spike's Cloudtop Challenge LEGO set" src="https://images.brickset.com/news/82125_IMG_1871.jpg" /&gt;&lt;/p&gt;</content><category term="LEGO Reviews"></category></entry><entry><title>LEGO IDEAS Review Results</title><link href="https://marmaladeofcourse.com/2022/10/27/lego-ideas-review-results/" rel="alternate"></link><published>2022-10-27T00:00:00+01:00</published><updated>2022-10-27T00:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2022-10-27:/2022/10/27/lego-ideas-review-results/</id><summary type="html">&lt;p&gt;LEGO have announced the results of the latest &lt;a href="https://ideas.lego.com/"&gt;IDEAS&lt;/a&gt; review (the mechanism by which fan-designed models can get made into real LEGO sets, should they reach 10,000 votes and pass the review). Four projects were accepted this time, and I particularly like the look of the Space Age designs …&lt;/p&gt;</summary><content type="html">&lt;p&gt;LEGO have announced the results of the latest &lt;a href="https://ideas.lego.com/"&gt;IDEAS&lt;/a&gt; review (the mechanism by which fan-designed models can get made into real LEGO sets, should they reach 10,000 votes and pass the review). Four projects were accepted this time, and I particularly like the look of the Space Age designs, which will hopefully be an instant buy when it eventually comes out some time in 2023/2024. The other three ideas also look fantastic, and it will be interesting to see how LEGO adapts them to become production-ready sets. I hope they include &lt;a href="https://chrismcveigh.com/cm/welcome.html"&gt;Chris McVeigh&lt;/a&gt; on the team designing the Polaroid, as that is right up his alley!&lt;/p&gt;
&lt;h3 id="tales-of-the-space-age-by-john-carter"&gt;&lt;a href="https://ideas.lego.com/projects/08ccddc2-e926-4a7b-8287-26b40649bada"&gt;Tales of the Space Age&lt;/a&gt; by &lt;a href="https://ideas.lego.com/profile/53c6910b-7bac-4e93-8def-e81172084bb8/entries?query=&amp;amp;sort=top"&gt;John Carter&lt;/a&gt;&lt;a class="headerlink" href="#tales-of-the-space-age-by-john-carter" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img alt="Tales of the Space Age LEGO IDEAS by John Carter" src="https://ideascdn.lego.com/media/generate/lego_ci/7c69dc4b-3a42-4458-82ac-c2c1dbf55b50/resize:950:633/webp" /&gt;&lt;/p&gt;
&lt;h3 id="lego-insects-by-hachiroku24"&gt;&lt;a href="https://ideas.lego.com/projects/39eb392e-0a3d-481b-b157-8f586082761e"&gt;LEGO Insects&lt;/a&gt; by &lt;a href="https://ideas.lego.com/profile/622fee18-3adf-4c66-8b4e-eb8f78d27eef/entries?query=&amp;amp;sort=top"&gt;hachiroku24&lt;/a&gt;&lt;a class="headerlink" href="#lego-insects-by-hachiroku24" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img alt="LEGO Insects Ideas by hachiroku24" src="https://ideascdn.lego.com/media/generate/public/lego_ci/317ac69b-ba4c-4840-ac9d-beb3bd65b61e/resize:1600:900/legacy" /&gt;&lt;/p&gt;
&lt;h3 id="polaroid-onestep-sx-70-by-minibrick-productions"&gt;&lt;a href="https://ideas.lego.com/projects/200dd32e-8ec8-44aa-8f7d-e4dcc6f74e5c"&gt;Polaroid OneStep SX-70&lt;/a&gt; by &lt;a href="https://ideas.lego.com/profile/cb41754b-5ba3-425f-8051-5bb0be7b5c19/entries?query=&amp;amp;sort=top"&gt;Minibrick Productions&lt;/a&gt;&lt;a class="headerlink" href="#polaroid-onestep-sx-70-by-minibrick-productions" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img alt="Polaroid OneStep SX-70 LEGO IDEAS by Minibrick Productions" src="https://ideascdn.lego.com/media/generate/public/lego_ci/fb14594a-f8bc-4511-854a-6b01c08e7daa/resize:1600:900/legacy" /&gt;&lt;/p&gt;
&lt;h3 id="the-orient-express-a-legendary-train-by-letsgo"&gt;&lt;a href="https://ideas.lego.com/projects/568ee861-3b62-413a-9432-ce1d3a98c61a"&gt;The Orient Express, a Legendary Train&lt;/a&gt; by &lt;a href="https://ideas.lego.com/profile/78b8fed9-8061-4f24-845d-314b5c9aa4ec/entries?query=&amp;amp;sort=top"&gt;LEt.sGO&lt;/a&gt;&lt;a class="headerlink" href="#the-orient-express-a-legendary-train-by-letsgo" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img alt="The Orient Express LEGO IDEAS by LEt.sGO" src="https://ideascdn.lego.com/media/generate/public/lego_ci/0b5fbaf0-206e-4a27-8951-eca80e883ba3/resize:1600:900/legacy" /&gt;&lt;/p&gt;</content><category term="LEGO"></category></entry><entry><title>Darker Sublime Text Plugin</title><link href="https://marmaladeofcourse.com/2022/10/26/darker-sublime-text-plugin/" rel="alternate"></link><published>2022-10-26T00:00:00+01:00</published><updated>2022-10-26T00:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2022-10-26:/2022/10/26/darker-sublime-text-plugin/</id><summary type="html">&lt;p&gt;&lt;a href="https://github.com/psf/black"&gt;Black&lt;/a&gt; is a popular code formatter for Python code, known for its opinionated uncompromising stance. It&amp;rsquo;s incredibly helpful for teams working on common Python code to write in the same style, and Black makes that easy, without having to maintain a common set of configuration options between the team …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;a href="https://github.com/psf/black"&gt;Black&lt;/a&gt; is a popular code formatter for Python code, known for its opinionated uncompromising stance. It&amp;rsquo;s incredibly helpful for teams working on common Python code to write in the same style, and Black makes that easy, without having to maintain a common set of configuration options between the team members. After all, there aren&amp;rsquo;t any.&lt;/p&gt;
&lt;p&gt;However, adding it to an existing codebase is difficult. It wants to reformat every source file, which can be a pain with version history by creating commits that are purely formatting changes, or adding misleading diffs to commits that are intended for something else. To help overcome this, &lt;a href="https://github.com/akaihola/darker"&gt;Darker&lt;/a&gt; was created, which runs Black but only on the parts of code that have changed since the last commit. This is perfect for running as a post-save hook in your IDE, to consistently keep your code up to style without altering the parts of the source you haven&amp;rsquo;t changed.&lt;/p&gt;
&lt;p&gt;The Darker documentation includes instructions on how to integrate the formatter with PyCharm, IntelliJ IDEA, Visual Studio Code, Vim, and Emacs—but I use Sublime Text. All it took was to write a simple Sublime Plugin, however, and we&amp;rsquo;re off to the races:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;sublime_plugin&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;subprocess&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DarkerOnSave&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sublime_plugin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EventListener&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_post_save_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file_name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;match_selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;source.python&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;darker&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;To add this yourself, go to &lt;strong&gt;Tools &amp;gt; Developer &amp;gt; New Plugin…&lt;/strong&gt; from the menubar, and replace the contents of the file with the above. Save the file as something like &lt;code&gt;darker-on-save.py&lt;/code&gt; in the same location it was created in (the default in the save dialog box), and now every time you hit Save on a Python file, it&amp;rsquo;ll ensure that the code you added or altered is up to scratch with your style guide. Simple!&lt;/p&gt;</content><category term="Development"></category><category term="Python"></category></entry><entry><title>Useful Avrae Aliases</title><link href="https://marmaladeofcourse.com/2022/10/10/useful-avrae-aliases/" rel="alternate"></link><published>2022-10-10T00:00:00+01:00</published><updated>2022-10-10T00:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2022-10-10:/2022/10/10/useful-avrae-aliases/</id><summary type="html">&lt;p&gt;&lt;a href="https://avrae.io/"&gt;Avrae&lt;/a&gt; is a Discord bot that makes running and playing D&amp;amp;D play-by-post games much easier—it provides useful tools such as integration with &lt;a href="https://www.dndbeyond.com/"&gt;D&amp;amp;D Beyond&lt;/a&gt;, powerful dice-rolling options, and combat initiative tracking.&lt;/p&gt;
&lt;p&gt;However, there are some things it doesn&amp;rsquo;t do well natively. For this, it provides a …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;a href="https://avrae.io/"&gt;Avrae&lt;/a&gt; is a Discord bot that makes running and playing D&amp;amp;D play-by-post games much easier—it provides useful tools such as integration with &lt;a href="https://www.dndbeyond.com/"&gt;D&amp;amp;D Beyond&lt;/a&gt;, powerful dice-rolling options, and combat initiative tracking.&lt;/p&gt;
&lt;p&gt;However, there are some things it doesn&amp;rsquo;t do well natively. For this, it provides a powerful scripting API via &lt;a href="https://avrae.readthedocs.io/en/latest/aliasing/aliasing.html"&gt;aliases and snippets&lt;/a&gt; for you to extend its functionality with your own commands. Many people have written their own, and these are a few of the custom commands that we have set up in our games to make life just a little bit easier.&lt;/p&gt;
&lt;p&gt;To use any of these aliases, paste the text in the code blocks below into a DM with Avrae (or any channel they are a member of).&lt;/p&gt;
&lt;h2 id="quick-character-switcher"&gt;Quick Character Switcher&lt;a class="headerlink" href="#quick-character-switcher" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;When you link a character from D&amp;amp;D Beyond, Avrae will remember it and allow you to use the character in combat and for ability checks, etc. However, if you&amp;rsquo;re playing in multiple games, you have to constantly remember to change the active character to the correct one before continuing. We found a handy alias on the Avrae Developer&amp;rsquo;s Discord for assigning characters to channels, so instead of remembering that &amp;ldquo;in this channel, I&amp;rsquo;m Kaith, but in this channel, I&amp;rsquo;m Elmer&amp;rdquo;, you can just run &lt;code&gt;!ch&lt;/code&gt; to switch to the right character for that game. I&amp;rsquo;m reproducing that here (tweaked for a recent Avrae update that deprecated a couple of the commands):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;H&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;load_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chars&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;chars&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;c&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;s&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;guild&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;guild&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ARGS&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:],&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ARGS&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;- This Channel&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;- This Server&amp;#39;&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;%x&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;roll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;1d16777216&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)}}{{&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;add&amp;#39;&lt;/span&gt;  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;delete&amp;#39;&lt;/span&gt;  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;roster&amp;#39;&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;help?&amp;#39;&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}}{{&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isdigit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;chan&amp;#39;&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)}}{{&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isdigit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;chan&amp;#39;&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)}}{{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;)}}{{&lt;/span&gt;&lt;span class="n"&gt;emb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39; -title &amp;quot;Quick Character Changer&amp;quot; -footer &amp;quot;!ch [help|?] - Bring up the help window&amp;quot; -color &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt; &amp;#39;&lt;/span&gt;&lt;span class="p"&gt;}}{{&lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])}}{{&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt;  &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;set_uvar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;chars&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;dump_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]}}{{&lt;/span&gt;&lt;span class="n"&gt;delete&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;set_uvar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;chars&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dump_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;}}{{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;embed &amp;quot;&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;emb&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;-t 10 -desc &amp;quot;Added `&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;` to ID `&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;`.&amp;quot;&amp;#39;&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;-t 10 -desc &amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;No char found for ID&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Removed `&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;` with ID&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt; `&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;`.&amp;quot;&amp;#39;&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;-t 20 -f &amp;quot;Roster| &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;`&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;` - `&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; ` &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;H&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*None*&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;&amp;quot;&amp;#39;&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;-f &amp;quot;!ch|Changes to the appropriate character for the channel/server.&amp;quot; -f &amp;quot;!ch roster|View a list of all channel/server id&lt;/span&gt;&lt;span class="se"&gt;\&amp;#39;&lt;/span&gt;&lt;span class="s1"&gt;s and the character they will load&amp;quot; -f &amp;quot;!ch add &amp;lt;name&amp;gt; [chan⏐id]|Adds `name` to the selected id. Default is server id, `chan` selects the channel id, or you can input the channel/server id manually&amp;quot; -f &amp;quot;!ch delete [chan⏐id]|Deletes the given id. Default is server id, `chan` selects the channel id, or you can input the channel/server id manually&amp;quot; -f &amp;quot;Current ID&lt;/span&gt;&lt;span class="se"&gt;\&amp;#39;&lt;/span&gt;&lt;span class="s1"&gt;s|`Channel` - `&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;`&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;`Server` - `&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;`&amp;quot;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;char &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;embed -t 5 -desc &amp;#39;Channel not found in list&amp;#39;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;emb&lt;/span&gt;&lt;span class="p"&gt;)}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With that alias set, you can run &lt;code&gt;!ch add [character] chan&lt;/code&gt; to assign the provided character to the current channel. Now, when you switch channels, you just have to run &lt;code&gt;!ch&lt;/code&gt; to activate the right one.&lt;/p&gt;
&lt;p&gt;However, remembering to run &lt;code&gt;!ch&lt;/code&gt; is easier said than done. How many times have I switched to a channel and run &lt;code&gt;!g lr&lt;/code&gt; to give my character a long rest, before realising that I hadn&amp;rsquo;t switched and had just given one of my other characters a poorly-timed rest instead? To solve this, we put together an alias (worked out mostly by my friend &lt;a href="https://www.flexpotential.com/"&gt;Madi&lt;/a&gt;) designed to prefix any command you may want to run, that automatically switches before executing the actual command you asked for:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="n"&gt;multiline&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;drac2&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;c&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;s&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;guild&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;guild&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;load_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chars&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;chars&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;
&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;!char &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;drac2&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;*&amp;amp;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For me it&amp;rsquo;s aliased to &lt;code&gt;x&lt;/code&gt;, just to make it quick and easy to type. &lt;code&gt;!x check str&lt;/code&gt; will always do a strength check for the right character for the game in &lt;em&gt;this&lt;/em&gt; channel. &lt;code&gt;!x g lr&lt;/code&gt; will always give the correct character a long rest. In combination with the &lt;code&gt;!ch&lt;/code&gt; alias, this is probably the most handy alias I have set up—I only have to remember to always use &lt;code&gt;!x&lt;/code&gt; instead of &lt;code&gt;!&lt;/code&gt;, and it always just does the right thing.&lt;/p&gt;
&lt;h2 id="feat-spells"&gt;Feat Spells&lt;a class="headerlink" href="#feat-spells" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The other problem we frequently run into is using spells provided by feats. There are many feats that provide the with character additional spells; one&amp;rsquo;s they&amp;rsquo;re able to cast a particular number of times between rest, without using up spell slots. A good example is the &lt;a href="https://www.dndbeyond.com/feats/magic-initiate"&gt;Magic Initiate&lt;/a&gt; feat:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Choose a class: bard, cleric, druid, sorcerer, warlock, or wizard. You learn two cantrips of your choice from that class&amp;rsquo;s spell list.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In addition, choose one 1st-level spell from that same list. You learn that spell and can cast it at its lowest level. Once you cast it, you must finish a long rest before you can cast it again using this feat.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Avrae only partially understands these. Pulling from D&amp;amp;D Beyond, it creates a custom counter for each spell you&amp;rsquo;ve learned, with the correct number of &amp;ldquo;bubbles&amp;rdquo; for the amount of times you can use it, such as the example in the screenshot below:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Avrae custom counter output showing Magic Initiate - Command counter" src="/assets/avrae-1.png" /&gt;&lt;/p&gt;
&lt;p&gt;Here, Scrat (my Hadozee character) has the &lt;strong&gt;Magic Initiate (Cleric): Command&lt;/strong&gt; counter, that corresponds to his background feat providing that 1-st level spell. Avrae&amp;rsquo;s also added the spell to Scrat&amp;rsquo;s spellbook (&lt;code&gt;!sb&lt;/code&gt;), meaning Scrat is able to cast it. However, they&amp;rsquo;re not linked together in any way. Casting the spell uses a spell slot (it shouldn&amp;rsquo;t) and doesn&amp;rsquo;t update the counter (it should). This means that instead, there are a number of things I need to remember to do when casting the spell:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Firstly, check the custom counter to see if it&amp;rsquo;s been used or not (i.e. is the spell available to me, or do I need a long rest first).&lt;/li&gt;
&lt;li&gt;Cast the spell without using a spell slot (the &lt;code&gt;-i&lt;/code&gt; flag to &lt;code&gt;!cast&lt;/code&gt; or &lt;code&gt;!i cast&lt;/code&gt; will do this).&lt;/li&gt;
&lt;li&gt;Decrement the counter.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To solve this, I wrote a short alias I call &lt;code&gt;!featcast&lt;/code&gt;, which can be used in place of &lt;code&gt;!cast&lt;/code&gt; (or &lt;code&gt;!i cast&lt;/code&gt;) to handle the above three things:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="n"&gt;featcast&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ARGS&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;}}{{&lt;/span&gt;&lt;span class="n"&gt;H&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;help&amp;quot;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;?&amp;quot;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;}}{{&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;H&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="n"&gt;H&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot; &amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:])}}&lt;/span&gt;&lt;span class="n"&gt;multiline&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;drac2&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;H&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;character&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;ccs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consumables&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;:&amp;quot;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;:&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;: &amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()))]&lt;/span&gt;
    &lt;span class="n"&gt;cc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ccs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ccs&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;:&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;combat&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;i &amp;quot;&lt;/span&gt;
            &lt;span class="n"&gt;cc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;!&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;cast &amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#39; -i &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;!embed -title &amp;#39;Cannot cast &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;!&amp;#39; -desc &amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; does not have any uses of &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; from the &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; feat available.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;**&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;**&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#39;&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;!embed -title &amp;#39;Could not find a &lt;/span&gt;&lt;span class="se"&gt;\&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\&amp;quot;&lt;/span&gt;&lt;span class="s2"&gt; custom counter&amp;#39; -desc &amp;#39;Check that your feats for **&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;** are set up correctly.&amp;#39; -thumb &amp;lt;image&amp;gt;&amp;quot;&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;!embed -title &amp;#39;Help for feat-casting&amp;#39; -desc &amp;#39;Use `!featcast &amp;lt;spell&amp;gt;` to cast a spell provided by a feat, instead of using your spell slots.&amp;#39;&amp;quot;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;drac2&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It relies on the fact that Avrae names the custom counters sensibly, in the format of &amp;ldquo;Feat Name: Spell Name&amp;rdquo;. When typing &lt;code&gt;!featcast command -t Target1 -t Target2&lt;/code&gt;, it will look for a custom counter named in that way that could possibly match the provided spell name. When it finds one, it checks for a free slot, and casts the spell (with the provided arguments), decrementing the counter.&lt;/p&gt;
&lt;p&gt;There are many feats and backgrounds that provide spells, and this alias has made handling them &lt;em&gt;much&lt;/em&gt; easier. Hopefully it can be of some use in your games too!&lt;/p&gt;</content><category term="D&amp;D"></category><category term="Avrae"></category></entry><entry><title>Review: 71406 Yoshi's Gift House</title><link href="https://marmaladeofcourse.com/2022/10/07/review-71406-yoshis-gift-house/" rel="alternate"></link><published>2022-10-07T00:00:00+01:00</published><updated>2022-10-07T00:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2022-10-07:/2022/10/07/review-71406-yoshis-gift-house/</id><summary type="html">&lt;p&gt;I was given a few more of the LEGO &lt;a href="https://brickset.com/news/category-Set-review/theme-Super-Mario"&gt;Super Mario&lt;/a&gt; sets to review for Brickset, including &lt;a href="https://brickset.com/article/82124/review-71406-yoshi-s-gift-house"&gt;71407&lt;/a&gt; Yoshi&amp;rsquo;s Gift House:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Since I was a child, Yoshi has always been my favourite Super Mario character. I couldn&amp;rsquo;t put my finger on why, but there&amp;rsquo;s something very whimsical …&lt;/p&gt;&lt;/blockquote&gt;</summary><content type="html">&lt;p&gt;I was given a few more of the LEGO &lt;a href="https://brickset.com/news/category-Set-review/theme-Super-Mario"&gt;Super Mario&lt;/a&gt; sets to review for Brickset, including &lt;a href="https://brickset.com/article/82124/review-71406-yoshi-s-gift-house"&gt;71407&lt;/a&gt; Yoshi&amp;rsquo;s Gift House:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Since I was a child, Yoshi has always been my favourite Super Mario character. I couldn&amp;rsquo;t put my finger on why, but there&amp;rsquo;s something very whimsical and fun about the dinosaur-like cartoon creature that&amp;rsquo;s willing to carry Mario around on his back and help out wherever he can.&lt;/p&gt;
&lt;p&gt;He&amp;rsquo;s an iconic part of Mario lore, and has fittingly appeared in a number of sets so far in the Super Mario line. &lt;a href="https://brickset.com/sets/71406-1"&gt;71406&lt;/a&gt; Yoshi&amp;rsquo;s Gift House is the latest to feature the character, and is the largest with him as the sole protagonist.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img alt="Yoshi's Gift House LEGO set" src="https://images.brickset.com/news/82124_IMG_1847.jpg" /&gt;&lt;/p&gt;</content><category term="LEGO Reviews"></category></entry><entry><title>The Lost Jungles of Rabbad: Part 2</title><link href="https://marmaladeofcourse.com/2022/10/06/the-lost-jungles-of-rabbad-part-2/" rel="alternate"></link><published>2022-10-06T00:00:00+01:00</published><updated>2022-10-06T00:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2022-10-06:/2022/10/06/the-lost-jungles-of-rabbad-part-2/</id><summary type="html">&lt;p&gt;This is part two in my ongoing series to summarise the &lt;em&gt;Lost Jungles of Rabbad&lt;/em&gt; D&amp;amp;D adventure I am taking part in. I previously wrote up &lt;a href="https://marmaladeofcourse.com/2022/07/07/the-lost-jungles-of-rabbad/"&gt;the first part&lt;/a&gt;, so I highly recommend reading that before continuing here, if you haven&amp;rsquo;t already!&lt;/p&gt;
&lt;p&gt;&lt;img alt="overgrown jungle" src="/assets/jungle.png" /&gt;&lt;/p&gt;
&lt;p&gt;When we last left our fearless …&lt;/p&gt;</summary><content type="html">&lt;p&gt;This is part two in my ongoing series to summarise the &lt;em&gt;Lost Jungles of Rabbad&lt;/em&gt; D&amp;amp;D adventure I am taking part in. I previously wrote up &lt;a href="https://marmaladeofcourse.com/2022/07/07/the-lost-jungles-of-rabbad/"&gt;the first part&lt;/a&gt;, so I highly recommend reading that before continuing here, if you haven&amp;rsquo;t already!&lt;/p&gt;
&lt;p&gt;&lt;img alt="overgrown jungle" src="/assets/jungle.png" /&gt;&lt;/p&gt;
&lt;p&gt;When we last left our fearless heroes, they had just defeated Dawn Song; on some level, at least—she had turned into a necrodragon, and flown off. Well, Deimos and Qhell weren&amp;rsquo;t content to leave it at that, and chased after her but were unable to catch up. After licking their wounds, the group turned to examine the items she&amp;rsquo;d left behind. They found:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the &lt;strong&gt;Helm of Earth&lt;/strong&gt;, made of a simple dark brown wood;&lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;Staff of Sight&lt;/strong&gt;, golden, smooth and well-made;&lt;/li&gt;
&lt;li&gt;five diamonds; and&lt;/li&gt;
&lt;li&gt;an amulet made of a small animal skull dyed blood red.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Helm and Staff, together with Sumunar&amp;rsquo;s Hammer, seem to make up the three of the four items that bind Fulgrin in his tomb. Kosta takes the helm, Elmer takes the staff.&lt;/p&gt;
&lt;h3 id="kostas-absence-explained"&gt;Kosta&amp;rsquo;s Absence Explained&lt;a class="headerlink" href="#kostas-absence-explained" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Kosta disappeared down the mine, and was taken by Pelor to another plane. There, they meet with The Celestial, the patron with which Kosta did a deal previously. The gods appear to be fighting over him and his allegiance, with Pelor attempting to release Kosta from the binding deal he had unwittingly made with Mr C. They come to an arrangement: Mr C shall only demand four more tasks of Kosta, and Pelor will give him &amp;ldquo;the amulet&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Kosta is taken to his old city of Fallen Oak, where he is watching the town under attack, two armies clashing. He watches Father Thason, his adopted father, struck down and killed. Kosta is told he has a choice to make: save Father Thason, or save the city from plunder, pillage, and murder. It is not clear whether the attack is happening in real time, the past, or the future, or even real. Kosta chooses the city, to the Celestial&amp;rsquo;s anger.&lt;/p&gt;
&lt;p&gt;Back at the plane with Pelor, as one of his tasks Kosta is made to pick up a rapier by the Celestial that shocks him with a blast of energy and ages him a few decades. For the final task, the Celestial takes Kosta&amp;rsquo;s right eye, replacing it with a pulsing purple orb, and disappears.&lt;/p&gt;
&lt;p&gt;Pelor then opens the heavens, looking down on the battle between the group and Dawn Song below. With a cry, Kosta jumps, and the portal closes behind him.&lt;/p&gt;
&lt;h3 id="qhells-backstory"&gt;Qhell&amp;rsquo;s Backstory&lt;a class="headerlink" href="#qhells-backstory" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Qhell explains that he has met Dawn Song before:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Several years ago I was taken as a slave, captured by some gang of outlaws. I was a curiosity, sold repeatedly, until I ended up on a ship. I didn’t know where I was or what they intended to do with me, except for that cat. She would come down to the hold every single day to leer at her cargo, torturing one lucky creature each day. She is evil, that’s all I know of her, and I vowed never to be in that position again.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="kostas-eye-father-thason-and-contacting-the-gods"&gt;Kosta&amp;rsquo;s Eye, Father Thason, and Contacting the Gods&lt;a class="headerlink" href="#kostas-eye-father-thason-and-contacting-the-gods" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Kosta asks Elmer to study the glowing purple orb that has replaced his eye. Elmer can tell that it&amp;rsquo;s deeply enchanted with powerful magic, beyond that of mortals.&lt;/p&gt;
&lt;p&gt;Kosta then heads off to find the clerics, who help him with the &lt;em&gt;scry&lt;/em&gt; spell to try and locate Father Thason, but they are unable to. Without a clear direction, he tries to contact Pelor, to no avail, as Deimos also tries in vain to get in touch with Gargauth.&lt;/p&gt;
&lt;h3 id="sankras-ice"&gt;Sankra&amp;rsquo;s Ice&lt;a class="headerlink" href="#sankras-ice" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;At Elmer&amp;rsquo;s suggestion, Nib takes another look through the Celestial book given to them by the General. He finds a passage that mentions:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Moradin&amp;rsquo;s Might, The Wild Mother&amp;rsquo;s Clay, Savras&amp;rsquo;s Eye, at the heart, Sankra&amp;rsquo;s Ice&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The names line up with the four items, leaving Sankra&amp;rsquo;s Ice to be the Crystal Blade. The group decide to head to Dawn Song&amp;rsquo;s camp the following day to see what she had left behind.&lt;/p&gt;
&lt;p&gt;That night, Nib, Deimos, and Sumunar dream of the black plain again. Around the fire this time is Katrina, the devil, and a pale blue figure. Katrina tells them that the crystal blade is part of the lock, and the group should take the remaining three items back to the door to seal it again, at which point the three magical items they&amp;rsquo;ve found will be returned to their hiding places.&lt;/p&gt;
&lt;h3 id="we-canoe"&gt;We Canoe!&lt;a class="headerlink" href="#we-canoe" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Leaving Rafael at the fort, the party take a pair of canoes to cross the river&amp;rsquo;s mouth and avoid going back through the gulch. After the crocodile fiasco, Kosta is put safely inside a magical ball for the journey. They&amp;rsquo;re attacked on the way by giant sharks, and fight them off with difficulty, but eventually make it to the other shore. Due to the loss of a canoe, Elmer has to ride Qhell (as a Giant Sea Horse) for the rest of the journey.&lt;/p&gt;
&lt;h3 id="campfire-tales"&gt;Campfire Tales&lt;a class="headerlink" href="#campfire-tales" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;They camp on the treeline of the beach. Nib tells stories of their excursions north:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Last time we camped by the sea, was much colder than this. Was up north, near Tressmouth. We’d been hired to go track down the rumours of a lost tribe of elves deep in the arctic tundra. We’d sailed on a old ship, The Orphans Rage, north out of Nook, but hit difficulties about two days out of Northwatch. Ship went down, half the crew lost to the dark waves. Those of us that made it, washed up on this thin spit of land. We burnt what wreckage we could, the flames green with the salt, just to stave off the cold.&lt;/p&gt;
&lt;p&gt;As dawn broke, we had to head out. Only 12 of us left then. The cold claimed two more that first day, another the next.&lt;/p&gt;
&lt;p&gt;We hit a valley, some shelter from the winds, but soon lost in their midst. We turned one bend and there stood our doom, a dark beast of the ice. It feel on us, blood-stained claws tearing into us. We ran but it was faster. The end was upon us as a thick black arrow sailed into the crevasse. Then another, and another, another. Under the hail, the beast fell, and looking up we saw them, The Lost Elves, bows drawn. Their leader nodded at me, and like that, were gone. Just saved our lives and disappeared into the snow.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id="sumunars-story"&gt;Sumunar&amp;rsquo;s Story&lt;a class="headerlink" href="#sumunars-story" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;You all know that Jadel and I knew each other,&amp;rdquo; &lt;em&gt;Sumunar takes up.&lt;/em&gt; &amp;ldquo;We were in nearby villages, and played together sometimes as young orcs. Well, she had a sweet tusk like you would not believe. Always loved a bit of honey or sap-sugar. So one day we were out rambling, and came across a hive high up in a tree. She wanted to raid it, and I was not hard to convince. Well anyways, we got our little selves about halfway up, then realized we couldn&amp;rsquo;t reach any higher. We tried going back down, but it was at an awkward angle and we couldn&amp;rsquo;t manage. We must have sat there for three hours, arguing about whose fault it was and yelling for help occasionally. At last I grew so sick of waiting that I jumped down, and naturally broke my wrist. So then she was still stuck, and I was on the ground but holding my wrist and crying. Finally I got it together enough to run to the nearest village, and the grown-ups got Jadel down.&amp;rdquo; &lt;em&gt;She pauses and laughs fondly.&lt;/em&gt; &amp;ldquo;I forgave her quickly enough, but her nickname was Honey-Tusk for years afterwards.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="a-visit-from-elmers-father"&gt;A Visit From Elmer&amp;rsquo;s Father&lt;a class="headerlink" href="#a-visit-from-elmers-father" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;As Elmer takes watch that night, the figure of his father walks out of the water and berates him for running away, tells him to come home. An argument ensues, and Elmer refuses to go anywhere with him.&lt;/p&gt;
&lt;h3 id="sakhunas-statue"&gt;Sa&amp;rsquo;khuna&amp;rsquo;s Statue&lt;a class="headerlink" href="#sakhunas-statue" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The next day, on their trek through the forest, the group come across a statue of a long-dead god of the hells, Sa&amp;rsquo;khuna. Coloured keys sit in locks, and as Kosta plays with them, a loud boom explodes from the statue. The roc approaches from the sky, and the party escape with the help of Jul&amp;rsquo;s Pass Without Trace spell.&lt;/p&gt;
&lt;h3 id="areka"&gt;Areka&lt;a class="headerlink" href="#areka" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;On the outskirts of Dawn Song&amp;rsquo;s camp, the party rest for the night. Deimos is visited during his watch by a wooden automaton with a black spear. It talks with Deimos, though doesn&amp;rsquo;t appear threatening. Apparently the roc is called Barda, and it warns of Ghosts to the south, and to stay out of the tunnels.&lt;/p&gt;
&lt;h3 id="the-return-of-night-bane"&gt;The Return of Night Bane&lt;a class="headerlink" href="#the-return-of-night-bane" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;During Elmer&amp;rsquo;s watch, Night Bane turns up, limping, draging a broken leg behind them. He has a conversation with Elmer, saying that Kosta killed him from behind, and that he&amp;rsquo;d come for him. Deimos and Kosta wake, and Night Bane challenges Kosta to a fight. Instead, Kosta talks him down, and then heals him slightly at his request. He leaves to find his partner, claiming that &amp;ldquo;Vecna is all, and will be all soon&amp;rdquo;.&lt;/p&gt;
&lt;h3 id="dawn-songs-camp"&gt;Dawn Song&amp;rsquo;s Camp&lt;a class="headerlink" href="#dawn-songs-camp" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In the morning, the group confront four figures still at the camp, &amp;ldquo;Brazen Guard of the Golden Stars, servants to the Undying Mistress&amp;rdquo;. Elmer fireballs them, which sets the camp alight and starts a fight. Defeating the guards and putting the fire out, they find the same books they have a copy of, as well as an updated map of the jungle with more locations marked. There&amp;rsquo;s also an annotated book, &lt;em&gt;The Travels of Magil the Lost, Vol 5: Mauldold, Illwind, and the Jungle&lt;/em&gt;. Her notes also show that they had found the helm at a shrine at the bottom of the river. &lt;/p&gt;
&lt;p&gt;Kosta attempts to talk to Pelor for direction, but is blocked by a voice saying &amp;ldquo;he can&amp;rsquo;t hear you, you&amp;rsquo;re on my land now. She knows where you are now. I&amp;rsquo;ve told her.&amp;rdquo; Kosta is overwhelmed by twisting energy pulsing from his eye.&lt;/p&gt;
&lt;h3 id="dawn-song-the-necrodragon-again"&gt;Dawn Song the Necrodragon. Again.&lt;a class="headerlink" href="#dawn-song-the-necrodragon-again" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In the far distance, Qhell sees Dawn Song lift off from the mountains headed towards the group. Deimos puts Kosta in the ball once more, and lifts off to distract her while the others run for the trees. A brief fight in the air comes to an end when Nib casts Dimension Door to pull Deimos out of the dragon&amp;rsquo;s way and disappear into the forest.&lt;/p&gt;
&lt;p&gt;Safe in the trees, they release Kosta from the ball, returned to normal.&lt;/p&gt;
&lt;h3 id="dont-smell-the-flowers"&gt;Don&amp;rsquo;t Smell the Flowers&lt;a class="headerlink" href="#dont-smell-the-flowers" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Heading onward through the forest towards a place marked on Dawn Song&amp;rsquo;s map as Fulgrin&amp;rsquo;s Tomb, Kosta stops to sniff some flowers, which turn out to be nasty man-eating plants. Fighting them off, the party have to get creative with methods of removing the corrosive sap from their skin without any water to hand.&lt;/p&gt;
&lt;h3 id="the-crashed-balloon"&gt;The Crashed Balloon&lt;a class="headerlink" href="#the-crashed-balloon" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Further on, they come across a crashed hot air balloon with the skeletons of some elves. Their notes show them to be explorers who had set out long ago to find the lost city of Maudold, and crashed here. Elmer takes a ring, intending to return it to their family one day. There&amp;rsquo;s also a chest there, with an inscription that indicates it can only be opened at midnight. The party bury the elves, and then camp for the night.&lt;/p&gt;
&lt;p&gt;During the night, Qhell is visited by another chwinga like the one that came to Sumunar, which has apparently placed leaves on the graves of the elves. He gives Qhell a ball of twine.&lt;/p&gt;
&lt;p&gt;At midnight, they open the chest and find a load of gems, four spell scrolls, a wand, a red amulet, an old map, and a pair of glowing stones. The amulet is to stop somebody from being magically tracked, and the wand helps find hidden doors and compartments. The stones are teleportation anchors. Holding one stone lets you teleport to either of the other two (of which the group only found one).&lt;/p&gt;
&lt;h3 id="the-endless"&gt;The Endless&lt;a class="headerlink" href="#the-endless" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Deimos takes a stone, and teleports to the unknown one. He finds himself in a large dusty room, where he is promptly arrested and taken across a large city (Rabbad, later explained by Nib) to talk to The Endless, a tall thin figure with golden skin. He seems to know all about the group, what they have been trying to do, and even talks of the Tabaxi twins sibling, Dusk, who they presume lost. Deimos is given the third stone, and he returns to the others.&lt;/p&gt;
&lt;h3 id="furry-creature-fight"&gt;Furry Creature Fight&lt;a class="headerlink" href="#furry-creature-fight" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;While Deimos is gone, the others move on through the jungle, where they&amp;rsquo;re set upon by small furry creatures. Just as they manage to fight them off, Qhell scaring a few away as a Black Bear, Deimos pops back into existence with them.&lt;/p&gt;
&lt;h3 id="fulgrins-tomb-entrance"&gt;Fulgrin&amp;rsquo;s Tomb Entrance&lt;a class="headerlink" href="#fulgrins-tomb-entrance" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;More treking through the jungle, and the group eventual stumble on what looks to be the entrance to Fulgrin&amp;rsquo;s Tomb. A pile of stone juts up out of the ground, with a jet black door set in it. The clearing is patrolled by several figures in black armour. Talking with the figures, they are told to leave. They will not let anybody &amp;ldquo;disturb the imprisonment&amp;rdquo; and won&amp;rsquo;t say any more. Qhell discerns that they are engraved with binding sigils—something is bound magically within each suit of armour. The group retreat for the night.&lt;/p&gt;
&lt;h3 id="areka-again"&gt;Areka, Again&lt;a class="headerlink" href="#areka-again" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Before sleep, Elmer studies the books again and finds mention of &amp;ldquo;a cave of glass behind a door of nothing&amp;rdquo; in the &lt;strong&gt;Da Dark Under&lt;/strong&gt;, as well as &amp;ldquo;the land of their sleep, a black river of death, a battle with The Night&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;That night, Deimos is once again visited by Areka, who tells him and Elmer that the third cat, Dusk, is beneath the jungle, behind that door, attempting to break open the lock without the rest of the key, which he refers to as the Ashes.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;As night continues, so does our story. The plan for the morning? To get past the guards, through the black door, and down below to Fulgrin&amp;rsquo;s Tomb. Are the party nearing the conclusion of their adventure? Are they walking to certain death, and will they all return? Find out… when we do, and I do another write up!&lt;/p&gt;</content><category term="D&amp;D"></category><category term="The Lost Jungles of Rabbad"></category></entry><entry><title>Relay FM for St. Jude</title><link href="https://marmaladeofcourse.com/swift/apps/development/2022/10/02/relay-fm-for-st-jude/" rel="alternate"></link><published>2022-10-02T00:00:00+01:00</published><updated>2022-10-02T00:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2022-10-02:/swift/apps/development/2022/10/02/relay-fm-for-st-jude/</id><summary type="html">&lt;p&gt;September is over, and that means the end of another &lt;a href="https://www.cclg.org.uk/ccam"&gt;Childhood Cancer Awareness Month&lt;/a&gt;. For the last four years, the &lt;a href="https://www.relay.fm/"&gt;Relay FM&lt;/a&gt; community has used the opportunity to raise money for &lt;a href="https://www.stjude.org/about-st-jude.html?sc_icid=us-mm-missionstatement#mission"&gt;St. Jude Children&amp;rsquo;s Research Hospital&lt;/a&gt;, an institution dedicated to understanding, treating, and discovering new ways to defeat childhood …&lt;/p&gt;</summary><content type="html">&lt;p&gt;September is over, and that means the end of another &lt;a href="https://www.cclg.org.uk/ccam"&gt;Childhood Cancer Awareness Month&lt;/a&gt;. For the last four years, the &lt;a href="https://www.relay.fm/"&gt;Relay FM&lt;/a&gt; community has used the opportunity to raise money for &lt;a href="https://www.stjude.org/about-st-jude.html?sc_icid=us-mm-missionstatement#mission"&gt;St. Jude Children&amp;rsquo;s Research Hospital&lt;/a&gt;, an institution dedicated to understanding, treating, and discovering new ways to defeat childhood cancer. Last year, they raised over $700,000. This year, they&amp;rsquo;re set to match that amount again, taking the total raised in the last four years to over $2 million.&lt;/p&gt;
&lt;p&gt;For Stephen, one of the founders of Relay FM, the cause is particularly important. You can read more about his story and why they fundraise over on &lt;a href="https://512pixels.net/2022/08/relay-st-jude-2022/"&gt;512pixels.net&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;When the campaign started last year, I had been a part of the &lt;a href="https://relay.fm/membership"&gt;Relay FM members Discord&lt;/a&gt; for a few months. People were sharing the fundraising total in the Discord in various ways, and a few enterprising individuals came up with a variety of ways to get that total into a iOS Home Screen widget, such as &lt;a href="https://zmknox.com/2021/08/21/building-a-donation-tracker-widget.html"&gt;this Scriptable solution by Zach Knox&lt;/a&gt;. At the time, I was just starting to play with iOS app development and I thought &amp;ldquo;we can do better than this. Let&amp;rsquo;s make a native app and widget!&amp;rdquo;&lt;/p&gt;
&lt;p&gt;A group of us had already gotten together and built a Discord bot for the server, so I had the perfect &lt;a href="https://tildy.dev"&gt;group of developers&lt;/a&gt; to solicit for help. Together, we built a small app that pulled the campaign information from the Tiltify public API (reverse engineered from &lt;a href="https://tiltify.com/@relay-fm/relay-fm-for-st-jude-2022"&gt;their website&lt;/a&gt;, rather than the official APIs), displayed it in the app, and provided a widget. It could also pull in the campaign&amp;rsquo;s milestones. We distributed it via TestFlight, so we didn&amp;rsquo;t have to deal with full-on App Review, and it was a great success (amongst Relay FM members, at least).&lt;/p&gt;
&lt;p&gt;It was a brilliant learning experience for me, and greatly increased my knowledge of how a iOS app is built, albeit a very simple one. I am very grateful to all the people who took the time to explain things to me, and review and comment on my code to help me become a better Swift developer.&lt;/p&gt;
&lt;p&gt;This year, we dusted off the app and plugged in the new campaign details a few days before September started, in order to get it up and running again. However, the campaign organisers threw us a last-minute curveball—there wasn&amp;rsquo;t going to be just one campaign this year, but many! Relay would still have their main one, but anybody could set up &amp;ldquo;subfundraisers&amp;rdquo;, all of which would add towards the overall total. So how was the app going to handle that?&lt;/p&gt;
&lt;p&gt;We spent some time reverse engineering the Tiltify API for the new campaign format, and reworked the app from a simple &amp;ldquo;here&amp;rsquo;s a widget&amp;rdquo; to a more full-featured discovery app, showcasing not only the overall total but also each individual fundraiser and their own goals and rewards. We extended the widgets to allow you to choose which fundraiser it should show, provided a variety of appearance options, and added a share screen to let users post pictures of the fundraiser progress on social media.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Relay FM for St. Jude iOS App Screenshots" src="/assets/IMG_0929.PNG" /&gt;&lt;/p&gt;
&lt;p&gt;And of course, we threw in Lock Screen widgets and donation charts for those who&amp;rsquo;d braved the iOS 16 beta, or regular people upgrading when it was released publicly half way through the month.&lt;/p&gt;
&lt;p&gt;Throughout the project I&amp;rsquo;ve learned more about Swift, SwiftUI, and iOS app development than I ever would have working on my own with no real goal in mind. I&amp;rsquo;ve learned how to handle the various iPhone and iPad screen sizes, store data locally with GRDB, and use custom intents to provide widgets of all sizes with dynamic options.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s been a lot of fun to have a project to work on that&amp;rsquo;s reached so many people (despite never leaving TestFlight!) and helped towards raising the absolutely phenomenal amount that the Relay FM community achieved for St. Jude this year. The app is &lt;a href="https://github.com/Lovely-Development-Team/St-Jude-Widget-App"&gt;open source&lt;/a&gt;, and we will hopefully be resurrecting it next September when the community comes together once more in the fight against childhood cancer.&lt;/p&gt;</content><category term="Development"></category><category term="Swift"></category><category term="Apps"></category></entry><entry><title>UserDefaults, @AppStorage, and Data Types</title><link href="https://marmaladeofcourse.com/2022/09/29/userdefaults-appstorage-and-data-types/" rel="alternate"></link><published>2022-09-29T00:00:00+01:00</published><updated>2022-09-29T00:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2022-09-29:/2022/09/29/userdefaults-appstorage-and-data-types/</id><summary type="html">&lt;p&gt;As I&amp;rsquo;m starting to play more seriously with iOS app development, Xcode, and Swift, I&amp;rsquo;m starting to come up with a variety of patterns I use in the various toy apps I mess around with that make working with certain APIs or frameworks easier. One of these is …&lt;/p&gt;</summary><content type="html">&lt;p&gt;As I&amp;rsquo;m starting to play more seriously with iOS app development, Xcode, and Swift, I&amp;rsquo;m starting to come up with a variety of patterns I use in the various toy apps I mess around with that make working with certain APIs or frameworks easier. One of these is &lt;code&gt;UserDefaults&lt;/code&gt;, which provides an easy way to store persistent data between app launches.&lt;/p&gt;
&lt;p&gt;The basic way to interact with &lt;code&gt;UserDefaults&lt;/code&gt; is to set values by assigning a data type to a particular key (which are strings), and reading that key later:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;// Storing a boolean value&lt;/span&gt;
&lt;span class="n"&gt;UserDefaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;standard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;forKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;hasPerformedInitialSync&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Retrieving a boolean value&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;hasPerformedInitialSync&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;UserDefaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;standard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;forKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;hasPerformedInitialSync&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There are two problems with this, though:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Using string-based keys is error-prone; it can&amp;rsquo;t be checked by the compiler, so an overlooked typo can lead to unexpected behaviour that&amp;rsquo;s difficult to debug.&lt;/li&gt;
&lt;li&gt;SwiftUI is based around watching state variables for changes, and redrawing views based upon this; how can we tie &lt;code&gt;UserDefaults&lt;/code&gt; into that?&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="a-userdefaults-extension"&gt;A &lt;code&gt;UserDefaults&lt;/code&gt; extension&lt;a class="headerlink" href="#a-userdefaults-extension" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The first problem is one I&amp;rsquo;ve started solving by creating an extension to the &lt;code&gt;UserDefaults&lt;/code&gt; class. I put this in a new Swift file (usually &lt;code&gt;Extensions/UserDefaults.swift&lt;/code&gt;), with code such as the following:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;extension&lt;/span&gt; &lt;span class="nc"&gt;UserDefaults&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;hasPerformedInitialSync&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;hasPerformedInitialSync&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kr"&gt;get&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;forKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hasPerformedInitialSync&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="kr"&gt;set&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;forKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hasPerformedInitialSync&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This has promoted the previous string-based keys we were using to an &lt;code&gt;enum&lt;/code&gt;: the compiler is now able to check our keys for us and produce errors at compile time if we use one we haven&amp;rsquo;t defined. An additional computed property on the &lt;code&gt;UserDefaults&lt;/code&gt; class hides the get and set logic from us, so in the rest of our code we need only do the following:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;// Storing a boolean value&lt;/span&gt;
&lt;span class="n"&gt;UserDefaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;standard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hasPerformedInitialSync&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="c1"&gt;// Retrieving a boolean value&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;hasPerformedInitialSync&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;UserDefaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;standard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hasPerformedInitialSync&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Hurray! No more string-based keys to remember. But what about the second problem?&lt;/p&gt;
&lt;p&gt;Up until recently, I was syncing &lt;code&gt;UserDefaults&lt;/code&gt; changes with SwiftUI view state by storing a related &lt;code&gt;@State&lt;/code&gt; variable, and tying it to the corresponding &lt;code&gt;UserDefaults&lt;/code&gt; key using &lt;code&gt;onAppear&lt;/code&gt; and &lt;code&gt;onChange(of:)&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;ContentView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;hasPerformedInitialSync&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;some&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;NavigationView&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// View code&lt;/span&gt;
      &lt;span class="n"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;hasPerformedInitialSync&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Sync Done!&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;onAppear&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;hasPerformedInitialSync&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;UserDefaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;standard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hasPerformedInitialSync&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;hasPerformedInitialSync&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;newValue&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
      &lt;span class="n"&gt;UserDefaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;standard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hasPerformedInitialSync&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;newValue&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This works by fetching the value stored in &lt;code&gt;UserDefaults&lt;/code&gt; when the view first appears and assigning it to the state variable, and then watching the state variable for changes, and syncing those back to the &lt;code&gt;UserDefaults&lt;/code&gt; key. But there are still problems with this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It&amp;rsquo;s easy to forget to add the required line to &lt;code&gt;onAppear&lt;/code&gt; or the &lt;code&gt;onChange&lt;/code&gt; handler for a new variable.&lt;/li&gt;
&lt;li&gt;The view won&amp;rsquo;t react to changes that happen to the &lt;code&gt;UserDefaults&lt;/code&gt; key outside of its own interaction, such as if a presented sheet changes the value.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Fortunately, SwiftUI introduced a new property wrapper similar to &lt;code&gt;@State&lt;/code&gt; to help us with this.&lt;/p&gt;
&lt;h2 id="introducing-appstorage"&gt;Introducing &lt;code&gt;@AppStorage&lt;/code&gt;&lt;a class="headerlink" href="#introducing-appstorage" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Called &lt;code&gt;@AppStorage&lt;/code&gt;, the new property wrapper lets you reference a &lt;code&gt;UserDefaults&lt;/code&gt; key directly and bind it to a variable that will automatically sync changes between itself and &lt;code&gt;UserDefaults&lt;/code&gt;. It can be used as such:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;ContentView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;AppStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UserDefaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hasPerformedInitialSync&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;hasPerformedInitialSync&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;some&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// View code&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Note that there&amp;rsquo;s no longer any need for the value to be read &lt;code&gt;onAppear&lt;/code&gt;, or the changes to be observed using &lt;code&gt;onChange&lt;/code&gt;. SwiftUI will take care of that for us, automatically syncing data back when actions within the view change the state variable&amp;rsquo;s value. Very handy!&lt;/p&gt;
&lt;h2 id="supported-data-types"&gt;Supported Data Types&lt;a class="headerlink" href="#supported-data-types" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;However, there are still limitations with this approach, one of which I ran into today and was banging my head against for quite some time until I realised the issue.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;UserDefaults&lt;/code&gt; supports the storage of &lt;a href="https://developer.apple.com/documentation/foundation/userdefaults"&gt;a variety of primitive data types&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Boolean&lt;/li&gt;
&lt;li&gt;String&lt;/li&gt;
&lt;li&gt;Integer&lt;/li&gt;
&lt;li&gt;Data&lt;/li&gt;
&lt;li&gt;URL&lt;/li&gt;
&lt;li&gt;Double&lt;/li&gt;
&lt;li&gt;Float&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It also supports storing arrays or dictionaries containing these primitive types, using the &lt;code&gt;array(forKey:)&lt;/code&gt;, &lt;code&gt;stringArray(forKey:)&lt;/code&gt;, and &lt;code&gt;dictionary(forKey:)&lt;/code&gt; methods. &lt;code&gt;AppStorage&lt;/code&gt;, however, does not support this. Apple&amp;rsquo;s &lt;a href="https://developer.apple.com/documentation/swiftui/appstorage"&gt;documentation for &lt;code&gt;AppStorage&lt;/code&gt;&lt;/a&gt; lists the &lt;code&gt;init&lt;/code&gt; methods available, and they can return:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;String&lt;/li&gt;
&lt;li&gt;Integer&lt;/li&gt;
&lt;li&gt;Data&lt;/li&gt;
&lt;li&gt;URL&lt;/li&gt;
&lt;li&gt;Double&lt;/li&gt;
&lt;li&gt;Boolean&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It&amp;rsquo;s missing &lt;code&gt;Float&lt;/code&gt;—I didn&amp;rsquo;t care about that—and the &lt;code&gt;Array&lt;/code&gt; and &lt;code&gt;Dictionary&lt;/code&gt; methods—I &lt;em&gt;did&lt;/em&gt; care about those. I had created my &lt;code&gt;UserDefaults&lt;/code&gt; extension, as normal:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;extension&lt;/span&gt; &lt;span class="nc"&gt;UserDefaults&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;favouriteColours&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;favouriteColours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kr"&gt;get&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;stringArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;forKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;favouriteColours&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="kr"&gt;set&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;forKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;favouriteColours&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That worked fine, as expected. But when I tried to use it with &lt;code&gt;@AppStorage&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;AppStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UserDefaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;favouriteColours&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;favouriteColours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;the compiler threw up the error &lt;code&gt;No exact matches in call to initializer&lt;/code&gt;. Helpful. It turns out that&amp;rsquo;s because of the lack of support for the collection methods, and I had to resort to using my original &lt;code&gt;onAppear&lt;/code&gt;/&lt;code&gt;onChange&lt;/code&gt; workaround.&lt;/p&gt;
&lt;h2 id="its-not-all-bad-though"&gt;It&amp;rsquo;s not all bad, though!&lt;a class="headerlink" href="#its-not-all-bad-though" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Despite this frustrating limitation with &lt;code&gt;AppStorage&lt;/code&gt; (which I hope is fixed in a future SwiftUI release), I really like the &lt;code&gt;UserDefaults&lt;/code&gt; extension method for working with the API. There&amp;rsquo;s two more things I want to mention about it. One, it can be used to store or return any data type, as long as you can easily convert it to or from one of the supported types. For example, storing a custom &lt;code&gt;enum&lt;/code&gt; is easy. Make the &lt;code&gt;enum&lt;/code&gt; inherit from &lt;code&gt;Int&lt;/code&gt; or &lt;code&gt;String&lt;/code&gt;, and you&amp;rsquo;re off to the races:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;Mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Int&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;light&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;dark&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;extension&lt;/span&gt; &lt;span class="nc"&gt;UserDefaults&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;preferredMode&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;preferredMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mode&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kr"&gt;get&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;forKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;preferredMode&lt;/span&gt;&lt;span class="p"&gt;.,&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;light&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kr"&gt;set&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;forKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;preferredMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Secondly, if you want to be able to access the data you&amp;rsquo;re storing in other targets, such as a widget, the extension is a great place to create a static property that uses a group identifier:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;extension&lt;/span&gt; &lt;span class="nc"&gt;UserDefaults&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;shared&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;UserDefaults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;suiteName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;group.com.myapp.AppName&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Accessing &lt;code&gt;UserDefaults.shared&lt;/code&gt; instead of &lt;code&gt;UserDefaults.standard&lt;/code&gt; will allow it to be read and written from any targets that have access to the specified group identifier. Additionally, SwiftUI provides two different ways of specifying that you want to use &lt;code&gt;.shared&lt;/code&gt; instead of &lt;code&gt;.standard&lt;/code&gt;. It can be done on a per-variable basis by passing the &lt;code&gt;store:&lt;/code&gt; parameter to &lt;code&gt;@AppStorage&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;AppStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UserDefaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;preferredMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UserDefaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;preferredMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;light&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Or it can be made the default for all child views within a hierarchy:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;MyApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;App&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;some&lt;/span&gt; &lt;span class="n"&gt;Scene&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;WindowGroup&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ContentView&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;defaultAppStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UserDefaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Both of which make working with &lt;code&gt;UserDefaults&lt;/code&gt; cross-application much easier.&lt;/p&gt;
&lt;p&gt;Anyway, I&amp;rsquo;m mostly writing this down so that the next time I&amp;rsquo;m struggling with it I can prompt myself on how it works and why I&amp;rsquo;ve done things a certain way; but maybe it can help you, too!&lt;/p&gt;</content><category term="Development"></category><category term="Swift"></category><category term="Apps"></category></entry><entry><title>Review: 71408 Princess Peach's Castle</title><link href="https://marmaladeofcourse.com/2022/09/11/review-71408-princess-peachs-castle/" rel="alternate"></link><published>2022-09-11T00:00:00+01:00</published><updated>2022-09-11T00:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2022-09-11:/2022/09/11/review-71408-princess-peachs-castle/</id><summary type="html">&lt;p&gt;My final review of this year&amp;rsquo;s new Princess Peach-themed Super Mario LEGO sets, this time for &lt;a href="https://brickset.com/sets/71408-1/Princess-Peach-s-Castle"&gt;Peach&amp;rsquo;s Castle&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://brickset.com/sets/71408-1/Princess-Peach-s-Castle"&gt;71408&lt;/a&gt; Princess Peach&amp;rsquo;s Castle is the largest of the Peach-themed additions to the Super Mario line this year, and is in fact the largest of all the game&amp;rsquo;s …&lt;/p&gt;&lt;/blockquote&gt;</summary><content type="html">&lt;p&gt;My final review of this year&amp;rsquo;s new Princess Peach-themed Super Mario LEGO sets, this time for &lt;a href="https://brickset.com/sets/71408-1/Princess-Peach-s-Castle"&gt;Peach&amp;rsquo;s Castle&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://brickset.com/sets/71408-1/Princess-Peach-s-Castle"&gt;71408&lt;/a&gt; Princess Peach&amp;rsquo;s Castle is the largest of the Peach-themed additions to the Super Mario line this year, and is in fact the largest of all the game&amp;rsquo;s sets so far at 1,216 parts. It includes a variety of characters, both new to the range and returning, and commands some impressive space when laid out ready to play. Let&amp;rsquo;s take a look at what&amp;rsquo;s inside!&lt;/p&gt;
&lt;/blockquote&gt;</content><category term="LEGO Reviews"></category></entry><entry><title>Review: 71407 Cat Peach Suit and Frozen Tower</title><link href="https://marmaladeofcourse.com/2022/08/05/review-71407-cat-peach-suit-and-frozen-tower/" rel="alternate"></link><published>2022-08-05T00:00:00+01:00</published><updated>2022-08-05T00:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2022-08-05:/2022/08/05/review-71407-cat-peach-suit-and-frozen-tower/</id><summary type="html">&lt;p&gt;I took look at &lt;a href="https://brickset.com/article/79600/review-71407-cat-peach-suit-and-frozen-tower"&gt;another one&lt;/a&gt; of the Princess Peach-themed Super Mario LEGO sets that were released at the start of August:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;There&amp;rsquo;s no doubt that this set is visually striking - it has an impressive height, bright colours, and a baddie flying around on a broomstick. It&amp;rsquo;s also …&lt;/p&gt;&lt;/blockquote&gt;</summary><content type="html">&lt;p&gt;I took look at &lt;a href="https://brickset.com/article/79600/review-71407-cat-peach-suit-and-frozen-tower"&gt;another one&lt;/a&gt; of the Princess Peach-themed Super Mario LEGO sets that were released at the start of August:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;There&amp;rsquo;s no doubt that this set is visually striking - it has an impressive height, bright colours, and a baddie flying around on a broomstick. It&amp;rsquo;s also great to see a costume change for Peach introduced already (and I&amp;rsquo;m glad that the other characters can use it too—although Luigi did need a software update in order to understand).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;One more to go in this batch of reviews!&lt;/p&gt;</content><category term="LEGO Reviews"></category></entry><entry><title>Elsewhen 1.5: Time Zones, Translated</title><link href="https://marmaladeofcourse.com/2022/07/28/elsewhen-15-time-zones-translated/" rel="alternate"></link><published>2022-07-28T09:00:00+01:00</published><updated>2022-07-28T09:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2022-07-28:/2022/07/28/elsewhen-15-time-zones-translated/</id><summary type="html">&lt;p&gt;We&amp;rsquo;ve recently released &lt;a href="https://tildy.dev/elsewhen"&gt;Elsewhen&lt;/a&gt; 1.5, a time zone app that I develop with a group of friends. Jason Snell featured it over at &lt;a href="https://sixcolors.com/post/2022/07/get-universal-times-into-discord-and-elsewhere/"&gt;Six Colors&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;One app that can help is &lt;a href="https://apps.apple.com/app/elsewhen/id1588708173"&gt;Elsewhen&lt;/a&gt; from &lt;a href="https://tildy.dev/"&gt;The Lovely Developers&lt;/a&gt;, a fun group that sprung out of the Relay FM Discord. Elsewhen …&lt;/p&gt;&lt;/blockquote&gt;</summary><content type="html">&lt;p&gt;We&amp;rsquo;ve recently released &lt;a href="https://tildy.dev/elsewhen"&gt;Elsewhen&lt;/a&gt; 1.5, a time zone app that I develop with a group of friends. Jason Snell featured it over at &lt;a href="https://sixcolors.com/post/2022/07/get-universal-times-into-discord-and-elsewhere/"&gt;Six Colors&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;One app that can help is &lt;a href="https://apps.apple.com/app/elsewhen/id1588708173"&gt;Elsewhen&lt;/a&gt; from &lt;a href="https://tildy.dev/"&gt;The Lovely Developers&lt;/a&gt;, a fun group that sprung out of the Relay FM Discord. Elsewhen lets you quickly set a date and time and then translate it — either into a bunch of human-readable time zones, or into Discord’s time-code format.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img alt="" src="/assets/elsewhen.png" /&gt;&lt;/p&gt;
&lt;p&gt;The latest version not only showcases a brand new UI and the ability to create and save groups of time zones, but also brings it to the Mac for the first time. If you ever have the need to share a given time in a multitude of time zones around the world, or are part of an iternational Discord community where their time codes feature comes in handy, &lt;a href="https://apps.apple.com/app/elsewhen/id1588708173"&gt;give it a whirl&lt;/a&gt;. It&amp;rsquo;s been a blast working on it.&lt;/p&gt;</content><category term="Development"></category><category term="Apps"></category></entry><entry><title>My D&amp;D Adventures</title><link href="https://marmaladeofcourse.com/2022/07/26/my-dd-adventures/" rel="alternate"></link><published>2022-07-26T15:00:00+01:00</published><updated>2022-07-26T15:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2022-07-26:/2022/07/26/my-dd-adventures/</id><summary type="html">&lt;p&gt;When I first got into D&amp;amp;D a year or so ago, I created my first character for a play-by-post (PbP) game called &lt;em&gt;The Lost Jungles of Rabbad&lt;/em&gt;. Since then, I&amp;rsquo;ve joined other PbP games, played a few live one-shots (via video call), starting running my own PbP game …&lt;/p&gt;</summary><content type="html">&lt;p&gt;When I first got into D&amp;amp;D a year or so ago, I created my first character for a play-by-post (PbP) game called &lt;em&gt;The Lost Jungles of Rabbad&lt;/em&gt;. Since then, I&amp;rsquo;ve joined other PbP games, played a few live one-shots (via video call), starting running my own PbP game with a pre-rolled adventure from D&amp;amp;D Beyond, fought other players in two player-vs-player (PvP) arenas, and play-tested a couple of new campaign ideas from our DM.&lt;/p&gt;
&lt;p&gt;I hope to write more about my characters in the future, but here&amp;rsquo;s a summary of the games so far.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The Lost Jungles of Rabbad&lt;/strong&gt;: PbP, playing as &lt;em&gt;Elmer Raloren&lt;/em&gt;, a noble-born High Elf Wizard (School of Abjuration). Currently at level 6. A homebrew adventure run by our DM, Rob, in a world of his design. I wrote a &lt;a href="https://marmaladeofcourse.com/2022/07/07/the-lost-jungles-of-rabbad/"&gt;summary of this campaign&lt;/a&gt; previously.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A Darkness Gathering&lt;/strong&gt;: PbP, playing as &lt;em&gt;Dagoth the Ancient&lt;/em&gt;, a Bugbear Paladin (Oath of the Ancients). Currently at level 5. A campaign that a first-time-DM, Jake, has adapted from an old 2e adventure, I believe. It started out as &lt;em&gt;&lt;a href="https://www.dndbeyond.com/sources/wa/frozen-sick"&gt;Frozen Sick&lt;/a&gt;&lt;/em&gt;, a free sourcebook on D&amp;amp;D Beyond, which the players quickly completed and Jake transitioned.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Princess Bride&lt;/strong&gt;: PbP, playing as &lt;em&gt;Stardust Nightwhisper&lt;/em&gt;, a Fairy Bard. Currently at level 3. The game was conceived of and run by Nic, who had to put it on pause after he and his wife had a baby! Star, as my character was known by, is waiting patiently for when the game resumes and I can play a bard once more!&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Wave Walker&lt;/strong&gt;: PbP, playing as &lt;em&gt;Kaith&lt;/em&gt;, an irritable Aarakocra Monk. Currently at level 2, the game was started by another first-time DM, Madi, in a world of her own creation. She ended up not having the time to continue it, so it is currently on haitus, but hopefully will return!&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Wave Walker: Windborn Demons&lt;/strong&gt;: Although Madi didn&amp;rsquo;t have the time to full a full-blown campaign, she plans to run a series of smaller one-shots for characters in the &lt;em&gt;Wave Walker&lt;/em&gt; world, starting with Kaith. The game has only just begun, but I am excited because I was enjoying roleplaying Kaith&amp;rsquo;s surly demeanour, and am looking forward to attempting to play a monk in combat.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Winter&amp;rsquo;s Crest&lt;/strong&gt;: A live playtest of a winter-themed one-shot campaign Rob was writing, playing as &lt;em&gt;Peregrin Puddlefoot&lt;/em&gt;, a Halfling Bard, at level 5. The campaign was very fun, and we were successful as a party—although there were definitely times it felt like we weren&amp;rsquo;t going to be!&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Dragon of Icespire Peak&lt;/strong&gt;: PbP, where I&amp;rsquo;m the DM! A pre-rolled &lt;a href="https://www.dndbeyond.com/sources/doip"&gt;adventure on D&amp;amp;D Beyond&lt;/a&gt;, I&amp;rsquo;m having a lot of fun making it my own. The characters are currently at level 3, though they chickened out of completing the latest job, which would have earned them a level bump. One of them was bitten by a Wererat, so they&amp;rsquo;re currently distracted by that.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Hawkstone Alliance&lt;/strong&gt;: A series of live games played in Rob&amp;rsquo;s world, with a rotating set of players each time. Playing as &lt;em&gt;Tabasco&lt;/em&gt;, a level 4 Tabaxi Artificer (Alchemist), I&amp;rsquo;ve currently been involved in two adventures so far of the four that have been played.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Pit: The Isle of Death&lt;/strong&gt;: A PbP PvP game, playing &lt;em&gt;Olrim&lt;/em&gt;, a level 10 Fallen Aasimar Paladin Oathbreaker. There were five others in the game, and Olrim ultimately came second, and was &lt;em&gt;so close&lt;/em&gt; to beating Skulkas!&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Pit: The Temple of the Light&lt;/strong&gt;: The second PbP PvP game, this time playing &lt;em&gt;Kolasi&lt;/em&gt;, a multiclassing Zariel Tiefling - level 3 Rogue, level 7 Paladin. This is my first attempt at multiclassing, and I find level 10 pretty difficult to work with (there are so many options). We&amp;rsquo;re working in pairs this time, but it&amp;rsquo;s not going particularly well for Kolasi or his team mate - somebody brought a pack of bears and a dragon to the fight, which was terribly unfair of them!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We&amp;rsquo;re also playtesting a new campaign idea from Rob, which I won&amp;rsquo;t say much about other than it&amp;rsquo;s not D&amp;amp;D-based this time, but uses fate dice. In the first playthrough I played as &lt;em&gt;Kalamae, Goddess of Life and Destiny, Mother of Creation, Watcher of the Sands&lt;/em&gt;, and in the second (currently in-progress) as &lt;em&gt;Sa&amp;rsquo;khuna, God of the Night and the Underworld, Lord of Darkness, Keeper of the Keys.&lt;/em&gt; It&amp;rsquo;s a lot of fun!&lt;/p&gt;
&lt;p&gt;One of the most fun aspects of character creation is their visual design, and &lt;a href="https://www.heroforge.com/"&gt;Hero Forge&lt;/a&gt; is a fantastic place to get inspiration and build colourful 3D models. You can even buy them as STL files or 3D printed figures for your in-person games, though I haven&amp;rsquo;t done that yet. Here&amp;rsquo;s what all my characters look like:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Hero Forge designs of my characters" src="/assets/heroes.png" /&gt;&lt;/p&gt;
&lt;p&gt;I never realised that D&amp;amp;D could be as fun as all this. I have found a fantastic group of people to play with and seeing everybody&amp;rsquo;s creativity is truly amazing.&lt;/p&gt;</content><category term="D&amp;D"></category></entry><entry><title>LEGO Review: 71403 Adventures with Princess Peach</title><link href="https://marmaladeofcourse.com/2022/07/26/lego-review-71403-adventures-with-princess-peach/" rel="alternate"></link><published>2022-07-26T00:00:00+01:00</published><updated>2022-07-26T00:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2022-07-26:/2022/07/26/lego-review-71403-adventures-with-princess-peach/</id><summary type="html">&lt;p&gt;LEGO&amp;rsquo;s expanding their Super Mario range this summer, and I&amp;rsquo;ve written a review of the latest character to the lineup, Princess Peach, over on &lt;a href="https://brickset.com/article/79599"&gt;Brickset&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This year, another well-known figure from the Mushroom Kingdom joins the lineup, with &lt;a href="https://brickset.com/sets/71403-1"&gt;71403&lt;/a&gt; Adventures with Peach! The previous two starter courses were …&lt;/p&gt;&lt;/blockquote&gt;</summary><content type="html">&lt;p&gt;LEGO&amp;rsquo;s expanding their Super Mario range this summer, and I&amp;rsquo;ve written a review of the latest character to the lineup, Princess Peach, over on &lt;a href="https://brickset.com/article/79599"&gt;Brickset&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This year, another well-known figure from the Mushroom Kingdom joins the lineup, with &lt;a href="https://brickset.com/sets/71403-1"&gt;71403&lt;/a&gt; Adventures with Peach! The previous two starter courses were designed to provide everything you need to get going with the range, even if you don&amp;rsquo;t have the previous sets, and Peach is no different, and is in fact the largest of the three.&lt;/p&gt;
&lt;/blockquote&gt;</content><category term="LEGO Reviews"></category></entry><entry><title>The Lost Jungles of Rabbad</title><link href="https://marmaladeofcourse.com/2022/07/07/the-lost-jungles-of-rabbad/" rel="alternate"></link><published>2022-07-07T00:00:00+01:00</published><updated>2022-07-07T00:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2022-07-07:/2022/07/07/the-lost-jungles-of-rabbad/</id><summary type="html">&lt;p&gt;&lt;em&gt;It is high summer in Illwind, the cool air off the sea balanced well against the heat from the desert to the east. The port town is busier than ever today, with the Feast Of Forgotten Gods in full swing. All throughout the streets, children run with green and yellow …&lt;/em&gt;&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;em&gt;It is high summer in Illwind, the cool air off the sea balanced well against the heat from the desert to the east. The port town is busier than ever today, with the Feast Of Forgotten Gods in full swing. All throughout the streets, children run with green and yellow streamers, horns and whistles blaring. In the town square, the inns have thrown open their doors, each serving feasts for the revellers, here you see a whole buffalo being roasted, there the largest fish pie you&amp;rsquo;ve ever seen. The smells and sounds are a heady mix, not to mention the free flowing beer, wine, ale and mead.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Most of the festival goers are gathering around here, to enjoy the food and watch the Momerie taking place there. This year telling the tales of a lost goddess of the plains, who fell in love with a mortal man, and forsook her divine power to live and age and die at his side.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Welcome to Illwind, a small city-state bordering the jungle, the sea, and the desert. This is a town of Rob&amp;rsquo;s creation, one of many settings in the large world that he&amp;rsquo;s created as a Dungeons and Dragons (D&amp;amp;D) game master, where he runs many campaigns for experienced and new players alike.&lt;/p&gt;
&lt;p&gt;About a year ago, I joined my first Dungeons and Dragons group. It&amp;rsquo;s something I&amp;rsquo;d been vaguely interested in, but had never had the opportunity to get involved with. These games were set up by Rob as a way to introduce new people to the hobby, and play in a no-pressure, no-rush environment as text chat in a private Discord server. Play-by-post, as it&amp;rsquo;s called, means it doesn&amp;rsquo;t matter that none of our time zones line up, or if we&amp;rsquo;re busy with &amp;ldquo;life&amp;rdquo; - we can play at our own pace, and somebody&amp;rsquo;s absence only holds up the storyline during the more rigid turn-based combat parts. I eagerly joined, and was grouped together with four others into a campaign called &lt;em&gt;The Lost Jungles of Rabbad&lt;/em&gt;, the initial hook being described as follows:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The Jungles of Rabbad are a wild and unexplored wilderness, replete with tales of lost cities, roaming undead and wild forgotten beasts. Two weeks ago, a party from the Temple of Oghma left on a expedition to find a hidden temple. Three days ago, contact was lost. Someone needs to go in and get them....&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We then created our characters, starting at Level 2, and put together this band of misfits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Elmer Raloren&lt;/strong&gt;, a male High Elf wizard,&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deimos&lt;/strong&gt;, a male Feral Tiefling rogue,&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sumunar&lt;/strong&gt;, a female Half-Orc barbarian,&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tylress, the Victorious&lt;/strong&gt;, a male Dragonborn fighter, and&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kosta Dragonstone&lt;/strong&gt;, a male Hill Dwarf cleric.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It was a good mix - a squishy spellcaster, a sneaky rogue, a couple of melee fighters, and a healer. It was time to jump in, and start playing the game.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;The campaign has now been running for almost a year, and the characters have levelled up from two to six. A lot has happened, some of it following the initial path of the DM&amp;rsquo;s campaign, and much of it not! I thought I would write up the campaign from the start to where we are now, summarising each event, as a log for future reference.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="albs-assignment"&gt;Alb&amp;rsquo;s Assignment&lt;a class="headerlink" href="#albs-assignment" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The characters, never having met each other before, all (except Sumunar) come together in an inn in Illwind, &lt;em&gt;The Half-Turned World&lt;/em&gt;, presided over by the barkeep, &lt;strong&gt;Undril&lt;/strong&gt;. New to the others, Tylress is a regular, a place he meets with his friend &lt;strong&gt;Sylvie&lt;/strong&gt; (an elf with severe PTSD). Together, the crew head on down to the docks in search of &lt;strong&gt;Alb&lt;/strong&gt;, a man known in the town for offering coin in exchange for jobs, not all of which are legal.&lt;/p&gt;
&lt;p&gt;Joined by Sumunar on their way onto Alb&amp;rsquo;s boat, &lt;em&gt;The Orphans Lament&lt;/em&gt;, they convince Alb of their competence by way of a fight against four animated and armed mannequins. He promises them 30 gold now, and 50 gold on return, for delivering a chest to &lt;strong&gt;Fulsi&lt;/strong&gt; at the &lt;em&gt;Farr Inn&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;After deceiving Alb&amp;rsquo;s minions and getting 45 gold out of them instead, they head off to the south towards the inn, a midpoint on the road between Illwind and Barrow&amp;rsquo;s Edge, a journey of a few days.&lt;/p&gt;
&lt;h3 id="nib-and-jul"&gt;Nib and Jul&lt;a class="headerlink" href="#nib-and-jul" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;During the second day on the road, the group meet a pair of elves, destined for Barrow&amp;rsquo;s Edge, and they fall into travel together. These are the siblings &lt;strong&gt;Nib&lt;/strong&gt;, a white-haired bard, and &lt;strong&gt;Jul&lt;/strong&gt;, a mute monk with some unknown dark magic about her. The group learn that the pair have been hired by The General (the leader of the barracks town Barrow&amp;rsquo;s Edge), on a &amp;ldquo;dangerous and urgent&amp;rdquo; mission, so of course Deimos offers their services.&lt;/p&gt;
&lt;p&gt;Tylress is less than convinced about Jul, warning the rest of the group against her, which almost results in a fight on the road.&lt;/p&gt;
&lt;h3 id="the-farr-inn"&gt;The Farr Inn&lt;a class="headerlink" href="#the-farr-inn" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;At the inn, Elmer greets Fulsi (to whom he apparently owes coin), and they take the chest round to the barn at the back. Fulsi runs an illegal fighting ring from here, and Sumunar taps in to take her turn in a bout, losing to a tall goliath, &lt;strong&gt;Xeri&lt;/strong&gt;. Exhausted, the party all retire to bed.&lt;/p&gt;
&lt;p&gt;Tylress is woken in the night by Jul, gesturing to him to head outside in the rain. She challenges him to a fight, to put their previous quarrel behind them. It&amp;rsquo;s tough, but Tylress is victorious, knocking Jul unconscious.&lt;/p&gt;
&lt;p&gt;In the morning, the party awake. Deimos finds a small bronze coin with a compass engraved on it tucked in his pack - the assumption is it came from Nib, but Nib denies all knowledge of it.&lt;/p&gt;
&lt;h3 id="bandits-on-the-road"&gt;Bandits on the road!&lt;a class="headerlink" href="#bandits-on-the-road" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;On the road from the &lt;em&gt;Farr Inn&lt;/em&gt; down to Barrow&amp;rsquo;s Edge, the party is stopped by a group of figures in green uniforms, two mounted on horses. They demand a toll for usung the road, &amp;ldquo;in the Queen&amp;rsquo;s Name&amp;rdquo;. The group doesn&amp;rsquo;t fall for this, and a fight breaks out. The bandits are dispatched, along with one of the horses (unfortunately), and Elmer takes over looking after the other horse, whom he names Rafael.&lt;/p&gt;
&lt;p&gt;The bandits all have matching tattoos on their wrists of golden stars, that Kosta recognises as the mark of an undead cult that&amp;rsquo;s been operating out of the jungle. Rumours say they&amp;rsquo;re trying to build an army!&lt;/p&gt;
&lt;p&gt;Kosta calls upon Turn Undead, to make sure there are no undead around. Nothing happens, other than Jul winces as the shockwave from the spell passes over her.&lt;/p&gt;
&lt;p&gt;As night falls, the party share some of the elves&amp;rsquo; wine. Sumunar is plagued by dreams of burning cities, of skeletons crawling out of shallow graves, of shambling undead clawing at her face, and Nib sleeps badly, but nobody else is affected.&lt;/p&gt;
&lt;h3 id="barrows-edge"&gt;Barrow&amp;rsquo;s Edge&lt;a class="headerlink" href="#barrows-edge" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Reaching the city after lunch, Nib leads the group to meet with The General, a white dragonborn. She presents them with a rough, old map of the jungle, and tasks them with the following:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;The other planes are slowly encroaching on us, and this is where we believe the key, and the lock, are. I need you to go here, find the key, find the lock and seal the gates again.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;They&amp;rsquo;re looking for the source of the weak points in the lines between the planes of existence, and they sent in two sets troops to deal with it. They were all lost, only one person returned.&lt;/p&gt;
&lt;p&gt;The group are given three old texts relating to the jungle:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;The Fulgrin Lament&lt;/em&gt;, written in Infernal. Fulgrin is a old myth, a fallen angel who raised a host against the gods of old, but banished to a distant plane.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Da Dark Under&lt;/em&gt;, written in Orcish. It appears to be the written history of an orc warband.&lt;/li&gt;
&lt;li&gt;Written in Celestial, it&amp;rsquo;s a collection of divine rituals and rites.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Kosta heads to the infirmary to go and talk with the only surviving member of the scouting group, a green tiefling called &lt;strong&gt;Hoc&lt;/strong&gt;. She&amp;rsquo;s missing a hand and an eye, and constantly speaking wordlessly, but uncommunicative.&lt;/p&gt;
&lt;p&gt;Kosta contacts some celestial power, a being that appears to speak through him, who pours energy into Hoc to help her speak:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&amp;hellip;from beneath one by two by one beneath the trees and the screams and the dark deep deeper deep&amp;hellip;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The group learn they&amp;rsquo;d been in the jungle for a week or so, lost their healer to a group of undead, who they&amp;rsquo;d then followed south to the river, where there was a long abandonded camp across the water. They found an altar, headed down some dark, wet steps beneath it, that just ended abruptly.&lt;/p&gt;
&lt;p&gt;Kosta&amp;rsquo;s new celestial patron then helps heal the tiefling, replacing her missing eye with a jet black orb, before the Barrow&amp;rsquo;s Edge healers step in.&lt;/p&gt;
&lt;h3 id="the-celestial"&gt;The Celestial&lt;a class="headerlink" href="#the-celestial" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Kosta reveals that the past few days he&amp;rsquo;s been … communing with a higer power. The Celestial, some kind of god/demon, is able to take over Kosta&amp;rsquo;s body, and speak through his voice. This appears to be the result of some kind of deal, details are scarce. It was the source of the power that helped heal Hoc, and it is quite happy to help in destroying any undead.&lt;/p&gt;
&lt;p&gt;It also does not seem to like Jul. It has told Kosta she is &amp;ldquo;touched by darkness&amp;rdquo;, and that the group needs to &amp;ldquo;deal with her&amp;rdquo;. Elmer&amp;rsquo;s unable to get anything out of Nib, other than vague talk of dangerous missions &amp;ldquo;up north&amp;rdquo; to look for a lost tribe of elves; and likewise Deimos out of Jul.&lt;/p&gt;
&lt;p&gt;That night, Deimos tails Jul to a building in Barrow&amp;rsquo;s Edge, which appears to be a tailors. She returns emptyhanded. Breaking in, Deimos finds nothing untoward inside.&lt;/p&gt;
&lt;h3 id="into-the-jungle"&gt;Into the Jungle&lt;a class="headerlink" href="#into-the-jungle" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The next morning, Nib&amp;rsquo;s been looking through the book written in celestial, and reveals that there is talk of binding spells that &amp;ldquo;lock&amp;rdquo; gates between the planes, and how to maintain them. This has previously been done at &amp;ldquo;the hollow of the world&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Jul disappears, and returns with a black bag slung over her shoulder. She doesn&amp;rsquo;t mention it, other than to say it contains &amp;ldquo;supplies&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;The General presents the group with a set of cloaks that offer some camoflage in the jungle, and after obtaining a slightly better-drawn map from the local cartographer&amp;rsquo;s, they set off, heading into the jungle in the general direction of a wizard&amp;rsquo;s tower marked on it.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s no path through the jungle, the group having to pick their way over the tree roots etc. Passing a tree with an ancient small golden star carved into it, similar in design to the bandits&amp;rsquo; tattoos, they stop to let Elmer cast a ritual to detect magic. The star glows of abjuration and enchantment, and there&amp;rsquo;s a similar one in trees to the north and the south, forming a sizeable magic circle, amplfying some arcana within, serving as a ward of the area.&lt;/p&gt;
&lt;p&gt;Tylress destroys the magic in the star with his greatsword, with seemingly no effect to the rest of the sigils, and the team pass through further into the jungle.&lt;/p&gt;
&lt;h3 id="the-clearing"&gt;The Clearing&lt;a class="headerlink" href="#the-clearing" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The group comes to a clearing, which contains a large stone ring with four pillars surrounding it. Three of the pillars have a glowing red stone atop them, slowly pulsing. As Deimos circles the clearing to get a better look, the party are attacked by the vines, and have to fight hard to free themselves and clear the creatures. Deimos spends much of it poisoned and entrapped, with Kosta dolling out regular healing.&lt;/p&gt;
&lt;p&gt;Kosta&amp;rsquo;s patron seems to believe it&amp;rsquo;s being caused by &amp;ldquo;the darkness in Jul&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Eventually the creatures are cleared, the last one running into the foliage and disappearing. Poking about, a fourth red crystal is found on the ground, while Elmer scales a pillar and investigates the stone altar. It turns out to be some kind of teleportation circle. The group decide to rest for the night before activating the circle with the fourth crystal, and the conversation turns to their life stories.&lt;/p&gt;
&lt;h3 id="tylress-story"&gt;Tylress&amp;rsquo; Story&lt;a class="headerlink" href="#tylress-story" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Tylress sighs&lt;/em&gt; &amp;ldquo;Fine, if nobody will tell my story then I will tell it myself. I&amp;rsquo;m sure many of you have met a variety of Dragonborn, but us reds are a particularly warfaring lot. Like my brethren I rose through the ranks, conquest after conquest. Everyone loses a battle, but I never lost a war. It&amp;rsquo;s how I earned my title of &amp;ldquo;The Victorious&amp;rdquo;. But I have honor, and I will not commit atrocities for the sake of it. Unfortunately others do not share this mindset. They see the blood and death as sport. And since I hadn&amp;rsquo;t yet reached the highest rank, there were plenty of bastards above me who would rather have their fun than honor. One day, I was instructed to lead my men into a village of elves and to slaughter them all. Men, women, and children. I refused. My commander seemed to be waiting for this moment. I was set up. Apparently, he felt threatened by a younger and more skillful warrior. He had me arrested and charged with insubordination. I was stripped of my rank and banished.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="deimos-story"&gt;Deimos&amp;rsquo; Story&lt;a class="headerlink" href="#deimos-story" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Deimos hesitates for a moment and takes a long drink before sighing.&lt;/em&gt; &amp;ldquo;I suppose I did start this. Well I was born a poor devil child and have no idea where I&amp;rsquo;m actually from or any of those little details. I was a foundling near a small provincial farming village by a wonderful human family. They overlooked&amp;hellip;well all of this&amp;hellip;&amp;rdquo; &lt;em&gt;he indicates his wings and rather demonic appearance.&lt;/em&gt; &amp;ldquo;They raised me alongside their real daughter as a member of the family and taught me some of the finer arts.&amp;rdquo; &lt;em&gt;He nods to to cook pot.&lt;/em&gt; &amp;ldquo;They made the decision to hide my appearance from the locals, which seems insightful considering what comes next. I attended the small human school and survived as the &lt;em&gt;sickly&lt;/em&gt; child in robes. On the farm at night I was encouraged to fly and stretch my wings. All was good until&amp;hellip;that day. I was in school as usual as for some reason a fire broke out at the school. As the students were running out, I heard a scream from within. As became apparent in that last fight, I enjoy a certain resistance to fire so I ran back to help. Well I may be resistant but not so much the robes.&amp;rdquo; &lt;em&gt;Deimos is clearly uncomfortable at this point.&lt;/em&gt; &amp;ldquo;I was able to pull the boy out, but&amp;hellip;didn&amp;rsquo;t make it in time. He didn&amp;rsquo;t survive, and neither did my disguise. I probably don&amp;rsquo;t need to detail what happened when the not-particularly open minded townsfolk saw&amp;hellip;well me&amp;hellip;carrying a dead child from a fire that isn&amp;rsquo;t having any effect on him.&amp;rdquo; &lt;em&gt;A bit of sadness combined with anger is apparent now.&lt;/em&gt; &amp;ldquo;Well there&amp;rsquo;s a reason I&amp;rsquo;m not a &lt;em&gt;huge&lt;/em&gt; fan of pitchforks. The mob realized who I was and proceeded to the farm.  I was&amp;hellip;too late to save my parents, but managed to pull my sister [Marlena] out before they could get her. I flew us as far and fast as I could. In the chaos of the next few days we were separated. I&amp;rsquo;ve been on my own, surviving and trying to find out what happened to her since then.”&lt;/p&gt;
&lt;p&gt;“I&amp;rsquo;ve gotten pretty good at the survival part, but haven&amp;rsquo;t really made any progress on the other, and generally don&amp;rsquo;t stay in one place long enough to&amp;hellip;enjoy&amp;hellip;a similar reception.&amp;rdquo; &lt;em&gt;Deimos is very clearly out of quips at this point and sits in silence, taking another long drink.&lt;/em&gt; &lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="the-blackened-figure"&gt;The Blackened Figure&lt;a class="headerlink" href="#the-blackened-figure" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;During Deimos&amp;rsquo; shift, a figure steps out into the clearing, a figure only he can see. A man, about Deimos&amp;rsquo; age, half his face burnt away. He tells Deimos that they met, many years ago, and it was Deimos who did that to him, and that he will always be there waiting for him. None of the others in the group are able to see or hear the figure.&lt;/p&gt;
&lt;p&gt;Deimos tells the others, believing it to be the boy he tried to save.&lt;/p&gt;
&lt;p&gt;During Kosta&amp;rsquo;s shift that night, he casts Detect Magic, and sees that Jul glows brightly with necromancy, and the bag she is carrying with abjuration magic. He fights with his patron, The Celestial, who wants him to fight Jul and take her out. An internal argument between two higher powers happens within him.&lt;/p&gt;
&lt;p&gt;In the morning, he confronts Nib about it, who is defensive and protective of his sister. Nib reveals that Jul has done a deal with something, something Kosta or The Celestial find distasteful, but that it is not their business. She is not evil or corrupt.&lt;/p&gt;
&lt;h3 id="teleportation"&gt;Teleportation&lt;a class="headerlink" href="#teleportation" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Deimos and Elmer use mage hands to place the crystal atop the final tower, and a dark space appears in the middle of the ring. Obviously, the group all jump through!&lt;/p&gt;
&lt;p&gt;They find themselves in the basement of a half ruined church or temple, still in the middle of the jungle. The alter glows with a magic of an unknown school. It&amp;rsquo;s engraved with an incantation of an offering to The Dark One, possibly relating to Kiaransalee, a now-dead Drow god of revenge and undeath.&lt;/p&gt;
&lt;h3 id="crocodiles"&gt;Crocodiles&lt;a class="headerlink" href="#crocodiles" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The group leave the temple, and head towards the sound of running water. They soon come to a river, which they attempt to swim across but are beset by giant crocodiles. It&amp;rsquo;s a tough crossing, but they make it to the other side.&lt;/p&gt;
&lt;h3 id="katrina"&gt;Katrina&lt;a class="headerlink" href="#katrina" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The land this side is less jungle, more swamp. There is a large clearing to the south, with many gravestones made of reclaimed wood dotted across it, arranged in a wide spiral, culminating at a handful of buildings in the middle. Somebody watches them from a shaded porch, and Deimos approaches, hailing them. Their name is Katrina, they are a survivor from &lt;em&gt;The Eli Flyer&lt;/em&gt;, a ship that ran aground. The rest of the crew are marked by the gravestones, and Katrina says she remains here to keep the woods nearby free of bodies, lest the &lt;strong&gt;Golden Hands&lt;/strong&gt; raise them as undead - the cult with the star tattoos.&lt;/p&gt;
&lt;p&gt;She seemingly doesn&amp;rsquo;t realise that the graves are in a spiral, and says that she cannot find her way &amp;ldquo;out&amp;rdquo;. Deimos offers to help, once the group&amp;rsquo;s current mission is completed. Elmer determines that the place is infused with enchatment magic, and Katrina herself with transmutation magic. He confronts her about this, and she says she will release Deimos from his enchantment when they have destroyed a red crystal atop a nearby tower, which will remove the binds keeping her there.&lt;/p&gt;
&lt;p&gt;Having little choice, they head towards the tower she indicated.&lt;/p&gt;
&lt;h3 id="elmers-invisible-man"&gt;Elmer&amp;rsquo;s Invisible Man&lt;a class="headerlink" href="#elmers-invisible-man" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;That night, during Elmer&amp;rsquo;s shift, a figure steps into the clearing, invisible aside from an outline against the trees. As Elmer approaches, it says nothing but:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;HELP I. YOU HELP I. I YOU. I ARE YOU. HELP&amp;hellip;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It disappates at Elmer&amp;rsquo;s touch.&lt;/p&gt;
&lt;h3 id="the-red-hammers-tower"&gt;The Red Hammers&amp;rsquo; Tower&lt;a class="headerlink" href="#the-red-hammers-tower" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The tower sits within a square clearing, about five stories tall, with a red glow at the top. The top stories are half-exploded, with the chunks of masonry blown out but hovering in mid-air, as if the explosion had been paused. Deimos attempts to talk his way into the tower, but (of course) it turns into a fight, with the guards calling Katrina &amp;ldquo;the defiler&amp;rdquo;, and it is their duty to keep her bound.&lt;/p&gt;
&lt;p&gt;During the fight with the guards, an enormous bird (a roc) joins the fight, causing trouble for Deimos&amp;rsquo; usual flying tactics. The group dispatch the guards, but one flees. Nib is pushed from the top of the tower, and hits the ground, dead. Deimos blames himself, for his enchantment causing them to head to the tower. Kosta blames himself for abandoning his cleric path making a deal with another patron. &lt;/p&gt;
&lt;p&gt;Deimos destroys the crystal, and with it, the entire tower collapses. Tylress grabs Nib, picks him up, and runs off into the trees. Jul takes Rafael and follows. The others rest, badly hurt.&lt;/p&gt;
&lt;p&gt;Tylress takes Nib back to the clearing with the graves, to Katrina. All the graves are on fire. In return for a future favour, she is able to bring Nib back, though he is not quite the elf he was before. He is cold, his eyes misty, his skin grey.&lt;/p&gt;
&lt;h3 id="deimos-dream"&gt;Deimos&amp;rsquo; Dream&lt;a class="headerlink" href="#deimos-dream" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The others make it back to the clearing, finding Jul lost in the woods. Resting for the night, Deimos finds himself on an endless plain of black sand, where he meets Katrina, an old man, and a devil around a campfire. They call themselves allies against oblivion, saying their name is &lt;strong&gt;Gargauth&lt;/strong&gt;. They thank him.&lt;/p&gt;
&lt;p&gt;In the morning, Kosta reveals he is familiar with the name Gargauth, as the Tenth Lord, the banished Lord of Hell.&lt;/p&gt;
&lt;h3 id="zombie-t-rex"&gt;Zombie T-rex&lt;a class="headerlink" href="#zombie-t-rex" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Sounds of movement lead the party to a clearing, where a load of zombie workers are clearing out a mine entrance, guarded by a group of living overseers, including a tabaxi that they had tracked riding a zombie t-rex. Deimos flies over, invisible, and sees they are pouring over a map. The group attempt to forge the map and swap it, but the swap goes badly and they end up fighting a load of zombies and the zombie t-rex ridden by the tabaxi, &lt;strong&gt;Night Bane&lt;/strong&gt;. The rex also keeps vomiting up more zombies. It is a tough fight, with Elmer being seriously injured by the dinosaur, but they are victorious with the help of a jar of eyes that Sumunar had, turning into a pack of wolves when smashed.&lt;/p&gt;
&lt;p&gt;The tabaxi is tied up, unconscious. When he wakes, Deimos enchants him to believe the group are his friends, and he tells them that he &amp;ldquo;serves at the pleasure of Vecna&amp;rdquo;. The zombies were digging to search for the key, otherwise known as &amp;ldquo;the Bone&amp;rdquo;, which fits &amp;ldquo;everything, and opens the whole of creation to The Ascended One&amp;rdquo;. He reveals the location of their current base. The team untie him, but as he is walking away, Kosta swings his warhammer (newly-acquired from the tabaxi himself) and hits the cat, Sumunar following up with a killing blow.&lt;/p&gt;
&lt;h3 id="the-map"&gt;The Map&lt;a class="headerlink" href="#the-map" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;After studying the map the party retrieved, Nib translates the text on it. There are four circles surrounded by text in Celestial, with the smaller circles also written in Abyssal:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Big circle, bottom left: &lt;em&gt;In The Dark, Seek The Light, Open The Door&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Top left: &lt;em&gt;The Scale Needs Balance, The Scale Demands Fire&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Top right: &lt;em&gt;The Deep Is The Core, The Door&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Bottom right: &lt;em&gt;The Star Shall Die, The Stone Frost&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="kostas-freedom"&gt;Kosta&amp;rsquo;s Freedom&lt;a class="headerlink" href="#kostas-freedom" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Ashamed by his behaviour regarding the tabaxi, Kosta pleads with his patron Pelor to be free from his deal with The Celestial. He&amp;rsquo;s struck by a blinding white light, and wakes to find he feels free of the influence of the second god.&lt;/p&gt;
&lt;h3 id="the-mine"&gt;The Mine&lt;a class="headerlink" href="#the-mine" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Deimos is told by the old man from his dreams that going down the mine is going to be bad. &amp;ldquo;Take light. It&amp;rsquo;s less… underneath the world, more like between.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;The party take a long rest, but during the first watch Tylress feels the mine calling to him, and heads down into it, first on steps, then climbing down ropes. Deimos and Sumunar also feel the pull, but Nib and Elmer slap them out of it. They head down after Tylress, who has found his way to a rubble-filled tunnel, which he starts clearing. Deimos heads down after him, and they have an argument, but eventually come back out, and they all complete their rest.&lt;/p&gt;
&lt;p&gt;All down in the mine the next day, crossing some black water, Tylress, Kosta, and Sumunar are dragged below into the inky blackness, and disappear. At this, the party is split.&lt;/p&gt;
&lt;h4 id="deimos-elmer-nib-and-jul"&gt;Deimos, Elmer, Nib, and Jul&lt;a class="headerlink" href="#deimos-elmer-nib-and-jul" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;Continuing on, the party are attacked by a pair of basilisks, which turn Deimos and Nib to stone. After defeating them, Jul and Elmer explore the rest of the mine, finding more red crystals, which they destroy. At each impact, their reality shifts briefly into an alternative space, and then snaps back. At one point, Jul also vanishes and Elmer is alone. He eventually finds her and helps save her from a gelantinous cube. &lt;/p&gt;
&lt;h4 id="tylress-kosta-and-sumunar"&gt;Tylress, Kosta, and Sumunar&lt;a class="headerlink" href="#tylress-kosta-and-sumunar" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;The three fall through into a small circular room, with a red crystal high above. Exploring through the door, the find other rooms, and some statues of a tiefling and an elf - Deimos and Nib, presumably. They also smash red crystals they find, after one of which Kosta disappears in a flash of bright light. Eventually, Sumunar opens a door and find themselves face to face with Elmer and Jul&amp;hellip;&lt;/p&gt;
&lt;h3 id="the-staff-and-dawn-song"&gt;The Staff and Dawn Song&lt;a class="headerlink" href="#the-staff-and-dawn-song" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Suddenly, a bright blinding light fills the area and Deimos and Nib return from stone, but trapped in a cage. Sumunar is back with Elmer and Jul, Kosta and Tylress have disappeared…&lt;/p&gt;
&lt;p&gt;They find their way to the room which Deimos and Nib are caged in, and fight a swirl of ink that&amp;rsquo;s protecting a staff in the middle. They escape with the staff, out the mine, only to be met by Dawn Song, who fights them for the staff. Deimos is enchanted, and attacks Nib, and the party give up the staff to save themselves. Dawn Song disappears into the jungle.&lt;/p&gt;
&lt;h3 id="fulgrins-lament"&gt;Fulgrin&amp;rsquo;s Lament&lt;a class="headerlink" href="#fulgrins-lament" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;During a rest, Deimos discovers a couple of passages from the Fulgrin&amp;rsquo;s Lament book:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&amp;ldquo;…and the cowards bound him there, by staff and by blade, by hammer and by helm, rather than face their own failures…&amp;rdquo;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&amp;ldquo;…the ashes, cross-bound about his gate, the wood and glass and steel and gold, held tight by the elemental keys, his door bound, waiting to rise again…&amp;rdquo;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="the-camp"&gt;The Camp&lt;a class="headerlink" href="#the-camp" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;They find Dawn Song&amp;rsquo;s camp - another open mine, mostly undead; giants, dogs, and dinos. Dawn Song has the staff and the helm. During the night, Elmer creeps over to watch and sees them sacrificing some figures in a number of fires, while the rest chant.&lt;/p&gt;
&lt;p&gt;Meanwhile, Deimos dreams of the endless plain again, and is told:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;These items are parts of the key that will unlock the door. Taking them from where they were held has started to unlock that door. If they get all three, and reach the door, they will open it fully. They will need collecting, and taking to that door to relock the door too. The Crystal Blade is not part of the key, it is part of the lock. As you might expect, it&amp;rsquo;s location is hidden from the likes of us. Last we heard it was in the mountains. The stolen dead will know, seek their camp, it has what you need.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="the-mountains"&gt;The Mountains&lt;a class="headerlink" href="#the-mountains" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In the mountains, the group find The Dark Sun, a glowing black orb of nothingness inside a volcano crater. While there, they are ambushed by Dawn Song and her crew of undead and skeletons.&lt;/p&gt;
&lt;p&gt;In the fight, Deimos touches the black orb, and disappears. Elmer, Sumunar, and Nib die. Deimos is transported to an endless desert of sand, and finds man sitting beneath a tent at a table. They play a game of cards, Deimos is given &lt;strong&gt;a steel warhammer&lt;/strong&gt;, and is returned to the volcano holding a card. Using the card, Deimos brings Sumunar, Elmer, and Nib back from death. They fight again, and Dawn Song leaves with the staff, promising to come back for the hammer.&lt;/p&gt;
&lt;h3 id="to-the-red-hammers"&gt;To the Red Hammers&lt;a class="headerlink" href="#to-the-red-hammers" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The party decide to go to the Red Hammer&amp;rsquo;s base at Fort Hope and ask for help. On the way, they pass the body of the Red Hammer that fled the tower earlier, killed by arrows, half his face dissolved. The ground gets swampy and they are set upon by a group of frog creatures. They fight them off, and later come across an enormous snail being ridden by mice. This is Meile, a travelling tradesperson. The group pay for passage on the snail, Bessy, to Fort Hope.&lt;/p&gt;
&lt;h3 id="qhell"&gt;Qhell&lt;a class="headerlink" href="#qhell" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;At the fort, the group are given bed and board by Jadel, the leader, a friend of Sumunar&amp;rsquo;s. They also offer their help, and that of their clerics, in the fight against Dawn Song.&lt;/p&gt;
&lt;p&gt;Also at the fort the group meet &lt;strong&gt;Qhell&lt;/strong&gt;, an Aarakocra Druid. He is there as a guest of the clerics, and joins the party in their campaign.&lt;/p&gt;
&lt;h3 id="fighting-dawn-song-again"&gt;Fighting Dawn Song. Again&lt;a class="headerlink" href="#fighting-dawn-song-again" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Finally, Dawn Song attacks the fort to retrieve the hammer. She brings many zombies, including zombie aarakocra, velociraptors, giants, and an ankylosaurus. The fight is tough, and seemingly not going their way, when&amp;hellip;&lt;/p&gt;
&lt;h3 id="kosta-returns"&gt;Kosta Returns!&lt;a class="headerlink" href="#kosta-returns" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;A break in the clouds forms, and from it, you see a pillar of light streak down and strike the earth next to Jul. Shielding your eyes, you can see something zip down, crashing to the ground with a thunderous force. The light fades, and you see a dwarf adorned in shining armor. His hair is a brilliant white, and he’s missing an eye, but as he turns to you and smiles, you recognize him instantly.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Kosta returns from the heavens! His &lt;em&gt;Turn Undead&lt;/em&gt; proves incredibly effective, and eventually the tide of the battle turns and the group defeat the zombie hordes.&lt;/p&gt;
&lt;h3 id="the-necrodragon"&gt;The Necrodragon&lt;a class="headerlink" href="#the-necrodragon" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Just as Dawn Song is killed:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;As last of her life force drains away, you hear her whisper,&lt;/em&gt; &amp;ldquo;I am ascended....&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;As her body slumps to the floor, the blood and viscera that lines it begins to slowly boil. You all see around you, all the bodies and blood, mortal and undead jerk and wither before you, their blood being drawn deeper in to her. And you see her body start to twist, and bulge, a pair of black skeletal wings burst from her back, her claws distend into long talons, with a sickening crack her back breaks and grows, and in her place stands a haggard and undead dragon, it&amp;rsquo;s skin white as her fur and her eyes a deep shimmering blue.  She looks around at you all, and with a deep roar akin to the creaking of a crypt, she lifts into the air, and flies off, limping as she does&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In her wake, she leaves behind the helm, the staff, and various other belongings.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="the-story-continues"&gt;The Story Continues&amp;hellip;&lt;a class="headerlink" href="#the-story-continues" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;And that is where the group is currently up to! We have many questions to answer as we continue: is Dawn Song actually dead? Will the Necrodragon return? Where is the Crystal Blade and can the party find the door and lock it once more?&lt;/p&gt;
&lt;p&gt;The story continues to play out in it&amp;rsquo;s typical PbP fashion, with ebbs and flows. Maybe in another year (or sooner!) I&amp;rsquo;ll write up another update here, but for now… you&amp;rsquo;ll just have to wait and see, won&amp;rsquo;t you?&lt;/p&gt;</content><category term="D&amp;D"></category><category term="The Lost Jungles of Rabbad"></category></entry><entry><title>I'm into pens</title><link href="https://marmaladeofcourse.com/2022/07/05/im-into-pens/" rel="alternate"></link><published>2022-07-05T00:00:00+01:00</published><updated>2022-07-05T00:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2022-07-05:/2022/07/05/im-into-pens/</id><summary type="html">&lt;p&gt;I&amp;rsquo;m into pens. There, I said it.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve always had a vague, background interest in writing and writing implements. As a child, I had a couple of &amp;ldquo;teach yourself calligraphy&amp;rdquo; books, a few calligraphy pens, and I enjoyed finding excuses to use them: writing up the menu for …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I&amp;rsquo;m into pens. There, I said it.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve always had a vague, background interest in writing and writing implements. As a child, I had a couple of &amp;ldquo;teach yourself calligraphy&amp;rdquo; books, a few calligraphy pens, and I enjoyed finding excuses to use them: writing up the menu for tonight&amp;rsquo;s dinner, a sign for my bedroom door, etc. But then computers happened and I started writing by hand a lot less, and when I did, it was with whatever cheap biro happened to be close by.&lt;/p&gt;
&lt;p&gt;Fast forward a few years (too many than I&amp;rsquo;d like to count), and in 2020 I joined the &lt;a href="https://www.relay.fm/membership"&gt;Relay FM&lt;/a&gt; members Discord. This was a place where people who enjoyed the variety of podcasts that Relay FM offered could socialise and discuss the topics that interested them; I joined mainly because they talked about it so much on &lt;a href="https://relay.fm/upgrade"&gt;Upgrade&lt;/a&gt; and &lt;a href="https://relay.fm/connected"&gt;Connected&lt;/a&gt;, and it seemed like a good place to find new people to chat with.&lt;/p&gt;
&lt;p&gt;Turns out, I&amp;rsquo;ve made some of my best friends there. People I talk to every day, who know me better than I know some &amp;ldquo;in person&amp;rdquo; friends that I&amp;rsquo;ve known for years. But these friends have been a bad influence (on my wallet, at least).&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a channel in the Discord for each rough topic that the Relay FM shows cover. For example, there&amp;rsquo;s &lt;code&gt;#tech&lt;/code&gt;, &lt;code&gt;#apple&lt;/code&gt;, &lt;code&gt;#creativity&lt;/code&gt;, &lt;code&gt;#systems-and-themes&lt;/code&gt; etc. It makes sense, then, that there&amp;rsquo;s a &lt;code&gt;#pens&lt;/code&gt;, given that there&amp;rsquo;s an entire show on Relay FM dedicated to them—&lt;a href="https://www.relay.fm/penaddict"&gt;The Pen Addict&lt;/a&gt;. I&amp;rsquo;ve listened to the show a couple of times, but it&amp;rsquo;s not one I&amp;rsquo;m subscribed to, and it never totally grabbed me. However, people sharing photos of their fountain pens, beautiful handwriting in stunning inks, letters they were writing and sending around the world—I wanted in.&lt;/p&gt;
&lt;p&gt;I resisted for a while, I didn&amp;rsquo;t need another hobby to suck up what disposable income I had. So I started small—I asked for suggestions on a daily pen that could replace my cheap biros, nice to write with but nothing fancy. Madi suggested the &lt;a href="https://www.cultpens.com/i/q/ZB02975/zebra-sarasa-gel-rollerball-pen-medium-07mm"&gt;Zebra Sarasa&lt;/a&gt;, and a three-pack dropped through the letterbox shortly afterwards. I was blown away! These were &lt;em&gt;so&lt;/em&gt; much nicer to write with than what I&amp;rsquo;d been using before, and I was back to finding excuses to write by hand. I took to carrying one around in my pocket (a habit I&amp;rsquo;d never had before), along with a &lt;a href="https://fieldnotesbrand.com/"&gt;Field Notes&lt;/a&gt; pocket notebook that Madi had kindly sent across the ocean to me, in a package that also contained a whole host of different coloured refills. I was in.&lt;/p&gt;
&lt;p&gt;One of the other members of the Discord, Ellen, had started up a small pen pals group, an offshoot from the official Relay Discord. I joined the group and wrote a couple of letters with the Sarasas, but really I wanted a fountain pen and everything that came with it - the little bottles of ink, the choice of nib size, the process of flushing, cleaning, and refilling, etc. So I asked for advice, took the plunge, and bought a &lt;a href="https://www.cultpens.com/i/q/TW50831/twsbi-eco-fountain-pen-clear"&gt;TWSBI Eco Clear&lt;/a&gt;, and a bottle of &lt;a href="https://www.cultpens.com/i/q/TW69176/twsbi-1791-bottled-ink-18ml"&gt;TWSBI 1791 Orange&lt;/a&gt; ink. I mean, how else was I going to get to write in orange?!&lt;/p&gt;
&lt;p&gt;It arrived (along with a packet of Love Hearts and the invoice held together with a little colourful smiley face paper clip, nice cute touches from &lt;a href="https://www.cultpens.com"&gt;Cult Pens&lt;/a&gt;) and I quickly filled it with the ink and started writing. Needless to say, I love it. I had chosen a medium nib, because I don&amp;rsquo;t like too skinny or too fat writing, and the only thing letting my letters down now is my handwriting!&lt;/p&gt;
&lt;p&gt;Fast forward a few months, and I&amp;rsquo;m itching for a new colour. Don&amp;rsquo;t get me wrong, I love the orange, but I also fancied a nice vibrant pink. So what was I to do? How could I get the pink, without giving up having the orange easily to hand?&lt;/p&gt;
&lt;p&gt;Clearly, the answer was to get a second fountain pen. (I see how this snowballs, now…) Ellen recommended another starter pen, the &lt;a href="https://www.cultpens.com/i/q/LM30785/lamy-safari-fountain-pen-pink"&gt;Lamy Safari&lt;/a&gt;, which I ordered in pink, with a broad nib this time. (It does need a converter to use bottled inks, as it usually takes a cartridge.) I also intended to add a bottle of &lt;a href="https://www.cultpens.com/i/q/LM62360/lamy-t53-crystal-ink-30ml"&gt;Lamy Crystal ink&lt;/a&gt; in &lt;em&gt;Rhodonite&lt;/em&gt;, which is a vibrant pink, but when the order arrived it turned out I&amp;rsquo;d actually somehow clicked on &lt;em&gt;Agate&lt;/em&gt;, which is a rather flat grey. I was somewhat disappointed!&lt;/p&gt;
&lt;p&gt;Contacting Cult Pens, they were happy for me to return the Agate and they would send out a replacement Rhodonite, which was very kind of them. I packaged the ink back up and took it off to the post office.&lt;/p&gt;
&lt;p&gt;Ellen and another friend from the Discord, Alex, had been listening to me whine about not getting the pink, and a surprise package dropped through my letter box a day or so later. Opening it up, it contained a bottle of &lt;a href="https://www.cultpens.com/i/q/RB56355/robert-oster-signature-ink-50ml"&gt;Robert Oster Signature ink&lt;/a&gt; in &lt;em&gt;Hot Pink&lt;/em&gt;! Unbeknownst to me, they&amp;rsquo;d conspired to order me a replacement themselves, and chose Robert Oster in part because it&amp;rsquo;s good ink, but I suspect mostly because it&amp;rsquo;s Australian, and so is Ellen. They have a truly enormous range of colours, and the Hot Pink is fantastic. Exactly what I was after! It&amp;rsquo;s also in a 50ml bottle, which will last me literally forever.&lt;/p&gt;
&lt;p&gt;&lt;img alt="TWSBI Eco with Robert Oster Signature Hot Pink ink" src="/assets/IMG_7608.jpeg" /&gt;&lt;/p&gt;
&lt;p&gt;However, I was still going to be getting a replacement of the Lamy Crystal Rhodonite, so I contacted Cult Pens again and asked if they could instead send out a different colour (despite Ellen&amp;rsquo;s protestations that two pinks is not too many pinks, I want a nice range of colours before I start doubling up and comparing!)&lt;/p&gt;
&lt;p&gt;They agreed, and so we come to today: I have a TWSBI Eco (M) inked in Robert Oster Signature Hot Pink, a Lamy Safari (B) with a tiny amount of Lamy Crystal Agate, and a bottle of Lamy Crystal Amazonite (a really lovely turquoise blue) on its way. I can&amp;rsquo;t wait to try that out.&lt;/p&gt;
&lt;p&gt;&lt;img alt="The three inks and two pens described in this post" src="/assets/IMG_8439.jpeg" /&gt;&lt;/p&gt;
&lt;p&gt;The only question is: now I have three ink bottles, and just the two pens. There&amp;rsquo;s only one solution, surely… &lt;/p&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.cultpens.com/i/q/TW50831/twsbi-eco-fountain-pen-clear"&gt;TWSBI Eco Clear&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.cultpens.com/i/q/TW69176/twsbi-1791-bottled-ink-18ml"&gt;TWSBI 1791 ink&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="https://inkyfountainpens.wordpress.com/2021/04/03/ink-review-twsbi-1791-orange/"&gt;Review of TWSBI 1791 Orange on inkyfountainpens&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.cultpens.com/i/q/LM30785/lamy-safari-fountain-pen-pink"&gt;Lamy Safari Pink&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.cultpens.com/i/q/LM62360/lamy-t53-crystal-ink-30ml"&gt;Lamy Crystal ink&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="https://mountainofink.com/blog/lamy-rhodonite"&gt;Review of Lamy Crystal Rhodonite on Mountain of Ink&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://mountainofink.com/blog/lamy-agate"&gt;Review of Lamy Crystal Agate on Mountain of Ink&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://mountainofink.com/blog/lamy-amazonite"&gt;Review of Lamy Crystal Amazonite on Mountain of Ink&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.cultpens.com/i/q/RB56355/robert-oster-signature-ink-50ml"&gt;Robert Oster Signature ink&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="https://mountainofink.com/blog/robert-oster-hot-pink"&gt;Review of Robert Oster Signature Hot Pink on Moutain of Ink&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</content><category term="Pens &amp; Ink"></category></entry><entry><title>Python and the Magical OAuth Token</title><link href="https://marmaladeofcourse.com/2022/04/25/python-and-the-magical-oauth-token/" rel="alternate"></link><published>2022-04-25T00:00:00+01:00</published><updated>2022-04-25T00:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2022-04-25:/2022/04/25/python-and-the-magical-oauth-token/</id><summary type="html">&lt;p&gt;A &lt;a href="https://colinsent.me/The-Magical-Self-Rotating-Self-Getting-OAuth-Token-aa8e8a840e9643fd9f4ca8f9e97e53e9"&gt;recent post by Colin&lt;/a&gt; demonstrated a utility class they&amp;rsquo;d written to handle fetching and refreshing OAuth bearer tokens for APIs and services that are secured that way, to stop you having to worry about it in the majority of your code. For years, I&amp;rsquo;ve used a similar …&lt;/p&gt;</summary><content type="html">&lt;p&gt;A &lt;a href="https://colinsent.me/The-Magical-Self-Rotating-Self-Getting-OAuth-Token-aa8e8a840e9643fd9f4ca8f9e97e53e9"&gt;recent post by Colin&lt;/a&gt; demonstrated a utility class they&amp;rsquo;d written to handle fetching and refreshing OAuth bearer tokens for APIs and services that are secured that way, to stop you having to worry about it in the majority of your code. For years, I&amp;rsquo;ve used a similar class at work that I wrote to simplify this, and Colin&amp;rsquo;s post prompted me to share it here too.&lt;/p&gt;
&lt;p&gt;The main difference from Colin&amp;rsquo;s solution is that the class I use is a subclass of the &lt;a href="https://docs.python-requests.org/en/latest/"&gt;Requests&lt;/a&gt; &lt;code&gt;Session&lt;/code&gt; object, rather than a stand-alone object. It handles not only fetching and refreshing the bearer token where necessary, but also inserting the correct headers to the requests you make. Here&amp;rsquo;s the entire class (and a couple of helper exception classes):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;datetime&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OAuthResponseError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ne"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OAuthInvalidGrant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OAuthResponseError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OAuthInvalidScope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OAuthResponseError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TokenRefreshSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="n"&gt;ERROR_MAPPING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;invalid_grant&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OAuthInvalidGrant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;invalid_scope&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OAuthInvalidScope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client_secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oauth_client_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client_id&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oauth_client_secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client_secret&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oauth_scope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oauth_token_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;token_url&lt;/span&gt;
        &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;prepare_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;#39;&amp;#39;&amp;#39;Add the access token to the headers&amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Authorization&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oauth_access_token&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prepare_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@property&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;oauth_access_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;#39;&amp;#39;&amp;#39;Return a valid Access Token. Renews the token if it has expired&amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oauth_access_token_valid&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;

            &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="s2"&gt;&amp;quot;client_id&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oauth_client_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;&amp;quot;client_secret&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oauth_client_secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;&amp;quot;grant_type&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;client_credentials&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;&amp;quot;scope&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oauth_scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="s2"&gt;&amp;quot;Accept&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;application/json&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;&amp;quot;Content-Type&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;application/x-www-form-urlencoded&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oauth_token_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;response_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;error&amp;quot;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ERROR_MAPPING&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;response_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;error&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="n"&gt;OAuthResponseError&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;response_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;error_description&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_oauth_access_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;access_token&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_oauth_access_token_expiry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;response_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;expires_in&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_oauth_access_token&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;oauth_access_token_valid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;#39;&amp;#39;&amp;#39;Check the validity of the current access token&amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;

        &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;fudged_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;access_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;_oauth_access_token&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;access_token_expiry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;_oauth_access_token_expiry&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;access_token&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;access_token_expiry&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;fudged_time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The code is relatively simple. As a subclass of &lt;code&gt;requests.Session&lt;/code&gt;, it accepts all the usual arguments, but also requires the OAuth client ID, client secret, scope, and token endpoint. This could be hardcoded, or a default provided in the class definition as we do internally, to avoid having to specify it each time.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;prepare_request&lt;/code&gt; method is part of the &lt;code&gt;requests.Session&lt;/code&gt; mechanism for generating the HTTP call when the library is used. The overriden method in the &lt;code&gt;TokenRefreshSession&lt;/code&gt; object inserts the correct &lt;code&gt;Authorization: Bearer&lt;/code&gt; header and token into the request on behalf of the user.&lt;/p&gt;
&lt;p&gt;As with Colin&amp;rsquo;s solution, the &lt;code&gt;oauth_access_token&lt;/code&gt; property is computed when accessed, and performs the necessary update or refresh should the currently stored token have expired, by posting the client credentials data to the token endpoint provided.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s an example of the class in action:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TokenRefreshSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;123456&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;client_secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;shh&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;read&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;write&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;etc&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;token_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;https://api.my-great-app.com/v1.0/auth&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;https://api.my-great-app.com/v1.0/users/23&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The first API call will fetch the token and store it, and subsequent calls (using the same session object) will either use the stored token or fetch a new one as necessary. You never need worry about including the &lt;code&gt;Bearer&lt;/code&gt; header yourself. And, to quote Colin&amp;rsquo;s post:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Your token will magically manage itself, leaving you free to write good code against your favorite API.&lt;/p&gt;
&lt;/blockquote&gt;</content><category term="Development"></category><category term="Python"></category></entry><entry><title>LEGO 71374 Nintendo Entertainment System</title><link href="https://marmaladeofcourse.com/2020/08/17/lego-71374-nintendo-entertainment-system/" rel="alternate"></link><published>2020-08-17T00:00:00+01:00</published><updated>2020-08-17T00:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2020-08-17:/2020/08/17/lego-71374-nintendo-entertainment-system/</id><summary type="html">&lt;p&gt;Beyond my budget (for now!), but the new NES LEGO set looks fantastic:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://brickset.com/sets/71374-1/Nintendo-Entertainment-System"&gt;71374&lt;/a&gt; Nintendo Entertainment System achieves extraordinary realism, potentially surpassing any previous LEGO model in that regard. The external detail is absolutely spectacular and the designers have made splendid use of existing elements to recreate distinctive shapes from …&lt;/p&gt;&lt;/blockquote&gt;</summary><content type="html">&lt;p&gt;Beyond my budget (for now!), but the new NES LEGO set looks fantastic:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://brickset.com/sets/71374-1/Nintendo-Entertainment-System"&gt;71374&lt;/a&gt; Nintendo Entertainment System achieves extraordinary realism, potentially surpassing any previous LEGO model in that regard. The external detail is absolutely spectacular and the designers have made splendid use of existing elements to recreate distinctive shapes from the NES. I am particularly delighted with the controller and the casing around the connection ports on the console, both of which could be mistaken for their real equivalents.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Hopefully it will be available in the LEGO employee store when I visit Billund for the 2021 Inside Tour next year!&lt;/p&gt;</content><category term="LEGO"></category></entry><entry><title>Junos commit history: who did what?</title><link href="https://marmaladeofcourse.com/2019/11/16/junos-commit-history-who-did-what/" rel="alternate"></link><published>2019-11-16T00:00:00+00:00</published><updated>2019-11-16T00:00:00+00:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2019-11-16:/2019/11/16/junos-commit-history-who-did-what/</id><summary type="html">&lt;p&gt;Junos has always had one fantastic advantage over Cisco&amp;rsquo;s IOS: the ability to stage a pending commit, and see a history of changes, rather than each command immediately being executed on the running config a soon as you hit Enter. &lt;/p&gt;
&lt;p&gt;The &lt;code&gt;show | compare rollback X&lt;/code&gt; command has always been …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Junos has always had one fantastic advantage over Cisco&amp;rsquo;s IOS: the ability to stage a pending commit, and see a history of changes, rather than each command immediately being executed on the running config a soon as you hit Enter. &lt;/p&gt;
&lt;p&gt;The &lt;code&gt;show | compare rollback X&lt;/code&gt; command has always been able to show you the difference between your current config and what the config was like &lt;em&gt;X&lt;/em&gt; commits ago. However, I&amp;rsquo;ve always found it difficult to figure out what exactly was done in a single commit (or batch of commits) if they weren&amp;rsquo;t the most recent. That is, until I learnt of this command: &lt;code&gt;show system rollback compare X Y&lt;/code&gt;! This lets you see the changes that were introduced just between the &lt;em&gt;X&lt;/em&gt; and &lt;em&gt;Y&lt;/em&gt; commits. &lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a real life example. Below is the commit history for a device at work. You can see there were a number of batches of commits going back in time where different individuals have done sole changes on the device:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ben@switch&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;show&lt;span class="w"&gt; &lt;/span&gt;system&lt;span class="w"&gt; &lt;/span&gt;commit
&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-11-13&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;15&lt;/span&gt;:06:51&lt;span class="w"&gt; &lt;/span&gt;GMT&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;katerina&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli
&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-11-13&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;14&lt;/span&gt;:49:19&lt;span class="w"&gt; &lt;/span&gt;GMT&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;katerina&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli
&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-11-13&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;14&lt;/span&gt;:43:01&lt;span class="w"&gt; &lt;/span&gt;GMT&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;katerina&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli
&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-11-13&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;14&lt;/span&gt;:42:11&lt;span class="w"&gt; &lt;/span&gt;GMT&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;katerina&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli
&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-11-13&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;14&lt;/span&gt;:41:01&lt;span class="w"&gt; &lt;/span&gt;GMT&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;katerina&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli
&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-11-13&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;14&lt;/span&gt;:03:53&lt;span class="w"&gt; &lt;/span&gt;GMT&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;katerina&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli
&lt;span class="m"&gt;6&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-11-13&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;12&lt;/span&gt;:28:33&lt;span class="w"&gt; &lt;/span&gt;GMT&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;katerina&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli
&lt;span class="m"&gt;7&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-11-13&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;12&lt;/span&gt;:25:44&lt;span class="w"&gt; &lt;/span&gt;GMT&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;katerina&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli
&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-11-13&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;12&lt;/span&gt;:24:53&lt;span class="w"&gt; &lt;/span&gt;GMT&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;katerina&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli
&lt;span class="m"&gt;9&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-11-13&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;12&lt;/span&gt;:23:28&lt;span class="w"&gt; &lt;/span&gt;GMT&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;katerina&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli
…&lt;span class="w"&gt; &lt;/span&gt;output&lt;span class="w"&gt; &lt;/span&gt;truncated&lt;span class="w"&gt; &lt;/span&gt;…&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-09-13&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;14&lt;/span&gt;:16:43&lt;span class="w"&gt; &lt;/span&gt;BST&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;ben&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli
&lt;span class="m"&gt;21&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-09-13&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;14&lt;/span&gt;:15:27&lt;span class="w"&gt; &lt;/span&gt;BST&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;ben&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli
&lt;span class="m"&gt;22&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-09-13&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;14&lt;/span&gt;:13:48&lt;span class="w"&gt; &lt;/span&gt;BST&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;ben&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli
&lt;span class="m"&gt;23&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-09-13&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;14&lt;/span&gt;:13:06&lt;span class="w"&gt; &lt;/span&gt;BST&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;ben&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli
&lt;span class="m"&gt;24&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-09-13&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;14&lt;/span&gt;:08:43&lt;span class="w"&gt; &lt;/span&gt;BST&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;ben&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli
&lt;span class="m"&gt;25&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-09-13&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;14&lt;/span&gt;:08:13&lt;span class="w"&gt; &lt;/span&gt;BST&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;ben&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli
&lt;span class="m"&gt;26&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-09-13&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;14&lt;/span&gt;:06:50&lt;span class="w"&gt; &lt;/span&gt;BST&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;ben&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli
&lt;span class="m"&gt;27&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-09-13&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;14&lt;/span&gt;:06:12&lt;span class="w"&gt; &lt;/span&gt;BST&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;ben&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli
&lt;span class="m"&gt;28&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-08-07&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;14&lt;/span&gt;:21:33&lt;span class="w"&gt; &lt;/span&gt;BST&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;stuart&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli
&lt;span class="m"&gt;29&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-08-07&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;14&lt;/span&gt;:21:08&lt;span class="w"&gt; &lt;/span&gt;BST&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;stuart&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli
&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-08-07&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;14&lt;/span&gt;:18:43&lt;span class="w"&gt; &lt;/span&gt;BST&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;stuart&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli
&lt;span class="m"&gt;31&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-08-05&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;09&lt;/span&gt;:06:28&lt;span class="w"&gt; &lt;/span&gt;BST&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;stuart&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli
&lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-06-19&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;13&lt;/span&gt;:04:54&lt;span class="w"&gt; &lt;/span&gt;BST&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;lewis&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli
&lt;span class="m"&gt;33&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-06-19&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;13&lt;/span&gt;:02:34&lt;span class="w"&gt; &lt;/span&gt;BST&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;lewis&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli
&lt;span class="m"&gt;34&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-06-19&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;13&lt;/span&gt;:01:31&lt;span class="w"&gt; &lt;/span&gt;BST&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;lewis&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli&lt;span class="w"&gt; &lt;/span&gt;commit&lt;span class="w"&gt; &lt;/span&gt;confirmed,&lt;span class="w"&gt; &lt;/span&gt;rollback&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;3mins
&lt;span class="m"&gt;35&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-06-19&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;12&lt;/span&gt;:59:30&lt;span class="w"&gt; &lt;/span&gt;BST&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;lewis&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli
&lt;span class="m"&gt;36&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-06-19&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;12&lt;/span&gt;:58:39&lt;span class="w"&gt; &lt;/span&gt;BST&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;lewis&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli
&lt;span class="m"&gt;37&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-06-19&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;12&lt;/span&gt;:58:03&lt;span class="w"&gt; &lt;/span&gt;BST&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;lewis&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli&lt;span class="w"&gt; &lt;/span&gt;commit&lt;span class="w"&gt; &lt;/span&gt;confirmed,&lt;span class="w"&gt; &lt;/span&gt;rollback&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;3mins
&lt;span class="m"&gt;38&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-06-07&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;13&lt;/span&gt;:58:39&lt;span class="w"&gt; &lt;/span&gt;BST&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;bart&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli
&lt;span class="m"&gt;39&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-06-07&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;13&lt;/span&gt;:07:17&lt;span class="w"&gt; &lt;/span&gt;BST&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;bart&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli&lt;span class="w"&gt; &lt;/span&gt;commit&lt;span class="w"&gt; &lt;/span&gt;confirmed,&lt;span class="w"&gt; &lt;/span&gt;rollback&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;1mins
&lt;span class="m"&gt;40&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-06-06&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;12&lt;/span&gt;:27:51&lt;span class="w"&gt; &lt;/span&gt;BST&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;bart&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli
&lt;span class="m"&gt;41&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-05-15&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;13&lt;/span&gt;:07:07&lt;span class="w"&gt; &lt;/span&gt;BST&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;chris&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli
&lt;span class="m"&gt;42&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;2019&lt;/span&gt;-05-09&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;14&lt;/span&gt;:14:43&lt;span class="w"&gt; &lt;/span&gt;BST&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;chris&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;cli
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I needed to find what Stuart and Lewis had each introduced in their batches of changes back in August and June, respectively. This new command makes that trivially easy. &lt;/p&gt;
&lt;p&gt;Stuart&amp;rsquo;s batches of changes were in commits 28–31. Running the following command, I was able to easily extract just what those commits introduced:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ben@switch&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;show&lt;span class="w"&gt; &lt;/span&gt;system&lt;span class="w"&gt; &lt;/span&gt;rollback&lt;span class="w"&gt; &lt;/span&gt;compare&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;28&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;edit&lt;span class="w"&gt; &lt;/span&gt;interfaces&lt;span class="w"&gt; &lt;/span&gt;interface-range&lt;span class="w"&gt; &lt;/span&gt;ir_stp_edge&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;     &lt;/span&gt;member&lt;span class="w"&gt; &lt;/span&gt;ge-0/0/10&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;...&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
+&lt;span class="w"&gt;    &lt;/span&gt;member&lt;span class="w"&gt; &lt;/span&gt;ge-0/0/6&lt;span class="p"&gt;;&lt;/span&gt;
+&lt;span class="w"&gt;    &lt;/span&gt;member&lt;span class="w"&gt; &lt;/span&gt;ge-0/0/7&lt;span class="p"&gt;;&lt;/span&gt;
+&lt;span class="w"&gt;    &lt;/span&gt;member&lt;span class="w"&gt; &lt;/span&gt;ge-0/0/8&lt;span class="p"&gt;;&lt;/span&gt;
…&lt;span class="w"&gt; &lt;/span&gt;more&lt;span class="w"&gt; &lt;/span&gt;changes&lt;span class="w"&gt; &lt;/span&gt;…
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Note that you need to put the higher (older) commit number first, followed by the lower (more recent). Additionally, you need to increase the higher commits number by one, as you want to know what was introduced between those commits, &lt;em&gt;including the oldest one&lt;/em&gt;. &lt;/p&gt;
&lt;p&gt;Likewise, to see what Lewis introduced in his commits, we can run the following:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ben@switch&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;show&lt;span class="w"&gt; &lt;/span&gt;system&lt;span class="w"&gt; &lt;/span&gt;rollback&lt;span class="w"&gt; &lt;/span&gt;compare&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;38&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;
…&lt;span class="w"&gt; &lt;/span&gt;some&lt;span class="w"&gt; &lt;/span&gt;changes&lt;span class="w"&gt; &lt;/span&gt;...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This will definitely save me some time in future!&lt;/p&gt;</content><category term="Networking"></category><category term="Junos"></category></entry><entry><title>Tiny Git Tip</title><link href="https://marmaladeofcourse.com/2019/10/10/tiny-git-tip/" rel="alternate"></link><published>2019-10-10T00:00:00+01:00</published><updated>2019-10-10T00:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2019-10-10:/2019/10/10/tiny-git-tip/</id><summary type="html">&lt;p&gt;&lt;a href="https://twitter.com/brandur"&gt;Brandur&lt;/a&gt;, on Twitter:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Git tip I wish I&amp;rsquo;d discovered ten years ago: if you &lt;code&gt;git config --global diff.noprefix true&lt;/code&gt; it removes the silly &lt;code&gt;a/&lt;/code&gt; and &lt;code&gt;b/&lt;/code&gt; prefixes so that when you double-click select one to copy, you get a usable filename instead of a mangled path.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;a href="https://twitter.com/brandur"&gt;Brandur&lt;/a&gt;, on Twitter:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Git tip I wish I&amp;rsquo;d discovered ten years ago: if you &lt;code&gt;git config --global diff.noprefix true&lt;/code&gt; it removes the silly &lt;code&gt;a/&lt;/code&gt; and &lt;code&gt;b/&lt;/code&gt; prefixes so that when you double-click select one to copy, you get a usable filename instead of a mangled path.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is something I&amp;rsquo;ve always been annoyed by—I&amp;rsquo;m so glad there&amp;rsquo;s a fix!&lt;/p&gt;</content><category term="Development"></category><category term="Tip"></category><category term="Git"></category></entry><entry><title>NASA Administrator Remembers Mission Control Pioneer Chris Kraft</title><link href="https://marmaladeofcourse.com/2019/07/23/nasa-administrator-remembers-mission-control-pioneer-chris-kraft/" rel="alternate"></link><published>2019-07-23T00:00:00+01:00</published><updated>2019-07-23T00:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2019-07-23:/2019/07/23/nasa-administrator-remembers-mission-control-pioneer-chris-kraft/</id><summary type="html">&lt;p&gt;Chris Kraft, NASA&amp;rsquo;s first Flight Director, died yesterday, two days after the 50th anniversary of the first man on the moon. &lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Once comparing his complex work as a flight director to a conductor’s, Kraft said, ‘The conductor can&amp;rsquo;t play all the instruments&amp;ndash;he may not even be …&lt;/p&gt;&lt;/blockquote&gt;</summary><content type="html">&lt;p&gt;Chris Kraft, NASA&amp;rsquo;s first Flight Director, died yesterday, two days after the 50th anniversary of the first man on the moon. &lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Once comparing his complex work as a flight director to a conductor’s, Kraft said, ‘The conductor can&amp;rsquo;t play all the instruments&amp;ndash;he may not even be able to play any one of them. But, he knows when the first violin should be playing, and he knows when the trumpets should be loud or soft, and when the drummer should be drumming. He mixes all this up and out comes music. That&amp;rsquo;s what we do here.&amp;rsquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It&amp;rsquo;s almost as if he waited for the anniversary. The world remembers the names of the men who set foot on the moon, but Chris Kraft was another hugely important person managing the hundreds of highly skilled engineers and scientists without whom the moon landing would not have been possible. &lt;/p&gt;
&lt;p&gt;We&amp;rsquo;re fast approaching a time where there will be nobody left who was involved in project Apollo. It will never be forgotten, but it will be sad to no longer be remembered first hand. &lt;/p&gt;</content><category term="Space"></category></entry><entry><title>Self-Host Your Static Assets</title><link href="https://marmaladeofcourse.com/2019/07/11/self-host-your-static-assets/" rel="alternate"></link><published>2019-07-11T00:00:00+01:00</published><updated>2019-07-11T00:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2019-07-11:/2019/07/11/self-host-your-static-assets/</id><summary type="html">&lt;p&gt;Harry Roberts, writing at &lt;em&gt;CSS Wizardry&lt;/em&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;One of the quickest wins—and one of the first things I recommend my clients do—to make websites faster can at first seem counter-intuitive: you should self-host all of your static assets, forgoing others’ CDNs/infrastructure. In this short and hopefully very straightforward …&lt;/p&gt;&lt;/blockquote&gt;</summary><content type="html">&lt;p&gt;Harry Roberts, writing at &lt;em&gt;CSS Wizardry&lt;/em&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;One of the quickest wins—and one of the first things I recommend my clients do—to make websites faster can at first seem counter-intuitive: you should self-host all of your static assets, forgoing others’ CDNs/infrastructure. In this short and hopefully very straightforward post, I want to outline the disadvantages of hosting your static assets ‘off-site’, and the overwhelming benefits of hosting them on your own origin.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I&amp;rsquo;m a little late to this (the post was written back in May), but it&amp;rsquo;s an interesting counter argument to the common practice of serving third party resources from a provider&amp;rsquo;s CDN. The post goes into a lot more detail, but if you can, host it yourself. &lt;/p&gt;</content><category term="Development"></category><category term="Web"></category></entry><entry><title>The Two-Napkin Protocol</title><link href="https://marmaladeofcourse.com/2019/07/07/the-two-napkin-protocol/" rel="alternate"></link><published>2019-07-07T00:00:00+01:00</published><updated>2019-07-07T00:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2019-07-07:/2019/07/07/the-two-napkin-protocol/</id><summary type="html">&lt;p&gt;An interesting piece of history I didn&amp;rsquo;t previously know.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It was 1989. Kirk Lougheed of Cisco and Yakov Rekhter of IBM were having lunch in a meeting hall cafeteria at an Internet Engineering Task Force (IETF) conference.&lt;/p&gt;
&lt;p&gt;They wrote a new routing protocol that became RFC (Request for Comment …&lt;/p&gt;&lt;/blockquote&gt;</summary><content type="html">&lt;p&gt;An interesting piece of history I didn&amp;rsquo;t previously know.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It was 1989. Kirk Lougheed of Cisco and Yakov Rekhter of IBM were having lunch in a meeting hall cafeteria at an Internet Engineering Task Force (IETF) conference.&lt;/p&gt;
&lt;p&gt;They wrote a new routing protocol that became RFC (Request for Comment) 1105, the &lt;a href="//tools.ietf.org/html/rfc1105"&gt;Border Gateway Protocol (BGP)&lt;/a&gt;, known to many as the “Two Napkin Protocol” — in reference to the napkins they used to capture their thoughts.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The post is worth a read just to see the photos of the napkins. I&amp;rsquo;ve never really thought before about how RFCs come to be. I&amp;rsquo;d always assumed they were the result of clever people in offices, not really thought up on the back of a napkin over drinks!&lt;/p&gt;
&lt;p&gt;Also, as it&amp;rsquo;s 2019&amp;hellip; happy 30th birthday, BGP (and the World Wide Web). &lt;/p&gt;</content><category term="Networking"></category><category term="BGP"></category></entry><entry><title>The Infrastructure Mess Causing Countless Internet Outages</title><link href="https://marmaladeofcourse.com/2019/06/29/the-infrastructure-mess-causing-countless-internet-outages/" rel="alternate"></link><published>2019-06-29T00:00:00+01:00</published><updated>2019-06-29T00:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2019-06-29:/2019/06/29/the-infrastructure-mess-causing-countless-internet-outages/</id><summary type="html">&lt;p&gt;Roland Dobbins from Netscout Arbor, quoted in this &lt;em&gt;Wired&lt;/em&gt; article:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;Nonspecialists kind of view the internet as this high-tech, gleaming thing like the bridge of the starship Enterprise. It’s not like that at all. It’s more like an 18th-century Royal Navy frigate. There’s a lot of running …&lt;/p&gt;&lt;/blockquote&gt;</summary><content type="html">&lt;p&gt;Roland Dobbins from Netscout Arbor, quoted in this &lt;em&gt;Wired&lt;/em&gt; article:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;Nonspecialists kind of view the internet as this high-tech, gleaming thing like the bridge of the starship Enterprise. It’s not like that at all. It’s more like an 18th-century Royal Navy frigate. There’s a lot of running around and screaming and shouting and pulling on ropes to try to get things going in the right direction.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It&amp;rsquo;s amazing how fragile some of the technologies that power something the world takes for granted are; BGP is a great example of that. The Internet is everywhere, and it&amp;rsquo;s becoming increasingly more necessary to be connected in order to be able to just go about our lives.&lt;/p&gt;
&lt;p&gt;Regarding the choice of headline, however, I don&amp;rsquo;t think the word &amp;ldquo;mess&amp;rdquo; is fair. That does a disservice to the hundreds of very talented people that design, implement, and maintain the infrastructre that underpins our connected world. &lt;/p&gt;</content><category term="Networking"></category><category term="BGP"></category></entry><entry><title>Junos: Confirm a commit cleanly"</title><link href="https://marmaladeofcourse.com/2018/07/27/junos-confirm-a-commit-cleanly/" rel="alternate"></link><published>2018-07-27T00:00:00+01:00</published><updated>2018-07-27T00:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2018-07-27:/2018/07/27/junos-confirm-a-commit-cleanly/</id><summary type="html">&lt;p&gt;For years, I have loved the fact that Junos allows you to perform a &lt;code&gt;commit confirmed&lt;/code&gt; to apply the configuration with an automatic rollback in a certain number of minutes.&lt;/p&gt;
&lt;p&gt;I have always believed that the only way to confirm the commit (i.e. stop the automatic rollback) was to …&lt;/p&gt;</summary><content type="html">&lt;p&gt;For years, I have loved the fact that Junos allows you to perform a &lt;code&gt;commit confirmed&lt;/code&gt; to apply the configuration with an automatic rollback in a certain number of minutes.&lt;/p&gt;
&lt;p&gt;I have always believed that the only way to confirm the commit (i.e. stop the automatic rollback) was to &lt;code&gt;commit&lt;/code&gt; again. This creates two commits in the commit history, one containing the actual config diff, and an empty one purely used to stop the rollback. I&amp;rsquo;ve always thought that this creates a somewhat messy commit history, and confuses the use of &lt;code&gt;show | compare rollback&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;{% highlight bash %}
[edit]
ben@device&amp;gt; run show system commit
0   2018-07-27 08:44:26 BST by ben via cli
1   2018-07-27 08:44:07 BST by ben via cli commit confirmed, rollback in 5mins
2   2018-07-23 10:04:29 BST by ben via cli
3   2018-07-23 10:03:58 BST by ben via cli commit confirmed, rollback in 2mins&lt;/p&gt;
&lt;p&gt;[edit]
ben@device&amp;gt; show | compare rollback 1&lt;/p&gt;
&lt;p&gt;[edit]
ben@device&amp;gt; # Huh, it&amp;rsquo;s empty?! I&amp;rsquo;m sure I did some work&amp;hellip;&lt;/p&gt;
&lt;p&gt;[edit]
ben@device&amp;gt; show | compare rollback 2
[edit system]
-   host-name old-device
+   host-name device&lt;/p&gt;
&lt;p _="%" endhighlight="endhighlight"&gt;[edit]
ben@device&amp;gt; # Oh, there it is&amp;hellip;&lt;/p&gt;
&lt;p&gt;However, today I learnt that a &lt;code&gt;commit check&lt;/code&gt; is enough to stop the rollback, and doesn&amp;rsquo;t create an empty commit! My commit histories are now much cleaner, and &lt;code&gt;show | compare rollback&lt;/code&gt; commands a lot easier to work out what you&amp;rsquo;re actually looking at.&lt;/p&gt;
&lt;p&gt;{% highlight bash %}
[edit]
ben@device&amp;gt; run show system commit
1   2018-07-27 08:44:07 BST by ben via cli commit confirmed, rollback in 5mins
3   2018-07-23 10:03:58 BST by ben via cli commit confirmed, rollback in 2mins&lt;/p&gt;
&lt;p _="%" endhighlight="endhighlight"&gt;[edit]
ben@device&amp;gt; show | compare rollback 1
[edit system]
-   host-name old-device
+   host-name device&lt;/p&gt;
&lt;p&gt;Much better!&lt;/p&gt;</content><category term="Networking"></category><category term="Junos"></category></entry><entry><title>Lessons Learnt from an Outage</title><link href="https://marmaladeofcourse.com/2018/07/22/lessons-learnt-from-an-outage/" rel="alternate"></link><published>2018-07-22T00:00:00+01:00</published><updated>2018-07-22T00:00:00+01:00</updated><author><name>Ben Cardy</name></author><id>tag:marmaladeofcourse.com,2018-07-22:/2018/07/22/lessons-learnt-from-an-outage/</id><summary type="html">&lt;p&gt;I caused an outage last week. &lt;/p&gt;
&lt;p&gt;Not intentionally, and not a large outage by any stretch of the imagination. But it had various knock-on consequences that led to a lengthy application recovery time, long after full network connectivity was restored.&lt;/p&gt;
&lt;p&gt;We were replacing one of the two core MPLS routers …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I caused an outage last week. &lt;/p&gt;
&lt;p&gt;Not intentionally, and not a large outage by any stretch of the imagination. But it had various knock-on consequences that led to a lengthy application recovery time, long after full network connectivity was restored.&lt;/p&gt;
&lt;p&gt;We were replacing one of the two core MPLS routers at one of the three primary sites in our network. Upgrading the hardware to a newer model required costing the existing node out, powering it off, physically replacing it with the new hardware, and bringing the node back online.&lt;/p&gt;
&lt;p&gt;(For those of you interested in the details, we were upgrading a Juniper MX480 to an MX10003 - all logical connectivity was staying the same, but some links were being upgraded from 10Gbps to either 40 or 100).&lt;/p&gt;
&lt;p&gt;In preparation for the upgrade, I had taken the running config from the existing node, adapted what was necessary for the newer hardware (mainly interface name changes, etc) and preloaded it onto the new node waiting in the lab. So far so good.&lt;/p&gt;
&lt;p&gt;Unfortunately, the running config at the time I took it from the live node, was exactly that - the &lt;em&gt;live&lt;/em&gt; node. At that moment, it wasn&amp;rsquo;t in its costed-out state, meaning that the minute we started repatching links into the new hardware, they were coming back live.&lt;/p&gt;
&lt;p&gt;These core node provide connectivity between our three primary sites, and their three data centres. As the links came up live, the new node started drawing traffic from the local data centre fabric before it had any connectivity to the other sites (or even the other node in the same site). This meant that for a period of around four minutes, between one sixth and one half of all egress traffic from the local data centre was being blackholed, and dropped.&lt;/p&gt;
&lt;p&gt;This caused connectivity issues between our ceph clusters, which then struggled to make sense of the situation, and filled up the only remaining link between this site and the others with traffic. (I don&amp;rsquo;t know the full details of why it did this, or what that traffic was - I&amp;rsquo;m a network engineer, and not responsible for the rest of the infrastructure. But remember I said we were upgrading from 10 to 100Gbps on some links? This is partly why). This then exacerbated the connectivity problems (for other applications in addition to ceph), pinning the links at line rate long after the we had costed out the new node as originally intended, leading to the long recovery time for the application layer.&lt;/p&gt;
&lt;p&gt;So what did I learn from this outage? That&amp;rsquo;s the most important question when something like this happens - particularly if it was your fault. This is what I learnt.&lt;/p&gt;
&lt;h3 id="dont-become-complacent"&gt;Don&amp;rsquo;t become complacent&lt;a class="headerlink" href="#dont-become-complacent" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;We had done this exact procedure for the other node in this site just days before, and all had gone smoothly (I had remembered to cost out the new node as I was preparing its config, instead of running with the live config from the existing node). Even if you&amp;rsquo;ve done an identical change before, &lt;em&gt;always&lt;/em&gt; triple check what you&amp;rsquo;re doing if it has the potential to cause an impact on other parts of the business. Ideally, ask a colleague if they can spot anything you may have forgotten.&lt;/p&gt;
&lt;h3 id="unconditional-summarisation-isnt-always-a-good-thing"&gt;Unconditional summarisation isn&amp;rsquo;t always a good thing&lt;a class="headerlink" href="#unconditional-summarisation-isnt-always-a-good-thing" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Even though the links weren&amp;rsquo;t costed out, if all the core did was pass on routes learnt from the other sites, then the new node wouldn&amp;rsquo;t have started drawing traffic until it was able to route it.§&lt;/p&gt;
&lt;p&gt;Instead, our core currently advertises three aggregate routes to its clients - the three private &lt;a href="https://tools.ietf.org/html/rfc1918"&gt;RFC1918&lt;/a&gt; ranges. These aggregates are active if just a single contributing route is present in the routing table. In our case, the core node&amp;rsquo;s loopback is a perfectly valid contributing route to the 10.0.0.0/8 aggregate, causing it to be advertised even if every other link on the box was down, drawing (and dropping) traffic.&lt;/p&gt;
&lt;p&gt;Under normal circumstances, we&amp;rsquo;d obviously expect the core nodes to have full reachability to the rest of the core, and this wouldn&amp;rsquo;t be an issue. However, certain failure scenarios can cause a node to become isolated (not just configuration mistakes like this one!) and blackhole traffic. Advertising more specific routes actually learnt from other peers, or some conditions imposed on the generation of the aggregate routes, would help limit this.&lt;/p&gt;
&lt;h3 id="be-selective-about-the-order-links-are-repatched"&gt;Be selective about the order links are repatched&lt;a class="headerlink" href="#be-selective-about-the-order-links-are-repatched" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Core-facing links first! The outage was caused by the fact that the links to the data centre fabric were connected before the core-facing links. If they had been done the other way around, and core connectivity as part of the MPLS domain was confirmed before connecting any client-facing links, the issue would have been avoided.&lt;/p&gt;
&lt;p&gt;Core-facing links are more important than client-facing - most clients will have redundancy via other nodes, and even if they don&amp;rsquo;t, until your core node has reachability to the rest of the core, it&amp;rsquo;s useless to its clients anyway.&lt;/p&gt;
&lt;h3 id="dont-neglect-cos"&gt;Don&amp;rsquo;t neglect CoS&lt;a class="headerlink" href="#dont-neglect-cos" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;While not directly related to the outage itself, ceph filling the links with non-essential traffic (compared to, say, production web or database traffic) led to outages on other applications that could no longer communicate. Some quality of service markings and traffic shaping or policing would have a gone a long way to mitigating the impact, by restricting non-business-critical traffic to a subset of the link. Less important (but still useful) on the upgraded 100Gbps connections; but our single 10Gbps like couldn&amp;rsquo;t cope.&lt;/p&gt;</content><category term="Networking"></category></entry></feed>