<rss version="2.0" xmlns:atom="http://www.w3.org/2005/atom"><channel><atom:link href="https://rkrt.net/feed.rss" rel="self" type="application/rss+xml"/><title>Tyler Reckart</title><link>https://rkrt.net</link><description>Tyler Reckart is a programmer and designer based in Philadelphia, PA.</description><language>en-us</language><item><title>New Year, New Colors</title><pubDate>Fri, 16 Jan 2026 00:00:00 GMT</pubDate><link>https://rkrt.net/new-year-new-colors</link><guid>https://rkrt.net/new-year-new-colors</guid><description><![CDATA[<p>I rebuilt my website over the holidays. Not because anything was broken—the old one worked fine—but because I wanted something that felt more like me, and consolidated many of the various domains that I managed at the same time.</p>
<p>The biggest change isn&#39;t structural. I built the templating engine behind this website back in 2020, and it&#39;s still going strong. It&#39;s the little sparkle in the header that randomizes the entire color palette.</p>
<h2 id="why-randomize">Why Randomize?</h2>
<p>I&#39;ve always had a bold, retro-influenced design aesthetic—I&#39;ve never wanted my home on the web to look like every other website. Most sites pick a palette and assign meaning to it. Blue means trust, green means growth, whatever. The truth is, these choices are mostly arbitrary. I could have gone with mauve or mustard or seafoam and it wouldn&#39;t change what I have to say about my interests.</p>
<p>So instead of pretending the color choice is meaningful, I made it playful. Click the icon, get a new palette. The structure stays, the skin changes.</p>
<h2 id="controlled-randomness">Controlled Randomness</h2>
<p>Pure randomness would be chaos. You&#39;d get unreadable combinations—yellow text on white backgrounds, clashing accents, muddy pastels. The trick is constraining the randomness to a space where everything works.</p>
<p>The system defines three structural colors tied to a single base hue:</p>
<pre><code class="language-javascript"><span class="hljs-keyword">var</span> baseHue = <span class="hljs-built_in">Math</span>.floor(<span class="hljs-built_in">Math</span>.random() * <span class="hljs-number">360</span>);

<span class="hljs-comment">// Dark color (header, text) - base hue, low lightness</span>
<span class="hljs-keyword">var</span> newBlack =
  <span class="hljs-string">&quot;hsl(&quot;</span> +
  baseHue +
  <span class="hljs-string">&quot;, &quot;</span> +
  (<span class="hljs-number">30</span> + <span class="hljs-built_in">Math</span>.floor(<span class="hljs-built_in">Math</span>.random() * <span class="hljs-number">40</span>)) +
  <span class="hljs-string">&quot;%, &quot;</span> +
  (<span class="hljs-number">5</span> + <span class="hljs-built_in">Math</span>.floor(<span class="hljs-built_in">Math</span>.random() * <span class="hljs-number">12</span>)) +
  <span class="hljs-string">&quot;%)&quot;</span>;

<span class="hljs-comment">// Light color (cards, backgrounds) - complementary hue (180° offset), high lightness</span>
<span class="hljs-keyword">var</span> newWhite =
  <span class="hljs-string">&quot;hsl(&quot;</span> +
  ((baseHue + <span class="hljs-number">180</span>) % <span class="hljs-number">360</span>) +
  <span class="hljs-string">&quot;, &quot;</span> +
  (<span class="hljs-number">10</span> + <span class="hljs-built_in">Math</span>.floor(<span class="hljs-built_in">Math</span>.random() * <span class="hljs-number">30</span>)) +
  <span class="hljs-string">&quot;%, &quot;</span> +
  (<span class="hljs-number">92</span> + <span class="hljs-built_in">Math</span>.floor(<span class="hljs-built_in">Math</span>.random() * <span class="hljs-number">6</span>)) +
  <span class="hljs-string">&quot;%)&quot;</span>;

<span class="hljs-comment">// Page background - analogous hue (90° offset), mid lightness</span>
<span class="hljs-keyword">var</span> newBg =
  <span class="hljs-string">&quot;hsl(&quot;</span> +
  ((baseHue + <span class="hljs-number">90</span>) % <span class="hljs-number">360</span>) +
  <span class="hljs-string">&quot;, &quot;</span> +
  (<span class="hljs-number">20</span> + <span class="hljs-built_in">Math</span>.floor(<span class="hljs-built_in">Math</span>.random() * <span class="hljs-number">50</span>)) +
  <span class="hljs-string">&quot;%, &quot;</span> +
  (<span class="hljs-number">50</span> + <span class="hljs-built_in">Math</span>.floor(<span class="hljs-built_in">Math</span>.random() * <span class="hljs-number">25</span>)) +
  <span class="hljs-string">&quot;%)&quot;</span>;
</code></pre>
<p>The 180° and 90° offsets are doing most of the work here. Complementary colors (180° apart on the color wheel) create natural contrast—the header and content cards will always push against each other. The analogous offset (90°) for the page background mediates between them, creating cohesion without monotony.</p>
<p>These relationships hold no matter where the base hue lands. A warm base gives you burgundy headers with cream cards on a dusty rose background. A cool base gives you navy headers with ice-blue cards on a sage background. The math is the same; the mood shifts.</p>
<h2 id="accent-colors">Accent Colors</h2>
<p>The structural colors carry the palette, but accent colors (links, dates, highlights) need their own treatment. These are randomized independently:</p>
<pre><code class="language-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">randomHSL</span>(<span class="hljs-params">minS, maxS, minL, maxL</span>) </span>{
  <span class="hljs-keyword">var</span> h = <span class="hljs-built_in">Math</span>.floor(<span class="hljs-built_in">Math</span>.random() * <span class="hljs-number">360</span>);
  <span class="hljs-keyword">var</span> s = minS + <span class="hljs-built_in">Math</span>.floor(<span class="hljs-built_in">Math</span>.random() * (maxS - minS));
  <span class="hljs-keyword">var</span> l = minL + <span class="hljs-built_in">Math</span>.floor(<span class="hljs-built_in">Math</span>.random() * (maxL - minL));
  <span class="hljs-keyword">return</span> <span class="hljs-string">&quot;hsl(&quot;</span> + h + <span class="hljs-string">&quot;, &quot;</span> + s + <span class="hljs-string">&quot;%, &quot;</span> + l + <span class="hljs-string">&quot;%)&quot;</span>;
}

<span class="hljs-keyword">var</span> newYellow = randomHSL(<span class="hljs-number">90</span>, <span class="hljs-number">100</span>, <span class="hljs-number">50</span>, <span class="hljs-number">65</span>);
<span class="hljs-keyword">var</span> newBlue = randomHSL(<span class="hljs-number">80</span>, <span class="hljs-number">100</span>, <span class="hljs-number">40</span>, <span class="hljs-number">60</span>);
<span class="hljs-keyword">var</span> newGreen = randomHSL(<span class="hljs-number">70</span>, <span class="hljs-number">100</span>, <span class="hljs-number">35</span>, <span class="hljs-number">55</span>);
</code></pre>
<p>Each accent has its own saturation and lightness bounds tuned to what that role typically needs. Yellows stay high saturation and lightness so they pop. Greens stay lower lightness so they don&#39;t wash out. The hue floats freely, but the intensity is controlled.</p>
<p>This means you can get unexpected combinations—a warm palette with a cool blue accent, or muted backgrounds with a saturated pink link color. It works because these accents are used sparingly. They&#39;re punctuation, not prose.</p>
<h2 id="persistence">Persistence</h2>
<p>The palette persists in localStorage. Once you roll a palette, that&#39;s &quot;your&quot; version of the site until you change it. Different visitors, different memories of the same place.</p>
<pre><code class="language-javascript">colorVars.forEach(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">c</span>) </span>{
  <span class="hljs-built_in">localStorage</span>.setItem(<span class="hljs-string">&quot;theme-&quot;</span> + c, root.style.getPropertyValue(<span class="hljs-string">&quot;--&quot;</span> + c));
});
</code></pre>
<p>For a personal website, I think that&#39;s the right energy.</p>
<hr>
<p>The source is <a href="https://github.com/tylerreckart/rkrt.net">on GitHub</a> if you want to see how the rest of the site works.</p>
]]></description></item><item><title>Guiding Users Reviews: Better Prompting in iOS</title><pubDate>Sat, 07 Jun 2025 00:00:00 GMT</pubDate><link>https://rkrt.net/better-reviews-on-ios</link><guid>https://rkrt.net/better-reviews-on-ios</guid><description><![CDATA[<p>Over the past few years, as I have published multiple projects to the iOS app store, one of the most difficult aspects post-launch has been soliciting user feedback intelligently. As part of the development of my latest app, <a href="https://apps.apple.com/us/app/solar-sun-tracker-forecast/id6745826724">Solar</a>, I wrote a simple review engine with the goal of solving this problem. It&#39;s smart about when it asks, focusing on those &quot;happy path&quot; moments when your users are genuinely enjoying your app.</p>
<p>I&#39;ve now open-sourced that engine and it&#39;s available on Github as <a href="https://github.com/tylerreckart/HappyPath.git">HappyPath</a>.</p>
<h3 id="what-it-does-and-why-i-think-its-cool">What it Does (and Why I Think It&#39;s Cool)</h3>
<ul>
<li><strong>Smart Asking:</strong> It&#39;s not just a simple counter. HappyPath looks at how many times your app has been opened, how many &quot;significant actions&quot; a user has taken, and how long they&#39;ve been using your app. It&#39;s all about catching them when they&#39;re happy.</li>
<li><strong>You&#39;re in Control:</strong> I&#39;ve made the thresholds fully customizable. You decide how many launches, actions, or days pass before a prompt is considered. This means you can tailor it perfectly for your app.</li>
<li><strong>Native &amp; Seamless:</strong> It uses Apple&#39;s own SKStoreReviewController, so the review prompt feels like a natural part of the app, not an annoying pop-up.</li>
<li><strong>No Nagging:</strong> It&#39;s version-aware. Once a user has reviewed a specific version, they won&#39;t be asked again for that same version. Phew!</li>
<li><strong>Easy to Use:</strong> It&#39;s built with a simple shared instance, so integrating it into your project is a breeze.</li>
</ul>
<h3 id="a-peek-under-the-hood">A Peek Under the Hood</h3>
<p>HappyPath keeps track of a few key things in UserDefaults:</p>
<ul>
<li><code>hp_appLaunchCount</code>: How many times my app has been launched.</li>
<li><code>hp_significantActionCount</code>: How many &quot;happy path&quot; actions a user has completed.</li>
<li><code>hp_lastReviewRequestDate</code>: The last time a review was asked for.</li>
<li><code>hp_lastVersionPromptedForReview</code>: The app version when a review was last prompted.</li>
<li><code>hp_firstLaunchDate</code>: The very first time the app ran.</li>
</ul>
<p>The magic happens in <code>requestReviewIfAppropriate()</code>. It checks all these conditions, and if everything aligns, it politely asks for that review.</p>
<p>You can use my default settings, or set your own thresholds:</p>
<pre><code class="language-swift"><span class="hljs-keyword">import</span> HappyPath
<span class="hljs-keyword">import</span> SwiftUI

@main
<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">MyApp</span>: <span class="hljs-title">App</span> </span>{
    <span class="hljs-keyword">init</span>() {
        <span class="hljs-comment">// I usually just go with the defaults:</span>
        <span class="hljs-number">_</span> = <span class="hljs-type">ReviewManager</span>.shared

        <span class="hljs-comment">// But if I want to tweak it:</span>
        <span class="hljs-comment">// let customThresholds = ReviewThresholds(</span>
        <span class="hljs-comment">//     minLaunchesBeforePrompt: 10, // My app needs more launches</span>
        <span class="hljs-comment">//     minSignificantActionsBeforePrompt: 5, // More actions needed</span>
        <span class="hljs-comment">//     minDaysSinceFirstLaunchBeforePrompt: 14, // Give them 2 weeks</span>
        <span class="hljs-comment">//     minDaysBetweenPrompts: 180 // Only every 6 months</span>
        <span class="hljs-comment">// )</span>
        <span class="hljs-comment">// _ = ReviewManager(thresholds: customThresholds)</span>
    }

    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">Scene</span> {
        <span class="hljs-type">WindowGroup</span> {
            <span class="hljs-type">ContentView</span>()
        }
    }
}
</code></pre>
<h3 id="track-launches">Track Launches</h3>
<p>I call <code>incrementAppLaunchCount()</code> every time my app starts up. Easiest place is usually in your main ContentView&#39;s onAppear:</p>
<pre><code class="language-swift"><span class="hljs-keyword">import</span> SwiftUI
<span class="hljs-keyword">import</span> HappyPath

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ContentView</span>: <span class="hljs-title">View</span> </span>{
    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">Text</span>(<span class="hljs-string">&quot;Welcome to my awesome app!&quot;</span>)
            .onAppear {
                <span class="hljs-type">ReviewManager</span>.shared.incrementAppLaunchCount()
                <span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;App Launch Count Incremented!&quot;</span>)
            }
    }
}
</code></pre>
<h3 id="ask-when-active">Ask When Active</h3>
<p>I also trigger <code>requestReviewOnAppActive()</code> when my app becomes active. I like a little delay to ensure the UI is fully loaded:</p>
<pre><code class="language-swift"><span class="hljs-keyword">import</span> SwiftUI
<span class="hljs-keyword">import</span> HappyPath

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ContentView</span>: <span class="hljs-title">View</span> </span>{
    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">Text</span>(<span class="hljs-string">&quot;Your amazing content is here.&quot;</span>)
            .onAppear {
                <span class="hljs-type">ReviewManager</span>.shared.incrementAppLaunchCount()
            }
            .onReceive(<span class="hljs-type">NotificationCenter</span>.<span class="hljs-keyword">default</span>.publisher(<span class="hljs-keyword">for</span>: <span class="hljs-type">UIApplication</span>.didBecomeActiveNotification)) { <span class="hljs-number">_</span> <span class="hljs-keyword">in</span>
                <span class="hljs-comment">// Give the UI a second to settle</span>
                <span class="hljs-type">DispatchQueue</span>.main.asyncAfter(deadline: .now() + <span class="hljs-number">1.0</span>) {
                    <span class="hljs-type">ReviewManager</span>.shared.requestReviewOnAppActive()
                }
            }
    }
}
</code></pre>
<p>If conditions aren&#39;t met, the system logs might look like this:</p>
<pre><code>🌟 HappyPath: Not prompting (launch count 1 &lt; 5).
🌟 HappyPath: Not prompting (days since first launch 0 &lt; 7).
</code></pre>
<p>But when they are, it&#39;s a beautiful sight:</p>
<pre><code>🌟 HappyPath: All conditions met. Requesting review.
🌟 HappyPath: Review requested. Last prompt date and version updated.
</code></pre>
<h3 id="log-happy-moments">Log Happy Moments</h3>
<p>This is the key! I call logSignificantAction() whenever a user does something great in my app – saves something, completes a level, whatever makes sense for your app.</p>
<pre><code class="language-swift"><span class="hljs-keyword">import</span> SwiftUI
<span class="hljs-keyword">import</span> HappyPath

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">TaskCompletionView</span>: <span class="hljs-title">View</span> </span>{
    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">Button</span>(<span class="hljs-string">&quot;Mark Task as Done&quot;</span>) {
            <span class="hljs-comment">// My task completion logic here</span>
            <span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;Task completed successfully!&quot;</span>)
            <span class="hljs-type">ReviewManager</span>.shared.logSignificantAction()
        }
        .padding()
    }
}
</code></pre>
<p>System logs will confirm the action:</p>
<pre><code>👍 HappyPath: Significant action count: 1
</code></pre>
<p>(And if that action pushes the total over a threshold, a review prompt might pop up right after!)</p>
<p>Find HappyPath on GitHub: <a href="https://github.com/tylerreckart/HappyPath.git">https://github.com/tylerreckart/HappyPath.git</a></p>
<p>Hope this helps you on your own app development journey! Let me know what you think.</p>
<p>Happy coding,<br><strong>Tyler</strong></p>
]]></description></item><item><title>Aulos update- moving to pure seed hardware</title><pubDate>Fri, 31 Jan 2025 00:00:00 GMT</pubDate><link>https://rkrt.net/aulos-update</link><guid>https://rkrt.net/aulos-update</guid><description><![CDATA[<p><img src="https://s3.us-east-2.amazonaws.com/reckart.blog-images/1.jpeg" alt="Image showing the current Aulos hardware prototype running through an oscilloscope"></p>
<p>In my last post, <a href="https://reckart.blog/posts/sound-synthesis-in-cpp/">Sound Synthesis in C++</a>, I introduced the instrument I have been prototyping for the last few months on the Daisy Patch module. Since then, things have evolved quite a bit—I&#39;ve ditched the Patch submodule, gone all-in on the Seed hardware, and added additional components such as multiplexers to unlock way more control over CV and potentiometer input.</p>
<p>This was the next logical step in the prototyping process. If I wanted to turn the prototype into a real instrument, I needed to ditch the pre-built hardware and start prototyping my own circuit. Sure, this means designing a custom PCB and handling more of the low-level wiring myself, but that’s part of the fun, right?</p>
<p>One of the main limitations of the Seed is the number of available analog inputs. In order to circumvent this, I&#39;ve utilized two <strong>CD74HC4067 multiplexers</strong>—game-changing ICs that lets me drastically expand the number of CV and pot inputs without needing extra ADC pins.</p>
<p>This setup means I can have a ton of tweakable parameters without running out of inputs, which is huge for the amount of tweakability that I want this instrument to have.</p>
<h2 id="whats-next">What’s Next?</h2>
<p>Now that the hardware is mostly sorted, here’s what’s next on my plate:</p>
<ol>
<li><strong>Refining the firmware</strong>—tweaking CV scaling, optimizing scanning, and making sure everything runs rock solid.</li>
<li><strong>Real-world testing</strong>—pushing the multiplexer setup in different modulation scenarios to see how it holds up.</li>
<li><strong>Finalizing the hardware design</strong>—getting it ready for a clean, reliable build and testing initial PCB designs.</li>
</ol>
<p>Got thoughts, feedback, or just want to nerd out about synth design? Drop me a message or follow along on <a href="https://github.com/tylerreckart/aulos">GitHub</a>!</p>
]]></description></item><item><title>Aulos- Sound Synthesis in C++</title><pubDate>Wed, 08 Jan 2025 00:00:00 GMT</pubDate><link>https://rkrt.net/sound-synthesis-in-cpp</link><guid>https://rkrt.net/sound-synthesis-in-cpp</guid><description><![CDATA[<p>Over the last few years, I&#39;ve become very interested in modular synthesis. I&#39;ve played instruments since I was a child, and as a programmer, the composability of a modular synthesizer has always been very attractive to me. One early form of synthesis technology that has fascinated me for years is the <a href="https://www.youtube.com/watch?v=KqnLZXOySyY">Trautonium</a>. When I first heard recordings of the instrument, its rich harmonic textures and gliding tones captured my imagination. It wasn’t just the sound itself but the way the instrument invited a performer to interact with it—fluid, expressive, and unlike anything a traditional keyboard could achieve.</p>
<p>Over the course of the last year, I have been heavily invested in building and designing my own instruments that allow for deep interaction with sound. Modular synthesis taught me that the process of shaping sound can be as rewarding as the final result. The Trautonium’s focus on subharmonic synthesis felt like a perfect match for the kind of exploratory sound design I enjoy—an intersection of historical technique and modern modular sensibilities.</p>
<p>This project is my attempt to modernize the sound of the Trautonium using digital signal processing (DSP). The implementation starts with a core oscillator that generates a fundamental frequency, and from there, additional oscillators create subharmonics by dividing the fundamental pitch. Each subharmonic oscillator contributes harmonics that can be dynamically mixed, recreating the harmonic depth that made the original instrument so special. Here&#39;s how this works programmatically:</p>
<pre><code class="language-cpp"><span class="hljs-comment">// Generate subharmonic frequencies</span>
pitch_sub[<span class="hljs-number">0</span>] = pitch_main / <span class="hljs-number">2.f</span>;
pitch_sub[<span class="hljs-number">1</span>] = pitch_main / <span class="hljs-number">3.f</span>;
pitch_sub[<span class="hljs-number">2</span>] = pitch_main / <span class="hljs-number">4.f</span>;
pitch_sub[<span class="hljs-number">3</span>] = pitch_main / <span class="hljs-number">5.f</span>;
</code></pre>
<p>Each oscillator supports multiple waveforms—sine, triangle, saw, ramp, and square—allowing for additional tone shaping. Another key feature is scale quantization. The original Trautonium offered continuous pitch control via a ribbon controller, but in this digital version, MIDI notes can be quantized to predefined scales for musical structure. Pitch information is quantized like this:</p>
<pre><code class="language-cpp"><span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">QuantizeToScale</span><span class="hljs-params">(<span class="hljs-keyword">int</span> note, <span class="hljs-keyword">int</span> scale_index)</span>
</span>{
    <span class="hljs-keyword">int</span> octave = note / <span class="hljs-number">12</span>;
    <span class="hljs-keyword">int</span> note_in_octave = note % <span class="hljs-number">12</span>;
    <span class="hljs-keyword">const</span> <span class="hljs-keyword">int</span> *scale = scales[scale_index];

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">7</span> &amp;&amp; scale[i] != <span class="hljs-number">-1</span>; i++)
    {
        <span class="hljs-keyword">if</span> (note_in_octave &lt;= scale[i])
            <span class="hljs-keyword">return</span> octave * <span class="hljs-number">12</span> + scale[i];
    }
    <span class="hljs-keyword">return</span> (octave + <span class="hljs-number">1</span>) * <span class="hljs-number">12</span> + scale[<span class="hljs-number">0</span>];
}
</code></pre>
<p>Please take a look at this short demo of the current firmware for insight into the Aulos&#39; sonic character:</p>
<iframe width="560" height="315" src="https://www.youtube.com/embed/ZTuEySEleiw?si=QhjEwcg9u15bxqwA" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

<p>If you&#39;re curious to learn more or experiment with this implementation, the project is <a href="https://github.com/tylerreckart/aulos">open-source</a>.</p>
]]></description></item><item><title>Writing a Tab View Controller in Swift UI</title><pubDate>Thu, 01 Jun 2023 00:00:00 GMT</pubDate><link>https://rkrt.net/writing-a-tab-view-controller-in-swiftui</link><guid>https://rkrt.net/writing-a-tab-view-controller-in-swiftui</guid><description><![CDATA[<p>In my opinion, Swift UI is a fantastic jumping-off point for less experienced developers wanting to get into iOS development. It reminds me of my earliest days of programming; just turn the clock back 15 years and swap Swift with HTML. One of the great things is that <em>a lot</em> of the groundwork has been laid by Apple. It&#39;s relatively easy to use Swift&#39;s built-in components to put together an app that looks at home on Apple&#39;s platforms. However, Swift&#39;s built-ins aren&#39;t exactly the most flexible components when it comes to adapting for custom UIs. Writing a custom navigation controller gives you the ability to completely control the user experience as they flow through your apps. Let&#39;s see just how easy that is.</p>
<p>So, the first thing we&#39;ll need to do is define the view hierarchy. When working with custom navigation, I have found that it is often easier and more natural to have the tab view context live above the app&#39;s main navigation state, then nesting navigation views within each of the individual tab paths. Let&#39;s define these parent-level views through an <code>enum</code> declaration.</p>
<pre><code class="language-swift"><span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">TabView</span> </span>{
    <span class="hljs-keyword">case</span> home
    <span class="hljs-keyword">case</span> search
    <span class="hljs-keyword">case</span> discover
    <span class="hljs-keyword">case</span> profile
    <span class="hljs-keyword">case</span> settings
}
</code></pre>
<p>Then we need to consider the context of how the tab bar UI will render in relation to our views. The simplest way to do this is by using a <code>ZStack {}</code> wrapper to render the tab bar higher up in the application&#39;s z-axis and thus above each of our child views. The child views will need to account for the height of the wrapper, but that is a simple calculation. All we need to do to handle the view switching is utilize a simple switch statement that will render the target view based on the application&#39;s current state.</p>
<pre><code class="language-swift"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ContentView</span>: <span class="hljs-title">View</span> </span>{
    <span class="hljs-comment">// Define the currently active view. This should default to the home screen.</span>
    @<span class="hljs-type">State</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> activeView: <span class="hljs-type">TabView</span> = .home

    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">ZStack</span> {
            <span class="hljs-keyword">switch</span> (activeView) {
                <span class="hljs-keyword">case</span> .home:
                    <span class="hljs-type">Color</span>.red
                <span class="hljs-keyword">case</span> .search:
                    <span class="hljs-type">Color</span>.blue
                <span class="hljs-keyword">case</span> .discover:
                    <span class="hljs-type">Color</span>.green
                <span class="hljs-keyword">case</span> .profile:
                    <span class="hljs-type">Color</span>.yellow
                <span class="hljs-keyword">case</span> .settings:
                    <span class="hljs-type">Color</span>.purple
            }

            <span class="hljs-comment">// TabBar(activeView: $activeView)</span>
        }
    }
}
</code></pre>
<p>We can then define the markup to render the view itself. For now, each tab option will be represented by an SF Symbol until the button component is defined.</p>
<pre><code class="language-swift"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">TabBar</span>: <span class="hljs-title">View</span> </span>{
    @<span class="hljs-type">Binding</span> <span class="hljs-keyword">var</span> activeView: <span class="hljs-type">TabView</span>

    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">VStack</span>(spacing: <span class="hljs-number">0</span>) {
            <span class="hljs-type">Spacer</span>()
            
            <span class="hljs-type">Rectangle</span>()
                .fill(<span class="hljs-type">Color</span>(.systemGray5))
                .frame(height: <span class="hljs-number">0.5</span>)
            
            <span class="hljs-type">HStack</span>(spacing: <span class="hljs-number">0</span>) {
                <span class="hljs-type">Image</span>(systemName: <span class="hljs-string">&quot;1.circle.fill&quot;</span>)
                    .frame(maxWidth: .infinity)
                <span class="hljs-type">Image</span>(systemName: <span class="hljs-string">&quot;2.circle.fill&quot;</span>)
                    .frame(maxWidth: .infinity)
                <span class="hljs-type">Image</span>(systemName: <span class="hljs-string">&quot;3.circle.fill&quot;</span>)
                    .frame(maxWidth: .infinity)
                <span class="hljs-type">Image</span>(systemName: <span class="hljs-string">&quot;4.circle.fill&quot;</span>)
                    .frame(maxWidth: .infinity)
                <span class="hljs-type">Image</span>(systemName: <span class="hljs-string">&quot;5.circle.fill&quot;</span>)
                    .frame(maxWidth: .infinity)
            }
            .padding(.top, <span class="hljs-number">15</span>)
            .padding(.bottom, <span class="hljs-number">30</span>)
            .background(<span class="hljs-type">Color</span>(.systemBackground))
        }
        .edgesIgnoringSafeArea(.bottom)
    }
}
</code></pre>
<p>At this stage you should have a screen that looks similar to this:</p>
<p><img src="https://s3.us-east-2.amazonaws.com/reckart.blog-images/test.jpg" alt="An image showing the in-progress tab view UI"></p>
<p>The tab bar group item is similarly straightforward in its markup:</p>
<pre><code class="language-swift"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">TabBarGroupItem</span>: <span class="hljs-title">View</span> </span>{
    @<span class="hljs-type">Binding</span> <span class="hljs-keyword">var</span> activeView: <span class="hljs-type">TabView</span>

    <span class="hljs-keyword">var</span> targetView: <span class="hljs-type">TabView</span>
    <span class="hljs-keyword">var</span> image: <span class="hljs-type">String</span>
    
    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">Button</span>(action: {
            withAnimation(.spring()) {
                <span class="hljs-comment">// Update the view.</span>
                <span class="hljs-keyword">self</span>.activeView = targetView
                <span class="hljs-comment">// Provide haptic feedback.</span>
                <span class="hljs-type">UIImpactFeedbackGenerator</span>(style: .medium).impactOccurred()
            }
        }) {
            <span class="hljs-type">VStack</span>(spacing: <span class="hljs-number">6</span>) {
                <span class="hljs-type">Image</span>(systemName: image)
                    .font(.system(size: <span class="hljs-number">22</span>, weight: .medium))
                    .foregroundColor(activeView == targetView ? .blue : .gray)
                
                <span class="hljs-type">Circle</span>()
                    .fill(activeView == targetView ? .blue : .clear)
                    .frame(width: <span class="hljs-number">4</span>, height: <span class="hljs-number">4</span>)
            }
            .frame(maxWidth: .infinity)
        }
    }
}
</code></pre>
<p>With that defined, the <code>TabBar</code> component can be updated with the view-switching markup.</p>
<pre><code class="language-swift"><span class="hljs-type">HStack</span>(spacing: <span class="hljs-number">0</span>) {
    <span class="hljs-type">TabBarGroupItem</span>(activeView: $activeView, targetView: .home, image: <span class="hljs-string">&quot;house&quot;</span>)
    <span class="hljs-type">TabBarGroupItem</span>(activeView: $activeView, targetView: .search, image: <span class="hljs-string">&quot;magnifyingglass&quot;</span>)
    <span class="hljs-type">TabBarGroupItem</span>(activeView: $activeView, targetView: .discover, image: <span class="hljs-string">&quot;safari&quot;</span>)
    <span class="hljs-type">TabBarGroupItem</span>(activeView: $activeView, targetView: .profile, image: <span class="hljs-string">&quot;person.crop.circle&quot;</span>)
    <span class="hljs-type">TabBarGroupItem</span>(activeView: $activeView, targetView: .settings, image: <span class="hljs-string">&quot;gearshape&quot;</span>)
}
</code></pre>
<p>Now there are just a few more pieces we&#39;ll need to implement. Because we&#39;re using the z-axis to render the tab bar above the application&#39;s content, a shared <code>Screen</code> view can be used to provide a wrapper around the content for each tab that will let you adjust behavior and placement with a few simple modifiers. As seen in the snippet below, this is where you can use Swift&#39;s built-in transition modifiers to add some flavor to the way your views switch.</p>
<pre><code class="language-swift"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">Screen</span>&lt;<span class="hljs-title">Content</span>&gt;: <span class="hljs-title">View</span> <span class="hljs-title">where</span> <span class="hljs-title">Content</span>: <span class="hljs-title">View</span> </span>{
    <span class="hljs-keyword">let</span> content: () -&gt; <span class="hljs-type">Content</span>

    <span class="hljs-keyword">init</span>(@<span class="hljs-type">ViewBuilder</span> content: @escaping () -&gt; <span class="hljs-type">Content</span>) {
        <span class="hljs-keyword">self</span>.content = content
    }
    
    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        content()
            .edgesIgnoringSafeArea(.top)
            .transition(.push(from: .leading))
    }
}
</code></pre>
<p>If you&#39;re feeling ambitious, you could easily implement an intelligent &quot;push&quot; animation by defining the navigation scheme and using the index of the current screen with the target screen to determine which direction to animate the push from.</p>
<p>With that, the <code>Screen</code> view wrapper can be integrated into our application&#39;s root view.</p>
<pre><code class="language-swift"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ContentView</span>: <span class="hljs-title">View</span> </span>{
    @<span class="hljs-type">State</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> activeView: <span class="hljs-type">TabView</span> = .home

    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">ZStack</span> {
            <span class="hljs-keyword">switch</span> (activeView) {
                <span class="hljs-keyword">case</span> .home:
                    <span class="hljs-type">Screen</span> { <span class="hljs-type">Color</span>.red }
                <span class="hljs-keyword">case</span> .search:
                    <span class="hljs-type">Screen</span> { <span class="hljs-type">Color</span>.blue }
                <span class="hljs-keyword">case</span> .discover:
                    <span class="hljs-type">Screen</span> { <span class="hljs-type">Color</span>.green }
                <span class="hljs-keyword">case</span> .profile:
                    <span class="hljs-type">Screen</span> { <span class="hljs-type">Color</span>.red }
                <span class="hljs-keyword">case</span> .settings:
                    <span class="hljs-type">Screen</span> { <span class="hljs-type">Color</span>.purple }
            }

            <span class="hljs-type">TabBar</span>(activeView: $activeView)
        }
    }
}
</code></pre>
<p>That&#39;s it! You now have a tab view controller at the root of your app that you have complete control over. You can add custom animations between views, interactions on tab changes, and more. </p>
<p><img src="https://s3.us-east-2.amazonaws.com/reckart.blog-images/test.gif" alt="An animated GIF of the tab controller UI resulting from this tutorial"></p>
]]></description></item><item><title>For The Love of The Game</title><pubDate>Tue, 14 Mar 2023 00:00:00 GMT</pubDate><link>https://rkrt.net/for-the-love-of-the-game</link><guid>https://rkrt.net/for-the-love-of-the-game</guid><description><![CDATA[<p>I played <a href="https://magic.wizards.com/en">Magic: The Gathering</a> for the first time in 2006 when I was eleven years old. I remember spending time at a neighbor&#39;s house over the summer and seeing that friend&#39;s older brother playing this card game that I was wholly unfamiliar with. I was instantly interested.</p>
<p>By this time, I had spent much of my childhood playing collectible card games. First playing Pokemon as an extension of the Gameboy games that I <a href="https://en.wikipedia.org/wiki/Pok%C3%A9mon_Emerald">still hold dear</a>, then transitioned into years of playing and collecting <a href="https://en.wikipedia.org/wiki/Yu-Gi-Oh!">Yu-Gi-Oh</a> cards as I went through elementary school. It didn&#39;t take long after I discovered Magic for the game to become the primary focus for my collecting and playing. The 15-minute walk down the road to my local game shop to spend a hard-earned $3 on a booster pack of <a href="https://mtg.fandom.com/wiki/Guildpact">Guildpact</a>, <a href="https://mtg.fandom.com/wiki/Planar_Chaos">Planar Chaos</a>, and <a href="https://mtg.fandom.com/wiki/Future_Sight">Future Sight</a> became a weekly ritual. As I went through high school and college, my interest in the game faded over time. Programming became my driving interest, and the rest, as they say, is history.</p>
<p>Fast forward to the summer of 2022. I have a family of my own and a little one that is discovering his own interests. In a place that I think many parents can relate to, my wife and I were looking for a way to spend more intentional time together after we put our son down to bed at night. It was on one of those nights that my memories of playing Magic as a kid came back to me. I remember turning to my wife and saying:</p>
<blockquote>
<p><em>&quot;There was this game that I played when I was a kid. I think if we gave it a shot it could be something that we both could really enjoy.&quot;</em> </p>
</blockquote>
<p>She agreed, and we&#39;ve been playing regularly since.  </p>
<p>Returning to the game after nearly 15 years was odd. In my time away, the number of cards available for play had exploded and there were completely new ways to play the game. Cards that I remember pulling out of $3 boosters are currently trading for <a href="https://www.cardkingdom.com/mtg/future-sight/tarmogoyf-foil">hundreds of dollars</a>. One of the aspects that hadn&#39;t changed much, however, was how most players tracked their board state during play.  </p>
<p>When I was a kid we used small memo pads and dice to track life totals and counters. That is still largely the case today. While there are apps out there that are built for this exact purpose, none of them had the feel and features that I wanted in a tabletop companion app. So I decided to build one. A little more than a month&#39;s worth of work later and <a href="https://apps.apple.com/us/app/spindown/id1671844369">Spindown</a> is now available on the app store for iOS, iPadOS, and MacOS devices. The app provides an easy-to-use interface for tracking board state, player counters, and more. One of the features that I am most pleased with is an indexed and fully searchable copy of the official Magic rulebook. Magic is an <a href="https://www.youtube.com/watch?v=pdmODVYPDLA">incredibly complex game</a>, with an equally complex set of rules. Spindown&#39;s rulebook feature lets players spend less time on rules checks and more time playing the game they love.</p>
<p><img src="https://s3.us-east-2.amazonaws.com/reckart.blog-images/spindown_ios.png" alt="An image showing three different views from the Spindown app for iOS"></p>
<p>My wife and I have been using the app for our games since the early builds when the idea first occurred to me. We enjoy augmenting our play sessions with the app, and I am hopeful that others will too.</p>
<p><a href="https://apps.apple.com/us/app/spindown/id1671844369">Download Spindown on iOS, iPadOS, and MacOS today.</a></p>
<p><img src="https://s3.us-east-2.amazonaws.com/reckart.blog-images/spindown_ipad.png" alt="An image showing a view from the Spindown app for iPadOS"></p>
]]></description></item><item><title>Retrieving Exposure Data From A Capture Device In SwiftUI</title><pubDate>Tue, 06 Dec 2022 00:00:00 GMT</pubDate><link>https://rkrt.net/retrieving-exposure-data-for-use-in-swiftui</link><guid>https://rkrt.net/retrieving-exposure-data-for-use-in-swiftui</guid><description><![CDATA[<p>In my <a href="/posts/writing-a-camera-view-for-swift-ui/">last blog post</a>, I wrote about the process of putting together a simple UIKit view model that would allow the <code>AVCaptureDevice</code> API to be used in a SwiftUI-heavy application. I also explained that the purpose at the outset of this experiment was to take the exposure data gathered by the iPhone’s camera and use that as a method for calculating metered exposures for film photography. This post serves as a direct follow-up where I’ll detail the process for retrieving exposure information from the EXIF data provided by the <code>AVCapturePhotoCaptureDelegate</code> after a successful capture. Since this is a follow-up post, I won’t go into detail on much of the existing structure for the <code>CameraViewModel</code>.</p>
<p>The iPhone’s camera is accurate enough to calculate a proper exposure in almost all cases.  The <code>device</code> object exposes plenty of methods for manually setting ISO and exposure modes. These methods are incredibly handy when you want to control the values of your exposure in real-time, but when put into the context of light metering, their usefulness diminishes. All of the data needed to calculate each piece of the exposure triangle is already included in the EXIF data generated when an image is captured.</p>
<p>This starts with writing a method to make an exposure and capture its data. This method will live in the existing <code>CameraViewModel</code> class.</p>
<pre><code class="language-swift"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">capture</span><span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> photoOutputConnection = <span class="hljs-keyword">self</span>.photoOutput.connection(with: .video) {
        photoOutputConnection.videoOrientation = .portrait
    }
        
    <span class="hljs-keyword">var</span> photoSettings = <span class="hljs-type">AVCapturePhotoSettings</span>()
        
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">self</span>.device.isFlashAvailable {
        photoSettings.flashMode = .off
    }
        
    photoSettings.photoQualityPrioritization = .balanced
        
    <span class="hljs-type">DispatchQueue</span>.global(qos: .userInteractive).async {
        <span class="hljs-keyword">self</span>.photoOutput.capturePhoto(with: photoSettings, delegate: <span class="hljs-keyword">self</span>)
    }
}
</code></pre>
<p>The important line to note above is <code>self.photoOutput.capturePhoto(with: photoSettings, delegate: self)</code>. This statement declares that the model’s <code>photoOutput</code> should capture a photo with the settings declared in the method above, with the model itself serving as the delegate. This means that the model will need an additional method to handle the output. However, first we’ll need to define a class that can serve as a container for the captured metadata.</p>
<pre><code class="language-swift"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PhotoMetadata</span>: <span class="hljs-title">Equatable</span> </span>{
    <span class="hljs-keyword">static</span> <span class="hljs-function"><span class="hljs-keyword">func</span> == <span class="hljs-params">(lhs: PhotoMetadata, rhs: PhotoMetadata)</span></span> -&gt; <span class="hljs-type">Bool</span> {
        <span class="hljs-keyword">return</span> lhs.captureId == rhs.captureId
    }
    
    <span class="hljs-keyword">var</span> captureId: <span class="hljs-type">UUID</span> = <span class="hljs-type">UUID</span>()
    
    <span class="hljs-keyword">var</span> iso: <span class="hljs-type">Int64?</span>
    <span class="hljs-keyword">var</span> lensAperture: <span class="hljs-type">Double?</span>
    <span class="hljs-keyword">var</span> exposureDurationSeconds: <span class="hljs-type">Double?</span>
}
</code></pre>
<p>With the metadata class defined, the output method can be written as:</p>
<pre><code class="language-swift"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">photoOutput</span><span class="hljs-params">(<span class="hljs-number">_</span> output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?)</span></span> {
    <span class="hljs-keyword">for</span> category <span class="hljs-keyword">in</span> photo.metadata {
        <span class="hljs-keyword">if</span> category.key == <span class="hljs-string">&quot;{Exif}&quot;</span> {
            <span class="hljs-keyword">let</span> container = <span class="hljs-type">PhotoMetadata</span>()

            <span class="hljs-keyword">let</span> isoRatings = (category.value <span class="hljs-keyword">as</span> <span class="hljs-type">AnyObject</span>).object(forKey: <span class="hljs-string">&quot;ISOSpeedRatings&quot;</span>) <span class="hljs-keyword">as</span>! [<span class="hljs-type">Any?</span>]
            container.iso = isoRatings.first <span class="hljs-keyword">as</span>? <span class="hljs-type">Int64</span>
                
            <span class="hljs-keyword">let</span> apertureValue = (category.value <span class="hljs-keyword">as</span> <span class="hljs-type">AnyObject</span>).object(forKey: <span class="hljs-string">&quot;ApertureValue&quot;</span>)
            container.lensAperture = apertureValue <span class="hljs-keyword">as</span>? <span class="hljs-type">Double</span>

            <span class="hljs-keyword">let</span> exposureTime = (category.value <span class="hljs-keyword">as</span> <span class="hljs-type">AnyObject</span>).object(forKey: <span class="hljs-string">&quot;ExposureTime&quot;</span>)
            container.exposureDurationSeconds = exposureTime <span class="hljs-keyword">as</span>? <span class="hljs-type">Double</span>
                
            <span class="hljs-keyword">self</span>.metadata = container
        }
    }
}
</code></pre>
<p>With the output method defined, the needed markup for making the exposure and displaying the captured metadata is simple and straightforward. We’ll start by adding a few state variables to the parent view.</p>
<pre><code class="language-swift">@<span class="hljs-type">State</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> calculated: <span class="hljs-type">Bool</span> = <span class="hljs-literal">false</span>
@<span class="hljs-type">State</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> iso: <span class="hljs-type">Int64?</span>
@<span class="hljs-type">State</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> lensAperture: <span class="hljs-type">Double?</span>
@<span class="hljs-type">State</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> exposureDurationSeconds: <span class="hljs-type">Double?</span>
</code></pre>
<p>From there, the view markup can be written as follows:</p>
<pre><code class="language-swift"><span class="hljs-type">ZStack</span> {
    <span class="hljs-type">CameraPreview</span>()
        .environmentObject(camera)
        .edgesIgnoringSafeArea(.all)
                
    <span class="hljs-keyword">if</span> device != <span class="hljs-literal">nil</span> {
        <span class="hljs-type">VStack</span> {
            <span class="hljs-keyword">if</span> <span class="hljs-keyword">self</span>.calculated {
                <span class="hljs-type">VStack</span>(alignment: .leading) {
                    <span class="hljs-type">Text</span>(<span class="hljs-string">&quot;ISO: \(Int(self.iso!))&quot;</span>)
                    <span class="hljs-type">Text</span>(<span class="hljs-string">&quot;F-Stop: \(self.lensAperture!)&quot;</span>)
                    <span class="hljs-type">Text</span>(<span class="hljs-string">&quot;Shutter Speed: \(self.exposureDurationSeconds!)seconds&quot;</span>)
                }
                .foregroundColor(<span class="hljs-type">Color</span>.black)
                .padding()
                .background(<span class="hljs-type">Color</span>.white)
                .cornerRadius(<span class="hljs-number">8</span>)
            }
                        
            <span class="hljs-type">Spacer</span>()
                        
            <span class="hljs-type">Button</span>(action: { <span class="hljs-keyword">self</span>.camera.capture() }) {
                <span class="hljs-type">Text</span>(<span class="hljs-string">&quot;Calculate&quot;</span>)
            }
        }
        .padding()
    }
}
.onAppear {
    camera.setUp()
                
    <span class="hljs-keyword">if</span> camera.device != <span class="hljs-literal">nil</span> {
        device = camera.device
    }
}
</code></pre>
<p>The capture method written earlier can be accessed through the already defined camera object in a SwiftUI button with <code>self.camera.capture()</code>.  The final step is just to chain a change handler to track when the camera’s <code>metadata</code> object changes and display that information to the user.</p>
<pre><code class="language-swift">.onChange(of: camera.metadata) { newState <span class="hljs-keyword">in</span>
    <span class="hljs-keyword">self</span>.iso = newState?.iso
    <span class="hljs-keyword">self</span>.lensAperture = newState?.lensAperture
    <span class="hljs-keyword">self</span>.exposureDurationSeconds = newState?.exposureDurationSeconds
                
    <span class="hljs-keyword">self</span>.calculated = <span class="hljs-literal">true</span>
}
</code></pre>
<p>That’s it! As long as Xcode doesn’t yell at you or throw and build errors your way, when launching the app in its current state you should be able to view the camera output and capture exposure information. Setting ISO, aperture, or exposure length values will come in subsequent posts.</p>
<div style="display:flex;justify-content:center">
    <img src="https://media4.giphy.com/media/nLXP6ajtjNbLX8B1Wi/giphy.gif" max-height="720px" style="border-radius:18px;margin-top:20px;">
</div>
]]></description></item><item><title>Writing a UIKit Camera View Wrapper for use in SwiftUI</title><pubDate>Tue, 22 Nov 2022 00:00:00 GMT</pubDate><link>https://rkrt.net/writing-a-camera-view-for-swift-ui</link><guid>https://rkrt.net/writing-a-camera-view-for-swift-ui</guid><description><![CDATA[<p>At the time of writing, one of the iOS APIs that hasn&#39;t been made fully interoperable with SwiftUI is <a href="https://developer.apple.com/documentation/avfoundation"><code>AVFoundation</code></a>, and more specifically the <a href="https://developer.apple.com/documentation/avfoundation/avcapturedevice"><code>AVCaptureDevice</code></a> API for creating a preview of the current camera view. One of my long term goals for <a href="https://apps.apple.com/us/app/aspen-photographers-notebook/id1643250194">Aspen</a> has been to build in a spot metering function that will let me leave my <a href="https://www.ebay.com/sch/i.html?_from=R40&amp;_trksid=p2334524.m570.l1313&amp;_nkw=sekonic+l-508&amp;_sacat=0&amp;LH_TitleDesc=0&amp;_osacat=0&amp;_odkw=sekonic+l-508&amp;LH_PrefLoc=2">Sekonic L-508</a> light meter at home. </p>
<p>It took me a long time to conceptualize how the metering system would work. Metering for large format photography is something that I really struggled with early on. Thanks to a recent episode of <a href="https://thespookyparkbench.podbean.com/e/episode-27-it-s-alex-burke/">The Spooky Park Bench Podcast</a> featuring large format landscape photographer <a href="https://www.alexburkephoto.com">Alex Burke</a>, things clicked into place for me when Alex shared his metering ethos and how he use a digital camera to compose for large format.</p>
<p>Alex uses a <a href="https://en.wikipedia.org/wiki/Micro_Four_Thirds_system">mirco four thirds</a> camera to compose and make his initial exposure calculations. The 4:3 aspect ratio isn&#39;t too far from 4:5, which is the format Alex shoots. So, with careful consideration using a digital camera to compose for large format is fairly simple.</p>
<p>What&#39;s more interesting though, is that he uses his digital camera&#39;s built-in light meter to calculate the base exposure and then compensates for aperture or conditions from there. This makes sense. Modern digital cameras have excellent light meters. I have long struggled with my dedicated meter. I have gotten fantastic results, but I have also miscalculated and ended up with drastically underexposed images even when the calculations seemed right in the moment.</p>
<p>Like all other modern digital cameras, the iPhone also has an excellent light meter built in. You can get fantastic images from an iPhone. Consistent metering is one of the most crucial aspect for getting those results. So, I decided to change my ethos and focus on using the tools at hand to acheive more consistent results. That starts with interacting directly with the iPhone&#39;s camera.</p>
<p>In order to do so, we&#39;ll need to start by building out the view model that the camera session will run in and apply any user inputs to the camera&#39;s session.</p>
<pre><code class="language-swift"><span class="hljs-keyword">import</span> SwiftUI
<span class="hljs-keyword">import</span> AVFoundation

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CameraViewModel</span>: <span class="hljs-title">NSObject</span>, <span class="hljs-title">ObservableObject</span>, <span class="hljs-title">AVCapturePhotoCaptureDelegate</span> </span>{
    @<span class="hljs-type">Published</span> <span class="hljs-keyword">var</span> device: <span class="hljs-type">AVCaptureDevice!</span>

    @<span class="hljs-type">Published</span> <span class="hljs-keyword">var</span> session = <span class="hljs-type">AVCaptureSession</span>()
    @<span class="hljs-type">Published</span> <span class="hljs-keyword">var</span> photoOutput = <span class="hljs-type">AVCapturePhotoOutput</span>()
    
    @<span class="hljs-type">Published</span> <span class="hljs-keyword">var</span> preview: <span class="hljs-type">AVCaptureVideoPreviewLayer!</span>
    @<span class="hljs-type">Published</span> <span class="hljs-keyword">var</span> previewURL: <span class="hljs-type">URL?</span>
}
</code></pre>
<p>Below the defined variables, the model also needs to instantiate a <a href="https://developer.apple.com/documentation/avfoundation/avcapturedevice/discoverysession"><code>AVCaptureDevice.DiscoverySession</code></a> in order to retrieve capture data from the device&#39;s cameras. For the purpose of this test I&#39;ve decided to limit it to the rear wide angle camera, but camera can be accessed from this object.</p>
<pre><code class="language-swift"><span class="hljs-keyword">let</span> discoverySession = <span class="hljs-type">AVCaptureDevice</span>.<span class="hljs-type">DiscoverySession</span>(
    deviceTypes: [.builtInWideAngleCamera],
    mediaType: .video,
    position: .back
)
</code></pre>
<p>With the class and base variables defined, we can now define a <code>setUp</code> method to initialize the camera session.</p>
<pre><code class="language-swift"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">setUp</span><span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">do</span> {
        <span class="hljs-comment">// Start the AVCaptureSession</span>
        <span class="hljs-keyword">self</span>.session.beginConfiguration()
        
        <span class="hljs-comment">// Ensure all inputs/outputs area cleared at run time.</span>
        <span class="hljs-keyword">for</span> input <span class="hljs-keyword">in</span> session.inputs { session.removeInput(input) }
        <span class="hljs-keyword">for</span> output <span class="hljs-keyword">in</span> session.outputs { session.removeOutput(output) }
        
        <span class="hljs-comment">// Manually select rear camera and declare the AVCaptureDeviceInput</span>
        <span class="hljs-keyword">self</span>.device = <span class="hljs-keyword">self</span>.discoverySession.devices[<span class="hljs-number">0</span>]
        <span class="hljs-keyword">let</span> input = <span class="hljs-keyword">try</span> <span class="hljs-type">AVCaptureDeviceInput</span>(device: <span class="hljs-keyword">self</span>.device)
        
        <span class="hljs-comment">// Add the input to the active session.</span>
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">self</span>.session.canAddInput(input) {
            <span class="hljs-keyword">self</span>.session.addInput(input)
        }
        
        <span class="hljs-comment">// Additional configuration if photo capture/output is desired.</span>
        <span class="hljs-comment">// if self.session.canAddOutput(self.photoOutput) {</span>
        <span class="hljs-comment">//     self.session.addOutput(self.photoOutput)</span>
        <span class="hljs-comment">// }</span>
        
        <span class="hljs-comment">// Commit the configuration.</span>
        <span class="hljs-keyword">self</span>.session.commitConfiguration()
    } <span class="hljs-keyword">catch</span> {
        <span class="hljs-comment">// Log any errors.</span>
        <span class="hljs-built_in">print</span>(error.localizedDescription)
    }
}
</code></pre>
<p>Now that the base view model has been defined, we&#39;ll need to write a simple <a href="https://developer.apple.com/documentation/swiftui/uiviewrepresentable/"><code>UIViewRepresentable</code></a> UIKit wrapper for <code>CameraViewModel</code> for use in SwiftUI.</p>
<pre><code class="language-swift"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">CameraPreview</span>: <span class="hljs-title">UIViewRepresentable</span> </span>{
    @<span class="hljs-type">StateObject</span> <span class="hljs-keyword">var</span> camera = <span class="hljs-type">CameraViewModel</span>()
    
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">makeUIView</span><span class="hljs-params">(context: Context)</span></span> -&gt; <span class="hljs-type">UIView</span> {
        <span class="hljs-keyword">let</span> view = <span class="hljs-type">UIView</span>(frame: <span class="hljs-type">UIScreen</span>.main.bounds)
        
        <span class="hljs-comment">// Add the session to the preview layer</span>
        camera.preview = <span class="hljs-type">AVCaptureVideoPreviewLayer</span>(session: camera.session)
        <span class="hljs-comment">// Display property configuration</span>
        camera.preview.frame = view.frame
        camera.preview.videoGravity = .resizeAspectFill

        view.layer.addSublayer(camera.preview)
        
        <span class="hljs-type">DispatchQueue</span>.global(qos: .userInteractive).async {
            camera.session.startRunning()
        }
        
        <span class="hljs-keyword">return</span> view
    }
    
    
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">updateUIView</span><span class="hljs-params">(<span class="hljs-number">_</span> uiView: UIView, context: Context)</span></span> {}
}
</code></pre>
<p>That&#39;s all that you need to get a basic camera session running in a SwiftUI View.</p>
<pre><code class="language-swift">@main
<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">App</span>: <span class="hljs-title">App</span> </span>{
    @<span class="hljs-type">StateObject</span> <span class="hljs-keyword">var</span> camera = <span class="hljs-type">CameraViewModel</span>()

    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">Scene</span> {
        <span class="hljs-type">WindowGroup</span> {
            <span class="hljs-type">ZStack</span> {
                <span class="hljs-type">CameraPreview</span>()
                    .environmentObject(camera)
                    .edgesIgnoringSafeArea(.all)
            }
            .onAppear { camera.setUp() }
        }
    }
}
</code></pre>
]]></description></item><item><title>Introducing Aspen</title><pubDate>Wed, 14 Sep 2022 00:00:00 GMT</pubDate><link>https://rkrt.net/introducing-aspen</link><guid>https://rkrt.net/introducing-aspen</guid><description><![CDATA[<p><img src="https://s3.us-east-2.amazonaws.com/reckart.blog-images/ansel_screens.jpg" alt="An image showing the three primary screens of the Aspen iOS App"></p>
<p>As I have grown as a photographer, especially as a film photographer, I have consistently found myself looking for a centralized tool for tracking the data that goes into each exposure. Whether that be reciprocity, bellows extension, or just understanding how different film stocks brought their own unique characteristics to each image. For the majority of my time as a photographer, this process has been completely manual. For each emulsion I would have to read the data sheet and look at the reciprocity failure curve to understand how to compensate for light fall-off on longer exposures. Calculations and formulas would then live in a small notebook that I kept in my photography bag.</p>
<p>This is not a unique system. It&#39;s proven and it works, but I wanted something better. I wanted something in my pocket that would have the ability to make the calculations I needed, save the data, and act as a universal notebook for me to put that data to understand it better and grow as a photographer.</p>
<p>As I&#39;ve <a href="/tools-for-analog-photography">written about</a> on this blog already, I decided to build myself a little utility to solve this problem. For a long time it was a buggy, fickle app that would crash if the inputs weren&#39;t 100% right. That was fine for me, because I knew the edge cases, but as I continued to add functionality it seemed more and more clear to me that this could be a useful tool for other photographers as well.</p>
<p>That&#39;s when I started to share my idea with other photographers. I started to get feedback on the kinds of tools that they&#39;d want to use in the field. That&#39;s when this app turned from a pocket companion for myself into what it is now. And as of the time of this writing, <a href="https://apps.apple.com/us/app/aspen-photographers-notebook/id1643250194">Aspen is available on the iOS App Store</a> for devices running <a href="https://www.apple.com/ios/ios-16/">iOS 16</a> or newer.</p>
<p>There&#39;s still a lot that I want to put into the app. There&#39;s a Sekonic-inspired spot metering function on its way, but that undertaking was cut from the original release. I&#39;ll continue to write about my progress as the app gains features and feedback from its users.</p>
]]></description></item><item><title>Drag-and-Drop with SwiftUI</title><pubDate>Sun, 28 Aug 2022 00:00:00 GMT</pubDate><link>https://rkrt.net/rich-drag-and-drop-experiences-with-swiftui</link><guid>https://rkrt.net/rich-drag-and-drop-experiences-with-swiftui</guid><description><![CDATA[<p>One of the requirements I decided on early on in the development of Aspen was a completely customizable dashboard. Aspen ships bundled with several different tools for calculating exposure, however not all photographers use every one of these tools. It was important to me that the app allowed users to add and remove tiles as well as reorder their position on the dashboard.  </p>
<p>There are several ways to accomplish this behavior. My initial implementation was to use native Gesture recognition. However, the amount of overhead required to allow the additional tiles to recognize the current gesture was more than I wanted to try and implement as part of the app&#39;s MVP. With that in mind, I turned to <code>.onDrag</code> and <code>.onDrop</code> handlers. While the handlers themselves didn&#39;t have what I needed out of the box, defining a custom <code>DropDelegate</code> was an easy enough solution.</p>
<p>This post provides an overview of that implementation. It&#39;s not a step-by-step tutorial, but it should suffice for those with even a passing knowledge of Swift and SwiftUI.</p>
<p>Let&#39;s start with a simple data model:</p>
<pre><code class="language-swift"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">DashboardTile</span>: <span class="hljs-title">Identifiable</span>, <span class="hljs-title">Equatable</span> </span>{
    <span class="hljs-keyword">static</span> <span class="hljs-function"><span class="hljs-keyword">func</span> ==<span class="hljs-params">(lhs: DashboardTile, rhs: DashboardTile)</span></span> -&gt; <span class="hljs-type">Bool</span> {
        <span class="hljs-keyword">return</span> lhs.id == rhs.id
    }

    <span class="hljs-keyword">var</span> id: <span class="hljs-type">String</span> {
        <span class="hljs-keyword">self</span>.key
    }

    <span class="hljs-keyword">var</span> key: <span class="hljs-type">String</span>
    <span class="hljs-keyword">var</span> label: <span class="hljs-type">String</span>
}
</code></pre>
<p>This model provides a simple data structure with two properties, <code>key</code> and <code>label</code>. With our data structure in hand, we need to define our <code>DroppableTileDelegate</code> that will handle override the default drag-and-drop handler&#39;s behavior.</p>
<pre><code class="language-swift"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">DroppableTileDelegate</span>&lt;<span class="hljs-title">DashboardTile</span>: <span class="hljs-title">Equatable</span>&gt;: <span class="hljs-title">DropDelegate</span> </span>{
    <span class="hljs-keyword">let</span> tile: <span class="hljs-type">DashboardTile</span>
    <span class="hljs-keyword">var</span> listData: [<span class="hljs-type">DashboardTile</span>]

    @<span class="hljs-type">Binding</span> <span class="hljs-keyword">var</span> current: <span class="hljs-type">DashboardTile?</span>
    @<span class="hljs-type">Binding</span> <span class="hljs-keyword">var</span> hasLocationChanged: <span class="hljs-type">Bool</span>
    
    <span class="hljs-keyword">var</span> moveAction: (<span class="hljs-type">IndexSet</span>, <span class="hljs-type">Int</span>) -&gt; <span class="hljs-type">Void</span>
    
    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">dropEntered</span><span class="hljs-params">(info: DropInfo)</span></span> -&gt; <span class="hljs-type">Void</span> {
        <span class="hljs-keyword">guard</span> tile != current, <span class="hljs-keyword">let</span> current = current <span class="hljs-keyword">else</span> { <span class="hljs-keyword">return</span> }

        <span class="hljs-keyword">let</span> from = listData.firstIndex(of: current)
        <span class="hljs-keyword">let</span> to = listData.firstIndex(of: tile) <span class="hljs-keyword">else</span> { <span class="hljs-keyword">return</span> }
        
        hasLocationChanged = <span class="hljs-literal">true</span>
        
        <span class="hljs-keyword">if</span> listData[to] != current {
            moveAction(
                <span class="hljs-type">IndexSet</span>(integer: from), to &gt; from ? to + <span class="hljs-number">1</span> : to
            )
        }
    }

    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">dropUpdated</span><span class="hljs-params">(info: DropInfo)</span></span> -&gt; <span class="hljs-type">DropProposal?</span> {
        <span class="hljs-type">DropProposal</span>(operation: .move)
    }
    
    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">performDrop</span><span class="hljs-params">(info: DropInfo)</span></span> -&gt; <span class="hljs-type">Bool</span> {
        hasLocationChanged = <span class="hljs-literal">false</span>
        current = <span class="hljs-literal">nil</span>
        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>
    }
}
</code></pre>
<p>With the <code>DroppableTileDelegate</code> defined, we can define the view that our layout will live in. Note that the tiles and the binding representing the currently dragged tiles are passed into this view from the parent view. This allows data to be shared with other views if necessary. The default drag preview behavior is overwritten by the <code>preview</code> closure that follows the definition of the <code>.onDrag</code> handler. If we don&#39;t override this with our UI, the default hard-edged previews will be shown. For some apps that might be all that is necessary, but since my dashboard tiles had rounded edges, it required that a custom overlay that used <code>contentShape</code> to trim those corners be used.</p>
<pre><code class="language-swift"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">DashboardTileView</span>&lt;<span class="hljs-title">Content</span>: <span class="hljs-title">View</span>, <span class="hljs-title">DashboardTile</span>: <span class="hljs-title">Identifiable</span> &amp; <span class="hljs-title">Equatable</span>&gt;: <span class="hljs-title">View</span> </span>{
    @<span class="hljs-type">Binding</span> <span class="hljs-keyword">var</span> tiles: [<span class="hljs-type">DashboardTile</span>]
    @<span class="hljs-type">Binding</span> <span class="hljs-keyword">var</span> draggingTile: <span class="hljs-type">DashboardTile?</span>

    <span class="hljs-keyword">let</span> content: (<span class="hljs-type">DashboardTile</span>) -&gt; <span class="hljs-type">Content</span>
    <span class="hljs-keyword">let</span> moveAction: (<span class="hljs-type">IndexSet</span>, <span class="hljs-type">Int</span>) -&gt; <span class="hljs-type">Void</span>

    @<span class="hljs-type">State</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> hasLocationChanged: <span class="hljs-type">Bool</span> = <span class="hljs-literal">false</span>
    
    <span class="hljs-keyword">init</span>(
        tiles: <span class="hljs-type">Binding</span>&lt;[<span class="hljs-type">DashboardTile</span>]&gt;,
        draggingTile: <span class="hljs-type">Binding</span>&lt;<span class="hljs-type">DashboardTile?</span>&gt;,
        @<span class="hljs-type">ViewBuilder</span> content: @escaping (<span class="hljs-type">DashboardTile</span>) -&gt; <span class="hljs-type">Content</span>,
        moveAction: @escaping (<span class="hljs-type">IndexSet</span>, <span class="hljs-type">Int</span>) -&gt; <span class="hljs-type">Void</span>
    ) {
        <span class="hljs-keyword">self</span>._tiles = tiles
        <span class="hljs-keyword">self</span>.content = content
        <span class="hljs-keyword">self</span>.moveAction = moveAction
        <span class="hljs-keyword">self</span>._draggingTile = draggingTile
    }
    
    <span class="hljs-keyword">let</span> screenWidth = <span class="hljs-type">UIScreen</span>.main.bounds.width
    
    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">VStack</span> {
            <span class="hljs-type">ForEach</span>(tiles) { tile <span class="hljs-keyword">in</span>
                <span class="hljs-type">VStack</span> {
                    content(tile)
                        .overlay(
                            draggingTile == tile
                            ? <span class="hljs-type">RoundedRectangle</span>(cornerRadius: <span class="hljs-number">17</span>).fill(.thinMaterial)
                            : <span class="hljs-literal">nil</span>
                        )
                        .onDrag {
                            draggingTile = tile
                            <span class="hljs-keyword">return</span> <span class="hljs-type">NSItemProvider</span>(object: <span class="hljs-string">&quot;\(tile.id)&quot;</span> <span class="hljs-keyword">as</span> <span class="hljs-type">NSString</span>)
                        } preview: {
                            content(tile)
                                .frame(minWidth: screenWidth - <span class="hljs-number">20</span>, minHeight: <span class="hljs-number">80</span>)
                                .contentShape(.dragPreview, <span class="hljs-type">RoundedRectangle</span>(cornerRadius: <span class="hljs-number">18</span>, style: .continuous))
                        }
                        .onDrop(
                            of: [<span class="hljs-type">UTType</span>.text],
                            delegate: <span class="hljs-type">DroppableTileDelegate</span>(
                                tile: tile,
                                listData: tiles,
                                current: $draggingTile,
                                hasLocationChanged: $hasLocationChanged
                            ) { from, to <span class="hljs-keyword">in</span>
                                withAnimation {
                                    moveAction(from, to)
                                }
                            }
                        )
                }
            }
        }
    }
}
</code></pre>
<p>The custom drop delegate that we defined above is passed into the <code>delegate</code> parameter of the <code>.onDrop</code> handler. A trailing closure also allows for the view to be animated when the <code>moveAction</code> is invoked. From there, all we need to do is define the action itself.</p>
<pre><code class="language-swift"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">moveTile</span><span class="hljs-params">(<span class="hljs-number">_</span> from: IndexSet, <span class="hljs-number">_</span> to: Int)</span></span> -&gt; <span class="hljs-type">Void</span> {
    layout.move(fromOffsets: from, toOffset: to)
}
</code></pre>
<p>With the above all defined (and compiling without error), all we have to do is reference the <code>DashboardTileView</code> and pass in the associated bindings.</p>
<pre><code class="language-swift"><span class="hljs-type">DashboardTileView</span>(tiles: $layout, draggingTile: $draggingTile) { tile <span class="hljs-keyword">in</span>
    <span class="hljs-type">SimpleTile</span>(tile: tile)
} moveAction: { from, to <span class="hljs-keyword">in</span>
    moveTile(from, to)
}
</code></pre>
<p>Voila!</p>
<p><img src="https://s3.us-east-2.amazonaws.com/reckart.blog-images/dashboard_dnd.gif" alt="A GIF depicting the UI that results from the post above"></p>
]]></description></item><item><title>Tools for Analog Photography</title><pubDate>Sun, 21 Aug 2022 00:00:00 GMT</pubDate><link>https://rkrt.net/tools-for-analog-photography</link><guid>https://rkrt.net/tools-for-analog-photography</guid><description><![CDATA[<p>One of the benefits of specializing in large format photography is the flexibility that comes with using a view camera. The angles and movements available allow for nearly endless flexibility in the composition of a photograph. While that creates a lot of room for creative freedom, some considerations need to be made. The image circle projected by the lens onto the film plane needs to be wide enough to cover the movements, but more importantly, the extension of the bellows needs to be factored into the final exposure.</p>
<p>The culprit is the <a href="https://en.wikipedia.org/wiki/Inverse-square_law">inverse square law</a>. As the bellows are extended, the intensity of light available to expose the negative is inversely proportional to the square of the distance from the film plane. That fall off of light needs to be factored into the final exposure. Otherwise, you&#39;ll end up with a dark, underexposed image.</p>
<p>The calculation to compensate for bellows extension is simple, but it&#39;s a pain to do in the field or working with extreme extensions for macro photography. I&#39;ve had the formula in a small notebook that I keep with my large format gear to reference when out in the field, but I thought there could be a better way. So I decided to build one.</p>
<p><img src="https://s3.us-east-2.amazonaws.com/reckart.blog-images/ansel_recording-1.gif" alt=""></p>
<p>This little photography app, which I&#39;ve taken to calling Aspen, solves that problem. In its current state, you can adjust for bellows extension, film reciprocity, as well as filter factors. It also includes a handy notebook for referencing exposures in the future.</p>
<p>The app is still very early in its development, but it&#39;s been a very handy tool for me as I have been exploring extreme macros and working with alternative printing processes. I haven&#39;t put it on the app store yet, but if you&#39;re handy with Xcode, you can <a href="https://github.com/tylerreckart/Aspen">view the source</a> on Github and build it locally to run on your device.</p>
<p>More updates will come as I add additional features and build out the user experience.</p>
]]></description></item><item><title>Finding an Outlet</title><pubDate>Wed, 17 Aug 2022 00:00:00 GMT</pubDate><link>https://rkrt.net/finding-an-outlet</link><guid>https://rkrt.net/finding-an-outlet</guid><description><![CDATA[<p>I’ve always considered myself a creative person. When I was a kid, I would spend hours on end drawing characters from my favorite games and television shows. When I was a teenager in the early 2000s, that focus turned digital. Back in the early days of YouTube, you could completely customize the background of your channel to personalize it to the content and audience. I managed to get my hands on a license to Photoshop CS4 and started making those backgrounds for creators on the platform. That eventually evolved into making tutorial videos, and then into being hired to design iPhone apps for the newly released iOS 4.</p>
<p>By the time I graduated high school, a lot of that creative energy had been put on the back burner. I stopped taking on client work and focused on getting myself into college. It took a few years of finding out who I wanted to become, but I eventually found myself back in the industry. However, this time was different. One of the limitations of my early design work is that I desperately wanted to take the screens and apps that I had designed and turn them into real, functional products. So that’s what I focused on. I started to build my software development skillset. As I learned more and more about how to build feature-rich applications, write tests, and ship products, the design-oriented aspect of my work fell back into the background.</p>
<p>Fast forwarding to today, I’ve been writing software professionally for nearly a decade. I wouldn’t trade that career choice for the world. And while my day-to-day work is primarily technical, developing robust and scalable software requires a lot of creative problem-solving. It’s incredibly rewarding when you can come to a clean, functional solution to a complex problem or difficult task. With as much as I enjoy my work, the itch to create for creation’s sake has never left. It was time to find a new outlet.</p>
<h3 id="enter-the-camera">Enter the camera</h3>
<p>I bought my first real camera in the summer of 2020. My wife and I were preparing for the birth of our first child, and like many fathers-to-be, I wanted to capture as many of those early moments as possible. So I did my research. I found a camera with the right balance of image quality and budget-friendliness and made the commitment. It was a gently used <a href="https://www.bhphotovideo.com/c/product/1433710-REG/canon_eos_r_mirrorless_digital.html">Canon EOS R</a> in practically new condition. I cycled through a few lenses at first as I learned the system, but eventually landed on a <a href="https://www.bhphotovideo.com/c/product/1502500-REG/canon_3680c002_rf_24_70mm_f_2_8l_is.html">Cannon RF 24-70mm f2.8L</a> with its fast aperture and a great balance of focal ranges that suited my needs.</p>
<p><img src="https://s3.us-east-2.amazonaws.com/reckart.blog-images/canon-1.jpeg" alt="A photo of a Canon camera on top of a desk"></p>
<p>After my son was born, the Canon ended up within arm&#39;s reach for much of those early weeks and months. Every time I used it, I became a little more comfortable with it. I started to understand how changing settings like shutter speed, aperture, and ISO could affect the image. Before long, I was hooked. I started to go out to take pictures just for the sake of taking pictures. I had found an outlet that let me step back from the computer screen and enjoy the process of creation.</p>
<h3 id="and-then-there-was-film">And then there was film</h3>
<p>It wasn&#39;t long before I got the itch to try my hand at shooting film. I felt confident in my abilities to understand how to capture a well-exposed image, and the unforgiving nature of film photography was the next step for me to hone my abilities with the camera. It started with 35mm and quickly moved into medium format before settling finally on a large format <a href="https://www.chamonixviewcamera.com/">4x5 view camera</a>.</p>
<p><img src="https://s3.us-east-2.amazonaws.com/reckart.blog-images/hyacinth-capture-2.jpeg" alt="A photo of a large format view camera mounted on a tripod"></p>
<p>Large format forces you to slow down. Each movement of the camera subtly affects the focus and depth of field of the image. If you rush the process, you&#39;ll likely forget to set everything correctly. The shutter might be too long. The aperture might be too wide. You may not have accounted for bellows extension and how the light diminishes as you extend the distance between the lens and film plane. A miscalculation more than likely leads to an unusable image. However, when you can focus on the composition and find the right light, the results are breathtaking.</p>
<p><img src="https://s3.us-east-2.amazonaws.com/reckart.blog-images/lorien-blog-2.jpg" alt="A landscape scene shot in Great Smoky Mountains National Park with a large format view camera"></p>
<p>Finding photography, and large format film photography has brought that creative energy back from the background. It is something that I have grown incredibly passionate about over the few years since I fell into the hobby. I think we all need an outlet, and I feel very grateful to have found mine.</p>
]]></description></item><item><title>The Longest Year</title><pubDate>Mon, 06 Dec 2021 00:00:00 GMT</pubDate><link>https://rkrt.net/the-longest-year</link><guid>https://rkrt.net/the-longest-year</guid><description><![CDATA[<p><img src="https://upload.wikimedia.org/wikipedia/commons/9/93/Siege-alesia-vercingetorix-jules-cesar.jpg" alt="Lionel Royer - Vercingetorix throws down his arms at the feet of Julius Caesar, 189"></p>
<blockquote>
<p>&quot;Happy he who has passed his whole life mid his own fields, he of whose birth and old age the same house is witness...For him the recurring seasons, not the consuls, mark the year; he knows autumn by his fruits and spring by her flowers.&quot;<br>Claudian, Carmina Minora (XX)  </p>
</blockquote>
<p>A large part of my interests outside of programming, a healthy portion of the <a href="https://www.goodreads.com/user/show/28962435-tyler-reckart">stacks of books</a> I get through each year, centers around one subject: antiquity. I&#39;ve had a strong interest in history since I was a kid. The story that follows is one that I&#39;ve known for a few years, but I thought it was an interesting subject and wanted to share it in my own way.</p>
<p>The early Roman pre-republican calendar, attributed by the Romans to the eponymous first king <a href="https://en.wikipedia.org/wiki/Romulus">Romulus</a> himself, was lunar. Built around the phases of the moon and the agricultural seasons of the Italian peninsula, the Roman year began in March at the start of spring and ended in December with the autumn planting. It consisted of ten months, just 304 days.</p>
<p>A year of just 304 days is missing 61 days. That&#39;s not an insignificant number of days. A single generation of the error led to seasons wildly out of sync with the calendar date. <a href="https://www.britannica.com/biography/Livy">According to Livy</a> in his <em>The Early History of Rome</em>, Numa Pompilius, the second king of Rome, attempted to correct the issue with the calendar by dividing the year into twelve lunar months. First by adding an additional fifty days to the year and secondly by taking a day from each month of thirty days to allow for two winter months, January and February, both of which had twenty-eight days.</p>
<p>A year closer to what we would recognize in the modern era had been established, however, the Romans were a superstitious people <a href="https://www.obliquity.com/calendar/rome.html">plagued with an aversion to even numbers</a>. An additional day was added to January to avoid the unlucky number. The <em>entire month</em> of February was already considered to be unlucky, so they left it untouched. The Romans now had a year of 355 days, just ten days off from the astronomical year.</p>
<p>This calendar system went on essentially untouched until 46 BC. The Romans kept the seasons in sync by manually <a href="http://penelope.uchicago.edu/~grout/encyclopaedia_romana/calendar/intercalation.html">intercalating</a> (inserting) those missing days. However, left untouched the Roman calendar as before would fall out of synchonicity with the seasons. That responsibility fell on the <a href="https://en.wikipedia.org/wiki/Pontifex_maximus">Pontifex Maximus</a>; Rome&#39;s chief priest. In 46 BC, the Pontifex Maximus was one of the most famous people to have ever lived: Julius Caesar.</p>
<p>Caesar was elected Pontifex Maximus in 63 BC. It was a position he would occupy until his death until 44 BC. Just six years into his role, conflict over the migration of a Celtic tribe exploded into war. By 58 BC Caesar was fully consumed in his conquest of Gaul. Only a year after returning to the Rome in <a href="https://exhibits.library.villanova.edu/ancient-rome/roman-activities/roman-triumph">triumph</a>, rivalries in the senate would boil over into a new war, Roman against Roman. Fought across the burgeoning empire, the four year conflict would see the death of <a href="https://en.wikipedia.org/wiki/Pompey">Pompey the Great</a> and Caesar elected by the Senate and people of Rome as <a href="https://en.wikipedia.org/wiki/Roman_dictator">dictator</a> for life. There hadn’t been an intercalation since Caesar&#39;s election to the proconsulship in 58 BC. By his return to Rome from Egypt in 46 BC and his election to the dictatorship, there was a three month discrepancy between the seasons and the calendar date. The harvest was being celebrated before the crops had even been taken in.</p>
<p>When Caesar returned from Egypt, he returned with a familiarity with the Egyptian calendar. The Egyptians, unlike the Romans, used a solar calendar to track the passage of their year. With his power as Pontifex Maximus and at the direction of the Alexandrian astronomer Sosigenes, Caesar added ninety extra days to the year. <em>Ninety</em>. The addition meant that the year 46 BC had a length of 455 days. The addition also meant that 46 BC was now the longest year in human history. The calendar that resulted consisted of four nearly equal seasons divided into a more familiar 365 days. Because the solar year is approximately a quarter of a day longer than the calendar year, Caesar automatically inserted a single intercalary day every four years at the end of February, inventing the concept of the leap year.</p>
<p>Caesar went on to do a number of <a href="https://en.wikipedia.org/wiki/Assassination_of_Julius_Caesar">notable things</a> after his reforms, now referred to as the Julian calendar, but that is where this post will leave him. The Julian year averaged exactly 365.25 days. That is extremely close, but exactly <em>11 minutes</em> too long. Meaning that the calendar would gain almost a day every 128 years. By the mid sixteenth century the discrepancy had amounted to around ten days. The spring equinox, the annual event used by the Catholic Church used to calculate the date of Easter, was no longer occurring on March 21. In 1582, Pope Gregory XIII stepped in to reform the system once again. To correct the immediate issue, the Pope omitted the extra ten days from that year. 1582 only consisted of 355 days. October 4 was followed by October 15. To prevent the issue from happening in the centuries and millennia that followed, three leap years were to be omitted every four-hundred years. </p>
<p>The Gregorian calendar has been in use ever since.</p>
<h3 id="related-reading--header">Related Reading</h3>  

<ul>
<li><a href="https://www.amazon.com/Caesar-Life-Colossus-Adrian-Goldsworthy/dp/0300126891">Caesar: Life of a Colossus</a></li>
<li><a href="https://en.wikipedia.org/wiki/Roman_timekeeping">Roman timekeeping</a></li>
<li><a href="https://www.nist.gov/pml/time-and-frequency-division/leap-seconds-faqs">Leap Seconds</a></li>
</ul>
]]></description></item><item><title>On Social Media</title><pubDate>Tue, 30 Nov 2021 00:00:00 GMT</pubDate><link>https://rkrt.net/on-social-media</link><guid>https://rkrt.net/on-social-media</guid><description><![CDATA[<p>I’ve been on social media for almost as long as I’ve been on the internet. I made my Facebook account in April of 2009. I was 13. That, however, wasn’t my introduction to the social internet. I signed up for my first YouTube account shortly after the service launched in 2006. As I’ve grown up with the internet, finding and forging my own communities as a teenager and then building my professional career on it as an adult, the social aspect of internet culture has always been at the heart of it. Experiencing much of my life through the lens of the internet has helped make me who I am today. Without an <a href="https://web.archive.org/web/20111102212107/http://www.rzdesign.nl/2010/10/get-into-forrst/">invitation</a> to an emerging social network for graphic designers called <a href="https://web.archive.org/web/20110808061508/http://forrst.com/">Forrst</a> in 2010, I likely wouldn’t be where I am today.  </p>
<p>While I’ve grown up and made a career in large part because of the social internet, over the past few years as the spread of <a href="https://www.npr.org/2021/10/25/1049015366/the-facebook-papers-what-you-need-to-know">misinformation and vitriol</a> has grown exponentially I’ve started to see the way most of us currently interact with social media as irreparably harmful to our society. That’s not to say that there aren’t benefits to the core ideas at the foundation of the social internet; I just believe that more harm is being done than good.  </p>
<p>I also believe strongly that the algorithms that control the feeds on these platforms also plays a large roll in the toll these platforms can on us as individuals. Like countless others, on many occasions I’ve found myself endlessly scrolling through my feeds. Twitter, Instagram, Facebook, repeat. I would get to the point to where I wasn’t even really paying attention to what I was doing. Just scrolling through the feeds to occupy empty space.  </p>
<p>The algorithms behind the feeds on these platforms are <em>designed</em> to elicit this kind of behavior. When you run out of content, they’ll automatically populate the feed with suggested content. It keeps you scrolling. It keeps you on their platform. The longer you’re on their platform, the more ads you’ll see and the more likely you are to click through to one of them.  </p>
<p><strong>This behavior isn’t healthy.</strong>  </p>
<p>The combination of the damage these platforms do to our communities and the unhealthy behavior I’ve recognized in myself have led me to restructure how I interact with social media. Finding what works for me has taken a few months, and it might not work for everyone, but I have found myself on these platforms less and less since I’ve put the following measures into place.  </p>
<ol>
<li>As a general rule, unfollow everyone except those that you <em>truly</em> want to see in your feeds. No more influencers. No more brands. For me this means restricting my follow list to just friends and family.</li>
<li>Privatize everything. Lock down your privacy settings and only allow the content you share to be shared with those that you choose to be shared with.</li>
<li>When you reach the end of a feed, close the app. No more listless scrolling.</li>
<li>Find an outlet. The time you’ll gain back is valuable. Fill it with something that you truly enjoy. For me, this has been <a href="https://reckart.photo">photography</a>.</li>
</ol>
<p>I’m not removing myself from social media. At this point in our society, I think that route isn’t sustainable in the long run. These platforms are here to stay. However, I believe that deliberate, conscious use can go a long way to a healthy relationship with the social internet.</p>
]]></description></item><item><title>Private Class Elements in TypeScript 4.3</title><pubDate>Wed, 09 Jun 2021 00:00:00 GMT</pubDate><link>https://rkrt.net/private-class-elements-in-typescript</link><guid>https://rkrt.net/private-class-elements-in-typescript</guid><description><![CDATA[<p>Among the <a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-3.html">many additions</a> to TypeScript in its 4.3 release is the ability to define which elements in a class can be given private property names, making them truly private at run-time. Properties, methods and accessors can all be given private names.</p>
<pre><code class="language-ts"><span class="hljs-keyword">class</span> Thing {
  #name = <span class="hljs-string">&quot;&quot;</span>;

  get name() {
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.#name;
  }

  set name(value) {
    <span class="hljs-built_in">this</span>.#name = value;
  }

  #someMethod() {
    <span class="hljs-built_in">this</span>.#name = <span class="hljs-built_in">this</span>.#name.toUpperCase();
  }

  publicMethod() {
    <span class="hljs-comment">// Private-named properties and methods can</span>
    <span class="hljs-comment">// be accessed inside the class.</span>
    <span class="hljs-built_in">this</span>.#someMethod();
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.#name;
  }
}

<span class="hljs-keyword">new</span> Thing().#someMethod();
<span class="hljs-comment">//          ~~~~~~~~~~~</span>
<span class="hljs-comment">// error!</span>
<span class="hljs-comment">// Property &#x27;#someMethod&#x27; is not accessible</span>
<span class="hljs-comment">// outside class &#x27;Thing&#x27; because it has a private identifier.</span>

<span class="hljs-keyword">const</span> myThing = <span class="hljs-keyword">new</span> Thing();
myThing.name = <span class="hljs-string">&quot;Ted Lasso&quot;</span>;
<span class="hljs-keyword">const</span> jazzyName = myThing.publicMethod(); <span class="hljs-comment">// &quot;TED LASSO&quot;</span>
myThing.#name;
<span class="hljs-comment">//      ~~~~~</span>
<span class="hljs-comment">// error!</span>
<span class="hljs-comment">// Property &#x27;#name&#x27; is not accessible</span>
<span class="hljs-comment">// outside class &#x27;Thing&#x27; because it has a private identifier.</span>
</code></pre>
<p>To make methods, getters/setters or fields private, all you have to do is prefix the name with <code>#</code>.</p>
<p>It&#39;s worth noting that private fields and methods exist only as declared up front. They cannot be created later, ad-hoc, or through assignment. You also can&#39;t declare private fields or methods in object literals. If you implement your class by adding individual methods to the prototype, or using a class framework, private methods and fields can&#39;t be used.</p>
]]></description></item><item><title>Server-side rendering in React 18</title><pubDate>Tue, 08 Jun 2021 00:00:00 GMT</pubDate><link>https://rkrt.net/ssr-in-react-18</link><guid>https://rkrt.net/ssr-in-react-18</guid><description><![CDATA[<p>Among the many improvements in React 18, one of the standouts in my opinion are the updates to server-side rendering (SSR) performance. For those that don&#39;t know, SSR lets your app generate HTML from React components directly on the server, which then gets served to your users. This allows your users to see the page&#39;s content before your app&#39;s JavaScript bundle is loaded and run. Most of the improvements are behind in the scenes, but bundled in are a few opt-in methods that are worth exploring.</p>
<p>At the outset, we should establish how SSR works in React:</p>
<ol>
<li>A request is sent from the client.</li>
<li>The server fetches the data for the entire app.</li>
<li>The server renders the entire app as HTML and sends it in the response.</li>
<li>The client loads the JavaScript bundle for the app.</li>
<li>The client runs the code in the JavaScript bundle, connecting it to the generated HTML.</li>
</ol>
<p>This approach works, and it has for a long time, but it&#39;s not an optimal solution.</p>
<p>The first problem is that your app has to fetch <em>everything</em> from the server before your app can show anything. By the time the DOM is painted with the server-generated HTML, you must already have all the data ready for your components on the server. The second issue, building on the first, is that your app has to load <em>everything</em> before it can hydrate the server-generated HTML with the JavaScript code. Prior to React 18, React programmatically stepped through the server-generated HTML while rendering the components, attaching every listeners, and executing functions. The performance hit comes from the fact that due to this method of execution, <strong>your app must load the JavaScript for <em>every</em> component in the render tree before it can start hydrating <em>any</em> of them</strong>. The final consequence from this design methodology is that your app must hydrate everything before the end-user can interact with anything.</p>
<p>With prior versions of React, rendering is all or nothing. Your server responds with HTML, which is received by the client and is then hydrated when the bundle is loaded.</p>
<pre><code class="language-html"><span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>My webpage<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">main</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">header</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>My webpage<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">nav</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">&quot;/about&quot;</span>&gt;</span>About<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">nav</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">header</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">article</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>Post title<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>...<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">article</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">section</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;comments&quot;</span>&gt;</span>...<span class="hljs-tag">&lt;/<span class="hljs-name">section</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">main</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>In React 18, you can now wrap part of a component tree with the <a href="https://github.com/reactwg/react-18/discussions/37"><code>&lt;Suspense&gt;</code></a> component.</p>
<pre><code class="language-jsx">&lt;Flex&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Header</span> /&gt;</span></span>
  {posts.map(<span class="hljs-function">(<span class="hljs-params">post</span>) =&gt;</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Flex.Item</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Post</span>
          <span class="hljs-attr">title</span>=<span class="hljs-string">{post.title}</span>
          <span class="hljs-attr">body</span>=<span class="hljs-string">{post.body}</span>
        /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Suspense</span> <span class="hljs-attr">fallback</span>=<span class="hljs-string">{</span>&lt;<span class="hljs-attr">Loading</span> /&gt;</span>}&gt;
          <span class="hljs-tag">&lt;<span class="hljs-name">Comments</span> <span class="hljs-attr">comments</span>=<span class="hljs-string">{post.comments}</span> /&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">Suspense</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">Flex.Item</span>&gt;</span></span>
  )}
&lt;/Flex&gt;
</code></pre>
<p>By wrapping the <code>&lt;Comments&gt;</code> component with <code>&lt;Suspense&gt;</code>, React won&#39;t wait for the comments to start streaming to render the HTML for the rest of the page. Instead, React will render a placeholder (fallback) component until the comments are loaded.</p>
<p>The HTML sent by the server will now look something like this:</p>
<pre><code class="language-html"><span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>My webpage<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">main</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">header</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>My webpage<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">nav</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">&quot;/about&quot;</span>&gt;</span>About<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">nav</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">header</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">article</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>Post title<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>...<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">article</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">section</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;loading&quot;</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>Loading...<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">section</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">main</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>Then, when the data for the comments is ready on the server React will automatically inject the new HTML into the stream as well as a <code>&lt;script&gt;</code> tag that includes logic to replace the component in the HTML stream. That&#39;s where the power of the <code>&lt;Suspense&gt;</code> component lives. Your app doesn&#39;t have to fetch <em>all</em> of the data before your server can render anything. If some part of the call stack delays the initial response, you don&#39;t have to choose between delaying all of the HTML in the response or substituting it until the data is ready.</p>
<p>The features introduced by <code>&lt;Suspense&gt;</code> solve all three of the existing problems with SSR in React:</p>
<ol>
<li>Your app no longer needs to wait for all of the data to load on the server before rendering HTML.</li>
<li>Your app no longer needs to wait for all JavaScript to load to start hydrating.</li>
<li>Your app no longer needs to wait for all the components in the current render tree to be hydrated for users to start interacting with the page. Instead, the <code>&lt;Suspense&gt;</code> component along with <a href="https://github.com/reactwg/react-18/discussions/5"> <code>createRoot</code> </a> uses selective hydration to prioritize the components the user is interacting with and hydrate them early.</li>
</ol>
<p>The <code>&lt;Suspense&gt;</code> component is completely opt-in. Simply replace your <code>if (isLoading) { ... }</code> conditionals with <code>&lt;Suspense&gt;</code> wrappers. The improvements are automatic, but it serves as an illustrative example of the power of declarative component loading states. Your users will see your app&#39;s content sooner and will be able to start interacting with it faster. The slowest parts of your app will no longer hold back the parts that are fast. If your app uses SSR to serve content, the <code>&lt;Suspense&gt;</code> component in React 18 is a no-brainer.</p>
]]></description></item><item><title>Perpetual Motion: The Atmos Clock</title><pubDate>Wed, 16 Dec 2020 00:00:00 GMT</pubDate><link>https://rkrt.net/perpetual-motion</link><guid>https://rkrt.net/perpetual-motion</guid><description><![CDATA[<p><img src="https://s3.amazonaws.com/reckart.blog/6M6A5291-min.jpg" alt="Image of my personal Jaeger-LeCoultre Atmos Clock"></p>
<p>The Jaeger-LeCoultre (or LeCoultre &amp; Cie as it was marketed in North America in the mid twentieth century) Atmos is a unique member of the horological pantheon. The 259-serial pictured above is from my personal collection and is an excellent example of how the design of the Atmos contributes to the clock’s longevity; having left the factory in the mid 60s.</p>
<p>What makes an Atmos clock unique among the class of pendulum-driven mantle clocks is that it does not need to be manually wound. Like all mechanical timepieces, the Atmos is driven by a mainspring. However, unlike most clocks of its class and construction, the Atmos’ mainspring is wound by the expansion and contraction of a volatile liquid, ethyl chloride, inside of a hermetically sealed bellows system.</p>
<p>As the surrounding temperature rises the ethyl chloride vaporizes, compressing a spiral spring. When the temperature falls the vapor condenses back into a liquid, allowing the spring to return to its resting state. The constant motion generated by the compression and expansion of this spring drives the bellows system and subsequently winds the clock’s mainspring.</p>
<p><em>A temperature change of only one degree can sufficiently power the clock for up to two days.</em></p>
<p>While the Atmos’ construction is unique among many clocks today, clocks powered by changes in atmospheric pressure date all the way back to the early 17th century. The inventor of the mechanism, <a href="https://en.wikipedia.org/wiki/Cornelis_Drebbel">Cornelis Drebbel</a>, built as many as 18 atmospherically-driven clocks that we know about today.</p>
<p>If you ever get a chance to inspect an Atmos clock in person or add one to your collection, I highly recommend it. The smooth motion of the pendulum and openness of the mechanism it drives is mesmerizing.</p>
]]></description></item><item><title>Starting Over</title><pubDate>Thu, 01 Oct 2020 00:00:00 GMT</pubDate><link>https://rkrt.net/starting-over</link><guid>https://rkrt.net/starting-over</guid><description><![CDATA[<p>In the decade that I&#39;ve been building things for the web; first just noodling around as a teenager picking up freelance projects here and there, then professionally, my personal website has gone through an immeasurable amount of iteration. I actually can&#39;t count the number of times I&#39;ve rebuilt or redesigned my website because most of the early iterations came and went long before I knew anything about version control.</p>
<p>Early iterations were simple static HTML and CSS bundles with a tinge of JavaScript thrown in for effect. Over the years my website would evolve to take the form of a CMS-backed blog, a blog with hand-written post content as HTML templates (I definitely don&#39;t recommend that one), a single-page React application, and most recently a <a href="https://github.com/tylerreckart/tylerreckart.com">statically-rendered React blog</a>.</p>
<p>Looking back, I can see each iteration as a reflection of where I was in my journey as a software developer. As my skillset increased and depth of knowledge expanded, my goals for what my website could be grew in turn. The last few iterations of my website have been built in React; and why shouldn&#39;t they have been? I&#39;ve been working with React professionally since 2015. I love React and the flexibility it gives developers to build and ship modern apps for the web. However, what I haven&#39;t always loved about React is the sheer amount of boilerplate code required to get an app up and running.</p>
<p>I should note that things have gotten better over the years. There are great tools like <a href="https://github.com/facebook/create-react-app">create-react-app</a>, <a href="https://github.com/vercel/next.js">Next.js</a>, and <a href="https://github.com/gatsbyjs/gatsby">Gatsby</a> that make the tooling and configuration steps needed to get a React app up and running as minimal as possible. This is nice, but they don&#39;t get rid of the problem. The configuration and boilerplate still exists behind the scenes in the respective package&#39;s build system.</p>
<p>When I decided to rethink my website for this iteration, I had to think long and hard about that reality. I decided that I just didn&#39;t need an entire Babel and Webpack configuration and the immeasurable amount of dependencies that come with one just to publish content for the web. It&#39;s all of those little idiosyncrasies specific to React development that just added more friction to the publishing process than I wanted. I didn&#39;t want to have to worry about making sure my dependencies were up to date just to build the website. I shouldn&#39;t have to write an entire component hierarchy just to build a blog. </p>
<p>It was that friction that kept me from publishing more on the blogs I&#39;ve built in the past. So, I set out to build a publishing system that I actually <em>wanted</em> to use. I ended up with three requirements:</p>
<ol>
<li>The website should be statically rendered.</li>
<li>The website should have as few dependencies as possible.</li>
<li>All post and page content should be rendered with Markdown templates.</li>
</ol>
<p>Those requirements, and a week of playing around with Node&#39;s file system tools have led me to this iteration. All of the content on the website is written in Markdown with the HTML markup being generated through <a href="https://github.com/pugjs/pug">Pug</a> templates. The only dependencies the build system has are related to asset minification, code-block formatting, templating, and XML generation for the RSS feed. That&#39;s it. No complex build configurations.</p>
<p>During the build process for this website, I had a realization about how extensible the templating system is and how easy it would be to write additional themes that could be quickly configured with a single point of configuration. I didn&#39;t set out to build a tool that other people could use to build static websites quickly and easily, but what I ended up with is what I am calling <a href="https://github.com/tylerreckart/mortar">Mortar</a>.</p>
<p>Mortar is built to be exceptionally simple. I didn&#39;t want it to make any bold assumptions about what a user would want or to burden them with complex configurations and deployment requirements. Simply put, it doesn&#39;t try to outsmart the user. Mortar gets out of the way and allows the user to focus on what actually matters: the content.</p>
<p>So, how does it work. As mentioned briefly above, it&#39;s a straightforward adaptation of Node&#39;s file system tools and a few additional libraries that aid with asset minification, markdown parsing, and templating. Markdown files placed in the pages and posts directories are automatically parsed, fed through the templating system and compiled into HTML templates. All the web server has to do is serve content from the <code>build</code> folder. The exact method for accomplishing that will vary based on the server a configuration a user might be utilizing (NGINX, Apache, etc), but the process is exceptionally simple.</p>
<p>I chose Pug as the templating engine for the HTML markup because of its performance and specific bend towards Node apps. The syntax, heavily influenced by <a href="https://haml.info/">Haml</a>, is simple and only takes a few minutes to pick up. Inline JavaScript is a breeze and variable scope is automatically passed into included components, meaning that you don&#39;t have to worry about prop inheritance or explicitly passing what you need into the component.</p>
<pre><code class="language-pug">include time.pug
doctype html
html(lang=&quot;en&quot;)
  head
    if seo.title
      title=seo.title
    else
      title=&quot;&quot;
    if seo.description
      meta(name=&quot;description&quot;, content=seo.description)
    else
      meta(name=&quot;description&quot;, content=&quot;&quot;)
    include templates/head.pug
  body
    main.content
      include templates/nav.pug
      include templates/intro.pug
      each post in posts
        article
          div.article--header
            a(href=post.path) #[h2=post.attributes.title]
            +time(post.attributes.date)
          div.article--content
            | !{post.body}
      include templates/footer.pug
</code></pre>
<p>That&#39;s the pug template that generates all of the markup for the homepage of this website. That&#39;s it. More complex pieces of functionality, such as generating the time tags underneath post titles is easily handled by Pug mixins that can be included in a number of templates.</p>
<pre><code class="language-pug">-
  function toISODate(date) {
    return (new Date(Date.parse(date))).toISOString();
  }

mixin time(date)
  time(datetime!=toISODate(date), pubdate=&quot;pubdate&quot;)=&quot;Posted on &quot; + date
</code></pre>
<p>Over the years I&#39;ve used a lot of different templating engines. Early in my career, Twig was the templating system I used to build Craft CMS websites. In the intervening time I&#39;ve used Handlebars templates, EJS templates, and of course, JSX templates in the context of React apps. Of all of those varying templating engines, Pug has to be the simplest I have used. The syntax is clean, declarative, and easy to understand. It makes building themes for Mortar websites a breeze.</p>
<p>Mortar still has a long way to go as a tool before it&#39;s ready for a wider audience. It&#39;s still firmly in it&#39;s &quot;happy accident&quot; phase. It has several aspects that can be optimized further and I have lofty ambitions for automatic deployments. Mortar started as a way for me to remove friction from the process of publishing content on my website. My hope is that if someone else out there has the same frustrations I&#39;ve had when it comes to writing content for a personal website without the weight that comes from a full-fledged CMS, that Mortar can be an effective tool for them too.</p>
<p>I&#39;m extremely pleased with this iteration of my website. I&#39;m excited to use this clean slate and the build system I&#39;ve put together as a springboard to publish more content to my home on the web.</p>
]]></description></item></channel></rss>