<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Cameron Little's Website</title><link>https://camlittle.com/</link><description>Recent on Cameron Little's Website</description><image><title>Cameron Little's Website</title><url>https://camlittle.com/profile.jpg?s=144</url><link>https://camlittle.com/</link><width>144</width></image><language>en-us</language><managingEditor>cameron@camlittle.com (Cameron Little)</managingEditor><webMaster>cameron@camlittle.com (Cameron Little)</webMaster><lastBuildDate>Wed, 03 Sep 2025 12:30:00 +0000</lastBuildDate><atom:link href="https://camlittle.com/index.xml" rel="self" type="application/rss+xml"/><item><title>Robber Fly</title><link>https://camlittle.com/photos/2025-09-03-robber-fly/</link><pubDate>Wed, 03 Sep 2025 12:30:00 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2025-09-03-robber-fly/</guid><enclosure url="https://camlittle.com/photos/2025-09-03-robber-fly/2025-09-03-robber-fly_128.jpg" length="2879" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2025-09-03-robber-fly_1280.jpg" />

  


  <p>Middle Fork Gila River, Gila Wilderness</p>
<p><span title="Camera">📷</span> Sony α 7C II</p>

  ]]></content:encoded></item><item><title>Tussock Moth Caterpillar</title><link>https://camlittle.com/photos/2025-09-03-tussock-moth-caterpillar/</link><pubDate>Wed, 03 Sep 2025 12:20:00 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2025-09-03-tussock-moth-caterpillar/</guid><enclosure url="https://camlittle.com/photos/2025-09-03-tussock-moth-caterpillar/2025-09-03-tussock-moth-caterpillar_128.jpg" length="4077" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2025-09-03-tussock-moth-caterpillar_1280.jpg" />

  


  <p>Middle Fork Gila River, Gila Wilderness</p>
<p><span title="Camera">📷</span> Sony α 7C II</p>

  ]]></content:encoded></item><item><title>Gila Flowers</title><link>https://camlittle.com/photos/2025-09-03-gila-flowers/</link><pubDate>Wed, 03 Sep 2025 12:10:00 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2025-09-03-gila-flowers/</guid><enclosure url="https://camlittle.com/photos/2025-09-03-gila-flowers/2025-09-03-gila-flowers_128.jpg" length="5197" type="image/jpeg"/><description>&lt;p>Top-to-right, then top-to-bottom:&lt;/p>
&lt;ol>
&lt;li>Mexican Hat&lt;/li>
&lt;li>Red Morning Glory&lt;/li>
&lt;li>Dogweed&lt;/li>
&lt;li>Wallflower&lt;/li>
&lt;li>Sweet Four O&amp;rsquo;Clock&lt;/li>
&lt;li>Prickly Poppy&lt;/li>
&lt;li>Eyed Gilia&lt;/li>
&lt;li>Crag Lily&lt;/li>
&lt;li>New Mexico Hop&lt;/li>
&lt;li>Heartleaf Morning Glory&lt;/li>
&lt;li>Purple Geranium&lt;/li>
&lt;li>Red Cinquefoil&lt;/li>
&lt;/ol></description><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2025-09-03-gila-flowers_1280.jpg" />

  
<p>Top-to-right, then top-to-bottom:</p>
<ol>
<li>Mexican Hat</li>
<li>Red Morning Glory</li>
<li>Dogweed</li>
<li>Wallflower</li>
<li>Sweet Four O&rsquo;Clock</li>
<li>Prickly Poppy</li>
<li>Eyed Gilia</li>
<li>Crag Lily</li>
<li>New Mexico Hop</li>
<li>Heartleaf Morning Glory</li>
<li>Purple Geranium</li>
<li>Red Cinquefoil</li>
</ol>


  <p>Gila Wilderness</p>
<p><span title="Camera">📷</span> Sony α 7C II</p>

  ]]></content:encoded></item><item><title>Dives</title><link>https://camlittle.com/photos/2025-09-03-dives/</link><pubDate>Wed, 03 Sep 2025 12:00:00 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2025-09-03-dives/</guid><enclosure url="https://camlittle.com/photos/2025-09-03-dives/2025-09-03-dives_128.jpg" length="3976" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2025-09-03-dives_1280.jpg" />

  


  <p>Olympic National Park</p>
<p><span title="Camera">📷</span> Sony α 7C II</p>

  ]]></content:encoded></item><item><title>Standoff</title><link>https://camlittle.com/photos/2025-09-03-standoff/</link><pubDate>Wed, 03 Sep 2025 11:50:00 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2025-09-03-standoff/</guid><enclosure url="https://camlittle.com/photos/2025-09-03-standoff/2025-09-03-standoff_128.jpg" length="5117" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2025-09-03-standoff_1280.jpg" />

  


  <p>Olympic National Park</p>
<p><span title="Camera">📷</span> Sony α 7C II</p>

  ]]></content:encoded></item><item><title>MYOG Quilt</title><link>https://camlittle.com/posts/2025-05-08-myog-quilt/</link><pubDate>Thu, 08 May 2025 00:00:00 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/posts/2025-05-08-myog-quilt/</guid><enclosure url="https://camlittle.com/posts/2025-05-08-myog-quilt/thumbnail.jpeg" length="27910" type="image/jpeg"/><description>&lt;p>I recently made my own backpacking quilt. This was the largest &lt;abbr title="make your own gear">MYOG&lt;/abbr> project I&amp;rsquo;ve attempted, and I&amp;rsquo;m satisfied with how it turned out.&lt;/p>
&lt;h2 id="materials">&lt;a class="heading-link" href="#materials">&lt;/a>Materials&lt;/h2>
&lt;p>I&amp;rsquo;ve used Ripstop by the Roll in the past, and I still think they&amp;rsquo;re best general source for MYOG projects (wide selection, good pricing, although shipping isn&amp;rsquo;t great).&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://ripstopbytheroll.com/products/0-66-oz-membrane-10-taffeta-nylon-1">0.66 oz MEMBRANE 10 Taffeta Nylon&lt;/a>&lt;/strong>: One of the lightest options, fully downproof, and a nice feeling&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://ripstopbytheroll.com/products/0-5-oz-ns50-noseeum-mesh">0.5 oz Noseeum Mesh&lt;/a>&lt;/strong>: I cut 3.5&amp;quot; strips from this, as it&amp;rsquo;s lighter than the precut baffle tape&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://ripstopbytheroll.com/products/gutermann-tera-80-thread">Gutermann Tera 80 thread&lt;/a>&lt;/strong>&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://ripstopbytheroll.com/products/mil-spec-nylon-grosgrain-ribbon">Grosgrain ribbon&lt;/a>&lt;/strong>: 3/8&amp;quot; and 3/4&amp;quot; widths&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://ripstopbytheroll.com/products/low-profile-center-push-buckle">Low profile center push buckles&lt;/a>&lt;/strong>&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://ripstopbytheroll.com/products/shock-cord">1/16&amp;quot; Shock Cord&lt;/a>&lt;/strong>&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://ripstopbytheroll.com/products/anchorable-cordlock">Anchorable Cordlock&lt;/a>&lt;/strong> (glow-in-the-dark, so I can find them at night)&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://www.amazon.com/dp/B073T8NQT5?ref_=ppx_hzsearch_conn_dt_b_fed_asin_title_1">900 FP goose down&lt;/a>&lt;/strong> (from Amazon)&lt;/li>
&lt;/ul>
&lt;p>I considered 0.56 oz nylon, but my research concluded the weight savings weren&amp;rsquo;t worth the durability decrease.&lt;/p></description><content:encoded><![CDATA[
  
<p>I recently made my own backpacking quilt. This was the largest <abbr title="make your own gear">MYOG</abbr> project I&rsquo;ve attempted, and I&rsquo;m satisfied with how it turned out.</p>
<h2 id="materials"><a class="heading-link" href="#materials"></a>Materials</h2>
<p>I&rsquo;ve used Ripstop by the Roll in the past, and I still think they&rsquo;re best general source for MYOG projects (wide selection, good pricing, although shipping isn&rsquo;t great).</p>
<ul>
<li><strong><a href="https://ripstopbytheroll.com/products/0-66-oz-membrane-10-taffeta-nylon-1">0.66 oz MEMBRANE 10 Taffeta Nylon</a></strong>: One of the lightest options, fully downproof, and a nice feeling</li>
<li><strong><a href="https://ripstopbytheroll.com/products/0-5-oz-ns50-noseeum-mesh">0.5 oz Noseeum Mesh</a></strong>: I cut 3.5&quot; strips from this, as it&rsquo;s lighter than the precut baffle tape</li>
<li><strong><a href="https://ripstopbytheroll.com/products/gutermann-tera-80-thread">Gutermann Tera 80 thread</a></strong></li>
<li><strong><a href="https://ripstopbytheroll.com/products/mil-spec-nylon-grosgrain-ribbon">Grosgrain ribbon</a></strong>: 3/8&quot; and 3/4&quot; widths</li>
<li><strong><a href="https://ripstopbytheroll.com/products/low-profile-center-push-buckle">Low profile center push buckles</a></strong></li>
<li><strong><a href="https://ripstopbytheroll.com/products/shock-cord">1/16&quot; Shock Cord</a></strong></li>
<li><strong><a href="https://ripstopbytheroll.com/products/anchorable-cordlock">Anchorable Cordlock</a></strong> (glow-in-the-dark, so I can find them at night)</li>
<li><strong><a href="https://www.amazon.com/dp/B073T8NQT5?ref_=ppx_hzsearch_conn_dt_b_fed_asin_title_1">900 FP goose down</a></strong> (from Amazon)</li>
</ul>
<p>I considered 0.56 oz nylon, but my research concluded the weight savings weren&rsquo;t worth the durability decrease.</p>
<p>The goose down is my main risk on this project. It&rsquo;s not treated to handle water and is from an unknown source, but is way cheaper than <a href="https://ripstopbytheroll.com/products/hyperdry-850-fp-dwr-goose-down">RSBR&rsquo;s 850 DWR down</a>. Since I&rsquo;m currently in a drier climate, I&rsquo;m taking a risk on moisture management. Although I think the supplier shorted me a bit on weight (I think the 1 lb includes the bag it&rsquo;s shipped in), I&rsquo;m reasonably happy with it.</p>
<h2 id="patterning"><a class="heading-link" href="#patterning"></a>Patterning</h2>
<p>I mostly did my own patterning. <a href="https://dubberdesigns.com/downloads/down-quilt-pattern-generator/">Dubber Designs has a generator</a>, but I wasn&rsquo;t able to import it properly into Apple Numbers or Google Sheets. It did provide some good guidance on down calculations. <a href="https://cdn.shopify.com/s/files/1/0261/6507/files/How_to_Make_a_Quilt.pdf?v=1727186891">Ripstop by the Roll has a one-pager</a>, but I didn&rsquo;t do a full taper like they do.</p>
<p><a href="https://www.reddit.com/r/myog/comments/w4im76/partial_box_baffle_quilt_membrane_7_noseeum_900fp/">vanCapare</a>, <a href="https://www.reddit.com/r/myog/comments/13x3si1/first_down_backpacking_quilt/">HollowCartographer</a>, and <a href="https://www.reddit.com/r/myog/comments/zdhlip/made_my_own_ultralight_sleeping_bag_quilt/">durianbae</a> were good sources of inspiration on Reddit.</p>
<p><a href="https://docs.google.com/spreadsheets/d/12EN9bKKhYVg3KWGNHM6rn6EtfmkH-3rTnbDq5mud7SA/edit?usp=sharing">My rough down calculations are in Google Sheets.</a></p>
<p>Some of my decisions were:</p>
<ul>
<li>Long enough to fully cover my head. I generally sleep with my head fully covered and didn&rsquo;t want to construct a separate hood.</li>
<li>Partial taper around the legs. I get some weight savings this way, and it accommodates my side-sleeping.</li>
<li>Four sleeping pad straps spaced along the body.</li>
<li>Fully closed footbox. This means I don&rsquo;t have a cinch at the foot, and prevents slipping off the pad.</li>
</ul>
<p>My final measurements:</p>
<ul>
<li>50&quot; width</li>
<li>78&quot; height</li>
<li>34&quot; long taper, down to 35&quot; wide at the toes</li>
<li>Baffles are 3&quot; thick and approximately 5&quot; wide (10 vertical baffles, 6 horizontal baffles, and 2 in the footbox)</li>
<li>~10% overstuff</li>
</ul>
<p>
<img src="./notes.jpeg" alt="Handwritten notes and design" />

</p>
<p>This required two shapes, cut twice for the top and bottom. The arc in the footbox&rsquo;s length is the same length as the width of the bottom of the main body.</p>
<p>
<img src="./pattern.png" alt="Pattern" />

</p>
<h2 id="baffles"><a class="heading-link" href="#baffles"></a>Baffles</h2>
<p>Baffle design is a deep topic on its own, with <a href="https://www.reddit.com/r/Ultralight/comments/lbvrtv/battle_of_the_baffles_horizontal_vertical_chevron/">a lot of options</a>.</p>
<p>I seriously considered Karo baffles, but because I have experience sewing box baffles (from a pair of down pants I made my wife a few Christmases ago), I was confident enough to attempt them again.</p>
<p>I decided on a split vertical/horizontal design: upper body vertical and lower body horizontal. This seems to be the most popular option for DIY projects. Vertical avoids down clumping on the sides while sleeping, but it does make stitching a more complex (since you&rsquo;re sewing along the long edge in a narrow tube). Horizontal is easier to construct, but down can clump at the sides when sleeping.</p>
<p>I do think chevron or diagonal designs could have benefits, but the complexity and margin for error seem quite a bit higher.</p>
<h2 id="construction"><a class="heading-link" href="#construction"></a>Construction</h2>
<p>Construction of box baffling requires thinking inside out.</p>
<ol>
<li>I started by sewing the box baffles on the wrong side of the bottom side layer of fabric. I tried to curve along the baffle material from my seam allowance to the center at the edges of my fabric, which adds a rounded finished edge.</li>
</ol>
<p>
<img src="./construction-2.jpeg" alt="first six sewn" />

</p>
<ol start="2">
<li>I then sewed the footbox&rsquo;s arc onto the base of the main body of the quilt.</li>
</ol>
<p>
<img src="./construction-3.jpeg" alt="with footbox" />

</p>
<ol start="3">
<li>Then, I sewed along the edge of the entire quilt except the top edge, wrong side to wrong side. This includes my straps, ensuring they&rsquo;re on the inside. I now have an inside-out quilt, with box baffles sewn to one side, with raw edges exposed. When I flip this right side out, the edges are nicely finished, and my straps are exposed.</li>
</ol>
<p>
<img src="./construction-5-straps.jpeg" alt="Straps" />

</p>
<ol start="4">
<li>To fill with down, I used a length of PVC and PEX. I 3D-printed a removable cap for the PVC and a pusher to attach to the PEX. I filled and measured down into the PVC on a scale, slid the open end into my space to fill, removed the cap, and pushed the down into the cavity.</li>
</ol>
<p>
<img src="./construction-4.jpeg" alt="Down setup" />

</p>
<ol start="5">
<li>The next step is the most tedious. Working inside out from the base of the quilt, I sewed the unattached side of each box baffle to the wrong side of the top fabric. When I had 5 or so inches left to sew, I paused (with my needle down) and filled with the correct amount of down. Marking my lines was pretty tricky here, as the quilt was no longer a flat shape.</li>
</ol>
<p>
<img src="./construction-6.jpeg" alt="Sewing the baffles" />

</p>
<ol start="6">
<li>To finish the top edge I sewed a straight stitch, leaving a rough edge exposed. I then attached 3/8&quot; grosgrain loops with my cordlocks attached. I then made a strip of bias tape with a double fold and finished ends, and sewed it on to cover the rough edge. Finally, I fed my shock cord through to finish it off.</li>
</ol>
<p>
<img src="./construction-7.jpeg" alt="Finished top edge" />

</p>
<h2 id="final-product"><a class="heading-link" href="#final-product"></a>Final product</h2>
<p>The final product is around 22 oz and was more than warm enough for under 40℉.</p>
<p>
<img src="./complete-bottom.jpeg" alt="Completed bottom" />

</p>
<p>
<img src="./complete-top.jpeg" alt="Completed top" />

</p>
<h2 id="learnings"><a class="heading-link" href="#learnings"></a>Learnings</h2>
<ul>
<li>The most common error I made was letting the fabric fold under itself while sewing, meaning I was sewing into an additional layer. I needed to pick this out and redo it each time.</li>
<li>It was really difficult to mark up and keep accurate lines when I was partially through sewing. I&rsquo;d prefer to figure out a method to fully mark out sewing lines while the fabric is flat. I need to find something better than chalk for this, as it needs to hold up through the several days of sewing.</li>
<li>I&rsquo;m not sure if the head cinch is necessary, based on how I sleep. Similarly, I don&rsquo;t know if I need the strap closest to my head.</li>
<li>If I were to redo this, I&rsquo;d go with a shorter baffle with more overstuffing, to avoid down movement and save a little weight.</li>
</ul>
<p>
<img src="./in-use.jpeg" alt="In use" />

</p>


  
  ]]></content:encoded></item><item><title>Early Season Skiing</title><link>https://camlittle.com/photos/2024-11-15-nov-skiing/</link><pubDate>Fri, 15 Nov 2024 00:11:11 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2024-11-15-nov-skiing/</guid><enclosure url="https://camlittle.com/photos/2024-11-15-nov-skiing/2024-11-15-nov-skiing_128.jpg" length="4549" type="image/jpeg"/><description>&lt;p>Skiing in early November in New Mexico&lt;/p></description><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2024-11-15-nov-skiing_1280.jpg" />

  
<p>Skiing in early November in New Mexico</p>


  <p>Sipapu, NM</p>
<p><span title="Camera">📷</span> Sony α 7C II</p>

  ]]></content:encoded></item><item><title>Kachina</title><link>https://camlittle.com/photos/2024-11-15-kachina/</link><pubDate>Fri, 15 Nov 2024 00:11:10 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2024-11-15-kachina/</guid><enclosure url="https://camlittle.com/photos/2024-11-15-kachina/2024-11-15-kachina_128.jpg" length="7647" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2024-11-15-kachina_1280.jpg" />

  


  <p>Wheeler Peak Wilderness, NM</p>
<p><span title="Camera">📷</span> Sony α 7C II</p>

  ]]></content:encoded></item><item><title>Gold Hill</title><link>https://camlittle.com/photos/2024-11-15-gold-hill/</link><pubDate>Fri, 15 Nov 2024 00:09:28 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2024-11-15-gold-hill/</guid><enclosure url="https://camlittle.com/photos/2024-11-15-gold-hill/2024-11-15-gold-hill_128.jpg" length="2321" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2024-11-15-gold-hill_1280.jpg" />

  


  <p>Wheeler Peak Wilderness, NM</p>
<p><span title="Camera">📷</span> Sony α 7C II</p>

  ]]></content:encoded></item><item><title>Horny Toad</title><link>https://camlittle.com/photos/2024-11-15-horny-toad/</link><pubDate>Fri, 15 Nov 2024 00:09:13 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2024-11-15-horny-toad/</guid><enclosure url="https://camlittle.com/photos/2024-11-15-horny-toad/2024-11-15-horny-toad_128.jpg" length="5915" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2024-11-15-horny-toad_1280.jpg" />

  


  <p>Cerro Pedernal, NM</p>
<p><span title="Camera">📷</span> Sony α 7C II</p>

  ]]></content:encoded></item><item><title>Pounce!</title><link>https://camlittle.com/photos/2024-11-15-pounce/</link><pubDate>Fri, 15 Nov 2024 00:08:06 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2024-11-15-pounce/</guid><enclosure url="https://camlittle.com/photos/2024-11-15-pounce/2024-11-15-pounce_128.jpg" length="3766" type="image/jpeg"/><description>&lt;p>A successful hunt&lt;/p>
&lt;p>
&lt;picture title="chomp.jpeg">
&lt;source
media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 1200px), (min-resolution: 192dpi) and (min-width: 1200px)"
srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_1200x0at2x.webp"
type="image/webp"
/>
&lt;source
media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 1200px), (min-resolution: 192dpi) and (min-width: 1200px)"
srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_1200x0at2x.jpg"
type="image/jpeg"
/>
&lt;source
media="min-width: 1200px"
srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_1200x0at1x.webp"
type="image/webp"
/>
&lt;source
media="min-width: 1200px"
srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_1200x0at1x.jpg"
type="image/jpeg"
/>
&lt;source
media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 992px), (min-resolution: 192dpi) and (min-width: 992px)"
srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_992x0at2x.webp"
type="image/webp"
/>
&lt;source
media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 992px), (min-resolution: 192dpi) and (min-width: 992px)"
srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_992x0at2x.jpg"
type="image/jpeg"
/>
&lt;source
media="min-width: 992px"
srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_992x0at1x.webp"
type="image/webp"
/>
&lt;source
media="min-width: 992px"
srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_992x0at1x.jpg"
type="image/jpeg"
/>
&lt;source
media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 768px), (min-resolution: 192dpi) and (min-width: 768px)"
srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_768x0at2x.webp"
type="image/webp"
/>
&lt;source
media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 768px), (min-resolution: 192dpi) and (min-width: 768px)"
srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_768x0at2x.jpg"
type="image/jpeg"
/>
&lt;source
media="min-width: 768px"
srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_768x0at1x.webp"
type="image/webp"
/>
&lt;source
media="min-width: 768px"
srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_768x0at1x.jpg"
type="image/jpeg"
/>
&lt;source
media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 480px), (min-resolution: 192dpi) and (min-width: 480px)"
srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_480x0at2x.webp"
type="image/webp"
/>
&lt;source
media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 480px), (min-resolution: 192dpi) and (min-width: 480px)"
srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_480x0at2x.jpg"
type="image/jpeg"
/>
&lt;source
media="min-width: 480px"
srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_480x0at1x.webp"
type="image/webp"
/>
&lt;source
media="min-width: 480px"
srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_480x0at1x.jpg"
type="image/jpeg"
/>
&lt;img
height="300"
style="max-height: 300px;"
src="https://camlittle.com/photos/2024-11-15-pounce/chomp.22d4bac71bc2f59964c77ba44d4ea717.jpeg"
alt="chomp.jpeg"
/>
&lt;/picture>
&lt;/p></description><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2024-11-15-pounce_1280.jpg" />

  
<p>A successful hunt</p>
<p>





  
  
  
  
  
  
  
  
  

  
  
  
  
  
  

  <picture title="chomp.jpeg">
      
      
        
        
          







<source
    media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 1200px), (min-resolution: 192dpi) and (min-width: 1200px)"
    srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_1200x0at2x.webp"
    type="image/webp"
/>
<source
    media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 1200px), (min-resolution: 192dpi) and (min-width: 1200px)"
    srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_1200x0at2x.jpg"
    type="image/jpeg"
/>



<source
    
    media="min-width: 1200px"
    
    srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_1200x0at1x.webp"
    type="image/webp"
/>
<source
    
    media="min-width: 1200px"
    
    srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_1200x0at1x.jpg"
    type="image/jpeg"
/>


        
          







<source
    media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 992px), (min-resolution: 192dpi) and (min-width: 992px)"
    srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_992x0at2x.webp"
    type="image/webp"
/>
<source
    media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 992px), (min-resolution: 192dpi) and (min-width: 992px)"
    srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_992x0at2x.jpg"
    type="image/jpeg"
/>



<source
    
    media="min-width: 992px"
    
    srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_992x0at1x.webp"
    type="image/webp"
/>
<source
    
    media="min-width: 992px"
    
    srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_992x0at1x.jpg"
    type="image/jpeg"
/>


        
          







<source
    media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 768px), (min-resolution: 192dpi) and (min-width: 768px)"
    srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_768x0at2x.webp"
    type="image/webp"
/>
<source
    media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 768px), (min-resolution: 192dpi) and (min-width: 768px)"
    srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_768x0at2x.jpg"
    type="image/jpeg"
/>



<source
    
    media="min-width: 768px"
    
    srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_768x0at1x.webp"
    type="image/webp"
/>
<source
    
    media="min-width: 768px"
    
    srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_768x0at1x.jpg"
    type="image/jpeg"
/>


        
          







<source
    media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 480px), (min-resolution: 192dpi) and (min-width: 480px)"
    srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_480x0at2x.webp"
    type="image/webp"
/>
<source
    media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 480px), (min-resolution: 192dpi) and (min-width: 480px)"
    srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_480x0at2x.jpg"
    type="image/jpeg"
/>



<source
    
    media="min-width: 480px"
    
    srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_480x0at1x.webp"
    type="image/webp"
/>
<source
    
    media="min-width: 480px"
    
    srcset="https://content.camlittle.com/site-media/photos/2024-11-15-pounce/chomp_22d4bac71bc2f59964c77ba44d4ea717_480x0at1x.jpg"
    type="image/jpeg"
/>


        
      
      <img
          height="300"
          
          style="max-height: 300px;"
          src="https://camlittle.com/photos/2024-11-15-pounce/chomp.22d4bac71bc2f59964c77ba44d4ea717.jpeg"
          alt="chomp.jpeg"
      />
  </picture>



</p>


  <p>Grand Teton National Park</p>
<p><span title="Camera">📷</span> Sony α 7C II</p>

  ]]></content:encoded></item><item><title>Yellowstone Osprey</title><link>https://camlittle.com/photos/2024-11-15-yellowstone-osprey/</link><pubDate>Fri, 15 Nov 2024 00:08:06 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2024-11-15-yellowstone-osprey/</guid><enclosure url="https://camlittle.com/photos/2024-11-15-yellowstone-osprey/2024-11-15-yellowstone-osprey_128.jpg" length="3639" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2024-11-15-yellowstone-osprey_1280.jpg" />

  


  <p>Yellowstone National Park</p>
<p><span title="Camera">📷</span> Sony α 7C II</p>

  ]]></content:encoded></item><item><title>Lower Yellowstone Falls</title><link>https://camlittle.com/photos/2024-08-05-lower-yellowstone-falls/</link><pubDate>Fri, 15 Nov 2024 00:08:05 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2024-08-05-lower-yellowstone-falls/</guid><enclosure url="https://camlittle.com/photos/2024-08-05-lower-yellowstone-falls/2024-08-05-lower-yellowstone-falls_128.jpg" length="3810" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2024-08-05-lower-yellowstone-falls_1280.jpg" />

  


  <p>Yellowstone Canyon, Yellowstone National Park</p>
<p><span title="Camera">📷</span> Sony α 7C II</p>

  ]]></content:encoded></item><item><title>New Mexico Elephant Head</title><link>https://camlittle.com/photos/2024-07-28-nm-elephant-head/</link><pubDate>Fri, 15 Nov 2024 00:07:28 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2024-07-28-nm-elephant-head/</guid><enclosure url="https://camlittle.com/photos/2024-07-28-nm-elephant-head/2024-07-28-nm-elephant-head_128.jpg" length="8553" type="image/jpeg"/><description>&lt;p>&lt;em>Pedicularis groenlandica&lt;/em>, or elephant head, in the high alpine of northern New Mexico.&lt;/p></description><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2024-07-28-nm-elephant-head_1280.jpg" />

  
<p><em>Pedicularis groenlandica</em>, or elephant head, in the high alpine of northern New Mexico.</p>


  <p>Wheeler Peak Wilderness, NM</p>
<p><span title="Camera">📷</span> Sony α 7C II</p>

  ]]></content:encoded></item><item><title>Pika</title><link>https://camlittle.com/photos/2024-07-28-pika/</link><pubDate>Fri, 15 Nov 2024 00:07:28 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2024-07-28-pika/</guid><enclosure url="https://camlittle.com/photos/2024-07-28-pika/2024-07-28-pika_128.jpg" length="4422" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2024-07-28-pika_1280.jpg" />

  


  <p>Wheeler Peak, NM</p>
<p><span title="Camera">📷</span> Sony α6300</p>

  ]]></content:encoded></item><item><title>Freeze-ahead apple pie</title><link>https://camlittle.com/kitchen/2024-11-15-apple-pies/</link><pubDate>Fri, 15 Nov 2024 00:00:00 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/kitchen/2024-11-15-apple-pies/</guid><description>&lt;p>Earlier in the summer a friend gave me about 20 pounds of apples from his tree. It was a good year for apples in Taos, and I&amp;rsquo;ve been making full use of our newly remodeled kitchen, so I wanted to avoid them going to waste.&lt;/p>
&lt;p>I love pie, so I decided to do some holiday season prep. I baked down the apples with some white and brown sugar, cornstarch, water, and spices (mostly following &lt;a href="https://happymoneysaver.com/freezer-apple-pie-filling/">this recipe&lt;/a>) and bagged them into single-pie portions. A week or two later I made a couple batches of pie dough, baked off one pie, and froze the rest.&lt;/p></description><content:encoded><![CDATA[

  
<p>Earlier in the summer a friend gave me about 20 pounds of apples from his tree. It was a good year for apples in Taos, and I&rsquo;ve been making full use of our newly remodeled kitchen, so I wanted to avoid them going to waste.</p>
<p>I love pie, so I decided to do some holiday season prep. I baked down the apples with some white and brown sugar, cornstarch, water, and spices (mostly following <a href="https://happymoneysaver.com/freezer-apple-pie-filling/">this recipe</a>) and bagged them into single-pie portions. A week or two later I made a couple batches of pie dough, baked off one pie, and froze the rest.</p>
<p>Super worth doing - as long as I remember to defrost, I can put together a fresh pie in 10 minutes or so + bake time.</p>


  
  
  <a href="https://camlittle.com/kitchen/2024-11-15-apple-pies/1-prep.jpeg">
    




  
  
  
  
  
  
  
  
  

  
  
  
  
  
  

  <picture title="Preparing to cook down apples">
      
      
        
        
      
      <img
          height="300"
          
          style="max-height: 300px;"
          src="https://camlittle.com/kitchen/2024-11-15-apple-pies/1-prep.bc4f18bb505a0d2ee7c47f9ede93cdcb.jpeg"
          alt="Preparing to cook down apples"
      />
  </picture>


  </a>
  
  

  
  <a href="https://camlittle.com/kitchen/2024-11-15-apple-pies/2-baked.jpeg">
    




  
  
  
  
  
  
  
  
  

  
  
  
  
  
  

  <picture title="Part of a baked pie">
      
      
        
        
      
      <img
          height="300"
          
          style="max-height: 300px;"
          src="https://camlittle.com/kitchen/2024-11-15-apple-pies/2-baked.ba9af74d42927003654616d490efbeb2.jpeg"
          alt="Part of a baked pie"
      />
  </picture>


  </a>
  
  

  
  <a href="https://camlittle.com/kitchen/2024-11-15-apple-pies/3-frozen.jpeg">
    




  
  
  
  
  
  
  
  
  

  
  
  
  
  
  

  <picture title="The makings of a pie in the freezer">
      
      
        
        
      
      <img
          height="300"
          
          style="max-height: 300px;"
          src="https://camlittle.com/kitchen/2024-11-15-apple-pies/3-frozen.1cdd218afc85d5cdc8fb60ef0e0c01a6.jpeg"
          alt="The makings of a pie in the freezer"
      />
  </picture>


  </a>
  
  


  ]]></content:encoded></item><item><title>Bridging SwiftUI and Core Animations</title><link>https://camlittle.com/posts/2024-11-14-swiftui-core-animation/</link><pubDate>Thu, 14 Nov 2024 00:00:00 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/posts/2024-11-14-swiftui-core-animation/</guid><enclosure url="https://camlittle.com/posts/2024-11-14-swiftui-core-animation/thumbnail.png" length="16221" type="image/png"/><description>&lt;p>A project I&amp;rsquo;ve recently started heavily uses Core Animation. The app runs on macOS and iOS and relies on a custom CALayer subclass. Apple has provided &lt;a href="https://developer.apple.com/documentation/swiftui/unifying-your-app-s-animations">bridges between SwiftUI and AppKit or UIKit animations&lt;/a> but doesn&amp;rsquo;t provide guidance on how to bridge to Core Animation.&lt;/p>
&lt;p>I&amp;rsquo;ve been working on solving this problem. Though it&amp;rsquo;s not perfect, this post describes what I&amp;rsquo;ve come up with.&lt;/p>
&lt;p>For convenience, the example code in this post uses UIKit only and is simplified to focus on the essentials. Full code (runnable on iOS and macOS) can be found in the &lt;a href="https://github.com/apexskier/swiftui-coreanimation">sample code GitHub repository&lt;/a>. I assume familiarity with SwiftUI and Core Animation basics in this article, as Apple and others provide good documentation.&lt;/p></description><content:encoded><![CDATA[
  
<p>A project I&rsquo;ve recently started heavily uses Core Animation. The app runs on macOS and iOS and relies on a custom CALayer subclass. Apple has provided <a href="https://developer.apple.com/documentation/swiftui/unifying-your-app-s-animations">bridges between SwiftUI and AppKit or UIKit animations</a> but doesn&rsquo;t provide guidance on how to bridge to Core Animation.</p>
<p>I&rsquo;ve been working on solving this problem. Though it&rsquo;s not perfect, this post describes what I&rsquo;ve come up with.</p>
<p>For convenience, the example code in this post uses UIKit only and is simplified to focus on the essentials. Full code (runnable on iOS and macOS) can be found in the <a href="https://github.com/apexskier/swiftui-coreanimation">sample code GitHub repository</a>. I assume familiarity with SwiftUI and Core Animation basics in this article, as Apple and others provide good documentation.</p>
<h2 id="architecture"><a class="heading-link" href="#architecture"></a>Architecture</h2>
<p>First, we need to connect SwiftUI to the Core Animation layer. This uses <a href="https://developer.apple.com/documentation/swiftui/uiviewrepresentable"><code>UIViewRepresentable</code></a> and <a href="https://developer.apple.com/documentation/uikit/uiview"><code>UIView</code></a> (AppKit&rsquo;s <code>NS</code> equivalents on macOS).</p>
<pre class="mermaid">flowchart TD
    SwiftUI
    ViewRepresentable[NSViewRepresentable/UIViewRepresentable]
    View[NSView/UIView]
    CALayer

    SwiftUI --> ViewRepresentable --> View --> CALayer
</pre>

<h2 id="data-flow"><a class="heading-link" href="#data-flow"></a>Data flow</h2>
<p>Our animatable property can be passed through each of these layers. This is simpler with one-way data binding, but by providing access to the SwiftUI binding through the <code>UIViewRepresentable</code>&rsquo;s coordinator, my example code is bidirectional.</p>
<p>Here&rsquo;s how our animatable <code>value</code> is passed from SwiftUI into our <code>CALayer</code>&rsquo;s draw function.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">struct</span> <span class="nc">ContentView</span><span class="p">:</span> <span class="n">some</span> <span class="n">View</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="p">@</span><span class="n">State</span> <span class="kd">var</span> <span class="nv">value</span> <span class="p">=</span> <span class="mf">0.0</span> <span class="c1">// source of truth</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="nv">body</span><span class="p">:</span> <span class="n">some</span> <span class="n">View</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">CustomSwiftUIView</span><span class="p">(</span><span class="n">value</span><span class="p">:</span> <span class="n">value</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">struct</span> <span class="nc">CustomSwiftUIView</span><span class="p">:</span> <span class="n">UIViewRepresentable</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="p">@</span><span class="n">Binding</span> <span class="kd">var</span> <span class="nv">value</span><span class="p">:</span> <span class="nb">Double</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kd">func</span> <span class="nf">makeUIView</span><span class="p">(</span><span class="n">context</span><span class="p">:</span> <span class="n">Context</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="n">CustomPlatformView</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kd">let</span> <span class="nv">view</span> <span class="p">=</span> <span class="n">CustomPlatformView</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// this link allows our UIView to write value (and read if needed outside of standard SwiftUI updates)</span>
</span></span><span class="line"><span class="cl">        <span class="n">view</span><span class="p">.</span><span class="n">delegate</span> <span class="p">=</span> <span class="n">context</span><span class="p">.</span><span class="n">coordinator</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">view</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kd">func</span> <span class="nf">updateUIView</span><span class="p">(</span><span class="kc">_</span> <span class="n">uiView</span><span class="p">:</span> <span class="n">CustomPlatformView</span><span class="p">,</span> <span class="n">context</span><span class="p">:</span> <span class="n">Context</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// this &#34;pushes&#34; changes into our CALayer reactively, following standard SwiftUI updates</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="n">view</span><span class="p">.</span><span class="n">layer</span> <span class="k">as</span><span class="p">?</span> <span class="n">CustomLayer</span><span class="p">)?.</span><span class="n">update</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">with</span><span class="p">:</span> <span class="n">context</span><span class="p">.</span><span class="n">transaction</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">value</span><span class="p">:</span> <span class="n">value</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// The coordinator provides access to the value binding from CustomPlatformView</span>
</span></span><span class="line"><span class="cl">    <span class="kd">class</span> <span class="nc">Coordinator</span><span class="p">:</span> <span class="n">NSObject</span><span class="p">,</span> <span class="n">CustomDelegate</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kd">var</span> <span class="nv">parent</span><span class="p">:</span> <span class="n">CustomSwiftUIView</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="kd">init</span><span class="p">(</span><span class="kc">_</span> <span class="n">parent</span><span class="p">:</span> <span class="n">CustomSwiftUIView</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="kc">self</span><span class="p">.</span><span class="n">parent</span> <span class="p">=</span> <span class="n">parent</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kd">func</span> <span class="nf">makeCoordinator</span><span class="p">()</span> <span class="p">-&gt;</span> <span class="n">Coordinator</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">Coordinator</span><span class="p">(</span><span class="kc">self</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">class</span> <span class="nc">CustomPlatformView</span><span class="p">:</span> <span class="n">UIView</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// this coordinator provides read/write access to the value</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// for example, a custom gesture recognizer could update the binding</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="nv">coordinator</span><span class="p">:</span> <span class="n">CustomSwiftUIView</span><span class="p">.</span><span class="n">Coordinator</span><span class="p">?</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kr">override</span> <span class="kd">class</span> <span class="nc">var</span> <span class="n">layerClass</span><span class="p">:</span> <span class="nb">AnyClass</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">CustomLayer</span><span class="p">.</span><span class="kc">self</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">class</span> <span class="nc">CustomLayer</span><span class="p">:</span> <span class="n">CALayer</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kr">@NSManaged</span> <span class="kd">var</span> <span class="nv">value</span><span class="p">:</span> <span class="n">CGFloat</span> <span class="c1">// animatable property</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kr">override</span> <span class="kd">func</span> <span class="nf">draw</span><span class="p">(</span><span class="k">in</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">CGContext</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// use `value` as needed</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kd">func</span> <span class="nf">update</span><span class="p">(</span><span class="n">with</span> <span class="n">transaction</span><span class="p">:</span> <span class="n">SwiftUI</span><span class="p">.</span><span class="n">Transaction</span><span class="p">,</span> <span class="n">value</span><span class="p">:</span> <span class="nb">Double</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// we&#39;ll fill this in later to respect animations</span>
</span></span><span class="line"><span class="cl">        <span class="kc">self</span><span class="p">.</span><span class="n">value</span> <span class="p">=</span> <span class="n">value</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>To summarize why we need each copy of <code>value</code>:</p>
<ul>
<li><code>ContentView</code>: This is our main source of truth and should be considered the canonical one. SwiftUI and most reactive UI frameworks require top-down state passing.</li>
<li><code>CustomSwiftUIView</code>: Also inherent in SwiftUI is the data propagation model: passing properties throughout the hierarchy with a single source of truth. Because this is a binding, we can read and write it, but it could be a plain <code>var</code> if read-only access is needed.</li>
<li><code>CustomLayer</code>: This is needed for Core Animation to store intermediate values representing state mid-animation. Core Animation duplicates this again within the <a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/CoreAnimationBasics/CoreAnimationBasics.html#//apple_ref/doc/uid/TP40004514-CH2-SW19">&ldquo;model&rdquo; and &ldquo;presentation&rdquo; parts of the layer tree</a>. Writing to this value directly will result in state drift, you must use the <code>Coordinator</code> instead.</li>
</ul>
<p>Note that we do not need to store the <code>value</code> inside the <code>UIView</code> layer. (In theory, we could move the drawing code into the <code>UIView</code>, but that kind of defeats the point of this post.) Instead, <code>UIView</code> relies on the <code>Coordinator</code>.</p>
<h2 id="animation"><a class="heading-link" href="#animation"></a>Animation</h2>
<p>Now that we can read and write our animatable value, we want to animate it. Our goal is to drive all animation from a single place. Additionally, we&rsquo;d like to use the most modern methods. For these reasons, and because we have to, we&rsquo;ll use SwiftUI syntax.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">Button</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">withAnimation</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">value</span> <span class="p">=</span> <span class="mf">0.5</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="n">label</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">Text</span><span class="p">(</span><span class="s">&#34;Animate Value&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h3 id="custom-calayer-properties"><a class="heading-link" href="#custom-calayer-properties"></a>Custom CALayer Properties</h3>
<p>First, we need to enable Core Animation for our custom <code>value</code> property. Two things are required:</p>
<ol>
<li>Make <code>value</code> dynamic by specifying <code>@NSManaged</code></li>
<li>Ensure Core Animation redraws when <code>value</code> changes</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">class</span> <span class="nc">CustomLayer</span><span class="p">:</span> <span class="n">CALayer</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kd">static</span> <span class="kd">private</span> <span class="kd">let</span> <span class="nv">animationKeyPath</span> <span class="p">=</span> <span class="err">\</span><span class="n">CustomLayer</span><span class="p">.</span><span class="n">value</span>
</span></span><span class="line"><span class="cl">    <span class="kd">static</span> <span class="kd">private</span> <span class="kd">var</span> <span class="nv">nsAnimationKeyPath</span><span class="p">:</span> <span class="nb">String</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">NSExpression</span><span class="p">(</span><span class="n">forKeyPath</span><span class="p">:</span> <span class="n">animationKeyPath</span><span class="p">).</span><span class="n">keyPath</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="kr">@NSManaged</span> <span class="kd">var</span> <span class="nv">value</span><span class="p">:</span> <span class="n">CGFloat</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="kr">override</span> <span class="kd">class</span> <span class="nc">func</span> <span class="n">needsDisplay</span><span class="p">(</span><span class="n">forKey</span> <span class="n">key</span><span class="p">:</span> <span class="nb">String</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nb">Bool</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">key</span> <span class="p">==</span> <span class="n">nsAnimationKeyPath</span> <span class="o">||</span> <span class="kc">super</span><span class="p">.</span><span class="n">needsDisplay</span><span class="p">(</span><span class="n">forKey</span><span class="p">:</span> <span class="n">key</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h3 id="bridging-transactions"><a class="heading-link" href="#bridging-transactions"></a>Bridging Transactions</h3>
<p>The core component of this technique is using SwiftUI&rsquo;s <a href="https://developer.apple.com/documentation/swiftui/transaction"><code>Transaction</code></a> to determine if an animation is active when a property is changed. SwiftUI provides this in the context of a view representable&rsquo;s update, which we can pass directly into our custom <code>CALayer</code>&rsquo;s <code>update</code>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">struct</span> <span class="nc">CustomSwiftUIView</span><span class="p">:</span> <span class="n">UIViewRepresentable</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kd">func</span> <span class="nf">updateUIView</span><span class="p">(</span><span class="kc">_</span> <span class="n">uiView</span><span class="p">:</span> <span class="n">CustomPlatformView</span><span class="p">,</span> <span class="n">context</span><span class="p">:</span> <span class="n">Context</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="n">uiView</span><span class="p">.</span><span class="n">layer</span> <span class="k">as</span><span class="p">?</span> <span class="n">CustomLayer</span><span class="p">)?.</span><span class="n">update</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">with</span><span class="p">:</span> <span class="n">context</span><span class="p">.</span><span class="n">transaction</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">value</span><span class="p">:</span> <span class="n">value</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">class</span> <span class="nc">CustomLayer</span><span class="p">:</span> <span class="n">CALayer</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kd">static</span> <span class="kd">private</span> <span class="kd">let</span> <span class="nv">animationKey</span> <span class="p">=</span> <span class="s">&#34;value-animation-key&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kd">func</span> <span class="nf">update</span><span class="p">(</span><span class="n">with</span> <span class="n">transaction</span><span class="p">:</span> <span class="n">SwiftUI</span><span class="p">.</span><span class="n">Transaction</span><span class="p">,</span> <span class="n">value</span><span class="p">:</span> <span class="nb">Double</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// additional animation management is omitted</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="kd">let</span> <span class="nv">animation</span> <span class="p">=</span> <span class="n">transaction</span><span class="p">.</span><span class="n">animation</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="c1">// create Core Animation CAAnimation from transaction.animation</span>
</span></span><span class="line"><span class="cl">            <span class="kd">let</span> <span class="nv">caAnimation</span><span class="p">:</span> <span class="n">CAAnimation</span> <span class="p">=</span> <span class="p">...</span>
</span></span><span class="line"><span class="cl">            <span class="n">add</span><span class="p">(</span><span class="n">caAnimation</span><span class="p">,</span> <span class="n">forKey</span><span class="p">:</span> <span class="n">animationKey</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h3 id="bridging-animations"><a class="heading-link" href="#bridging-animations"></a>Bridging Animations</h3>
<p>Now, we need to transform the <code>SwiftUI.Animation</code> into a <code>CAAnimation</code>. This is a little more complicated. <code>SwiftUI.Animation</code> is fairly opaque. It doesn&rsquo;t provide access to the type of animation, its configuration, or any way to execute itself as a function.</p>
<p>Instead, we use the fairly hacky technique of parsing the animation&rsquo;s <a href="https://developer.apple.com/documentation/Swift/CustomStringConvertible"><code>description</code></a> and extracting parameters from it.</p>
<p>This is fairly straightforward for spring animations:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">nonisolated</span><span class="p">(</span><span class="n">unsafe</span><span class="p">)</span> <span class="kd">let</span> <span class="nv">fluidSpringAnimationRegex</span> <span class="p">=</span>
</span></span><span class="line"><span class="cl"><span class="p">#</span><span class="o">/</span><span class="n">FluidSpringAnimation</span><span class="err">\</span><span class="p">(</span><span class="n">response</span><span class="p">:</span> <span class="p">(?&lt;</span><span class="n">response</span><span class="p">&gt;</span><span class="err">\</span><span class="n">d</span><span class="o">+</span><span class="err">\</span><span class="p">.</span><span class="err">\</span><span class="n">d</span><span class="o">+</span><span class="p">),</span> <span class="n">dampingFraction</span><span class="p">:</span> <span class="p">(?&lt;</span><span class="n">dampingFraction</span><span class="p">&gt;</span><span class="err">\</span><span class="n">d</span><span class="o">+</span><span class="err">\</span><span class="p">.</span><span class="err">\</span><span class="n">d</span><span class="o">+</span><span class="p">),</span> <span class="n">blendDuration</span><span class="p">:</span> <span class="p">(?&lt;</span><span class="n">blendDuration</span><span class="p">&gt;</span><span class="err">\</span><span class="n">d</span><span class="o">+</span><span class="err">\</span><span class="p">.</span><span class="err">\</span><span class="n">d</span><span class="o">+</span><span class="p">)</span><span class="err">\</span><span class="p">)</span><span class="o">/</span><span class="p">#</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="kd">let</span> <span class="nv">springParameters</span> <span class="p">=</span> <span class="k">try</span> <span class="n">fluidSpringAnimationRegex</span><span class="p">.</span><span class="n">wholeMatch</span><span class="p">(</span><span class="k">in</span><span class="p">:</span> <span class="n">description</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kd">let</span> <span class="nv">caAnimation</span> <span class="p">=</span> <span class="n">CASpringAnimation</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="kd">let</span> <span class="nv">response</span> <span class="p">=</span> <span class="n">CGFloat</span><span class="p">(</span><span class="nb">Float</span><span class="p">(</span><span class="n">springParameters</span><span class="p">.</span><span class="n">response</span><span class="p">)</span><span class="o">!</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="kd">let</span> <span class="nv">dampingFraction</span> <span class="p">=</span> <span class="n">CGFloat</span><span class="p">(</span><span class="nb">Float</span><span class="p">(</span><span class="n">springParameters</span><span class="p">.</span><span class="n">dampingFraction</span><span class="p">)</span><span class="o">!</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="kd">let</span> <span class="nv">omegaN</span> <span class="p">=</span> <span class="mi">2</span> <span class="o">*</span> <span class="p">.</span><span class="n">pi</span> <span class="o">/</span> <span class="n">response</span>
</span></span><span class="line"><span class="cl">    <span class="n">caAnimation</span><span class="p">.</span><span class="n">stiffness</span> <span class="p">=</span> <span class="n">omegaN</span> <span class="o">*</span> <span class="n">omegaN</span> <span class="o">*</span> <span class="n">mass</span>
</span></span><span class="line"><span class="cl">    <span class="n">caAnimation</span><span class="p">.</span><span class="n">damping</span> <span class="p">=</span> <span class="n">dampingFraction</span> <span class="o">*</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">sqrt</span><span class="p">(</span><span class="n">caAnimation</span><span class="p">.</span><span class="n">stiffness</span> <span class="o">*</span> <span class="n">caAnimation</span><span class="p">.</span><span class="n">mass</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">caAnimation</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">nonisolated</span><span class="p">(</span><span class="n">unsafe</span><span class="p">)</span> <span class="kd">let</span> <span class="nv">springAnimationRegex</span> <span class="p">=</span>
</span></span><span class="line"><span class="cl"><span class="p">#</span><span class="o">/</span><span class="n">SpringAnimation</span><span class="err">\</span><span class="p">(</span><span class="n">mass</span><span class="p">:</span> <span class="p">(?&lt;</span><span class="n">mass</span><span class="p">&gt;</span><span class="o">-?</span><span class="err">\</span><span class="n">d</span><span class="o">+</span><span class="err">\</span><span class="p">.</span><span class="err">\</span><span class="n">d</span><span class="o">+</span><span class="p">),</span> <span class="n">stiffness</span><span class="p">:</span> <span class="p">(?&lt;</span><span class="n">stiffness</span><span class="p">&gt;</span><span class="o">-?</span><span class="err">\</span><span class="n">d</span><span class="o">+</span><span class="err">\</span><span class="p">.</span><span class="err">\</span><span class="n">d</span><span class="o">+</span><span class="p">),</span> <span class="n">damping</span><span class="p">:</span> <span class="p">(?&lt;</span><span class="n">damping</span><span class="p">&gt;</span><span class="o">-?</span><span class="err">\</span><span class="n">d</span><span class="o">+</span><span class="err">\</span><span class="p">.</span><span class="err">\</span><span class="n">d</span><span class="o">+</span><span class="p">),</span> <span class="n">initialVelocity</span><span class="p">:</span> <span class="n">SwiftUI</span><span class="p">.</span><span class="n">_Velocity</span><span class="p">&lt;</span><span class="n">Swift</span><span class="p">.</span><span class="nb">Double</span><span class="p">&gt;</span><span class="err">\</span><span class="p">(</span><span class="n">valuePerSecond</span><span class="p">:</span> <span class="p">(?&lt;</span><span class="n">initialVelocity</span><span class="p">&gt;</span><span class="o">-?</span><span class="err">\</span><span class="n">d</span><span class="o">+</span><span class="err">\</span><span class="p">.</span><span class="err">\</span><span class="n">d</span><span class="o">+</span><span class="p">)</span><span class="err">\</span><span class="p">)</span><span class="err">\</span><span class="p">)</span><span class="o">/</span><span class="p">#</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="kd">let</span> <span class="nv">springParameters</span> <span class="p">=</span> <span class="k">try</span> <span class="n">springAnimationRegex</span><span class="p">.</span><span class="n">wholeMatch</span><span class="p">(</span><span class="k">in</span><span class="p">:</span> <span class="n">description</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kd">let</span> <span class="nv">caAnimation</span> <span class="p">=</span> <span class="n">CASpringAnimation</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">caAnimation</span><span class="p">.</span><span class="n">mass</span> <span class="p">=</span> <span class="n">CGFloat</span><span class="p">(</span><span class="nb">Float</span><span class="p">(</span><span class="n">springParameters</span><span class="p">.</span><span class="n">mass</span><span class="p">)</span><span class="o">!</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">caAnimation</span><span class="p">.</span><span class="n">stiffness</span> <span class="p">=</span> <span class="n">CGFloat</span><span class="p">(</span><span class="nb">Float</span><span class="p">(</span><span class="n">springParameters</span><span class="p">.</span><span class="n">stiffness</span><span class="p">)</span><span class="o">!</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">caAnimation</span><span class="p">.</span><span class="n">damping</span> <span class="p">=</span> <span class="n">CGFloat</span><span class="p">(</span><span class="nb">Float</span><span class="p">(</span><span class="n">springParameters</span><span class="p">.</span><span class="n">damping</span><span class="p">)</span><span class="o">!</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">caAnimation</span><span class="p">.</span><span class="n">initialVelocity</span> <span class="p">=</span> <span class="n">CGFloat</span><span class="p">(</span><span class="nb">Float</span><span class="p">(</span><span class="n">springParameters</span><span class="p">.</span><span class="n">initialVelocity</span><span class="p">)</span><span class="o">!</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">caAnimation</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>And we can fairly easily recreate Apple&rsquo;s default animation:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="k">if</span> <span class="n">description</span> <span class="p">==</span> <span class="s">&#34;DefaultAnimation()&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// defaults changed in more recent versions</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="cp">#available</span><span class="p">(</span><span class="cp">iOS</span> <span class="mi">17</span><span class="p">,</span> <span class="cp">macOS</span> <span class="mi">14</span><span class="p">,</span> <span class="cp">tvOS</span> <span class="mi">17</span><span class="p">,</span> <span class="cp">watchOS</span> <span class="mi">10</span><span class="p">,</span> <span class="o">*</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kd">let</span> <span class="nv">caAnimation</span> <span class="p">=</span> <span class="n">CASpringAnimation</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">caAnimation</span><span class="p">.</span><span class="n">apply</span><span class="p">(</span><span class="n">response</span><span class="p">:</span> <span class="mf">0.55</span><span class="p">,</span> <span class="n">dampingFraction</span><span class="p">:</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">caAnimation</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kd">let</span> <span class="nv">caAnimation</span> <span class="p">=</span> <span class="n">CABasicAnimation</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">caAnimation</span><span class="p">.</span><span class="n">timingFunction</span> <span class="p">=</span> <span class="p">.</span><span class="kd">init</span><span class="p">(</span><span class="n">name</span><span class="p">:</span> <span class="p">.</span><span class="n">easeInEaseOut</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">caAnimation</span><span class="p">.</span><span class="n">duration</span> <span class="p">=</span> <span class="mf">0.35</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">caAnimation</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>However, traditional bezier animation curves are more difficult. SwiftUI produces descriptions that can be regex matched using:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="p">#</span><span class="o">/</span><span class="n">BezierAnimation</span><span class="err">\</span><span class="p">(</span><span class="n">duration</span><span class="p">:</span> <span class="p">(?&lt;</span><span class="n">duration</span><span class="p">&gt;</span><span class="err">\</span><span class="n">d</span><span class="o">+</span><span class="err">\</span><span class="p">.</span><span class="err">\</span><span class="n">d</span><span class="o">+</span><span class="p">),</span> <span class="n">curve</span><span class="p">:</span> <span class="err">\</span><span class="p">(</span><span class="kd">extension</span> <span class="nc">in</span> <span class="n">SwiftUI</span><span class="err">\</span><span class="p">):(?&lt;</span><span class="n">cubicSolver</span><span class="p">&gt;</span><span class="n">SwiftUI</span><span class="p">.</span><span class="n">UnitCurve</span><span class="p">.</span><span class="n">CubicSolver</span><span class="err">\</span><span class="p">(</span><span class="n">ax</span><span class="p">:</span> <span class="p">(?&lt;</span><span class="n">ax</span><span class="p">&gt;</span><span class="o">-?</span><span class="err">\</span><span class="n">d</span><span class="o">+</span><span class="err">\</span><span class="p">.</span><span class="err">\</span><span class="n">d</span><span class="o">+</span><span class="p">),</span> <span class="n">bx</span><span class="p">:</span> <span class="p">(?&lt;</span><span class="n">bx</span><span class="p">&gt;</span><span class="o">-?</span><span class="err">\</span><span class="n">d</span><span class="o">+</span><span class="err">\</span><span class="p">.</span><span class="err">\</span><span class="n">d</span><span class="o">+</span><span class="p">),</span> <span class="n">cx</span><span class="p">:</span> <span class="p">(?&lt;</span><span class="n">cx</span><span class="p">&gt;</span><span class="o">-?</span><span class="err">\</span><span class="n">d</span><span class="o">+</span><span class="err">\</span><span class="p">.</span><span class="err">\</span><span class="n">d</span><span class="o">+</span><span class="p">),</span> <span class="n">ay</span><span class="p">:</span> <span class="p">(?&lt;</span><span class="n">ay</span><span class="p">&gt;</span><span class="o">-?</span><span class="err">\</span><span class="n">d</span><span class="o">+</span><span class="err">\</span><span class="p">.</span><span class="err">\</span><span class="n">d</span><span class="o">+</span><span class="p">),</span> <span class="n">by</span><span class="p">:</span> <span class="p">(?&lt;</span><span class="n">by</span><span class="p">&gt;</span><span class="o">-?</span><span class="err">\</span><span class="n">d</span><span class="o">+</span><span class="err">\</span><span class="p">.</span><span class="err">\</span><span class="n">d</span><span class="o">+</span><span class="p">),</span> <span class="n">cy</span><span class="p">:</span> <span class="p">(?&lt;</span><span class="n">cy</span><span class="p">&gt;</span><span class="o">-?</span><span class="err">\</span><span class="n">d</span><span class="o">+</span><span class="err">\</span><span class="p">.</span><span class="err">\</span><span class="n">d</span><span class="o">+</span><span class="p">)</span><span class="err">\</span><span class="p">))</span><span class="err">\</span><span class="p">)</span><span class="o">/</span><span class="p">#</span>
</span></span></code></pre></div><p><del>However, I haven&rsquo;t yet been able to figure out what the cubic solver is and how to convert its three two-dimensional coordinates (<code>(ax, ay), (bx, by), (cx, cy)</code>) into a bezier curve&rsquo;s two two-dimensional control points (<a href="https://developer.apple.com/documentation/quartzcore/camediatimingfunction/1522235-init"><code>CAMediaTimingFunction.init(controlPoints:_:_:_:)</code></a>).</del></p>
<p><strong>Update 2024-12-10</strong></p>
<p>Thanks to Eric from <a href="https://createwithplay.com">Play</a>, I now know how to convert SwiftUI&rsquo;s <code>CubicSolver</code> back to the original Bezier control points.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">pt1</span> <span class="p">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">x</span><span class="p">:</span> <span class="n">cx</span> <span class="o">/</span> <span class="mi">3</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">y</span><span class="p">:</span> <span class="n">cy</span> <span class="o">/</span> <span class="mi">3</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">pt2</span> <span class="p">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">x</span><span class="p">:</span> <span class="p">(</span><span class="n">bx</span> <span class="o">+</span> <span class="n">cx</span><span class="p">)</span> <span class="o">/</span> <span class="mi">3</span> <span class="o">+</span> <span class="n">pt1</span><span class="p">.</span><span class="n">x</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">y</span><span class="p">:</span> <span class="p">(</span><span class="n">by</span> <span class="o">+</span> <span class="n">cy</span><span class="p">)</span> <span class="o">/</span> <span class="mi">3</span> <span class="o">+</span> <span class="n">pt1</span><span class="p">.</span><span class="n">y</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">caAnimation</span><span class="p">.</span><span class="n">timingFunction</span> <span class="p">=</span> <span class="p">.</span><span class="kd">init</span><span class="p">(</span><span class="n">controlPoints</span><span class="p">:</span> <span class="n">pt1</span><span class="p">.</span><span class="n">x</span><span class="p">,</span> <span class="n">pt1</span><span class="p">.</span><span class="n">y</span><span class="p">,</span> <span class="n">pt2</span><span class="p">.</span><span class="n">x</span><span class="p">,</span> <span class="n">pt2</span><span class="p">.</span><span class="n">y</span><span class="p">)</span>
</span></span></code></pre></div><p>Previously, I resorted to explicitly matching against standard timing curves I can explicitly create using SwiftUI and Core Animation. For example, ease in&rsquo;s cubic solver is:</p>
<pre tabindex="0"><code>SwiftUI.UnitCurve.CubicSolver(ax: -0.7400000000000002, bx: 0.4800000000000002, cx: 1.26, ay: -2.0, by: 3.0, cy: 0.0)
</code></pre><p>And the Core Animation instance is:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">caAnimation</span> <span class="p">=</span> <span class="n">CABasicAnimation</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">caAnimation</span><span class="p">.</span><span class="n">duration</span> <span class="p">=</span> <span class="nb">Double</span><span class="p">(</span><span class="n">bezierParameters</span><span class="p">.</span><span class="n">duration</span><span class="p">)</span><span class="o">!</span>
</span></span><span class="line"><span class="cl"><span class="k">switch</span> <span class="n">bezierParameters</span><span class="p">.</span><span class="n">cubicSolver</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"><span class="k">case</span> <span class="n">easeInAnimationCubicSolver</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">caAnimation</span><span class="p">.</span><span class="n">timingFunction</span> <span class="p">=</span> <span class="p">.</span><span class="kd">init</span><span class="p">(</span><span class="n">name</span><span class="p">:</span> <span class="p">.</span><span class="n">easeIn</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="k">return</span> <span class="n">caAnimation</span>
</span></span></code></pre></div><p>Once the animation is created, we configure the keypath it modifies, and its start and end values without our layer&rsquo;s <code>update</code> function. We also need to ensure we update the spring animation&rsquo;s duration, as it depends on the value&rsquo;s range. Core Animation will cut off the animation without this.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kc">self</span><span class="p">.</span><span class="n">value</span> <span class="p">=</span> <span class="n">value</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="kd">let</span> <span class="nv">caAnimation</span> <span class="p">=</span> <span class="n">transaction</span><span class="p">.</span><span class="n">animation</span><span class="p">?.</span><span class="n">caAnimation</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">caAnimation</span><span class="p">.</span><span class="n">keyPath</span> <span class="p">=</span> <span class="kc">Self</span><span class="p">.</span><span class="n">nsAnimationKeyPath</span>
</span></span><span class="line"><span class="cl">    <span class="n">caAnimation</span><span class="p">.</span><span class="n">fromValue</span> <span class="p">=</span> <span class="p">(</span><span class="n">presentation</span><span class="p">()</span> <span class="p">??</span> <span class="kc">self</span><span class="p">).</span><span class="n">value</span>
</span></span><span class="line"><span class="cl">    <span class="n">caAnimation</span><span class="p">.</span><span class="n">toValue</span> <span class="p">=</span> <span class="n">value</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="kd">let</span> <span class="nv">caAnimation</span> <span class="p">=</span> <span class="n">caAnimation</span> <span class="k">as</span><span class="p">?</span> <span class="n">CASpringAnimation</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// Core Animation will cut off the animation if the duration is not updated</span>
</span></span><span class="line"><span class="cl">        <span class="n">caAnimation</span><span class="p">.</span><span class="n">duration</span> <span class="p">=</span> <span class="n">caAnimation</span><span class="p">.</span><span class="n">settlingDuration</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">add</span><span class="p">(</span><span class="n">caAnimation</span><span class="p">,</span> <span class="n">forKey</span><span class="p">:</span> <span class="kc">Self</span><span class="p">.</span><span class="n">animationKey</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h2 id="final-result"><a class="heading-link" href="#final-result"></a>Final Result</h2>
<p>For my final demo, I want to draw something that shows <code>value</code> animating. I&rsquo;ll draw a circle that will animate through the vertical center axis of the view. I&rsquo;ll also print the value as text to visually guarantee it&rsquo;s changing.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">diameter</span> <span class="p">=</span> <span class="mf">20.0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">override</span> <span class="kd">func</span> <span class="nf">draw</span><span class="p">(</span><span class="k">in</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">CGContext</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">ctx</span><span class="p">.</span><span class="n">clear</span><span class="p">(</span><span class="n">bounds</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// set graphics context to support NSString drawing</span>
</span></span><span class="line"><span class="cl">    <span class="n">UIGraphicsPushContext</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="s">&#34;</span><span class="si">\(</span><span class="n">value</span><span class="si">)</span><span class="s">&#34;</span><span class="p">.</span><span class="n">draw</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="k">in</span><span class="p">:</span> <span class="n">bounds</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">withAttributes</span><span class="p">:</span> <span class="n">fontAttributes</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">ctx</span><span class="p">.</span><span class="n">setFillColor</span><span class="p">(</span><span class="n">CGColor</span><span class="p">(</span><span class="n">red</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="n">green</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="n">blue</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="n">alpha</span><span class="p">:</span> <span class="mi">1</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="n">ctx</span><span class="p">.</span><span class="n">fillEllipse</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="k">in</span><span class="p">:</span> <span class="n">CGRect</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">origin</span><span class="p">:</span> <span class="n">CGPoint</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="n">x</span><span class="p">:</span> <span class="p">(</span><span class="n">bounds</span><span class="p">.</span><span class="n">width</span> <span class="o">-</span> <span class="n">diameter</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">y</span><span class="p">:</span> <span class="p">(</span><span class="n">bounds</span><span class="p">.</span><span class="n">height</span> <span class="o">-</span> <span class="n">diameter</span><span class="p">)</span> <span class="o">*</span> <span class="n">value</span>
</span></span><span class="line"><span class="cl">            <span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="n">size</span><span class="p">:</span> <span class="n">CGSize</span><span class="p">(</span><span class="n">width</span><span class="p">:</span> <span class="n">diameter</span><span class="p">,</span> <span class="n">height</span><span class="p">:</span> <span class="n">diameter</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Another feature I want to show is bidirectional data binding. When you click or tap on the custom view, it will interrupt the animation and set the parent to the current state, to keep everything in sync. We can also animate changes from the internal view using the same SwiftUI <code>withAnimation</code> function.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kr">@objc</span> <span class="kd">func</span> <span class="nf">handleTap</span><span class="p">(</span><span class="n">gestureRecognizer</span><span class="p">:</span> <span class="n">UIGestureRecognizer</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">guard</span> <span class="kd">let</span> <span class="nv">coordinator</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">coordinator</span><span class="p">.</span><span class="n">parent</span><span class="p">.</span><span class="n">value</span> <span class="p">=</span> <span class="p">(</span><span class="n">presentation</span><span class="p">()</span> <span class="p">??</span> <span class="kc">self</span><span class="p">).</span><span class="n">value</span>
</span></span><span class="line"><span class="cl">    <span class="n">removeAnimation</span><span class="p">(</span><span class="n">forKey</span><span class="p">:</span> <span class="kc">Self</span><span class="p">.</span><span class="n">animationKey</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">@objc</span> <span class="kd">func</span> <span class="nf">handleDoubleTap</span><span class="p">(</span><span class="n">gestureRecognizer</span><span class="p">:</span> <span class="n">UIGestureRecognizer</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">guard</span> <span class="kd">let</span> <span class="nv">coordinator</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">withAnimation</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">coordinator</span><span class="p">.</span><span class="n">parent</span><span class="p">.</span><span class="n">value</span> <span class="p">=</span> <span class="p">(</span><span class="n">presentation</span><span class="p">()</span> <span class="p">??</span> <span class="kc">self</span><span class="p">).</span><span class="n">value</span> <span class="o">&gt;</span> <span class="mf">0.5</span> <span class="p">?</span> <span class="mi">0</span> <span class="p">:</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Complete code with an example app and Xcode project can be found at <a href="https://github.com/apexskier/swiftui-coreanimation">https://github.com/apexskier/swiftui-coreanimation</a>.</p>
<figure>
    <video src="https://content.camlittle.com/swiftui-caanimation%20-%20swiftui-caanimation%20-%202024-11-14%20at%2017.41.53-converted.mp4" width="420" controls muted></video>
    <figcaption>Final demo of animation</figcaption>
</figure>
<h2 id="taking-it-further"><a class="heading-link" href="#taking-it-further"></a>Taking it further</h2>
<p>I&rsquo;m pretty happy with this approach, especially the ability to drive animations from multiple layers in the stack using the same mechanism. However, it&rsquo;s not ideal for a few reasons.</p>
<p>From <a href="https://developer.apple.com/documentation/swiftui/animation/spring">the SwiftUI <code>spring</code> documentation</a>:</p>
<blockquote>
<p>When mixed with other <code>spring()</code> or <code>interactiveSpring()</code> animations on the same property, each animation will be replaced by their successor, preserving velocity from one animation to the next. Optionally blends the response values between springs over a time period.</p>
</blockquote>
<p>My approach does neither of these things. In theory, when an animated update propagates while an animation is in progress, we could look up the current animation and set the new animation&rsquo;s initial velocity appropriately. The blocker is the opaqueness of CAAnimations, which don&rsquo;t provide APIs for access to value or velocity at a given time.</p>
<p>Blending response values is even more difficult, as it would require dynamically changing <code>CAAnimation</code>s over time, which isn&rsquo;t supported.</p>
<p><del>As mentioned above, I haven&rsquo;t figured out SwiftUI&rsquo;s <code>CubicSolver</code>, which I think is a private struct implementing <code>UnitCurve</code>. It represents at least bezier curves, but I haven&rsquo;t figured out the math or how it works. If you have any ideas, please get in touch!</del></p>
<p>Ultimately, these issues are due to the closed nature of SwiftUI and other Apple UIs. At the core, animation curves are math. Standard curves are simple functions over time (<code>f(time) = value</code>). Springs are more complex since they&rsquo;re semi-stateful by taking the animated value into account, but are also deterministic. I&rsquo;d like to be able to directly call these functions. Unfortunately, <code>CABasicAnimation</code> has no support for this. At first glance at the documentation, SwiftUI&rsquo;s <code>Animation</code> appears to support this with <a href="https://developer.apple.com/documentation/swiftui/animation/animate(value:time:context:)"><code>func animate&lt;V&gt;(value: V, time: TimeInterval, context: inout AnimationContext&lt;V&gt;) -&gt; V?</code></a>. However, I have not found a way to get ahold of an <code>AnimationContext</code> outside of a custom Animation subclass, which makes this non-viable. SwiftUI&rsquo;s <code>UnitCurve</code> has <a href="https://developer.apple.com/documentation/swiftui/unitcurve/value(at:)"><code>value(at:)</code></a> and <a href="https://developer.apple.com/documentation/swiftui/unitcurve/velocity(at:)"><code>velocity(at:)</code></a> functions that are exactly what I want, but don&rsquo;t have a spring equivalent, and <code>UnitCurve</code>s aren&rsquo;t accessible from <code>Animation</code>. I could directly implement timing curve functions, but I&rsquo;d prefer something built-in to avoid building it from scratch.</p>
<p><strong>Update 2024-12-10</strong></p>
<p>In additional to figuring out <code>CubicSolver</code> as mentioned above, I&rsquo;ve extended the <a href="https://github.com/apexskier/swiftui-coreanimation">demo project on GitHub</a> to show SwiftUI and CoreAnimation side by side, so you can visually confirm the animations are the same. SwiftUI has noticeable lag, but overall this approach works.</p>
<figure>
    <video src="https://content.camlittle.com/iPhone%2016%20Pro%20-%20Simulator%20-%202024-12-10%20at%2018.08.44.mp4" width="420" controls muted></video>
    <figcaption>Final demo of animation</figcaption>
</figure>


  
  ]]></content:encoded></item><item><title>Donuts</title><link>https://camlittle.com/kitchen/2024-09-28-donuts/</link><pubDate>Sat, 28 Sep 2024 00:00:00 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/kitchen/2024-09-28-donuts/</guid><description>&lt;p>I was commissioned to make donuts for a friends wedding party. I convinced them that donuts are best as fresh as can be, so I prepped dough the night before and did cold-ferment before getting up early to fry and glaze.&lt;/p>
&lt;p>I pretty much followed Serious Eats recipe directly, though I had to adjust for altitude. The last time I made donuts at altitude (Crested Butte for my sister-in-laws wedding party) I forgot my thermometer and had to gauge temp using a chopstick. This time, I looked it up properly and found I need to reduce my oil temp by 21℉, since I&amp;rsquo;m at 7000 feet, which worked perfectly.&lt;/p></description><content:encoded><![CDATA[
<b><a href="https://www.seriouseats.com/yeast-raised-doughnuts-make-at-home-glazed-recipe" target="_blank">Recipe from Serious Eats</a></b>


  
<p>I was commissioned to make donuts for a friends wedding party. I convinced them that donuts are best as fresh as can be, so I prepped dough the night before and did cold-ferment before getting up early to fry and glaze.</p>
<p>I pretty much followed Serious Eats recipe directly, though I had to adjust for altitude. The last time I made donuts at altitude (Crested Butte for my sister-in-laws wedding party) I forgot my thermometer and had to gauge temp using a chopstick. This time, I looked it up properly and found I need to reduce my oil temp by 21℉, since I&rsquo;m at 7000 feet, which worked perfectly.</p>


  
  
  
  <video controls height="300" alt="Sunrise">
    <source src="https://camlittle.com/kitchen/2024-09-28-donuts/0-sunrise.mov" />
  </video>
  

  
  <a href="https://camlittle.com/kitchen/2024-09-28-donuts/1-rising.jpeg">
    




  
  
  
  
  
  
  
  
  

  
  
  
  
  
  

  <picture title="Donuts proofing in the oven">
      
      
        
        
      
      <img
          height="300"
          
          style="max-height: 300px;"
          src="https://camlittle.com/kitchen/2024-09-28-donuts/1-rising.28d2daa5ff81e0e632aac064ed699b53.jpeg"
          alt="Donuts proofing in the oven"
      />
  </picture>


  </a>
  
  

  
  <a href="https://camlittle.com/kitchen/2024-09-28-donuts/2-frying-station.jpeg">
    




  
  
  
  
  
  
  
  
  

  
  
  
  
  
  

  <picture title="Frying station">
      
      
        
        
      
      <img
          height="300"
          
          style="max-height: 300px;"
          src="https://camlittle.com/kitchen/2024-09-28-donuts/2-frying-station.99bec693cb1ae853a38ad9e58e2d0723.jpeg"
          alt="Frying station"
      />
  </picture>


  </a>
  
  

  
  <a href="https://camlittle.com/kitchen/2024-09-28-donuts/3-frying.jpeg">
    




  
  
  
  
  
  
  
  
  

  
  
  
  
  
  

  <picture title="Frying donuts">
      
      
        
        
      
      <img
          height="300"
          
          style="max-height: 300px;"
          src="https://camlittle.com/kitchen/2024-09-28-donuts/3-frying.80f27648a3e1df34454687b070fa9bec.jpeg"
          alt="Frying donuts"
      />
  </picture>


  </a>
  
  

  
  
  <video controls height="300" alt="Frying donuts">
    <source src="https://camlittle.com/kitchen/2024-09-28-donuts/3-frying.mov" />
  </video>
  

  
  <a href="https://camlittle.com/kitchen/2024-09-28-donuts/4-assembly.jpeg">
    




  
  
  
  
  
  
  
  
  

  
  
  
  
  
  

  <picture title="Glazing">
      
      
        
        
      
      <img
          height="300"
          
          style="max-height: 300px;"
          src="https://camlittle.com/kitchen/2024-09-28-donuts/4-assembly.c286b15b3b6bd9891203b302400bce44.jpeg"
          alt="Glazing"
      />
  </picture>


  </a>
  
  


  ]]></content:encoded></item><item><title>Kinder Bueno Cake</title><link>https://camlittle.com/kitchen/2024-09-12-kinder-bueno-cake/</link><pubDate>Thu, 12 Sep 2024 00:00:00 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/kitchen/2024-09-12-kinder-bueno-cake/</guid><description>&lt;p>For Aisha&amp;rsquo;s birthday this year she requested a Kinder Bueno inspired cake. Chocolate&amp;rsquo;s a bit of an unusual request for her, so I knew it had to be good.&lt;/p>
&lt;p>My base recipe is &lt;a href="https://chelsweets.com/kinder-bueno-cake/">one from Chelsweets&lt;/a>: layers of shortbread, chocolate cake, hazelnut and white chocolate mouse, and a nutella frosting. The chocolate cake recipe was great!&lt;/p>
&lt;p>I changed a few things:&lt;/p>
&lt;ul>
&lt;li>I made my shortbread from scratch, pressing into the bottom of my cake tins with a flat bottomed glass, then baking for around 20 minutes before pouring in my cake batter. This whole &amp;ldquo;cake baked directly on a cookie&amp;rdquo; was a first for me and worked very well.&lt;/li>
&lt;li>I swapped the American buttercream described in the original recipe for an Italian meringue buttercream, which is far superior. I had some issues with my buttercream deflating, but was able to rescue it (this seems to happen 4/5 times I do Italian meringue).&lt;/li>
&lt;li>I didn&amp;rsquo;t have enough left in our Costco size box of real Kinder Bueno bars, so simplified the decorations.&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[
<b><a href="https://chelsweets.com/kinder-bueno-cake/" target="_blank">Recipe from Chelsweets</a></b>


  
<p>For Aisha&rsquo;s birthday this year she requested a Kinder Bueno inspired cake. Chocolate&rsquo;s a bit of an unusual request for her, so I knew it had to be good.</p>
<p>My base recipe is <a href="https://chelsweets.com/kinder-bueno-cake/">one from Chelsweets</a>: layers of shortbread, chocolate cake, hazelnut and white chocolate mouse, and a nutella frosting. The chocolate cake recipe was great!</p>
<p>I changed a few things:</p>
<ul>
<li>I made my shortbread from scratch, pressing into the bottom of my cake tins with a flat bottomed glass, then baking for around 20 minutes before pouring in my cake batter. This whole &ldquo;cake baked directly on a cookie&rdquo; was a first for me and worked very well.</li>
<li>I swapped the American buttercream described in the original recipe for an Italian meringue buttercream, which is far superior. I had some issues with my buttercream deflating, but was able to rescue it (this seems to happen 4/5 times I do Italian meringue).</li>
<li>I didn&rsquo;t have enough left in our Costco size box of real Kinder Bueno bars, so simplified the decorations.</li>
</ul>


  
  
  <a href="https://camlittle.com/kitchen/2024-09-12-kinder-bueno-cake/1-candles.jpeg">
    




  
  
  
  
  
  
  
  
  

  
  
  
  
  
  

  <picture title="Lit birthday cake">
      
      
        
        
      
      <img
          height="300"
          
          style="max-height: 300px;"
          src="https://camlittle.com/kitchen/2024-09-12-kinder-bueno-cake/1-candles.c72a6f8216a3f0a3128fe59ba499d143.jpeg"
          alt="Lit birthday cake"
      />
  </picture>


  </a>
  
  

  
  <a href="https://camlittle.com/kitchen/2024-09-12-kinder-bueno-cake/2-slice.jpeg">
    




  
  
  
  
  
  
  
  
  

  
  
  
  
  
  

  <picture title="A slice of cake">
      
      
        
        
      
      <img
          height="300"
          
          style="max-height: 300px;"
          src="https://camlittle.com/kitchen/2024-09-12-kinder-bueno-cake/2-slice.3b776be4856a77a5dafb5a9d1366a267.jpeg"
          alt="A slice of cake"
      />
  </picture>


  </a>
  
  


  ]]></content:encoded></item><item><title>Magic Cake</title><link>https://camlittle.com/kitchen/2024-07-17-magic-cake/</link><pubDate>Tue, 23 Jul 2024 00:00:00 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/kitchen/2024-07-17-magic-cake/</guid><description>&lt;p>Kind of a novelty, but some interesting science. A single bake of batter forms layers hard custard, soft custard, and normal cake.&lt;/p></description><content:encoded><![CDATA[
<b><a href="https://www.jocooks.com/recipes/magic-cake/" target="_blank">Recipe</a></b>


  
<p>Kind of a novelty, but some interesting science. A single bake of batter forms layers hard custard, soft custard, and normal cake.</p>


  
  
  <a href="https://camlittle.com/kitchen/2024-07-17-magic-cake/magic-cake.jpeg">
    




  
  
  
  
  
  
  
  
  

  
  
  
  
  
  

  <picture title="Magic cake">
      
      
        
        
      
      <img
          height="300"
          
          style="max-height: 300px;"
          src="https://camlittle.com/kitchen/2024-07-17-magic-cake/magic-cake.2baa8e58be4196521688bb302a042305.jpeg"
          alt="Magic cake"
      />
  </picture>


  </a>
  
  


  ]]></content:encoded></item><item><title>Peach faux-tarts</title><link>https://camlittle.com/kitchen/2024-07-23-peach-tarts/</link><pubDate>Tue, 23 Jul 2024 00:00:00 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/kitchen/2024-07-23-peach-tarts/</guid><description>&lt;p>Simple summer treat.&lt;/p>
&lt;ul>
&lt;li>Homemade puff pastry cut into circles (not really tarts, since I don&amp;rsquo;t have a tart pan).&lt;/li>
&lt;li>Pastry cream&lt;/li>
&lt;li>Peach poached in vanilla and prosecco&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[
<b><a href="https://www.nordljus.co.uk/en/vanilla-poached-white-peach-tart" target="_blank">Recipe</a></b>


  
<p>Simple summer treat.</p>
<ul>
<li>Homemade puff pastry cut into circles (not really tarts, since I don&rsquo;t have a tart pan).</li>
<li>Pastry cream</li>
<li>Peach poached in vanilla and prosecco</li>
</ul>


  
  
  <a href="https://camlittle.com/kitchen/2024-07-23-peach-tarts/1-assembled.jpeg">
    




  
  
  
  
  
  
  
  
  

  
  
  
  
  
  

  <picture title="Assembled tart">
      
      
        
        
      
      <img
          height="300"
          
          style="max-height: 300px;"
          src="https://camlittle.com/kitchen/2024-07-23-peach-tarts/1-assembled.f215036e47cf70aa87665d5e8553527f.jpeg"
          alt="Assembled tart"
      />
  </picture>


  </a>
  
  

  
  <a href="https://camlittle.com/kitchen/2024-07-23-peach-tarts/2-broken.jpeg">
    




  
  
  
  
  
  
  
  
  

  
  
  
  
  
  

  <picture title="Broken tart">
      
      
        
        
      
      <img
          height="300"
          
          style="max-height: 300px;"
          src="https://camlittle.com/kitchen/2024-07-23-peach-tarts/2-broken.a07089a1e7ddaee692f61be653414b47.jpeg"
          alt="Broken tart"
      />
  </picture>


  </a>
  
  


  ]]></content:encoded></item><item><title>Christmas Movies 2023</title><link>https://camlittle.com/posts/2023-12-26-christmas-movies-2023/</link><pubDate>Tue, 26 Dec 2023 00:00:00 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/posts/2023-12-26-christmas-movies-2023/</guid><enclosure url="https://camlittle.com/posts/2023-12-26-christmas-movies-2023/thumbnail.png" length="26671" type="image/png"/><description>&lt;p>My wife and I (and her parents) tend to watch a lot of Christmas movies this time of year. Here are my rankings of those we got to this year:&lt;/p>
&lt;h3 id="time-datetime2023-12-131213time---the-claus-familyhttpsenwikipediaorgwikithe_claus_family">&lt;a class="heading-link" href="#time-datetime2023-12-131213time---the-claus-familyhttpsenwikipediaorgwikithe_claus_family">&lt;/a>&lt;time datetime="2023-12-13">12/13&lt;/time> - &lt;a href="https://en.wikipedia.org/wiki/The_Claus_Family">The Claus Family&lt;/a>&lt;/h3>
&lt;p>Especially considering the &lt;em>two&lt;/em> sequels, not very good. Pretty unconvincing and to-be-expected storylines, both about Santa Claus and saving the biscuit factory. The humor wasn&amp;rsquo;t great either, but this was probably made worse by watching a dub.&lt;/p></description><content:encoded><![CDATA[
  
<p>My wife and I (and her parents) tend to watch a lot of Christmas movies this time of year. Here are my rankings of those we got to this year:</p>
<h3 id="time-datetime2023-12-131213time---the-claus-familyhttpsenwikipediaorgwikithe_claus_family"><a class="heading-link" href="#time-datetime2023-12-131213time---the-claus-familyhttpsenwikipediaorgwikithe_claus_family"></a><time datetime="2023-12-13">12/13</time> - <a href="https://en.wikipedia.org/wiki/The_Claus_Family">The Claus Family</a></h3>
<p>Especially considering the <em>two</em> sequels, not very good. Pretty unconvincing and to-be-expected storylines, both about Santa Claus and saving the biscuit factory. The humor wasn&rsquo;t great either, but this was probably made worse by watching a dub.</p>
<p>★☆☆☆☆</p>
<h3 id="time-datetime2023-12-161216time---family-switchhttpsenwikipediaorgwikifamily_switch"><a class="heading-link" href="#time-datetime2023-12-161216time---family-switchhttpsenwikipediaorgwikifamily_switch"></a><time datetime="2023-12-16">12/16</time> - <a href="https://en.wikipedia.org/wiki/Family_Switch">Family Switch</a></h3>
<p>Family <em>Freaky Friday</em>, but not as good. Tenuous connection to Christmas. Lots of writing that had the appearance of jokes.</p>
<p>★☆☆☆☆</p>
<h3 id="time-datetime2023-12-161216time---love-hardhttpsenwikipediaorgwikilove_hard"><a class="heading-link" href="#time-datetime2023-12-161216time---love-hardhttpsenwikipediaorgwikilove_hard"></a><time datetime="2023-12-16">12/16</time> - <a href="https://en.wikipedia.org/wiki/Love_Hard">Love Hard</a></h3>
<p>Pretty good Christmas romantic comedy. The writing and acting were quite good in general, and the characters had more dimension than most rom-coms, especially Josh.</p>
<p>★★★★☆</p>
<h3 id="time-datetime2023-12-171217time---holiday-in-the-vineyardshttpswwwrottentomatoescommholiday_in_the_vineyards"><a class="heading-link" href="#time-datetime2023-12-171217time---holiday-in-the-vineyardshttpswwwrottentomatoescommholiday_in_the_vineyards"></a><time datetime="2023-12-17">12/17</time> - <a href="https://www.rottentomatoes.com/m/holiday_in_the_vineyards">Holiday in the Vineyards</a></h3>
<p>Pretty standard Hallmark-style Christmas romantic comedy, meaning not much comedy and a stretched connection to Christmas (who has a real estate deadline on Christmas Eve?). The son of a fancy wine company has to sneak around a small town to get inside info on buying the vineyard. Falls in love with a sexy local wine lady, and has to reveal his lies. Some gratuitous shirtless abs, and way too public sexy dancing.</p>
<p>★★☆☆☆</p>
<h3 id="time-datetime2023-12-181218time---christmas-with-youhttpsenwikipediaorgwikichristmas_with_you_film"><a class="heading-link" href="#time-datetime2023-12-181218time---christmas-with-youhttpsenwikipediaorgwikichristmas_with_you_film"></a><time datetime="2023-12-18">12/18</time> - <a href="https://en.wikipedia.org/wiki/Christmas_with_You_(film)">Christmas with You</a></h3>
<p>A slight deviation from the standard format, an aging pop star surprises a fan for the holidays, falls in love with the dad, and writes a hit Christmas song with him. Not terrible, but not one I need to watch again.</p>
<p>★★★☆☆</p>
<h3 id="time-datetime2023-12-251225time---candy-cane-lanehttpsenwikipediaorgwikicandy_cane_lane_film"><a class="heading-link" href="#time-datetime2023-12-251225time---candy-cane-lanehttpsenwikipediaorgwikicandy_cane_lane_film"></a><time datetime="2023-12-25">12/25</time> - <a href="https://en.wikipedia.org/wiki/Candy_Cane_Lane_(film)">Candy Cane Lane</a></h3>
<p>This one was a wild ride. A neighborhood&rsquo;s Christmas decoration contest escalates dramatically. This one was laugh-out-loud funny and I found the animation for the figurines quite effective.</p>
<p>★★★★★</p>


  
  ]]></content:encoded></item><item><title>First snow in New Mexico</title><link>https://camlittle.com/photos/2023-09-16-first-snow-nm/</link><pubDate>Mon, 09 Oct 2023 18:00:10 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2023-09-16-first-snow-nm/</guid><enclosure url="https://camlittle.com/photos/2023-09-16-first-snow-nm/2023-09-16-first-snow-nm_128.jpg" length="1227" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2023-09-16-first-snow-nm_1280.jpg" />

  


  <p>Wheeler Peak, NM</p>
<p><span title="Camera">📷</span> Sony α6300</p>

  ]]></content:encoded></item><item><title>Below Wheeler</title><link>https://camlittle.com/photos/2023-09-16-below-wheeler/</link><pubDate>Mon, 09 Oct 2023 18:00:09 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2023-09-16-below-wheeler/</guid><enclosure url="https://camlittle.com/photos/2023-09-16-below-wheeler/2023-09-16-below-wheeler_128.jpg" length="3738" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2023-09-16-below-wheeler_1280.jpg" />

  


  <p>Wheeler Peak, NM</p>
<p><span title="Camera">📷</span> Sony α6300</p>

  ]]></content:encoded></item><item><title>Snow in the Black Canyon</title><link>https://camlittle.com/photos/2023-03-03-white-and-black-canyon/</link><pubDate>Mon, 09 Oct 2023 18:00:08 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2023-03-03-white-and-black-canyon/</guid><enclosure url="https://camlittle.com/photos/2023-03-03-white-and-black-canyon/2023-03-03-white-and-black-canyon_128.jpg" length="4895" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2023-03-03-white-and-black-canyon_1280.jpg" />

  


  <p>Black Canyon of the Gunnison</p>
<p><span title="Camera">📷</span> Sony α6300</p>

  ]]></content:encoded></item><item><title>Death Valley Layers</title><link>https://camlittle.com/photos/2023-02-13-death-valley-layers/</link><pubDate>Mon, 09 Oct 2023 18:00:07 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2023-02-13-death-valley-layers/</guid><enclosure url="https://camlittle.com/photos/2023-02-13-death-valley-layers/2023-02-13-death-valley-layers_128.jpg" length="3202" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2023-02-13-death-valley-layers_1280.jpg" />

  


  <p>Death Valley National Park</p>
<p><span title="Camera">📷</span> Sony α6300</p>

  ]]></content:encoded></item><item><title>Death Valley Colors</title><link>https://camlittle.com/photos/2023-02-12-death-valley-colors/</link><pubDate>Mon, 09 Oct 2023 18:00:06 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2023-02-12-death-valley-colors/</guid><enclosure url="https://camlittle.com/photos/2023-02-12-death-valley-colors/2023-02-12-death-valley-colors_128.jpg" length="1503" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2023-02-12-death-valley-colors_1280.jpg" />

  


  <p>Badwater Basin, Death Valley National Park</p>
<p><span title="Camera">📷</span> Sony α6300</p>

  ]]></content:encoded></item><item><title>McArthur-Burney Falls</title><link>https://camlittle.com/photos/2023-02-07-mcarthur-burney-falls/</link><pubDate>Mon, 09 Oct 2023 18:00:05 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2023-02-07-mcarthur-burney-falls/</guid><enclosure url="https://camlittle.com/photos/2023-02-07-mcarthur-burney-falls/2023-02-07-mcarthur-burney-falls_128.jpg" length="3790" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2023-02-07-mcarthur-burney-falls_1280.jpg" />

  


  <p>McArthur-Burney Falls Memorial State Park, CA</p>
<p><span title="Camera">📷</span> Sony α6300</p>

  ]]></content:encoded></item><item><title>Tree of Life</title><link>https://camlittle.com/photos/2023-01-28-tree-of-life/</link><pubDate>Mon, 09 Oct 2023 18:00:04 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2023-01-28-tree-of-life/</guid><enclosure url="https://camlittle.com/photos/2023-01-28-tree-of-life/2023-01-28-tree-of-life_128.jpg" length="3821" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2023-01-28-tree-of-life_1280.jpg" />

  


  <p>Kalaloch, WA</p>
<p><span title="Camera">📷</span> Sony α6300</p>

  ]]></content:encoded></item><item><title>Golden Ocean</title><link>https://camlittle.com/photos/2023-01-24-not-canon-beach/</link><pubDate>Mon, 09 Oct 2023 18:00:03 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2023-01-24-not-canon-beach/</guid><enclosure url="https://camlittle.com/photos/2023-01-24-not-canon-beach/2023-01-24-not-canon-beach_128.jpg" length="2173" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2023-01-24-not-canon-beach_1280.jpg" />

  


  <p>Cape Kiwanda State Natural Area, OR</p>
<p><span title="Camera">📷</span> Sony α6300</p>

  ]]></content:encoded></item><item><title>Tetons</title><link>https://camlittle.com/photos/2022-10-24-tetons/</link><pubDate>Mon, 09 Oct 2023 18:00:02 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2022-10-24-tetons/</guid><enclosure url="https://camlittle.com/photos/2022-10-24-tetons/2022-10-24-tetons_128.jpg" length="2694" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2022-10-24-tetons_1280.jpg" />

  


  <p>Wyoming</p>
<p><span title="Camera">📷</span> Sony α6300</p>

  ]]></content:encoded></item><item><title>Fall Gondola</title><link>https://camlittle.com/photos/2022-10-09-snowbasin-fall/</link><pubDate>Mon, 09 Oct 2023 18:00:01 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2022-10-09-snowbasin-fall/</guid><enclosure url="https://camlittle.com/photos/2022-10-09-snowbasin-fall/2022-10-09-snowbasin-fall_128.jpg" length="3938" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2022-10-09-snowbasin-fall_1280.jpg" />

  


  <p>Snowbasin, UT</p>
<p><span title="Camera">📷</span> Sony α6300</p>

  ]]></content:encoded></item><item><title>Great Sand Dunes</title><link>https://camlittle.com/photos/2022-12-29-great-sand-dunes/</link><pubDate>Thu, 29 Dec 2022 15:32:00 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2022-12-29-great-sand-dunes/</guid><enclosure url="https://camlittle.com/photos/2022-12-29-great-sand-dunes/2022-12-29-great-sand-dunes_128.jpg" length="3826" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2022-12-29-great-sand-dunes_1280.jpg" />

  


  <p>Great Sand Dunes National Park, CO</p>
<p><span title="Camera">📷</span> Sony α6300</p>

  ]]></content:encoded></item><item><title>Half Dome Cables</title><link>https://camlittle.com/photos/2022-12-29-half-dome-cables/</link><pubDate>Thu, 29 Dec 2022 15:31:00 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2022-12-29-half-dome-cables/</guid><enclosure url="https://camlittle.com/photos/2022-12-29-half-dome-cables/2022-12-29-half-dome-cables_128.jpg" length="2733" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2022-12-29-half-dome-cables_1280.jpg" />

  


  <p>Half Dome, Yosemite National Park, CA</p>
<p><span title="Camera">📷</span> Sony α6300</p>

  ]]></content:encoded></item><item><title>Lake Marie</title><link>https://camlittle.com/photos/2022-12-29-lake-marie/</link><pubDate>Thu, 29 Dec 2022 15:30:00 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2022-12-29-lake-marie/</guid><enclosure url="https://camlittle.com/photos/2022-12-29-lake-marie/2022-12-29-lake-marie_128.jpg" length="3714" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2022-12-29-lake-marie_1280.jpg" />

  


  <p>Lake Marie, John Muir Wilderness, CA</p>
<p><span title="Camera">📷</span> Sony α6300</p>

  ]]></content:encoded></item><item><title>Milky Way</title><link>https://camlittle.com/photos/2022-12-29-milky-way/</link><pubDate>Thu, 29 Dec 2022 15:29:00 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2022-12-29-milky-way/</guid><enclosure url="https://camlittle.com/photos/2022-12-29-milky-way/2022-12-29-milky-way_128.jpg" length="935" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2022-12-29-milky-way_1280.jpg" />

  


  <p>Sequoia &amp; Kings Canyon Wilderness, CA</p>
<p><span title="Camera">📷</span> Sony α6300</p>

  ]]></content:encoded></item><item><title>Pika</title><link>https://camlittle.com/photos/2022-12-29-pika/</link><pubDate>Thu, 29 Dec 2022 15:28:00 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2022-12-29-pika/</guid><enclosure url="https://camlittle.com/photos/2022-12-29-pika/2022-12-29-pika_128.jpg" length="4070" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2022-12-29-pika_1280.jpg" />

  


  <p>Kings Canyon National Park, CA</p>
<p><span title="Camera">📷</span> Sony α6300</p>

  ]]></content:encoded></item><item><title>Purple Paintbrush</title><link>https://camlittle.com/photos/2022-12-29-purple-paintbrush/</link><pubDate>Thu, 29 Dec 2022 15:27:00 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2022-12-29-purple-paintbrush/</guid><enclosure url="https://camlittle.com/photos/2022-12-29-purple-paintbrush/2022-12-29-purple-paintbrush_128.jpg" length="4093" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2022-12-29-purple-paintbrush_1280.jpg" />

  


  <p>Sequoia &amp; Kings Canyon Wilderness, CA</p>
<p><span title="Camera">📷</span> Sony α6300</p>

  ]]></content:encoded></item><item><title>Sunrise on Whitney</title><link>https://camlittle.com/photos/2022-12-29-sunrise-on-whitney/</link><pubDate>Thu, 29 Dec 2022 15:26:00 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2022-12-29-sunrise-on-whitney/</guid><enclosure url="https://camlittle.com/photos/2022-12-29-sunrise-on-whitney/2022-12-29-sunrise-on-whitney_128.jpg" length="1465" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2022-12-29-sunrise-on-whitney_1280.jpg" />

  


  <p>Mount Whitney, CA</p>
<p><span title="Camera">📷</span> Sony α6300</p>

  ]]></content:encoded></item><item><title>Green Badlands</title><link>https://camlittle.com/photos/2022-07-23-green-badlands/</link><pubDate>Sun, 11 Dec 2022 15:03:00 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2022-07-23-green-badlands/</guid><enclosure url="https://camlittle.com/photos/2022-07-23-green-badlands/2022-07-23-green-badlands_128.jpg" length="2720" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2022-07-23-green-badlands_1280.jpg" />

  


  <p>Badlands National Park, SD</p>
<p><span title="Camera">📷</span> Sony α6300</p>

  ]]></content:encoded></item><item><title>Birthday Bison</title><link>https://camlittle.com/photos/2022-07-16-birthday-bison/</link><pubDate>Sun, 11 Dec 2022 15:02:00 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2022-07-16-birthday-bison/</guid><enclosure url="https://camlittle.com/photos/2022-07-16-birthday-bison/2022-07-16-birthday-bison_128.jpg" length="4338" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2022-07-16-birthday-bison_1280.jpg" />

  


  <p>Theodore Roosevelt National Park, ND</p>
<p><span title="Camera">📷</span> Sony α6300</p>

  ]]></content:encoded></item><item><title>Big and Little Horns</title><link>https://camlittle.com/photos/2022-03-09-big-and-little-horns/</link><pubDate>Sun, 11 Dec 2022 15:01:00 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2022-03-09-big-and-little-horns/</guid><enclosure url="https://camlittle.com/photos/2022-03-09-big-and-little-horns/big-and-little-horns_128.jpg" length="4140" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/big-and-little-horns_1280.jpg" />

  


  <p>Capitol Reef National Park, UT</p>
<p><span title="Camera">📷</span> Sony α6300</p>

  ]]></content:encoded></item><item><title>Simple `struct` dependency mocking in Go</title><link>https://camlittle.com/posts/2022-09-25-go-struct-mocking/</link><pubDate>Sun, 25 Sep 2022 00:00:00 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/posts/2022-09-25-go-struct-mocking/</guid><enclosure url="https://camlittle.com/posts/2022-09-25-go-struct-mocking/thumbnail.png" length="10079" type="image/png"/><description>&lt;p>I work with many people using Go for the first time, and one of the common struggles I see is testing code that depends on external &lt;code>struct&lt;/code> types.&lt;/p>
&lt;p>Let&amp;rsquo;s start with a simple (though contrived) example. I&amp;rsquo;m writing some code that depends on the &lt;a href="https://github.com/google/go-github">go-github&lt;/a> library to interact with the GitHub API.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">package&lt;/span> &lt;span class="nx">before&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s">&amp;#34;context&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s">&amp;#34;fmt&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s">&amp;#34;github.com/google/go-github/v47/github&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">obj&lt;/span> &lt;span class="kd">struct&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">user&lt;/span> &lt;span class="kt">string&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">gh&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">github&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">OrganizationsService&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">New&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">user&lt;/span> &lt;span class="kt">string&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nx">obj&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">obj&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">user&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">user&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">gh&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">github&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewClient&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kc">nil&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">Organizations&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">m&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">obj&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">CountOrganizations&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Context&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kt">int&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">error&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">orgs&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">_&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">m&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">gh&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">List&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">m&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">user&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">nil&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Errorf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;couldn&amp;#39;t list organizations: %w&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">orgs&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="kc">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">m&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">obj&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">IsMemberOf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Context&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">org&lt;/span> &lt;span class="kt">string&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kt">bool&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">error&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">result&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">_&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">m&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">gh&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">IsMember&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">org&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">m&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">user&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Errorf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;couldn&amp;#39;t call IsMember: %w&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">result&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>How do you test this? &lt;code>github.OrganizationsService&lt;/code> is a &lt;code>struct&lt;/code> type, so we&amp;rsquo;re required to have a direct instance of it to pass type checking. In many cases, this will force your tests to make network calls. Usually, you can pass &lt;code>http.Client&lt;/code> and override its behavior, but that gets messy fast.&lt;/p></description><content:encoded><![CDATA[
  
<p>I work with many people using Go for the first time, and one of the common struggles I see is testing code that depends on external <code>struct</code> types.</p>
<p>Let&rsquo;s start with a simple (though contrived) example. I&rsquo;m writing some code that depends on the <a href="https://github.com/google/go-github">go-github</a> library to interact with the GitHub API.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">before</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">	<span class="s">&#34;context&#34;</span>
</span></span><span class="line"><span class="cl">	<span class="s">&#34;fmt&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="s">&#34;github.com/google/go-github/v47/github&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">obj</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="nx">user</span> <span class="kt">string</span>
</span></span><span class="line"><span class="cl">	<span class="nx">gh</span>   <span class="o">*</span><span class="nx">github</span><span class="p">.</span><span class="nx">OrganizationsService</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">New</span><span class="p">(</span><span class="nx">user</span> <span class="kt">string</span><span class="p">)</span> <span class="nx">obj</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="k">return</span> <span class="nx">obj</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="nx">user</span><span class="p">:</span> <span class="nx">user</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">		<span class="nx">gh</span><span class="p">:</span>   <span class="nx">github</span><span class="p">.</span><span class="nf">NewClient</span><span class="p">(</span><span class="kc">nil</span><span class="p">).</span><span class="nx">Organizations</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">m</span> <span class="o">*</span><span class="nx">obj</span><span class="p">)</span> <span class="nf">CountOrganizations</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span> <span class="p">(</span><span class="kt">int</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="nx">orgs</span><span class="p">,</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">m</span><span class="p">.</span><span class="nx">gh</span><span class="p">.</span><span class="nf">List</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">m</span><span class="p">.</span><span class="nx">user</span><span class="p">,</span> <span class="kc">nil</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">	<span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="k">return</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;couldn&#39;t list organizations: %w&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="cl">	<span class="k">return</span> <span class="nb">len</span><span class="p">(</span><span class="nx">orgs</span><span class="p">),</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">m</span> <span class="o">*</span><span class="nx">obj</span><span class="p">)</span> <span class="nf">IsMemberOf</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">org</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="kt">bool</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="nx">result</span><span class="p">,</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">m</span><span class="p">.</span><span class="nx">gh</span><span class="p">.</span><span class="nf">IsMember</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">org</span><span class="p">,</span> <span class="nx">m</span><span class="p">.</span><span class="nx">user</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">	<span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="k">return</span> <span class="kc">false</span><span class="p">,</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;couldn&#39;t call IsMember: %w&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="cl">	<span class="k">return</span> <span class="nx">result</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>How do you test this? <code>github.OrganizationsService</code> is a <code>struct</code> type, so we&rsquo;re required to have a direct instance of it to pass type checking. In many cases, this will force your tests to make network calls. Usually, you can pass <code>http.Client</code> and override its behavior, but that gets messy fast.</p>
<h2 id="setup"><a class="heading-link" href="#setup"></a>Setup</h2>
<p>The key to testing this behavior is to depend on an <code>interface</code> type instead of the <code>struct</code>. Go makes this easy, which you may not know if you&rsquo;re used to other languages. In Go, an interface describes behavior that both your code <em>or dependency code</em> can implement. This feature means we can define an interface in our code that <code>github.OrganizationService</code> implements and the compiler will recognize it.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">githubOrganizations</span> <span class="kd">interface</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">obj</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="nx">user</span> <span class="kt">string</span>
</span></span><span class="line"><span class="cl">	<span class="nx">gh</span>   <span class="nx">githubOrganizations</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>I usually start with an empty interface and add methods as needed. Doing this helps keep my interface as clean as possible, which makes understanding code when refactoring or changing dependencies later easier.</p>
<p>Swapping <code>*github.OrganizationsService</code> for <code>githubOrganizations</code> works because all structs conform to the empty interface. However, I now see errors wherever I use <code>.gh</code>: <code>m.gh.List undefined (type githubOrganizations has no field or method List)</code>.</p>
<p>We need to add each of the undefined methods to the interface. If I&rsquo;m too lazy to look at the docs, I&rsquo;ll temporarily swap <code>githubOrganizations</code> back to <code>*github.OrganizationsService</code>, jump to the definition of the previously undefined method and port it over.</p>
<p>
<img src="https://content.camlittle.com/2022-09-24-go-mocking-interface-filling.mp4" alt="Filling example" />

</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">githubOrganizations</span> <span class="kd">interface</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="nf">List</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">user</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">opts</span> <span class="o">*</span><span class="nx">github</span><span class="p">.</span><span class="nx">ListOptions</span><span class="p">)</span> <span class="p">([]</span><span class="o">*</span><span class="nx">github</span><span class="p">.</span><span class="nx">Organization</span><span class="p">,</span> <span class="o">*</span><span class="nx">github</span><span class="p">.</span><span class="nx">Response</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">	<span class="nf">IsMember</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">org</span><span class="p">,</span> <span class="nx">user</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="kt">bool</span><span class="p">,</span> <span class="o">*</span><span class="nx">github</span><span class="p">.</span><span class="nx">Response</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h2 id="writing-a-test"><a class="heading-link" href="#writing-a-test"></a>Writing a test</h2>
<p>Now let&rsquo;s use this interface to write a test verifying <code>CountOrganizations</code> works as expected.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">TestCountOrganizations</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="nx">obj</span> <span class="o">:=</span> <span class="nx">obj</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="nx">gh</span><span class="p">:</span> <span class="nx">mockGHList</span><span class="p">{},</span>
</span></span><span class="line"><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="nx">c</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">obj</span><span class="p">.</span><span class="nf">CountOrganizations</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">	<span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="nx">t</span><span class="p">.</span><span class="nf">Fatal</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="cl">	<span class="k">if</span> <span class="nx">c</span> <span class="o">!=</span> <span class="mi">3</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;unexpected number of organizations: %d&#34;</span><span class="p">,</span> <span class="nx">c</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Here&rsquo;s the real magic: <code>mockGHList</code> is a <code>struct</code> that embeds a pointer to <code>github.OrganizationsService</code>, which we originally depended on.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">mockGHList</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="o">*</span><span class="nx">github</span><span class="p">.</span><span class="nx">OrganizationsService</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Go pointer semantics and struct embedding rules mean all methods of <code>*github.OrganizationsService</code> can be called on <code>mockGHList</code>, even when the embedded value is <code>nil</code>. This allows us to run the test without compilation issues, but because <code>github.OrganizationService</code> is <code>nil</code>, the test panics.</p>
<pre tabindex="0"><code>--- FAIL: TestCountOrganizations (0.00s)
...
github.com/google/go-github/v47/github.(*OrganizationsService).List
</code></pre><p>A <code>nil</code> pointer was found when trying to call <code>.List</code> on the <code>nil</code> <code>*github.OrganizationsService</code>. Go struct embedding rules mean that methods defined on the <code>mockGHList</code> object override those on the nested <code>*github.OrganizationsService</code> object. This rule lets us write our mock method.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">mockGHList</span><span class="p">)</span> <span class="nf">List</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="kt">string</span><span class="p">,</span> <span class="o">*</span><span class="nx">github</span><span class="p">.</span><span class="nx">ListOptions</span><span class="p">)</span> <span class="p">([]</span><span class="o">*</span><span class="nx">github</span><span class="p">.</span><span class="nx">Organization</span><span class="p">,</span> <span class="o">*</span><span class="nx">github</span><span class="p">.</span><span class="nx">Response</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="k">return</span> <span class="p">[]</span><span class="o">*</span><span class="nx">github</span><span class="p">.</span><span class="nx">Organization</span><span class="p">{{},</span> <span class="p">{},</span> <span class="p">{}},</span> <span class="kc">nil</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Rerun the test, and it passes!</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="o">---</span> <span class="nx">PASS</span><span class="p">:</span> <span class="nf">TestCountOrganizations</span> <span class="p">(</span><span class="mf">0.00</span><span class="nx">s</span><span class="p">)</span>
</span></span></code></pre></div><h2 id="benefits"><a class="heading-link" href="#benefits"></a>Benefits</h2>
<h3 id="single-use-minimal-mocks"><a class="heading-link" href="#single-use-minimal-mocks"></a>Single-use, minimal mocks</h3>
<p>By wrapping the original struct, we only need to implement the methods our tests directly depend on. This encourages single-use, minimal mock objects instead of manually maintaining large shared mocks.</p>
<p>Any hardcoded mock codifies its expectations of how specific scenarios behave, usually the first tests it&rsquo;s used in. Those expectations make it harder to use for new tests. As the codebase grows, it either doesn&rsquo;t thoroughly test new behavior or combinatorially increases in complexity (by introducing [boolean flags](<a href="https://martinfowler.com/bliki/FlagArgument.html">https://martinfowler.com/bliki/FlagArgument.html</a>, for example). At some point, the mock deserves its own tests to track what it&rsquo;s doing.</p>
<h3 id="fail-by-default"><a class="heading-link" href="#fail-by-default"></a>Fail-by-default</h3>
<p>This pattern fails when executed code makes unexpected method calls, which can help catch accidental changes.</p>
<h3 id="mocking-libraries"><a class="heading-link" href="#mocking-libraries"></a>Mocking libraries</h3>
<p>I&rsquo;ve also successfully used this pattern with third-party mocking libraries.</p>
<p>Here&rsquo;s an example of use with <a href="https://github.com/stretchr/testify#mock-package"><code>&quot;github.com/stretchr/testify/mock&quot;</code></a>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">mockGHIsMember</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="nx">mock</span><span class="p">.</span><span class="nx">Mock</span>
</span></span><span class="line"><span class="cl">	<span class="o">*</span><span class="nx">github</span><span class="p">.</span><span class="nx">OrganizationsService</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">m</span> <span class="o">*</span><span class="nx">mockGHIsMember</span><span class="p">)</span> <span class="nf">IsMember</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">org</span><span class="p">,</span> <span class="nx">user</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="kt">bool</span><span class="p">,</span> <span class="o">*</span><span class="nx">github</span><span class="p">.</span><span class="nx">Response</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="nx">args</span> <span class="o">:=</span> <span class="nx">m</span><span class="p">.</span><span class="nf">Called</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">org</span><span class="p">,</span> <span class="nx">user</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">	<span class="k">return</span> <span class="nx">args</span><span class="p">.</span><span class="nf">Bool</span><span class="p">(</span><span class="mi">0</span><span class="p">),</span> <span class="nx">args</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="mi">1</span><span class="p">).(</span><span class="o">*</span><span class="nx">github</span><span class="p">.</span><span class="nx">Response</span><span class="p">),</span> <span class="nx">args</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">TestIsMember</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="nx">gh</span> <span class="o">:=</span> <span class="nx">mockGHIsMember</span><span class="p">{}</span>
</span></span><span class="line"><span class="cl">	<span class="nx">obj</span> <span class="o">:=</span> <span class="nx">obj</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="nx">user</span><span class="p">:</span> <span class="s">&#34;apexskier&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">		<span class="nx">gh</span><span class="p">:</span>   <span class="o">&amp;</span><span class="nx">gh</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="nx">gh</span><span class="p">.</span><span class="nf">On</span><span class="p">(</span><span class="s">&#34;IsMember&#34;</span><span class="p">,</span> <span class="nx">mock</span><span class="p">.</span><span class="nf">MatchedBy</span><span class="p">(</span><span class="kd">func</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span> <span class="kt">bool</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">true</span> <span class="p">}),</span> <span class="s">&#34;org&#34;</span><span class="p">,</span> <span class="nx">obj</span><span class="p">.</span><span class="nx">user</span><span class="p">).</span><span class="nf">Return</span><span class="p">(</span><span class="kc">true</span><span class="p">,</span> <span class="p">(</span><span class="o">*</span><span class="nx">github</span><span class="p">.</span><span class="nx">Response</span><span class="p">)(</span><span class="kc">nil</span><span class="p">),</span> <span class="kc">nil</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="nx">result</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">obj</span><span class="p">.</span><span class="nf">IsMemberOf</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">(),</span> <span class="s">&#34;org&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">	<span class="nx">require</span><span class="p">.</span><span class="nf">NoError</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">	<span class="nx">assert</span><span class="p">.</span><span class="nf">True</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">result</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>I still only need to mock the method being called.</p>
<h2 id="caveats"><a class="heading-link" href="#caveats"></a>Caveats</h2>
<ul>
<li>Defining an interface does not work when using struct fields since interfaces can only declare methods.</li>
<li>Auto-complete won&rsquo;t show other methods available on the original <code>struct</code> type. <a href="https://pkg.go.dev">https://pkg.go.dev</a> is your friend, or you can manually declare a variable of the original type and use auto-complete on that.</li>
</ul>
<hr>
<p>You can view the code from this post at <a href="https://github.com/apexskier/go-struct-mocking-demo/">https://github.com/apexskier/go-struct-mocking-demo/</a>.</p>


  
  ]]></content:encoded></item><item><title>Seasonal Transition</title><link>https://camlittle.com/photos/2021-10-10-seasonal-transition/</link><pubDate>Tue, 12 Oct 2021 19:44:00 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2021-10-10-seasonal-transition/</guid><enclosure url="https://camlittle.com/photos/2021-10-10-seasonal-transition/2021-10-10-seasonal-transition_128.jpg" length="3371" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2021-10-10-seasonal-transition_1280.jpg" />

  


  <p>Cache National Forest, UT</p>
<p><span title="Camera">📷</span> Sony α6300</p>

  ]]></content:encoded></item><item><title>Lightning</title><link>https://camlittle.com/photos/2021-09-11-lightning/</link><pubDate>Tue, 12 Oct 2021 19:43:00 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2021-09-11-lightning/</guid><enclosure url="https://camlittle.com/photos/2021-09-11-lightning/2021-09-11-lightning_128.jpg" length="1315" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2021-09-11-lightning_1280.jpg" />

  


  <p>San Rafael Reef, UT</p>
<p><span title="Camera">📷</span> Sony α6300</p>

  ]]></content:encoded></item><item><title>Little Cactus</title><link>https://camlittle.com/photos/2021-09-05-carlsbad-cactus/</link><pubDate>Tue, 12 Oct 2021 19:42:00 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2021-09-05-carlsbad-cactus/</guid><enclosure url="https://camlittle.com/photos/2021-09-05-carlsbad-cactus/2021-09-05-carlsbad-cactus_128.jpg" length="7647" type="image/jpeg"/><description/><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2021-09-05-carlsbad-cactus_1280.jpg" />

  


  <p>Carlsbad Caverns National Park, NM</p>
<p><span title="Camera">📷</span> Sony α6300</p>

  ]]></content:encoded></item><item><title>Little Snake</title><link>https://camlittle.com/photos/2021-09-05-carlsbad-snake/</link><pubDate>Tue, 12 Oct 2021 19:41:00 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2021-09-05-carlsbad-snake/</guid><enclosure url="https://camlittle.com/photos/2021-09-05-carlsbad-snake/2021-09-05-carlsbad-snake_128.jpg" length="2838" type="image/jpeg"/><description>&lt;p>
&lt;picture title="for-scale.jpeg">
&lt;source
media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 1200px), (min-resolution: 192dpi) and (min-width: 1200px)"
srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_1200x0at2x.webp"
type="image/webp"
/>
&lt;source
media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 1200px), (min-resolution: 192dpi) and (min-width: 1200px)"
srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_1200x0at2x.jpg"
type="image/jpeg"
/>
&lt;source
media="min-width: 1200px"
srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_1200x0at1x.webp"
type="image/webp"
/>
&lt;source
media="min-width: 1200px"
srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_1200x0at1x.jpg"
type="image/jpeg"
/>
&lt;source
media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 992px), (min-resolution: 192dpi) and (min-width: 992px)"
srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_992x0at2x.webp"
type="image/webp"
/>
&lt;source
media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 992px), (min-resolution: 192dpi) and (min-width: 992px)"
srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_992x0at2x.jpg"
type="image/jpeg"
/>
&lt;source
media="min-width: 992px"
srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_992x0at1x.webp"
type="image/webp"
/>
&lt;source
media="min-width: 992px"
srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_992x0at1x.jpg"
type="image/jpeg"
/>
&lt;source
media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 768px), (min-resolution: 192dpi) and (min-width: 768px)"
srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_768x0at2x.webp"
type="image/webp"
/>
&lt;source
media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 768px), (min-resolution: 192dpi) and (min-width: 768px)"
srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_768x0at2x.jpg"
type="image/jpeg"
/>
&lt;source
media="min-width: 768px"
srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_768x0at1x.webp"
type="image/webp"
/>
&lt;source
media="min-width: 768px"
srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_768x0at1x.jpg"
type="image/jpeg"
/>
&lt;source
media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 480px), (min-resolution: 192dpi) and (min-width: 480px)"
srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_480x0at2x.webp"
type="image/webp"
/>
&lt;source
media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 480px), (min-resolution: 192dpi) and (min-width: 480px)"
srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_480x0at2x.jpg"
type="image/jpeg"
/>
&lt;source
media="min-width: 480px"
srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_480x0at1x.webp"
type="image/webp"
/>
&lt;source
media="min-width: 480px"
srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_480x0at1x.jpg"
type="image/jpeg"
/>
&lt;img
height="300"
style="max-height: 300px;"
src="https://camlittle.com/photos/2021-09-05-carlsbad-snake/for-scale.546f1609bfb56360452cebc89be7a441.jpeg"
alt="for-scale.jpeg"
/>
&lt;/picture>
&lt;/p></description><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/2021-09-05-carlsbad-snake_1280.jpg" />

  
<p>





  
  
  
  
  
  
  
  
  

  
  
  
  
  
  

  <picture title="for-scale.jpeg">
      
      
        
        
          







<source
    media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 1200px), (min-resolution: 192dpi) and (min-width: 1200px)"
    srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_1200x0at2x.webp"
    type="image/webp"
/>
<source
    media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 1200px), (min-resolution: 192dpi) and (min-width: 1200px)"
    srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_1200x0at2x.jpg"
    type="image/jpeg"
/>



<source
    
    media="min-width: 1200px"
    
    srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_1200x0at1x.webp"
    type="image/webp"
/>
<source
    
    media="min-width: 1200px"
    
    srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_1200x0at1x.jpg"
    type="image/jpeg"
/>


        
          







<source
    media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 992px), (min-resolution: 192dpi) and (min-width: 992px)"
    srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_992x0at2x.webp"
    type="image/webp"
/>
<source
    media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 992px), (min-resolution: 192dpi) and (min-width: 992px)"
    srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_992x0at2x.jpg"
    type="image/jpeg"
/>



<source
    
    media="min-width: 992px"
    
    srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_992x0at1x.webp"
    type="image/webp"
/>
<source
    
    media="min-width: 992px"
    
    srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_992x0at1x.jpg"
    type="image/jpeg"
/>


        
          







<source
    media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 768px), (min-resolution: 192dpi) and (min-width: 768px)"
    srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_768x0at2x.webp"
    type="image/webp"
/>
<source
    media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 768px), (min-resolution: 192dpi) and (min-width: 768px)"
    srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_768x0at2x.jpg"
    type="image/jpeg"
/>



<source
    
    media="min-width: 768px"
    
    srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_768x0at1x.webp"
    type="image/webp"
/>
<source
    
    media="min-width: 768px"
    
    srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_768x0at1x.jpg"
    type="image/jpeg"
/>


        
          







<source
    media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 480px), (min-resolution: 192dpi) and (min-width: 480px)"
    srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_480x0at2x.webp"
    type="image/webp"
/>
<source
    media="(-webkit-min-device-pixel-ratio: 2) and (min-width: 480px), (min-resolution: 192dpi) and (min-width: 480px)"
    srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_480x0at2x.jpg"
    type="image/jpeg"
/>



<source
    
    media="min-width: 480px"
    
    srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_480x0at1x.webp"
    type="image/webp"
/>
<source
    
    media="min-width: 480px"
    
    srcset="https://content.camlittle.com/site-media/photos/2021-09-05-carlsbad-snake/for-scale_546f1609bfb56360452cebc89be7a441_480x0at1x.jpg"
    type="image/jpeg"
/>


        
      
      <img
          height="300"
          
          style="max-height: 300px;"
          src="https://camlittle.com/photos/2021-09-05-carlsbad-snake/for-scale.546f1609bfb56360452cebc89be7a441.jpeg"
          alt="for-scale.jpeg"
      />
  </picture>



</p>


  <p>Carlsbad Caverns National Park, NM</p>
<p><span title="Camera">📷</span> Sony α6300</p>

  ]]></content:encoded></item><item><title>Don't merge that TODO</title><link>https://camlittle.com/posts/2021-07-24-dont-merge-todos/</link><pubDate>Sat, 24 Jul 2021 00:00:00 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/posts/2021-07-24-dont-merge-todos/</guid><enclosure url="https://camlittle.com/posts/2021-07-24-dont-merge-todos/thumbnail.png" length="2279" type="image/png"/><description>&lt;p>One of my standard code review comments is, &amp;ldquo;Why is this a TODO&amp;rdquo;? I believe you should always avoid TODO comments in your code. They&amp;rsquo;re a familiar pattern, but they defer responsibility and make problems harder to solve.&lt;/p>
&lt;p>In most cases, the TODO should be filed in your team&amp;rsquo;s issue tracker immediately and cross-linked with the pull request. Some people have processes that track issues using TODOs, but I don&amp;rsquo;t believe source code works well for issue tracking. Source code represents the current state of your product. Reviewing historical context, though possible with version control, is not the primary interface. Issue tracking systems are built for prioritization, tracking context, linking related issues, and assigning responsibility.&lt;/p></description><content:encoded><![CDATA[
  
<p>One of my standard code review comments is, &ldquo;Why is this a TODO&rdquo;? I believe you should always avoid TODO comments in your code. They&rsquo;re a familiar pattern, but they defer responsibility and make problems harder to solve.</p>
<p>In most cases, the TODO should be filed in your team&rsquo;s issue tracker immediately and cross-linked with the pull request. Some people have processes that track issues using TODOs, but I don&rsquo;t believe source code works well for issue tracking. Source code represents the current state of your product. Reviewing historical context, though possible with version control, is not the primary interface. Issue tracking systems are built for prioritization, tracking context, linking related issues, and assigning responsibility.</p>
<p>TODOs are usually used to defer technical debt. Moving them to your issue tracker&rsquo;s backlog allows prioritization against feature work by putting them in the same playing field and can be used as a metric to understand the depth of technical debt.</p>
<p>TODOs often describe an expected future change. It&rsquo;s tough to predict the future, so the TODO might not make sense when the next person reads them. I still encourage using comments to describe the problem and the reasons for not addressing it, but not to prescribe action. In the future, you&rsquo;ll have much better context when deciding how to fix the issue, or you might be able to just delete it.</p>
<p>Even though I don&rsquo;t like seeing TODOs in a codebase, I often use them in my feature branches and scan through the diff once I&rsquo;ve opened my draft PR to decide if it&rsquo;s ready for review. They&rsquo;re lighter weight than an issue tracker and can act as a checklist for a single task. Generally, though, they&rsquo;re an indicator of poor quality and have more responsible alternatives.</p>


  
  ]]></content:encoded></item><item><title>Sopot Summer</title><link>https://camlittle.com/photos/2021-07-18-sopot-summer/</link><pubDate>Sun, 18 Jul 2021 00:00:00 +0000</pubDate><guid isPermaLink="true">https://camlittle.com/photos/2021-07-18-sopot-summer/</guid><enclosure url="https://camlittle.com/photos/2021-07-18-sopot-summer/sopot-summer_128.jpg" length="5190" type="image/jpeg"/><description>&lt;p>Looking towards the beach town of Sopot on a warm summer evening.&lt;/p></description><content:encoded><![CDATA[

<img src="https://content.camlittle.com/photos/sopot-summer_1280.jpg" />

  
<p>Looking towards the beach town of Sopot on a warm summer evening.</p>


  <p>Sopot, Poland</p>
<p><span title="Camera">📷</span> Sony α6300</p>

  ]]></content:encoded></item><item><title>Personalized container debugging</title><link>https://camlittle.com/posts/2021-06-24-docker-toolbox/</link><pubDate>Thu, 24 Jun 2021 08:00:00 +0200</pubDate><guid isPermaLink="true">https://camlittle.com/posts/2021-06-24-docker-toolbox/</guid><enclosure url="https://camlittle.com/posts/2021-06-24-docker-toolbox/feature-terminal-screenshot.png" length="88396" type="image/png"/><description>&lt;p>In my work and personal life, most of the services I deploy these days run in containers, and I need to debug them frequently. Last year, I set up some automation to maintain a &amp;ldquo;toolbox&amp;rdquo; docker image that&amp;rsquo;s pre-configured with my &lt;a href="https://github.com/apexskier/dotfiles">dotfiles&lt;/a> and the tools I use day-to-day.&lt;/p>
&lt;p>Generally, I try to follow standard conventions in my personal configuration. I try to use the command line in a way that I don&amp;rsquo;t feel like I&amp;rsquo;m constantly stumbling in unfamiliar environments like remote servers, docker images such as &lt;a href="https://hub.docker.com/_/node">&lt;code>node&lt;/code>&lt;/a> or &lt;a href="https://hub.docker.com/_/golang">&lt;code>golang&lt;/code>&lt;/a>, or a fresh Raspberry Pi. This practice also makes it much easier to help my coworkers—I can work within their terminal or help instruct them without needing to translate commands. It can be painful to watch someone with muscle memory for custom &lt;a rel="noreferrer" href="https://gist.github.com/mwhite/6887990#my-aliases">git aliases&lt;/a> or non-standard shells working in a freshly provisioned EC2 instance.&lt;/p></description><content:encoded><![CDATA[
  
<p>In my work and personal life, most of the services I deploy these days run in containers, and I need to debug them frequently. Last year, I set up some automation to maintain a &ldquo;toolbox&rdquo; docker image that&rsquo;s pre-configured with my <a href="https://github.com/apexskier/dotfiles">dotfiles</a> and the tools I use day-to-day.</p>
<p>Generally, I try to follow standard conventions in my personal configuration. I try to use the command line in a way that I don&rsquo;t feel like I&rsquo;m constantly stumbling in unfamiliar environments like remote servers, docker images such as <a href="https://hub.docker.com/_/node"><code>node</code></a> or <a href="https://hub.docker.com/_/golang"><code>golang</code></a>, or a fresh Raspberry Pi. This practice also makes it much easier to help my coworkers—I can work within their terminal or help instruct them without needing to translate commands. It can be painful to watch someone with muscle memory for custom <a rel="noreferrer" href="https://gist.github.com/mwhite/6887990#my-aliases">git aliases</a> or non-standard shells working in a freshly provisioned EC2 instance.</p>
<p>Even considering these benefits, a lot of customization provides more than enough value to be worthwhile. A familiar shell makes the debugging experience much more pleasant, and installing the tools I need to investigate takes time. I&rsquo;ve also spent years polishing my bash prompt, and I love using it.</p>
<p>
<img src="./feature-terminal-screenshot.png" alt="Screenshot of my terminal running the docker toolbox" />

</p>
<p>My most common use case is debugging docker and Kubernetes networking behavior. For example, I can spin up a pod in our cluster running the toolbox image and use <code>curl</code> and <code>dig</code> to verify DNS behavior.</p>
<p>I also occasionally need to run an image that takes a long time to start or run using a wrapper utility that doesn&rsquo;t bind ports on my host. In this case, I can run my toolbox within a local docker network and connect to the image without needing to restart it or dig into the internals of how it&rsquo;s initialized.</p>
<p>Keeping my toolbox image up to date is automated directly in my <a href="https://github.com/apexskier/dotfiles/">dotfiles repository</a>, using the same scripts I use when setting up a new computer. Whenever I push to the default branch, a <a href="https://github.com/apexskier/dotfiles/actions/workflows/dockerpublish.yml">GitHub action</a> builds the image and publishes it to GitHub&rsquo;s container registry<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>.</p>
<p>The docker image builds from <code>debian:stable-slim</code>, copies my dotfiles, runs my installer and setup scripts, then runs <code>bash</code>. Putting this together was a nice way to verify the cross-compatibility of my setup. The only serious issue I ran into was ensuring my setup scripts could run non-interactively. I also took the opportunity to double-check that my dotfiles don&rsquo;t contain too much personal information (like a preconfigured git user) to let other people use my setup without accidentally impersonating me.</p>
<p>Try it yourself by running</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">docker run --rm -it ghcr.io/apexskier/dotfiles/toolbox
</span></span></code></pre></div><p>You can <a href="https://github.com/apexskier/dotfiles#docker-image">read more about it</a> and see <a href="https://github.com/apexskier/dotfiles/blob/main/Dockerfile">exactly what&rsquo;s installed</a> by visiting my dotfiles.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>I also publish to Docker Hub, which I used before GitHub supported anonymous container access.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>


  
  ]]></content:encoded></item></channel></rss>