<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.3.3">Jekyll</generator><link href="https://zekesnider.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://zekesnider.com/" rel="alternate" type="text/html" /><updated>2026-04-30T05:23:18+00:00</updated><id>https://zekesnider.com/feed.xml</id><title type="html">Zeke Snider</title><subtitle>My personal website
</subtitle><author><name>GitHub User</name><email>your-email@domain.com</email></author><entry><title type="html">Customizing SwiftUI List Selection</title><link href="https://zekesnider.com/customizing-swiftui-list-selection/" rel="alternate" type="text/html" title="Customizing SwiftUI List Selection" /><published>2025-03-01T19:00:00+00:00</published><updated>2025-03-01T19:00:00+00:00</updated><id>https://zekesnider.com/customizing-swiftui-list-selection</id><content type="html" xml:base="https://zekesnider.com/customizing-swiftui-list-selection/"><![CDATA[<p>Recently, while working on my new app <a href="https://zeke.dev/projects/lyrigraphy/">Lyrigraphy</a>, I wanted to customize the behavior of list selection using gestures. The list was previously using a default SwiftUI <a href="https://developer.apple.com/documentation/swiftui/list">List</a>. When the user taps a cell, it would highlight it and add its index to the selected items array. This was simple and worked well, however I wanted to add some custom behavior on top of this.</p>

<p>I wanted the ability to tap and drag across cells to be able to select multiple items in the list at once. Tapping cells one by one can get tedious if you want to select a large number of cells after all. On its face this seemed like a simple ask, this is a fairly common UI pattern. However, this ended up requiring a fair amount of effort and custom logic to accomplish within SwiftUI’s frameworks. This is typical of SwiftUI, the default controls are very simple and easy to use, but extensive customization can be painful.</p>

<p>While I’m not advertising this as the best way to implement a list by any means, this post may be useful if you’re looking to accomplish something similar in SwiftUI. Surprisingly I ended up with something that I’m satisfied with.</p>

<p>If you’re just interested in the source code you can check it out <a href="https://gist.github.com/ZekeSnider/11839addeb2dde50437a0bf71c59c89a">here</a>. Be warned it is a bit messy, but it is self-contained and you can compile and play around with it. Or, you can check out <a href="https://apps.apple.com/us/app/lyrigraphy/id6740042606">Lyrigraphy on the App Store</a> to try the finished product in the lyrics screen!</p>

<h2 id="system-view">System view</h2>

<p><img class="fullwidthimg defaultimg" src="/assets/iOSSystemMultiSelection.png" alt="iOS system multi-selection" /><br />
<span class="caption"></span></p>

<p>The default list implementation <em>does</em> have a multi-selection mode similar to what I’m describing. However, it doesn’t quite meet what I was looking for visually or functionally. First, the list must be in an edit mode which changes its appearance. Second, the draggable area is only on the left side of the list. While technically this could have worked, it wasn’t quite the experience I wanted for this important view in my app. So I decided to delve further into this challenge.</p>

<h2 id="approach">Approach</h2>

<p>Since I would essentially need to implement this multi-row selection from scratch, let’s break down what the tap and drag gesture actually entails.</p>
<ol>
  <li>First a hold gesture needs to be made (~0.5s duration)</li>
  <li>Until the user releases, a drag gesture is recognized</li>
  <li>As the user drags across the screen, we need to update the pending selection of items and color them accordingly on screen using the coordinates of the drag</li>
  <li>If the user is reaching near the top or bottom of the screen, start auto-scrolling the list up or down. There should be a timeout so that it doesn’t repeatedly jump the list.</li>
  <li>When the user releases, we should add (or remove) items from the selection list and update the visual state accordingly.</li>
</ol>

<p>As you can see there is actually a lot of requirements under the hood for this gesture. Without a system provided view, we will be doing much of the heavy lifting including boundary math with coordinates.</p>

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

<p>These are the APIs that ended up being essential for my implementation:</p>

<ol>
  <li><a href="https://developer.apple.com/documentation/swiftui/draggesture">DragGesture</a> sequenced with <a href="https://developer.apple.com/documentation/swiftui/longpressgesture">LongPressGesture</a> for gesture recognizers</li>
  <li><a href="https://developer.apple.com/documentation/homekit/cameraview/highprioritygesture(_:including:)/">highPriorityGesture</a>
 for applying the gesture</li>
  <li><a href="https://developer.apple.com/documentation/swiftui/coordinatespace">CoordinateSpace</a> and <a href="https://developer.apple.com/documentation/SwiftUI/GeometryProxy">GeometryProxy</a> to retrieve coordinates within the scroll view</li>
  <li>Using <a href="https://developer.apple.com/documentation/swiftui/preferencekey">PreferenceKey</a>s with <a href="https://developer.apple.com/documentation/swiftui/geometryreader">GeometryReader</a> to pass coordinate data back up to the containing view</li>
  <li><a href="https://developer.apple.com/documentation/swiftui/scrollviewreader/">ScrollViewReader</a> to control scroll state</li>
</ol>

<p>It’s worth noting that you should <strong>not</strong> use a SwiftUI List view for this implementation. Instead use your own ScrollView with a contained ForEach block. A list comes with some nice styling defaults, but it really interferes with a lot of the custom handling required. As one example, it places padding in geometry space between each cell. You can achieve similar list styling with your own modifiers.</p>

<p>PreferenceKeys are at the core of the logic, allowing us to easily reference the exact coordinates of each cell. This allows live updates of the selected items as the gesture is being updated. Setting a custom CoordinateSpace ensures that the coordinates we reference are within the entire scroll view’s context, rather than what is currently on screen. This is important because we autoscroll when reaching the top or bottom of the screen, so the on screen coordinate space of items are not consistent.</p>

<p>We define a struct containing all the fields we need for each cell. We also store the global coordinates because we need to determine if the gesture is currently near the top or bottom of the screen.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">LinePreferenceData</span><span class="p">:</span> <span class="kt">Equatable</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">index</span><span class="p">:</span> <span class="kt">Int</span>
    <span class="k">let</span> <span class="nv">minY</span><span class="p">:</span> <span class="kt">Double</span>
    <span class="k">let</span> <span class="nv">maxY</span><span class="p">:</span> <span class="kt">Double</span>
    <span class="k">let</span> <span class="nv">globalMinY</span><span class="p">:</span> <span class="kt">Double</span>
    <span class="k">let</span> <span class="nv">globalMaxY</span><span class="p">:</span> <span class="kt">Double</span>
    
    <span class="nf">init</span><span class="p">(</span><span class="nv">index</span><span class="p">:</span> <span class="kt">Int</span><span class="p">,</span> <span class="nv">bounds</span><span class="p">:</span> <span class="kt">CGRect</span><span class="p">,</span> <span class="nv">globalBounds</span><span class="p">:</span> <span class="kt">CGRect</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">self</span><span class="o">.</span><span class="n">index</span> <span class="o">=</span> <span class="n">index</span>
        <span class="k">self</span><span class="o">.</span><span class="n">minY</span> <span class="o">=</span> <span class="n">bounds</span><span class="o">.</span><span class="n">minY</span>
        <span class="k">self</span><span class="o">.</span><span class="n">maxY</span> <span class="o">=</span> <span class="n">bounds</span><span class="o">.</span><span class="n">maxY</span>
        <span class="k">self</span><span class="o">.</span><span class="n">globalMinY</span> <span class="o">=</span> <span class="n">globalBounds</span><span class="o">.</span><span class="n">minY</span>
        <span class="k">self</span><span class="o">.</span><span class="n">globalMaxY</span> <span class="o">=</span> <span class="n">globalBounds</span><span class="o">.</span><span class="n">maxY</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The preference key simply wraps this in the outer view to create an array that we can reference. We aren’t guaranteed that this is in order, so we always reference the index of the <code class="language-plaintext highlighter-rouge">LinePreferenceData</code> for reconciliation.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">LinePreferenceKey</span><span class="p">:</span> <span class="kt">PreferenceKey</span> <span class="p">{</span>
    <span class="kd">typealias</span> <span class="kt">Value</span> <span class="o">=</span> <span class="p">[</span><span class="kt">LinePreferenceData</span><span class="p">]</span>
    
    <span class="kd">static</span> <span class="k">var</span> <span class="nv">defaultValue</span><span class="p">:</span> <span class="p">[</span><span class="kt">LinePreferenceData</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span>
    
    <span class="kd">static</span> <span class="kd">func</span> <span class="nf">reduce</span><span class="p">(</span><span class="nv">value</span><span class="p">:</span> <span class="k">inout</span> <span class="p">[</span><span class="kt">LinePreferenceData</span><span class="p">],</span> <span class="nv">nextValue</span><span class="p">:</span> <span class="p">()</span> <span class="o">-&gt;</span> <span class="p">[</span><span class="kt">LinePreferenceData</span><span class="p">])</span> <span class="p">{</span>
        <span class="n">value</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="nv">contentsOf</span><span class="p">:</span> <span class="nf">nextValue</span><span class="p">())</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Then in each cell in our ForEach, we place a clear background that sets the PreferenceKey’s values. This is a clever hack, capturing the entire geometry space of the cell without affecting its appearance in the UI.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">.</span><span class="nf">background</span><span class="p">()</span> <span class="p">{</span>
    <span class="kt">GeometryReader</span> <span class="p">{</span> <span class="n">geometry</span> <span class="k">in</span>
        <span class="kt">Rectangle</span><span class="p">()</span>
            <span class="o">.</span><span class="nf">fill</span><span class="p">(</span><span class="kt">Color</span><span class="o">.</span><span class="n">clear</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">preference</span><span class="p">(</span><span class="nv">key</span><span class="p">:</span> <span class="kt">LinePreferenceKey</span><span class="o">.</span><span class="k">self</span><span class="p">,</span>
                        <span class="nv">value</span><span class="p">:</span> <span class="p">[</span><span class="kt">LinePreferenceData</span><span class="p">(</span><span class="nv">index</span><span class="p">:</span> <span class="n">lyric</span><span class="o">.</span><span class="n">id</span><span class="p">,</span>
                                                    <span class="nv">bounds</span><span class="p">:</span> <span class="n">geometry</span><span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">in</span><span class="p">:</span> <span class="o">.</span><span class="nf">named</span><span class="p">(</span><span class="s">"container"</span><span class="p">)),</span>
                                                    <span class="nv">globalBounds</span><span class="p">:</span> <span class="n">geometry</span><span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">in</span><span class="p">:</span> <span class="o">.</span><span class="n">global</span><span class="p">))])</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>It is <em>critical</em> that the <code class="language-plaintext highlighter-rouge">.coordinateSpace</code> modifier is applied to appropriate element in your outer view. It must be set on the VStack inside the ScrollView. If it is placed on the wrong element it causes the geometries to be incorrect, and it is very difficult to debug. I ended up spending several hours on this, and the solution was moving the coordinateSpace designation up just one line.</p>

<p>To bring it all together, we need to configure the gesture. Most of this is straightforward, though we do have some business logic to find where the drag started to determine if we are selecting or unselecting with this drag.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">var</span> <span class="nv">drag</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">Gesture</span> <span class="p">{</span>
    <span class="kt">LongPressGesture</span><span class="p">(</span><span class="nv">minimumDuration</span><span class="p">:</span> <span class="mf">0.5</span><span class="p">)</span>
        <span class="o">.</span><span class="nf">sequenced</span><span class="p">(</span><span class="nv">before</span><span class="p">:</span> <span class="kt">DragGesture</span><span class="p">(</span><span class="nv">minimumDistance</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="nv">coordinateSpace</span><span class="p">:</span> <span class="o">.</span><span class="nf">named</span><span class="p">(</span><span class="s">"container"</span><span class="p">)))</span>
        <span class="o">.</span><span class="nf">updating</span><span class="p">(</span><span class="n">$isDragging</span><span class="p">,</span> <span class="nv">body</span><span class="p">:</span> <span class="p">{</span> <span class="n">value</span><span class="p">,</span> <span class="n">state</span><span class="p">,</span> <span class="n">transaction</span> <span class="k">in</span>
            <span class="k">switch</span> <span class="n">value</span> <span class="p">{</span>
            <span class="k">case</span> <span class="o">.</span><span class="nf">first</span><span class="p">(</span><span class="kc">true</span><span class="p">):</span>
                <span class="k">break</span>
            <span class="k">case</span> <span class="o">.</span><span class="nf">second</span><span class="p">(</span><span class="n">_</span><span class="p">,</span> <span class="k">let</span> <span class="nv">drag</span><span class="p">):</span>
                <span class="k">guard</span> <span class="k">let</span> <span class="nv">start</span> <span class="o">=</span> <span class="n">drag</span><span class="p">?</span><span class="o">.</span><span class="n">startLocation</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
                <span class="k">let</span> <span class="nv">end</span> <span class="o">=</span> <span class="n">drag</span><span class="p">?</span><span class="o">.</span><span class="n">location</span> <span class="p">??</span> <span class="n">start</span>
                
                <span class="k">if</span> <span class="n">isDraggingSelected</span> <span class="o">==</span> <span class="kc">nil</span> <span class="p">{</span>
                    <span class="k">let</span> <span class="nv">dragStartIndex</span> <span class="o">=</span> <span class="nf">getStartIndex</span><span class="p">(</span><span class="nv">from</span><span class="p">:</span> <span class="n">start</span><span class="o">.</span><span class="n">y</span><span class="p">)</span>
                    <span class="n">isDraggingSelected</span> <span class="o">=</span> <span class="n">dragStartIndex</span><span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">viewModel</span><span class="o">.</span><span class="n">selectedLyricIndexes</span><span class="o">.</span><span class="nf">contains</span><span class="p">(</span><span class="nv">$0</span><span class="p">)</span> <span class="p">}</span> <span class="p">??</span> <span class="kc">false</span>
                <span class="p">}</span>
                
                <span class="nf">handleDragChange</span><span class="p">(</span><span class="nv">start</span><span class="p">:</span> <span class="n">start</span><span class="p">,</span> <span class="nv">end</span><span class="p">:</span> <span class="n">end</span><span class="p">)</span>
            <span class="k">default</span><span class="p">:</span>
                <span class="k">return</span>
            <span class="p">}</span>
        <span class="p">})</span>
        <span class="o">.</span><span class="n">onEnded</span> <span class="p">{</span> <span class="n">value</span> <span class="k">in</span>
            <span class="k">switch</span> <span class="n">value</span> <span class="p">{</span>
            <span class="k">case</span> <span class="o">.</span><span class="nf">first</span><span class="p">(</span><span class="kc">true</span><span class="p">):</span>
                <span class="c1">// Long press succeeded</span>
                <span class="n">isSelecting</span> <span class="o">=</span> <span class="kc">true</span>
            <span class="k">case</span> <span class="o">.</span><span class="nf">second</span><span class="p">(</span><span class="kc">true</span><span class="p">,</span> <span class="n">_</span><span class="p">):</span>
                <span class="c1">// Drag ended</span>
                <span class="nf">updateSelectionRange</span><span class="p">()</span>
                <span class="n">isSelecting</span> <span class="o">=</span> <span class="kc">false</span>
                <span class="n">isDraggingSelected</span> <span class="o">=</span> <span class="kc">nil</span>
            <span class="k">default</span><span class="p">:</span>
                <span class="k">break</span>
            <span class="p">}</span>
        <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And finally, implement the method to handle update events to the gesture.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">func</span> <span class="nf">handleDragChange</span><span class="p">(</span><span class="nv">start</span><span class="p">:</span> <span class="kt">CGPoint</span><span class="p">,</span> <span class="nv">end</span><span class="p">:</span> <span class="kt">CGPoint</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">minY</span> <span class="o">=</span> <span class="nf">min</span><span class="p">(</span><span class="n">start</span><span class="o">.</span><span class="n">y</span><span class="p">,</span> <span class="n">end</span><span class="o">.</span><span class="n">y</span><span class="p">)</span>
    <span class="k">let</span> <span class="nv">maxY</span> <span class="o">=</span> <span class="nf">max</span><span class="p">(</span><span class="n">start</span><span class="o">.</span><span class="n">y</span><span class="p">,</span> <span class="n">end</span><span class="o">.</span><span class="n">y</span><span class="p">)</span>
    
    <span class="c1">// Clear the pending selection before recalculating</span>
    <span class="n">pendingSelection</span><span class="o">.</span><span class="nf">removeAll</span><span class="p">()</span>
    
    <span class="c1">// Find all lines that intersect with the drag range</span>
    <span class="k">let</span> <span class="nv">selectedLines</span> <span class="o">=</span> <span class="n">lineData</span><span class="o">.</span><span class="n">filter</span> <span class="p">{</span> <span class="n">line</span> <span class="k">in</span>
        <span class="k">return</span> <span class="o">!</span><span class="p">(</span><span class="n">line</span><span class="o">.</span><span class="n">maxY</span> <span class="o">&lt;</span> <span class="n">minY</span> <span class="o">||</span> <span class="n">line</span><span class="o">.</span><span class="n">minY</span> <span class="o">&gt;</span> <span class="n">maxY</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="k">let</span> <span class="nv">currentLine</span> <span class="o">=</span> <span class="n">lineData</span><span class="o">.</span><span class="n">first</span> <span class="p">{</span> <span class="n">line</span> <span class="k">in</span>
        <span class="k">return</span> <span class="n">end</span><span class="o">.</span><span class="n">y</span> <span class="o">&gt;=</span> <span class="n">line</span><span class="o">.</span><span class="n">minY</span> <span class="o">&amp;&amp;</span> <span class="n">end</span><span class="o">.</span><span class="n">y</span> <span class="o">&lt;=</span> <span class="n">line</span><span class="o">.</span><span class="n">maxY</span>
    <span class="p">}</span>
    
    <span class="n">pendingSelection</span> <span class="o">=</span> <span class="kt">Set</span><span class="p">(</span><span class="n">selectedLines</span><span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="nv">$0</span><span class="o">.</span><span class="n">index</span> <span class="p">})</span>
    
    <span class="k">if</span> <span class="k">let</span> <span class="nv">currentLine</span> <span class="p">{</span>
        <span class="nf">handleAutoScroll</span><span class="p">(</span><span class="nv">currentY</span><span class="p">:</span> <span class="n">currentLine</span><span class="o">.</span><span class="n">globalMaxY</span><span class="p">,</span> <span class="nv">index</span><span class="p">:</span> <span class="n">currentLine</span><span class="o">.</span><span class="n">index</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We use the ScrollViewProxy to trigger scrolling up or down. ScrollViewProxy only allows you to target specific elements, so I decided to scroll up or down 3 items from the current position (with bounds checking). We use a boolean state variable with a dispatch queue <code class="language-plaintext highlighter-rouge">asyncAfter</code> to isolate scrolling so that it doesn’t continually re-trigger.</p>

<p>There are some other small details not mentioned like coloring, gesture state, etc. But this is the gist of the implementation. It is very possible this implementation is not well optimized for a very large number of items, I was not designing for that use case in my app. The <a href="https://gist.github.com/ZekeSnider/11839addeb2dde50437a0bf71c59c89a">full code</a> is a bit messy, but figured it would be worth publishing in case it’s useful to anyone to reference.</p>

<h2 id="appendix-a-note-on-llms">Appendix: a note on LLMs</h2>

<p>As a side note, I also utilized LLMs throughout this process and saw both areas where they excel and struggle with this iteration process. Overall the models are familiar with SwiftUI, but sometimes require prompting to use the latest features. This is likely an aspect of a lot of open source code targeting older iOS versions. And of course with more difficult issues, it will frequently hallucinate out of the problem with non-existent APIs which you need to be careful of.</p>

<p>If you have a high level understanding of which tools to use, you can improve prompts by including specific APIs or design patterns you are trying to utilize. For example, “Use the @Observable macro instead of extending ObservableObject directly”. Directly linking to a specific Apple Documentation pages can also be beneficial. However, they do tend to struggle with more advanced logic regardless. Even newer thinking models like R1 were not able to debug some of the issues I encountered.</p>

<p>With a simple prompt describing my requirements and providing the existing view code, Claude was able to provide a solid high level approach. I started with <em>very</em> simple prompts like “How do I make it so that when you tap (and hold) and drag it allows you select multiple items at a time?”. Of course many, many details were wrong and it required tons of iteration. But with some follow up prompting, I was able to find my building blocks including some APIs I had not heard of before.</p>

<p>My general iteration steps were to:</p>
<ol>
  <li>Provide simple instructions to the model on my goal with my existing code</li>
  <li>Review output, incorporate relevant functionality into my implementation</li>
  <li>Re-prompt with new issues or bugs with the new implementation</li>
  <li>Manually correct bugs and continue to iterate</li>
</ol>

<p>A lot of its suggestions were valid at a surface level. But none of the models were very good at thinking through the overall functionality of a complex view, or debugging complex performance and logic issues. All the models (including thinking models) struggled to make any progress with esoteric SwiftUI bugs, or logical boundary math using coordinates.</p>

<p>Once I got to a high level of refinement, I had more luck manually scrutinizing the view and debugging. The models would give me seemingly increasingly random things to try, with high certainty in their incorrect theories. While you can prompt for assistance with high level debugging theories, it was not that useful for resolving detailed technical issues. To be fair, I found this task to be generally challenging as a human as well.</p>

<p>Also, using Xcode the LLM tooling is just much slower and tedious than other tools. My general workflow was just copy/pasting text between Xcode and the Anthropic console.</p>]]></content><author><name>GitHub User</name><email>your-email@domain.com</email></author><summary type="html"><![CDATA[Recently, while working on my new app Lyrigraphy, I wanted to customize the behavior of list selection using gestures. The list was previously using a default SwiftUI List. When the user taps a cell, it would highlight it and add its index to the selected items array. This was simple and worked well, however I wanted to add some custom behavior on top of this.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://zekesnider.com/CustomMultiSelectList.png" /><media:content medium="image" url="https://zekesnider.com/CustomMultiSelectList.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Using SwiftData Transaction History to Update Widgets</title><link href="https://zekesnider.com/swift-data-transction-history/" rel="alternate" type="text/html" title="Using SwiftData Transaction History to Update Widgets" /><published>2025-02-12T04:14:00+00:00</published><updated>2025-02-12T04:14:00+00:00</updated><id>https://zekesnider.com/swift-data-transction-history</id><content type="html" xml:base="https://zekesnider.com/swift-data-transction-history/"><![CDATA[<p>My app <a href="https://zeke.dev/projects/lyrigraphy/">Lyrigraphy</a> uses <a href="https://developer.apple.com/xcode/swiftui/">SwiftUI</a> and <a href="https://developer.apple.com/documentation/swiftdata">SwiftData</a> to manage its UI lifecycle and data storage, respectively. I was recently working on adding a Widget extension to it, and was considering how to handle updating the Widget views when the underlying data changes.</p>

<p>My goal was to selectively live update widgets when the underlying data model records are updated. The general flow of events I was envisioning was as follows:</p>

<ol>
  <li>The app subscribes to data model changes (local <a href="https://en.wikipedia.org/wiki/Change_data_capture">change data capture</a>)</li>
  <li>Filter changes to only those relevant to active widgets</li>
  <li>Trigger reload of those widgets</li>
</ol>

<p>Just interested in the sample code? <a href="https://gist.github.com/ZekeSnider/00f0c6bbdada67910886896b7c58e6c5">Check it out here</a>.</p>

<h2 id="widget-updates">Widget Updates</h2>

<p>Apple has provided <a href="https://developer.apple.com/documentation/widgetkit/keeping-a-widget-up-to-date">documentation</a> on how widget updates work under the hood; essentially your widget provides a timeline of events to render to the system. The system renders each timeline event statically, your view is not active when it is visible on the screen. APIs are provided to trigger reload of widgets when necessary. This prevents battery drain on the device, especially when there are many active widgets.</p>

<p>Because of this behavior, without intervention my widgets appear stale if the user edits the details of an object. They would have to wait for the system to request new timeline entries for it to refresh, not ideal. My goal was to keep the widget as up to date as possible without negatively impacting performance or getting throttled by system APIs. Thus my preference for tying into data model updates.</p>

<p>For context, my app exposes a single “kind” of widgets in various sizes. Users can configure the widget to display a specific song, or a random song. Most updates to the model are user initiated, but also could be remotely pushed by iCloud sync.</p>

<p>It’s worth noting that WidgetKit’s <a href="https://developer.apple.com/documentation/widgetkit/widgetcenter">WidgetCenter</a> API only allows you to trigger a reload of either 1. all widgets, or 2. all widgets of a specific kind. So you have limited granularity on which widgets to reload on demand. The more kinds of widgets you vend, the more flexibility you have. In my case, I could improve performance by separating the song widget into two kinds, “Specific Song” and “Random Song”. There’s also a UX tradeoff here, as more kinds of widgets crowds the list view when you add a widget to your home screen.</p>

<p>You also can’t query timeline events of widgets; my random song widget would always have to reload when any data record changes. This is because we can’t tell which record objects were placed into the widget timeline since they are randomly generated.</p>

<h2 id="swiftdata-transaction-history">SwiftData Transaction History</h2>

<p>Apple recently added <a href="https://developer.apple.com/documentation/SwiftData/Fetching-and-filtering-time-based-model-changes">transaction history</a> API to SwiftData in iOS 18. This allows you to easily query chronological transactions that were made to your data store. This <a href="https://developer.apple.com/videos/play/wwdc2024/10075">WWDC talk</a> goes into some more details of how it works. I thought this API would be a great fit for my use case. I could consume each transaction to determine if a relevant widget should be updated based on its update.</p>

<p>The transaction history APIs allow to query transactions using a history token as a cursor mechanism. Notably, there is no async/await API to live subscribe to new changes occurring, other than to frequently poll the query API. To bridge the gap I realized I could wait on NotificationCenter notifications for <a href="https://developer.apple.com/documentation/foundation/nsnotification/name/3180044-nspersistentstoreremotechange">NSPersistentStoreRemoteChange</a>, which notifies for both remote and local changes to the Core Data database.</p>

<p>So to recap the approach before getting into the code:</p>

<ol>
  <li>Wait for <code class="language-plaintext highlighter-rouge">NSPersistentStoreRemoteChange</code> notifications</li>
  <li>Poll SwiftData transaction history since the last history token</li>
  <li>Filter events to those relevant to widgets</li>
  <li>Reload relevant widget kinds</li>
  <li>Delete old transactions, store new history token</li>
</ol>

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

<p>First, let’s start by adding a <a href="https://developer.apple.com/documentation/swiftdata/modelactor">ModelActor</a> for performing these operations:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@ModelActor</span> <span class="kd">final</span> <span class="kd">actor</span> <span class="kt">DataMonitor</span> <span class="p">{</span>
    <span class="kd">func</span> <span class="nf">subscribeToModelChanges</span><span class="p">()</span> <span class="k">async</span> <span class="p">{</span>
        <span class="k">for</span> <span class="k">await</span> <span class="n">_</span> <span class="k">in</span> <span class="kt">NotificationCenter</span><span class="o">.</span><span class="k">default</span><span class="o">.</span><span class="nf">notifications</span><span class="p">(</span>
            <span class="nv">named</span><span class="p">:</span> <span class="o">.</span><span class="kt">NSPersistentStoreRemoteChange</span>
        <span class="p">)</span><span class="o">.</span><span class="nf">map</span><span class="p">({</span> <span class="n">_</span> <span class="nf">in</span> <span class="p">()</span> <span class="p">})</span> <span class="p">{</span>
            <span class="k">await</span> <span class="nf">processNewTransactions</span><span class="p">()</span>
        <span class="p">}</span>
    <span class="p">}</span>

<span class="o">...</span>
</code></pre></div></div>

<p>Now, add our logic for storing and retrieving the history tokens. For simplicity, we’re storing them as JSON serialized strings in NSUserDefaults as recommended by Apple’s documentation. Note that if we don’t have a history token, we’ll try consuming from the start of the transactions table.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">processNewTransactions</span><span class="p">()</span> <span class="k">async</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">tokenData</span> <span class="o">=</span> <span class="kt">UserDefaults</span><span class="o">.</span><span class="n">standard</span><span class="o">.</span><span class="nf">data</span><span class="p">(</span><span class="nv">forKey</span><span class="p">:</span> <span class="s">"historyToken"</span><span class="p">)</span>
        
    <span class="k">var</span> <span class="nv">historyToken</span><span class="p">:</span> <span class="kt">DefaultHistoryToken</span><span class="p">?</span> <span class="o">=</span> <span class="kc">nil</span>
    <span class="k">if</span> <span class="k">let</span> <span class="nv">tokenData</span> <span class="p">{</span>
        <span class="n">historyToken</span> <span class="o">=</span> <span class="k">try</span><span class="p">?</span> <span class="kt">JSONDecoder</span><span class="p">()</span><span class="o">.</span><span class="nf">decode</span><span class="p">(</span><span class="kt">DefaultHistoryToken</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">from</span><span class="p">:</span> <span class="n">tokenData</span><span class="p">)</span>
    <span class="p">}</span>
    
    <span class="k">let</span> <span class="nv">transactions</span> <span class="o">=</span> <span class="nf">findTransactions</span><span class="p">(</span><span class="nv">after</span><span class="p">:</span> <span class="n">historyToken</span><span class="p">)</span>
    <span class="k">let</span> <span class="p">(</span><span class="nv">updatedModelIds</span><span class="p">,</span> <span class="nv">newHistoryToken</span><span class="p">)</span> <span class="o">=</span> <span class="nf">findUpdatedModelIds</span><span class="p">(</span><span class="nv">in</span><span class="p">:</span> <span class="n">transactions</span><span class="p">)</span>
    <span class="k">if</span> <span class="k">let</span> <span class="nv">newHistoryToken</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">newTokenData</span> <span class="o">=</span> <span class="k">try</span><span class="p">?</span> <span class="kt">JSONEncoder</span><span class="p">()</span><span class="o">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">newHistoryToken</span><span class="p">)</span>
        <span class="kt">UserDefaults</span><span class="o">.</span><span class="n">standard</span><span class="o">.</span><span class="nf">set</span><span class="p">(</span><span class="n">newTokenData</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="s">"historyToken"</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="k">let</span> <span class="nv">historyToken</span> <span class="p">{</span>
        <span class="k">try</span><span class="p">?</span> <span class="nf">deleteTransactions</span><span class="p">(</span><span class="nv">before</span><span class="p">:</span> <span class="n">historyToken</span><span class="p">)</span>
    <span class="p">}</span>
    
    <span class="k">await</span> <span class="nf">maybeUpdateWidgets</span><span class="p">(</span><span class="nv">relevantTo</span><span class="p">:</span> <span class="n">updatedModelIds</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now we fetch transactions from the store, this logic is very simple. Similarly, the logic for cleaning up old transactions is just deleting them from the ModelContext.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">func</span> <span class="nf">findTransactions</span><span class="p">(</span><span class="n">after</span> <span class="nv">token</span><span class="p">:</span> <span class="kt">DefaultHistoryToken</span><span class="p">?)</span> <span class="o">-&gt;</span> <span class="p">[</span><span class="kt">DefaultHistoryTransaction</span><span class="p">]</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">historyDescriptor</span> <span class="o">=</span> <span class="kt">HistoryDescriptor</span><span class="o">&lt;</span><span class="kt">DefaultHistoryTransaction</span><span class="o">&gt;</span><span class="p">()</span>
    <span class="k">if</span> <span class="k">let</span> <span class="nv">token</span> <span class="p">{</span>
        <span class="n">historyDescriptor</span><span class="o">.</span><span class="n">predicate</span> <span class="o">=</span> <span class="k">#Predicate</span> <span class="p">{</span> <span class="n">transaction</span> <span class="nf">in</span>
            <span class="p">(</span><span class="n">transaction</span><span class="o">.</span><span class="n">token</span> <span class="o">&gt;</span> <span class="n">token</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">var</span> <span class="nv">transactions</span><span class="p">:</span> <span class="p">[</span><span class="kt">DefaultHistoryTransaction</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">do</span> <span class="p">{</span>
        <span class="n">transactions</span> <span class="o">=</span> <span class="k">try</span> <span class="n">modelContext</span><span class="o">.</span><span class="nf">fetchHistory</span><span class="p">(</span><span class="n">historyDescriptor</span><span class="p">)</span>
    <span class="p">}</span> <span class="k">catch</span> <span class="p">{</span>
        <span class="n">logger</span><span class="o">.</span><span class="nf">error</span><span class="p">(</span><span class="s">"Error while fetching history transactions </span><span class="se">\(</span><span class="n">error</span><span class="p">,</span> <span class="nv">privacy</span><span class="p">:</span> <span class="o">.</span><span class="kd">public</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="n">transactions</span>
<span class="p">}</span>

<span class="kd">private</span> <span class="kd">func</span> <span class="nf">deleteTransactions</span><span class="p">(</span><span class="n">before</span> <span class="nv">token</span><span class="p">:</span> <span class="kt">DefaultHistoryToken</span><span class="p">)</span> <span class="k">throws</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">descriptor</span> <span class="o">=</span> <span class="kt">HistoryDescriptor</span><span class="o">&lt;</span><span class="kt">DefaultHistoryTransaction</span><span class="o">&gt;</span><span class="p">()</span>
    <span class="n">descriptor</span><span class="o">.</span><span class="n">predicate</span> <span class="o">=</span> <span class="k">#Predicate</span> <span class="p">{</span>
        <span class="nv">$0</span><span class="o">.</span><span class="n">token</span> <span class="o">&lt;</span> <span class="n">token</span>
    <span class="p">}</span>

    <span class="k">let</span> <span class="nv">context</span> <span class="o">=</span> <span class="kt">ModelContext</span><span class="p">(</span><span class="n">modelContainer</span><span class="p">)</span>
    <span class="k">try</span> <span class="n">context</span><span class="o">.</span><span class="nf">deleteHistory</span><span class="p">(</span><span class="n">descriptor</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>With the transactions in hand, I now need to convert the <code class="language-plaintext highlighter-rouge">DefaultHistoryTransaction</code> objects to my actual model class (title SongArtworkViewModel). You can retrieve many details about the transactions, but in my case I just needed the object <code class="language-plaintext highlighter-rouge">id</code> field to determine if they’re relevant to my widget.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">func</span> <span class="nf">findUpdatedModelIds</span><span class="p">(</span><span class="k">in</span> <span class="nv">transactions</span><span class="p">:</span> <span class="p">[</span><span class="kt">DefaultHistoryTransaction</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="p">(</span><span class="kt">Set</span><span class="o">&lt;</span><span class="kt">UUID</span><span class="o">&gt;</span><span class="p">,</span> <span class="kt">DefaultHistoryToken</span><span class="p">?)</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">taskContext</span> <span class="o">=</span> <span class="kt">ModelContext</span><span class="p">(</span><span class="n">modelContainer</span><span class="p">)</span>
    <span class="k">var</span> <span class="nv">updatedModelIds</span><span class="p">:</span> <span class="kt">Set</span><span class="o">&lt;</span><span class="kt">UUID</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">transaction</span> <span class="k">in</span> <span class="n">transactions</span> <span class="p">{</span>
        <span class="k">for</span> <span class="n">change</span> <span class="k">in</span> <span class="n">transaction</span><span class="o">.</span><span class="n">changes</span> <span class="p">{</span>
            <span class="k">let</span> <span class="nv">transactionModifiedID</span> <span class="o">=</span> <span class="n">change</span><span class="o">.</span><span class="n">changedPersistentIdentifier</span>
            <span class="k">let</span> <span class="nv">fetchDescriptor</span> <span class="o">=</span> <span class="kt">FetchDescriptor</span><span class="o">&lt;</span><span class="kt">SongArtworkViewModel</span><span class="o">&gt;</span><span class="p">(</span><span class="nv">predicate</span><span class="p">:</span> <span class="k">#Predicate</span> <span class="p">{</span> <span class="n">model</span> <span class="k">in</span>
                <span class="n">model</span><span class="o">.</span><span class="n">persistentModelID</span> <span class="o">==</span> <span class="n">transactionModifiedID</span>
            <span class="p">})</span>
            <span class="k">let</span> <span class="nv">fetchResults</span> <span class="o">=</span> <span class="k">try</span><span class="p">?</span> <span class="n">taskContext</span><span class="o">.</span><span class="nf">fetch</span><span class="p">(</span><span class="n">fetchDescriptor</span><span class="p">)</span>
            <span class="k">guard</span> <span class="k">let</span> <span class="nv">matchedModel</span> <span class="o">=</span> <span class="n">fetchResults</span><span class="p">?</span><span class="o">.</span><span class="n">first</span> <span class="k">else</span> <span class="p">{</span>
                <span class="k">continue</span>
            <span class="p">}</span>
            <span class="k">switch</span> <span class="n">change</span> <span class="p">{</span>
            <span class="k">case</span> <span class="o">.</span><span class="nf">insert</span><span class="p">(</span><span class="n">_</span> <span class="k">as</span> <span class="kt">DefaultHistoryInsert</span><span class="o">&lt;</span><span class="kt">SongArtworkViewModel</span><span class="o">&gt;</span><span class="p">):</span>
                <span class="k">break</span>
            <span class="k">case</span> <span class="o">.</span><span class="nf">update</span><span class="p">(</span><span class="n">_</span> <span class="k">as</span> <span class="kt">DefaultHistoryUpdate</span><span class="o">&lt;</span><span class="kt">SongArtworkViewModel</span><span class="o">&gt;</span><span class="p">):</span>
                <span class="n">updatedModelIds</span><span class="o">.</span><span class="nf">update</span><span class="p">(</span><span class="nv">with</span><span class="p">:</span> <span class="n">matchedModel</span><span class="o">.</span><span class="n">id</span><span class="p">)</span>
            <span class="k">case</span> <span class="o">.</span><span class="nf">delete</span><span class="p">(</span><span class="n">_</span> <span class="k">as</span> <span class="kt">DefaultHistoryDelete</span><span class="o">&lt;</span><span class="kt">SongArtworkViewModel</span><span class="o">&gt;</span><span class="p">):</span>
                <span class="n">updatedModelIds</span><span class="o">.</span><span class="nf">update</span><span class="p">(</span><span class="nv">with</span><span class="p">:</span> <span class="n">matchedModel</span><span class="o">.</span><span class="n">id</span><span class="p">)</span>
            <span class="k">default</span><span class="p">:</span> <span class="k">break</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="nf">return</span> <span class="p">(</span><span class="n">updatedModelIds</span><span class="p">,</span> <span class="n">transactions</span><span class="o">.</span><span class="n">last</span><span class="p">?</span><span class="o">.</span><span class="n">token</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Finally, the logic of conditionally updating my widgets. The configuration intents can be cast to your specific configuration intent to retrieve their parameters. For my specific case, I want to reload if a random widget is active, or the specific song used was updated.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">func</span> <span class="nf">maybeUpdateWidgets</span><span class="p">(</span><span class="n">relevantTo</span> <span class="nv">modelIds</span><span class="p">:</span> <span class="kt">Set</span><span class="o">&lt;</span><span class="kt">UUID</span><span class="o">&gt;</span><span class="p">)</span> <span class="k">async</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">configurations</span> <span class="o">=</span> <span class="k">try</span><span class="p">?</span> <span class="k">await</span> <span class="kt">WidgetCenter</span><span class="o">.</span><span class="n">shared</span><span class="o">.</span><span class="nf">currentConfigurations</span><span class="p">()</span>
    <span class="k">guard</span> <span class="k">let</span> <span class="nv">configurations</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
    <span class="k">let</span> <span class="nv">relevantConfigurationKinds</span> <span class="o">=</span> <span class="n">configurations</span><span class="o">.</span><span class="n">filter</span> <span class="p">{</span> <span class="n">configuration</span> <span class="k">in</span>
        <span class="k">let</span> <span class="nv">config</span> <span class="o">=</span> <span class="n">configuration</span><span class="o">.</span><span class="nf">widgetConfigurationIntent</span><span class="p">(</span><span class="nv">of</span><span class="p">:</span> <span class="kt">SongConfigurationAppIntent</span><span class="o">.</span><span class="k">self</span><span class="p">)</span>
        <span class="k">guard</span> <span class="k">let</span> <span class="nv">config</span> <span class="k">else</span> <span class="p">{</span>
            <span class="k">return</span> <span class="kc">false</span>
        <span class="p">}</span>
        
        <span class="k">if</span> <span class="n">config</span><span class="o">.</span><span class="n">mode</span> <span class="o">==</span> <span class="o">.</span><span class="n">random</span> <span class="p">{</span>
            <span class="k">return</span> <span class="kc">true</span>
        <span class="p">}</span>
        
        <span class="k">guard</span> <span class="k">let</span> <span class="nv">entityId</span> <span class="o">=</span> <span class="n">config</span><span class="o">.</span><span class="n">specificSong</span><span class="p">?</span><span class="o">.</span><span class="n">id</span> <span class="k">else</span> <span class="p">{</span>
            <span class="k">return</span> <span class="kc">false</span>
        <span class="p">}</span>
        
        <span class="k">return</span> <span class="n">modelIds</span><span class="o">.</span><span class="nf">contains</span><span class="p">(</span><span class="n">entityId</span><span class="p">)</span>
    <span class="p">}</span><span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="nv">$0</span><span class="o">.</span><span class="n">kind</span> <span class="p">}</span>
    
    <span class="kt">Array</span><span class="p">(</span><span class="kt">Set</span><span class="p">(</span><span class="n">relevantConfigurationKinds</span><span class="p">))</span><span class="o">.</span><span class="n">forEach</span> <span class="p">{</span> <span class="n">kind</span> <span class="k">in</span>
        <span class="kt">WidgetCenter</span><span class="o">.</span><span class="n">shared</span><span class="o">.</span><span class="nf">reloadTimelines</span><span class="p">(</span><span class="nv">ofKind</span><span class="p">:</span> <span class="n">kind</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>With all the logic completed, I could then add a modifier to my app so that this occurs on startup.</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">.</span><span class="n">task</span> <span class="p">{</span>
    <span class="kt">Task</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">monitor</span> <span class="o">=</span> <span class="kt">DataMonitor</span><span class="p">(</span><span class="nv">modelContainer</span><span class="p">:</span> <span class="kt">ModelContainer</span><span class="o">.</span><span class="n">sharedModelContainer</span><span class="p">)</span>
        <span class="k">await</span> <span class="n">monitor</span><span class="o">.</span><span class="nf">subscribeToModelChanges</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Build and run and everything would work for sure. …Right?</p>

<p><img class="fullwidthimg defaultimg" src="/assets/SwiftDataTransactionHistoryCrash1.png" alt="XCode IDE with crash message" /><br />
<span class="caption"></span></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SwiftData/DataUtilities.swift:1305: Fatal error: Unexpected class type: CodableColor
</code></pre></div></div>

<p>Womp womp. Unfortunately this has been similar to much of my experience with SwiftData. Hard to decipher fatal errors that you cannot safely catch. In this case my model class was quite complex including several <code class="language-plaintext highlighter-rouge">@Attribute</code> annotations with<a href="https://developer.apple.com/documentation/swiftdata/schema/attribute/option/transformable(by:)-9d4xh">.transformable(by: )</a>. I’m not sure specifically what was causing this issue, it was hard to reproduce it outside of my app. I ended up refactoring my data model significantly to simplify its structure, and this resolved this crash issue. I may write another follow up article on my data model refactor.</p>

<p>After simplifying the structure of my model, the History Transaction APIs worked as expected! The widget reloads itself when relevant changes are made as expected.</p>

<p>While working on this, I realized I also could just use the simple solution of always reloading all widgets whenever I receive the <code class="language-plaintext highlighter-rouge">NSPersistentStoreRemoteChange</code> notification. In actuality this is much simpler and in my experience does not result in system throttling because most updates are made when the app is in the foreground. But it may be different for your use case if you receive many updates to the model while the app is not in the foreground. And this transactions harness would also be useful for other use cases in the future like exposing app data to <a href="https://developer.apple.com/videos/play/wwdc2021/10098/">Core Spotlight</a>.</p>

<p>If you’re interested in trying this architecture for yourself, you check out my <a href="https://gist.github.com/ZekeSnider/00f0c6bbdada67910886896b7c58e6c5">sample code</a>.</p>

<p>Takeaways:</p>
<ol>
  <li>For WidgetKit refresh granularity, it is better to vend multiple kinds of widgets</li>
  <li>The SwiftData Transaction History APIs can be used in conjunction with the <code class="language-plaintext highlighter-rouge">NSPersistentStoreRemoteChange</code> notification to trigger off of new updates to your data model</li>
  <li>SwiftData’s Transaction History APIs may have issues with very complex data models</li>
  <li>This approach would also be useful for other problem spaces like exposing your SwiftData to Spotlight</li>
</ol>

<p>Thanks for reading!</p>]]></content><author><name>GitHub User</name><email>your-email@domain.com</email></author><summary type="html"><![CDATA[My app Lyrigraphy uses SwiftUI and SwiftData to manage its UI lifecycle and data storage, respectively. I was recently working on adding a Widget extension to it, and was considering how to handle updating the Widget views when the underlying data changes.]]></summary></entry><entry><title type="html">SwiftUI Device Previews</title><link href="https://zekesnider.com/swiftui-device-previews/" rel="alternate" type="text/html" title="SwiftUI Device Previews" /><published>2024-12-07T16:14:00+00:00</published><updated>2024-12-07T16:14:00+00:00</updated><id>https://zekesnider.com/swiftui-device-previews</id><content type="html" xml:base="https://zekesnider.com/swiftui-device-previews/"><![CDATA[<p>A nice feature of using SwiftUI for your app is <a href="https://developer.apple.com/documentation/xcode/previewing-your-apps-interface-in-xcode">Preview macros</a> which allow you to live preview individual views in the IDE. This can be super useful for rapid iteration if you properly mock out datasources and dependencies of your view. Passing less data to your views and modularizing them also improves general testability and performance.</p>

<p>What I didn’t realize until recently is that you can also use these previews on a physical device. Including via wireless debugging, so your iPhone/iPad doesn’t need to be connected to your Mac. I specifically wanted to do this because of some keyboard weirdness in the simulator, but this is generally useful.</p>

<p><img class="fullwidthimg defaultimg" src="/assets/SwiftUIDevicePreviews.png" alt="SwiftUI preview device selection menu" /><br />
<span class="caption"></span></p>

<p>Unfortunately I ran into some provisioning errors when I tried to set this up.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"This app cannot be installed because its integrity could not be verified"
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>NSLocalizedRecoverySuggestion=Failed to install embedded profile
</code></pre></div></div>

<p><a href="https://stackoverflow.com/questions/61516408/getting-error-installing-provisioning-profile-on-mac-failed-to-install-one-or">This StackOverflow post</a> has some debugging tips. But my issue ended being solved by first running the full App itself on the device. Then previewing views worked as expected. There must be some provisioning profile bootstrapping that happens there, which I couldn’t find documented anywhere.</p>]]></content><author><name>GitHub User</name><email>your-email@domain.com</email></author><summary type="html"><![CDATA[A nice feature of using SwiftUI for your app is Preview macros which allow you to live preview individual views in the IDE. This can be super useful for rapid iteration if you properly mock out datasources and dependencies of your view. Passing less data to your views and modularizing them also improves general testability and performance.]]></summary></entry><entry><title type="html">2024 Canvassing</title><link href="https://zekesnider.com/2024-canvassing/" rel="alternate" type="text/html" title="2024 Canvassing" /><published>2024-11-21T14:53:52+00:00</published><updated>2024-11-21T14:53:52+00:00</updated><id>https://zekesnider.com/2024-canvassing</id><content type="html" xml:base="https://zekesnider.com/2024-canvassing/"><![CDATA[<p>I spent the 2 weekends before the 2024 election canvassing in Reno, NV and Tulare, CA. For the Harris campaign and Rudy Salas (Democrat candidate for CA-22) respectively. Obviously the election results did not go how I wanted, and that’s a lot to process. Nonetheless, the experience of canvassing was very rewarding and I don’t regret my efforts at all.</p>

<p>I think stretching outside your comfort limits and doing something new and uncomfortable can bring some personal growth. Being more introverted, it wasn’t my definition of comfortable to go solo on a weekend trip to another state to talk to strangers. Despite this, it was a positive experience that truly made me feel better about politics. I feel very strongly about politics, and it felt good to actually get out there and do something even if it was insignificantly small.</p>

<p>These were organized bus trips with 4-5 hour drives to each area. It definitely felt good to meet new people who are similarly politically engaged. The energy is great and it definitely takes a certain type of person to spend your weekend doing this. Especially living in the Bay Area and with most of my friend circles being people in tech, it was pretty refreshing to see the diversity in background of those volunteering.</p>

<p>Logistically, I surprised by the quality of the tech in some aspects of canvassing (the canvassing apps). But also surprised by the lack of tech in some aspects like assigning turf, rides, hotel rooms, and keeping track of folks to make sure they make it back in time. A lot of the on the ground organization seemed to be done with a menagerie of Google Sheets and post-it notes. Not an attack on the organizers at all, there’s a lot of hard-working volunteers making it all happen. But I just thought the process was interesting from a technology perspective. The canvassing apps provide all the tooling when you’re on the ground, but everything outside of that is mostly manual processes.</p>

<p>The canvassing apps MiniVan and PDI Connect (only used in CA apparently) were pretty decent. They each give you very specific houses and voters to try and contact for your shift. MiniVan uses Apple Maps and I found had much better consistency of finding specifically where houses were. PDI Connect uses OpenStreetMap and the locations were much less accurate. They both drain your battery quite quickly by using GPS constantly, so I definitely recommend a battery pack for canvassing.</p>

<p>As for the actual door knocking, it was pretty fun overall. You spend most of your time navigating the neighborhood to find the right doors to knock. You get a very specific on-the-ground feeling of the neighborhood you’re in that you just wouldn’t experience otherwise. Honestly, most people either aren’t home or don’t answer their door. The vast majority of people I talked to were quite nice. I talked to some very sweet people. Some people are uninterested and want to get back to their day (understandable!), but I hoped I was another data point to sway them to turn out. I really only had one negative experience the whole weekend.</p>

<p>The first few doors are kind of nerve wracking, but you start to get used to it after a couple. I equate it to 
jumping into the cold pool head first. You have to lower your inhibitions and realize the stakes are really low (no fear). Canvassing is a skill, I’m sure I would get better at it with more practice as well. I think you get better at engaging and having better conversations, it is a challenge to break through especially when the audience is uninterested. At this point in the campaign it is mostly a GOTV operation and I didn’t really have an opportunity talk substantive policy. But talking with actual voters helps you realize the common decency that is missed online.</p>

<p>There is a lot to be pessimistic about with regards to the election results. It can be tempting to believe that the ground game did not matter. But the reality of it is Harris performed much better in swing states than non swing states. The campaign operation and ground game did make a difference. And it mostly likely did actually matter, with Democrats holding on to senate seats in NV, AZ, MI, and WI.</p>

<p>It’s currently 712 days until the 2026 midterms. I hope to do whatever I can to most effectively volunteer in that election. And if it’s more canvassing, I’m well prepared!</p>]]></content><author><name>GitHub User</name><email>your-email@domain.com</email></author><summary type="html"><![CDATA[I spent the 2 weekends before the 2024 election canvassing in Reno, NV and Tulare, CA. For the Harris campaign and Rudy Salas (Democrat candidate for CA-22) respectively. Obviously the election results did not go how I wanted, and that’s a lot to process. Nonetheless, the experience of canvassing was very rewarding and I don’t regret my efforts at all.]]></summary></entry><entry><title type="html">Everett Parks Reviews Project</title><link href="https://zekesnider.com/everett-parks-project/" rel="alternate" type="text/html" title="Everett Parks Reviews Project" /><published>2024-10-22T03:39:52+00:00</published><updated>2024-10-22T03:39:52+00:00</updated><id>https://zekesnider.com/everett-parks-project</id><content type="html" xml:base="https://zekesnider.com/everett-parks-project/"><![CDATA[<p>Earlier this summer I visited all 46 city parks in Everett, WA with my dad over 2 days. Growing up in Everett, I frequented some of these parks, but never been to others. With so many parks to visit, it was a definite challenge to visit them in such a short period of time. Despite these parks not being famous or well-known, there’s quite a few very nice spaces! I decided to document the parks and also the journey to visit them all.</p>

<p><strong>If you’re just interested in the park reviews, you can <a href="/parks">check it out here</a></strong>. This post details the background of the project and some of the benign details of how I created the write up.</p>

<h2 id="the-preparation">The preparation</h2>

<p><img class="fullwidthimg defaultimg" src="/assets/ParksSpreadsheet.png" alt="Numbers spreadsheet screenshot listing Everett parks" /><br />
<span class="caption"></span></p>

<p>This project started off with some research a spreadsheet to track and categorize each park. The <a href="https://www.everettwa.gov/3195/Parks-Trails-and-Open-Space">city parks website</a> does a good job of listing them out. And was actually recently updated with new images. But the spreadsheet made it a lot easier to document coordinates/addresses, notes, and to keep track of which parks we visited during the outing. I used numbers for this, and the iCloud collaboration tools worked well and allowed us to update it on the go with the iPhone app.</p>

<p>After all the spreadsheet was listed out, I spent some time researching the exact location of some of the smaller and lesser known parks. There were a few that were not well labeled on Apple or Google maps. I made POI suggestions where possible after doing some research on the correct locations.</p>

<p><img class="fullwidthimg defaultimg" src="/assets/ParksOverview.png" alt="Parks all pinned on a map" /><br />
<span class="caption"></span></p>

<p>We also did some experimentation with path-finding to find the most efficient path to visit all the parks. We did have a limited time window for my visit to the Seattle area so we wanted to be efficient with the limited daylight time. Both ChatGPT and Claude did not produce great results for this, though we could likely have gotten better results by asking it to write a path finding algorithm to solve for it. Ultimately, we decided to do manual routing by looking at a map with all the parks pinned on it. It was definitely not <em>the most</em> efficient path, but the time loss was not substantial as the city of Everett is not very large.</p>

<h2 id="the-adventure">The adventure</h2>

<p><img class="fullwidthimg defaultimg" src="/assets/EverettParks/GreenLatern/IMG_9091.jpeg" alt="Park image" /><br />
<span class="caption"></span></p>

<p>On launch day we went straight from the airport to park visits. With 46 spots to visit the goal was daunting and we weren’t sure if we could complete the challenge in only 2-3 days. We definitely spent less time at some of the parks to meet our schedule. But we did take our time at other substantial parks. It wouldn’t be an enjoyable venture if we were just driving straight from place to place after all. We lucked out on the weather conditions, with it being peak Summer and no rain at all.</p>

<p>We also discussed how best to document this challenge. A video would be an option, but that seemed intrusive to need to film extensively during the day to capture the footage. I’m just not about that vlog life right now. So we decided to take some photos at each spot and do a write up of some sort afterwards.</p>

<p>It definitely was tiring going to so many parks in a short period of time. We had a break day to go see Bleachers at a music festival in Seattle in between which was a nice reprieve. We were exhausted at the end of each day but it was very rewarding at the same time. It was super satisfying when we finally met our goal on the 3rd day of visiting every park in the system. And while not all the parks were amazing stand-outs, it was still cool getting to see and experience new locations. I definitely want to re-visit some of these parks again in the future to more thoroughly enjoy them.</p>

<p>With the photos and notes taken, it was time to move on to documenting our memories!</p>

<h2 id="the-write-up">The write up</h2>

<p>For the first step of documentation, I made a significant number of corrections to Apple Maps location data so others can find the parks more easily. Some were missing, had incorrect names, or were in the wrong location. I also submitted the pictures that I took at each park. So if you look up any of these parks on Apple Maps you’ll likely see the same images as on this blog!</p>

<p>The next step was to start working on the blog page. This site hasn’t had a new blog post in over 4 years. While I modernized its deployment architecture a year or so ago with AWS Amplify, it hadn’t received significant attention. The Jekyll version and other dependencies were significantly out of date. While it sounded simple, this ended up causing a bunch of churn with plugin upgrades, theme updates, Dart Sass upgrades, etc. This was all menial tasks, but necessary to get the blog into better shape.</p>

<p>After this was taken care of, I moved on to implementing the functionality needed for the parks review page.
I had a few requirements I wanted from this page:</p>
<ul>
  <li>List all the parks in a paginated view</li>
  <li>Have a gallery image view for each</li>
  <li>Show where each park is on a map</li>
  <li>(Ideally) show where all the parks are on a map view</li>
</ul>

<h3 id="pagination">Pagination</h3>

<p>This was the most straightforward. Just needed to migrate to <a href="https://github.com/sverrirs/jekyll-paginate-v2"><code class="language-plaintext highlighter-rouge">jekyll-paginate-v2</code></a> to make use of the custom collections feature. This did cause some churn of various upgrade path rough edges to keep parity with existing pagination on my blog pages.</p>

<h3 id="images">Images</h3>

<p>The first step for the images was sorting through and tagging all the photos in my library from the parks. They were geo-tagged which made this easier, but still a manual process of going through and tagging each park. From there I exported them all to JPEGs in folders to start the process of hosting them on this site.</p>

<p>I didn’t find any fully featured gallery solution for Jekyll that would meet my needs well. So I decided to roll my own with <a href="https://photoswipe.com">Photoswipe</a>. I used a fairly simple <code class="language-plaintext highlighter-rouge">_include</code> that wraps a specific set of images in a gallery.</p>

<p>But there were a few complications with this approach:</p>

<ol>
  <li>Image dimensions</li>
</ol>

<p>Photoswipe requires you to tell the dimensions of each image. And I can’t hard-code it because some images were portrait and some were landscape. At first I just added a plugin which used ImageMagick to pull the image dimensions of each when the site was being generated. This quickly became very slow as it recalculated it for every single image each time the site was changed. So I pivoted and had Claude write a ruby script which precalculated it all and created a YML file listing all the image metadata. I also updated this script to create all the base scaffold pages for all 46 parks so I didn’t need to do that manually.</p>

<ol>
  <li>Image sizing</li>
</ol>

<p>When you load the page, it loads thumbnails for all the images. But if we don’t properly resize those, it would just load the full images for each which would be slow (and costly). So to solve for this, I looked into solutions using the img <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/srcset"><code class="language-plaintext highlighter-rouge">srcset</code></a> property which will dynamically load whichever asset dimension is needed. Luckily, there is already a great Jekyll plugin, <a href="https://github.com/rbuchberger/jekyll_picture_tag"><code class="language-plaintext highlighter-rouge">jekyll_picture_tag</code></a> which takes care of this. I installed it, wrapped my images tags with it, and everything worked perfectly!</p>

<p>…Except when I went to deploy and test my changes in AWS Amplify. Because the library uses a dependency called <a href="https://www.libvips.org">libvips</a> which isn’t in the default Amplify build image, the whole thing crashes and burns. I could install libvips in the build steps, but this turns out to be very slow. Taking multiple minutes on each build iteration. So I decided to set out and create a <a href="https://github.com/ZekeSnider/docker-amplify-with-libvips">custom dockerfile</a> that has all the dependencies I need. In the end I open-sourced it, so it may be useful to others using a similar setup in Amplify. The Github readme has some instructions on how to use it.</p>

<p>The dockerfile took me much longer than I would have liked to get in a working state. But the end outcome is ideal because Amplify can still build my site and the build is quite fast. I also used a small code snippet to cache the generated resized images between builds so that it doesn’t need to re-render images that have already been sized properly.</p>

<h3 id="maps">Maps</h3>

<p>For each park I wanted to include a screenshot of where it is on a map. The <a href="https://developer.apple.com/documentation/snapshots">Apple Maps Web Snapshots</a> seemed to fit the bill perfectly. I could have gone with the easiest option of scripting creation of the images I needed to include in my site. But instead I decided to create my own Jekyll plugin for it. Originally it was just a custom <code class="language-plaintext highlighter-rouge">_includes</code> but then I decided to refactor it to be a Liquid block. And at that point I had something that was in pretty good shape <a href="https://github.com/ZekeSnider/jekyll-apple-maps">so I decided to open source it</a>.</p>

<p>I did put in some more effort to clean up the code and write unit test coverage. For extra credit (and to just see where open source tooling is these days), I integrated test coverage and CI with <a href="https://app.circleci.com/pipelines/circleci/aW12qZgMpxbXNYTbdzFe5/FS3eDPqnpMpZJ2cKYi2aN9?branch=main">CircleCI</a> and <a href="https://app.codecov.io/gh/ZekeSnider/jekyll-apple-maps">Codecov</a>. Overall this was pretty smooth except some small hiccups that I might write about some other time. This was also the first time for me to publish my own open source gem, which was easy to figure out.</p>

<p>I experimented with using <a href="https://www.cursor.com">Cursor</a> and and <a href="https://www.anthropic.com/claude/sonnet">Claude 3.5 Sonnet</a> quite a bit while working on this plugin. It got me probably 60-70% of the way to most of the functionality I wanted, and the rest was just cleaning it up and productionizing it. It definitely has its limits, but was very useful for some of the boilerplate code that’s usually very tedious to work through for a personal project like this.</p>

<p>Once I had the plugin open sourced, I just pulled it into my site’s gemfile as a public repo and everything worked as expected! Then I started to work the interactive Apple Maps embed that lists all the parks in one place. This one was honestly more of a hack-job, but it does work. I considered putting more effort into it to include it in the Jekyll plugin, but it’s a pretty frontend project so I punted it for now. Currently it’s just some templated JS directly in that page and it does work for now.</p>

<h3 id="writing-the-content">Writing the content</h3>

<p>With all the technical details complete, I had to finish up by writing the actual content. We had written ratings already for each park in the spreadsheet so I just had to reference that for each write up. I could have written more content about each park, but decided to leave it fairly minimal for now. I might add more depth in the future, but I’m eager to ship, having worked on this on and off for several months on nights/weekends.</p>

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

<p>In this end, this took an somewhat absurd amount of effort to create a page that is quite simple in functionality. But I also created some open source software that is may be of use to others. And my site’s setup is now modernized, making future updates easier. I also learned a lot in the process, and experimented with LLM coding tools like Cursor to make my work more efficient.</p>

<p>I hope others enjoy viewing the park write ups. And even if not, it’s still satisfying to have finished this project and have my adventure documented. There’s something to be said for finishing the project and actually shipping even if it’s mundane, and I’m glad to have done that. There’s plenty of things that I want to improve about the page, but for now it’s functional and out there! And it’s fully hosted by me with full creative control, not beholden to any social network or hosting provider :).</p>

<p><img class="fullwidthimg defaultimg" src="/assets/FinalPark.jpeg" alt="Me and my dad at the final park we visited" /><br />
<span class="caption"></span></p>

<p>I really enjoyed visiting all these parks and experiencing something new. I can’t wait re-visit some of them later. And who knows, maybe I’ll tackle a <a href="https://en.wikipedia.org/wiki/List_of_parks_in_San_Francisco#City">similar challenge in San Francisco</a> sometime.</p>]]></content><author><name>GitHub User</name><email>your-email@domain.com</email></author><summary type="html"><![CDATA[Earlier this summer I visited all 46 city parks in Everett, WA with my dad over 2 days. Growing up in Everett, I frequented some of these parks, but never been to others. With so many parks to visit, it was a definite challenge to visit them in such a short period of time. Despite these parks not being famous or well-known, there’s quite a few very nice spaces! I decided to document the parks and also the journey to visit them all.]]></summary></entry><entry><title type="html">Automatic Reference Counting with `self`</title><link href="https://zekesnider.com/automatic-reference-counting-with-self/" rel="alternate" type="text/html" title="Automatic Reference Counting with `self`" /><published>2020-08-12T05:39:20+00:00</published><updated>2020-08-12T05:39:20+00:00</updated><id>https://zekesnider.com/automatic-reference-counting-with-self</id><content type="html" xml:base="https://zekesnider.com/automatic-reference-counting-with-self/"><![CDATA[<p>Recently, when working on <a href="https://github.com/zekesnider/jared">Jared</a>, I ran into an interesting  memory leak. Jared is written nearly entirely in Swift, which uses <a href="https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html">ARC</a> for memory allocation. As advertised, it usually “just works”. However in this case some of my callback code had unintended side effects.</p>

<p>Jared contains the ability to load <code class="language-plaintext highlighter-rouge">.bundle</code> plugin files to add additional commands. These bundle contain a principle class which conforms to a protocol and is loaded dynamically at runtime. In this instance, I was experimenting with a bundle whose class started a HTTP web server using <a href="https://github.com/Building42/Telegraph#">Telegraph</a>.</p>

<p>I originally noticed strange behavior because when reloading plugins. The application would crash upon reload, due to the server trying to re-bind the same port, even though I was stopping the server in the module’s <code class="language-plaintext highlighter-rouge">deinit</code> statement. Setting a breakpoint, the deinit was never called.</p>

<p>After unloading the server module, I ran an experiment by mashing the reload plugins button repeatedly.</p>

<p><img class="fullwidthimg defaultimg" src="/assets/MemoryPressure.png" alt="" /><br />
<span class="caption"></span></p>

<p>Digging into Xcode’s Debug Memory graph, and filtering on filtering on leaked blocks, it’s clear that old copies of RoutingModules are sticking around in memory.</p>

<p><img class="fullwidthimg defaultimg" src="/assets/VisualMemoryDebugger.png" alt="" /><br />
<span class="caption"></span></p>

<p>Looking closer into the pertinent part of the memory graph:</p>

<p><img class="fullwidthimg defaultimg" src="/assets/RetainCycle.png" alt="" /><br />
<span class="caption"></span></p>

<p>The route has a clear strong reference cycle between itself and an array, causing it to stick around. All modules do provide an array of routes. Narrowing down the pertinent code:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="k">var</span> <span class="nv">routes</span><span class="p">:</span> <span class="p">[</span><span class="kt">Route</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span>

<span class="nf">init</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">reload</span> <span class="o">=</span> <span class="kt">Route</span><span class="p">(</span><span class="nv">name</span><span class="p">:</span><span class="s">"/reload"</span><span class="p">,</span> <span class="nv">comparisons</span><span class="p">:</span> <span class="p">[</span><span class="o">.</span><span class="nv">startsWith</span><span class="p">:</span> <span class="p">[</span><span class="s">"/reload"</span><span class="p">]],</span>
      <span class="nv">call</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">reload</span><span class="p">,</span> <span class="nv">description</span><span class="p">:</span> <span class="nf">localized</span><span class="p">(</span><span class="s">"reloadDescription"</span><span class="p">))</span>
    
    <span class="n">routes</span> <span class="o">=</span> <span class="p">[</span><span class="n">reload</span><span class="p">]</span>
<span class="p">}</span>

<span class="kd">func</span> <span class="nf">reload</span><span class="p">(</span><span class="n">_</span> <span class="nv">message</span><span class="p">:</span> <span class="kt">Message</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span> <span class="p">{}</span></code></pre></figure>

<p>After some research, it was evident that the issue lied in the reference to a <code class="language-plaintext highlighter-rouge">self</code> method in the callback parameter. The purpose of the callback is to provide the routing module with the appropriate method to call when a triggering message is received. It is not meant to live beyond the memory scope of the containing class. However, because it was a <em>strong</em> <code class="language-plaintext highlighter-rouge">self</code> reference, it was causing any module to <em>never</em> be deallocated. The class contained a list of elements that depend on itself, so as far as ARC is concerned, the resource never reached a zero reference count.</p>

<p>This behavior is explicitly called out in the Swift documentation:</p>
<blockquote>
  <p>A strong reference cycle can also occur if you assign a closure to a property of a class instance, and the body of that closure captures the instance. This capture might occur because the closure’s body accesses a property of the instance, such as self.someProperty, or because the closure calls a method on the instance, such as self.someMethod(). In either case, these accesses cause the closure to “capture” self, creating a strong reference cycle.</p>
</blockquote>

<p>As a simple solution, I simply provided the callback as a <code class="language-plaintext highlighter-rouge">weak</code> self reference instead:</p>

<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="k">let</span> <span class="nv">reload</span> <span class="o">=</span> <span class="kt">Route</span><span class="p">(</span><span class="nv">name</span><span class="p">:</span><span class="s">"/reload"</span><span class="p">,</span> <span class="nv">comparisons</span><span class="p">:</span> <span class="p">[</span><span class="o">.</span><span class="nv">startsWith</span><span class="p">:</span> <span class="p">[</span><span class="s">"/reload"</span><span class="p">]],</span>
  <span class="nv">call</span><span class="p">:</span> <span class="p">{[</span><span class="k">weak</span> <span class="k">self</span><span class="p">]</span> <span class="k">in</span> <span class="k">self</span><span class="p">?</span><span class="o">.</span><span class="nf">reload</span><span class="p">(</span><span class="nv">$0</span><span class="p">)},</span>
  <span class="nv">description</span><span class="p">:</span> <span class="nf">localized</span><span class="p">(</span><span class="s">"reloadDescription"</span><span class="p">))</span></code></pre></figure>

<p>The <code class="language-plaintext highlighter-rouge">weak</code> reference allows you to reference self, without keeping a strong hold on it. This prevents a strong reference cycle, and allows my <code class="language-plaintext highlighter-rouge">Module</code> classes to be deallocated appropriately. After changing to a weak self reference in all Routes, the issue resolved itself, and I’m free to mash that reload button without incurring a memory leak. Thus solving a long lived bug in my application.</p>

<p><strong>tl;dr</strong>: You should be very careful when using <code class="language-plaintext highlighter-rouge">self</code> in callbacks. It is very likely that you should use a reference to <code class="language-plaintext highlighter-rouge">weak self</code> instead of a strong reference.</p>]]></content><author><name>GitHub User</name><email>your-email@domain.com</email></author><category term="programming" /><summary type="html"><![CDATA[Recently, when working on Jared, I ran into an interesting memory leak. Jared is written nearly entirely in Swift, which uses ARC for memory allocation. As advertised, it usually “just works”. However in this case some of my callback code had unintended side effects.]]></summary></entry><entry><title type="html">Best of the decade</title><link href="https://zekesnider.com/best-of-the-decade/" rel="alternate" type="text/html" title="Best of the decade" /><published>2020-01-01T02:15:40+00:00</published><updated>2020-01-01T02:15:40+00:00</updated><id>https://zekesnider.com/best-of-the-decade</id><content type="html" xml:base="https://zekesnider.com/best-of-the-decade/"><![CDATA[<p>This is the stuff that I enjoyed the most since 2010.</p>

<h1 id="music">Music</h1>
<ul>
  <li>
    <p>Melodrama - Lorde</p>
  </li>
  <li>
    <p>1989 - Taylor Swift</p>
  </li>
  <li>
    <p>E•MO•TION - Carly Rae Jepsen</p>
  </li>
  <li>
    <p>Norman Fucking Rockwell - Lana Del Rey</p>
  </li>
  <li>
    <p>Pure Heroine - Lorde</p>
  </li>
  <li>
    <p>Gone Now - Bleachers</p>
  </li>
  <li>
    <p>Ghost Stories - Coldplay</p>
  </li>
  <li>
    <p>Ultraviolence - Lana Del Rey</p>
  </li>
  <li>
    <p>Every Open Eye - Chvrches</p>
  </li>
  <li>
    <p>Days Are Gone - HAIM</p>
  </li>
  <li>
    <p>How Big, How Blue, How beautiful - Florence and the Machine</p>
  </li>
  <li>
    <p>Nothing’s Real - Shura</p>
  </li>
  <li>
    <p>BADLANDS - Halsey</p>
  </li>
</ul>

<h1 id="videogames">Videogames</h1>
<ul>
  <li>
    <p>Persona 4 Golden</p>
  </li>
  <li>
    <p>The Last of Us</p>
  </li>
  <li>
    <p>Persona 5</p>
  </li>
  <li>
    <p>Shin Megami Tensei IV</p>
  </li>
  <li>
    <p>Tetris 99 / Tetris Effect</p>
  </li>
</ul>

<h1 id="movie">Movie</h1>
<ul>
  <li>Lady Bird</li>
</ul>

<h1 id="tv-show">TV Show</h1>
<ul>
  <li>Lost</li>
</ul>

<p>The last episode aired in 2010 so it counts</p>]]></content><author><name>GitHub User</name><email>your-email@domain.com</email></author><summary type="html"><![CDATA[This is the stuff that I enjoyed the most since 2010.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://zekesnider.com/the-greatest.jpg" /><media:content medium="image" url="https://zekesnider.com/the-greatest.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Homekit Servo Blinds</title><link href="https://zekesnider.com/homekit-servo-blinds/" rel="alternate" type="text/html" title="Homekit Servo Blinds" /><published>2019-04-21T19:15:40+00:00</published><updated>2019-04-21T19:15:40+00:00</updated><id>https://zekesnider.com/homekit-servo-blinds</id><content type="html" xml:base="https://zekesnider.com/homekit-servo-blinds/"><![CDATA[<p>I have little experience with hardware, but had a goal that I wanted to retrofit my completely manually operated blinds so that I could control them via <a href="https://developer.apple.com/homekit/">HomeKit</a>. This post is about how I accomplished exactly that! The final solution is controllable down to a percentage via siri and the home app.</p>

<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Best thing I’ve done all year. <a href="https://twitter.com/hashtag/HomeKit?src=hash&amp;ref_src=twsrc%5Etfw">#HomeKit</a> <a href="https://twitter.com/hashtag/homebridge?src=hash&amp;ref_src=twsrc%5Etfw">#homebridge</a> <a href="https://t.co/4uWfUkBkt0">pic.twitter.com/4uWfUkBkt0</a></p>&mdash; Zeke (@ZekeSnider) <a href="https://twitter.com/ZekeSnider/status/1118720403424153600?ref_src=twsrc%5Etfw">April 18, 2019</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<h1 id="the-equipment">The Equipment</h1>
<ul>
  <li><a href="https://www.amazon.com/gp/product/B07BDR5PDW/ref=as_li_tl?ie=UTF8&amp;camp=1789&amp;creative=9325&amp;creativeASIN=B07BDR5PDW&amp;linkCode=as2&amp;tag=zeke082-20&amp;linkId=878ec655cdbf6a40bf226475b7c170d3">Raspberry Pi 3</a>
    <ul>
      <li>Alternatively, you can use a <a href="https://www.amazon.com/gp/product/B06XFZC3BX/ref=as_li_tl?ie=UTF8&amp;camp=1789&amp;creative=9325&amp;creativeASIN=B06XFZC3BX&amp;linkCode=as2&amp;tag=zeke082-20&amp;linkId=b2b6e81aae6b419ae2449eb99dce3a82">Raspberry Pi Zero</a> with a headboard soldered on (or solder directly)</li>
    </ul>
  </li>
  <li><a href="https://www.amazon.com/gp/product/B07F9P32PF/ref=as_li_tl?ie=UTF8&amp;camp=1789&amp;creative=9325&amp;creativeASIN=B07F9P32PF&amp;linkCode=as2&amp;tag=zeke082-20&amp;linkId=27ce384d7acc170d42d9cb6a80122fd6">360° servo</a>
    <ul>
      <li>Make sure you get a 360° servo.</li>
    </ul>
  </li>
  <li><a href="https://www.amazon.com/gp/product/B072L1XMJR/ref=as_li_tl?ie=UTF8&amp;camp=1789&amp;creative=9325&amp;creativeASIN=B072L1XMJR&amp;linkCode=as2&amp;tag=zeke082-20&amp;linkId=c85aa8e645e3a2830e4380c1ca382642">Jumper wires</a></li>
  <li><a href="https://www.amazon.com/gp/product/B0012Q54KW/ref=as_li_tl?ie=UTF8&amp;camp=1789&amp;creative=9325&amp;creativeASIN=B0012Q54KW&amp;linkCode=as2&amp;tag=zeke082-20&amp;linkId=61790ac13b129e3531c8635082b29393">Screwdriver extension</a>
    <ul>
      <li>This one is overkill for this use case, but I figure they’d be useful to have anyway.</li>
    </ul>
  </li>
  <li><a href="https://www.amazon.com/gp/product/B01FEJ3OA4/ref=as_li_tl?ie=UTF8&amp;camp=1789&amp;creative=9325&amp;creativeASIN=B01FEJ3OA4&amp;linkCode=as2&amp;tag=zeke082-20&amp;linkId=adaae4c8e3592d64ceffe3cce58ab598">Command strip</a></li>
  <li><a href="https://blindparts.com/product/vertical-blind-wand-grip/">Wand grip</a>
    <ul>
      <li>Note: I haven’t tested this one specifically because my blind already had a grip. I ordered a few of these and am planning on testing them with my other blinds.</li>
    </ul>
  </li>
</ul>

<p>(Note: the amazon links are affiliate links.)</p>

<h1 id="the-hardware">The hardware</h1>
<div class="sidebyimagecontainer">
	<img class="sidebyimage" src="/assets/originalBlinds.jpeg" alt="The original blinds" />  
	<span class="caption">The original blinds</span>
</div>

<p>The specifics will obviously differ based on what type of blinds you have. My blinds are twist blinds, which conveniently have a removable grip on the bottom of the rod, with has a hole on the bottom. I noticed this, and thought it would be perfect for screwing to a servo.</p>

<p><img class="fullwidthimg defaultimg" src="/assets/screwdriver.jpeg" alt="" /><br />
<span class="caption">screwing into the servo</span></p>

<p>You’ll need a screwdriver extension to properly screw into the servo through the grip. My extension was overkill on length, but got the job done.</p>

<p><img class="fullwidthimg defaultimg" src="/assets/screwed.jpeg" alt="" /><br />
<span class="caption">now how to attach to the wall</span></p>

<p>I attached the grip back onto the blind rod, and we’re almost good to go. Except I needed to attach it to the wall, and still have the flexibility to remove it in case I need to manually slide the blinds. I’m also renting this apartment so I didn’t want a permanent solution like screwing into the wall.</p>

<p><img class="fullwidthimg defaultimg" src="/assets/commandstrips.jpeg" alt="" /><br />
<span class="caption">modern problems require modern solutions</span></p>

<p>Command strips were a good solution for me. I’m sure this is super amateur hour, but it worked for my use case.</p>

<p><img class="fullwidthimg defaultimg" src="/assets/attached.jpeg" alt="" /><br />
<span class="caption">servo attached to wall</span></p>

<p>I then plugged in my raspberry pi to the wall (running raspbian), and wired up the servo to the pi using the <a href="https://github.com/fivdi/pigpio#servo-control">wiring guide here</a>.</p>

<h1 id="the-software">The software</h1>

<p>Because my ultimate goal was to connect to homekit, I installed <a href="https://github.com/nfarina/homebridge">homebridge</a>, and <a href="/running-homebridge-in-background/">set it up to run in the background</a>. Then I stumbled upon the <a href="https://github.com/Nicnl/homebridge-minimal-http-blinds">homebridge-minimal-http-blinds plugin</a>, which allowed me to bind a blinds accessory to an http server. So then I set out to write a server that exposed the correct endpoints.</p>

<p>I ended up writing it in Node, and using <a href="https://github.com/fivdi/pigpio">pigpio</a> to control the servo. You can see (and use) the source <a href="https://github.com/ZekeSnider/ServoBlinds">here</a>. The nice thing about this solution is it allowed me to implement percentage based setting of the blinds.</p>

<p>The server’s implementation also includes rubber-banding on request. So for example, if the blinds are at 0%, you request 100% then request 20% after they reach 50%, they will immediately turn back. Newest request takes priority, and it checks which direction it should be turning <a href="https://github.com/ZekeSnider/ServoBlinds/blob/master/blinds.js#L83">on every iteration of the control loop</a>.</p>

<p>After much tinkering via HTTP requests to determine the correct <a href="https://github.com/ZekeSnider/ServoBlinds/blob/master/config.json">config parameters</a>, I arrived at a final set of values. Then I setup my application to run persistently using systemd, and updated my homebridge config file to point at the <a href="https://github.com/ZekeSnider/ServoBlinds/blob/master/accessoryConfig.json">new accessory</a>. Afterwards it was all good to go.</p>

<p>I’m very happy with my final product and am currently working on implementing on the other blinds in my apartment!</p>]]></content><author><name>GitHub User</name><email>your-email@domain.com</email></author><category term="side-projects" /><summary type="html"><![CDATA[I have little experience with hardware, but had a goal that I wanted to retrofit my completely manually operated blinds so that I could control them via HomeKit. This post is about how I accomplished exactly that! The final solution is controllable down to a percentage via siri and the home app.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://zekesnider.com/blinds.png" /><media:content medium="image" url="https://zekesnider.com/blinds.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">How to run homebridge in the background</title><link href="https://zekesnider.com/running-homebridge-in-background/" rel="alternate" type="text/html" title="How to run homebridge in the background" /><published>2019-04-20T19:15:40+00:00</published><updated>2019-04-20T19:15:40+00:00</updated><id>https://zekesnider.com/running-homebridge-in-background</id><content type="html" xml:base="https://zekesnider.com/running-homebridge-in-background/"><![CDATA[<p>A no BS guide to running homebridge in the background on a unix system (requires systemd). <em>Works 70% of the time every time.</em></p>

<p>Add user to run homebridge under<br />
<code class="language-plaintext highlighter-rouge">sudo useradd --system homebridge</code></p>

<p>Create directory for it<br />
<code class="language-plaintext highlighter-rouge">sudo mkdir /var/lib/homebridge</code></p>

<p>Own the directory permissions<br />
<code class="language-plaintext highlighter-rouge">sudo chown -R homebridge:homebridge /var/lib/homebridge</code> 
<code class="language-plaintext highlighter-rouge">sudo chmod 777 -R /var/lib/homebridge</code></p>

<p>Copy your home directory’s config (if you don’t have one already, edit it instead)<br />
<code class="language-plaintext highlighter-rouge">sudo cp ~/.homebridge/config.json /var/lib/homebridge/config.json</code></p>

<p>Copy your home directory’s persist directory (if it exists)
<code class="language-plaintext highlighter-rouge">sudo cp -R ~/.homebridge/persist /var/lib/homebridge/persist</code></p>

<p>Determine where homebridge is aliased<br />
<code class="language-plaintext highlighter-rouge">which homebridge</code></p>

<p>Edit the systemd service<br />
<code class="language-plaintext highlighter-rouge">sudo nano /etc/systemd/system/homebridge.service</code></p>

<p>Paste in the contents of <code class="language-plaintext highlighter-rouge">homebridge.service</code> from <a href="https://gist.github.com/johannrichard/0ad0de1feb6adb9eb61a/">here</a>. Make sure to replace <code class="language-plaintext highlighter-rouge">/usr/local/bin/homebridge</code> with where homebridge is actually installed.</p>

<p>Exit nano<br />
<code class="language-plaintext highlighter-rouge">control + x, Y</code></p>

<p><code class="language-plaintext highlighter-rouge">sudo nano /etc/default/homebridge</code> <br />
Paste in the contents of <code class="language-plaintext highlighter-rouge">homebridge</code> from <a href="https://gist.github.com/johannrichard/0ad0de1feb6adb9eb61a/">here</a>.</p>

<p>Reload systemd configs<br />
<code class="language-plaintext highlighter-rouge">sudo systemctl daemon-reload</code></p>

<p>Enable and start the service<br />
<code class="language-plaintext highlighter-rouge">sudo systemctl enable homebridge</code><br />
<code class="language-plaintext highlighter-rouge">sudo systemctl start homebridge</code></p>

<p>Check on its status<br />
<code class="language-plaintext highlighter-rouge">sudo systemctl status homebridge</code></p>

<p>If something is wrong, check the logs.<br />
<code class="language-plaintext highlighter-rouge">journalctl -u homebridge</code></p>

<p>At this point if there are any issues, live vicariously via google and stackoverflow.</p>

<p>🏝</p>]]></content><author><name>GitHub User</name><email>your-email@domain.com</email></author><category term="side-projects" /><summary type="html"><![CDATA[A no BS guide to running homebridge in the background on a unix system (requires systemd). Works 70% of the time every time.]]></summary></entry><entry><title type="html">The Making of Jared</title><link href="https://zekesnider.com/the-making-of-jared/" rel="alternate" type="text/html" title="The Making of Jared" /><published>2018-11-21T05:15:40+00:00</published><updated>2018-11-21T05:15:40+00:00</updated><id>https://zekesnider.com/the-making-of-jared</id><content type="html" xml:base="https://zekesnider.com/the-making-of-jared/"><![CDATA[<p>I’ve realized that I’ve never written about my side project Jared here, and its background, so I figured now is a good time.</p>

<h1 id="background">Background</h1>
<p>For the uninitiated, iMessage is Apple’s messaging platform that is only available on Apple devices. If you text another iPhone user from an iPhone, the bubble will be blue (sent via iMessage).</p>

<p>Apple has tried to lock down iMessage as much as possible, and there is no public API for common chat bot hooks. As far as I know, I was the first to attempt to write any sort of chat bot for iMessage. Because of how obtuse it is, I assume most would rather write for a platform that actually encourages development of bots, but I was in it for the challenge! iMessage is my favorite messaging service, and I wanted to build on it.</p>

<p>There are only a few hooks available to iMessage:</p>

<ul>
  <li>
    <p>❌ iMessage Apps (introduced in iOS 10). These provide very limited hooks that would not provide a chat bot like is implemented in Jared. All input would need to be in the context of the message extension, as these apps cannot read messages, names, or any personal information. They also can only send messages with the user’s explicit consent.</p>
  </li>
  <li>
    <p>❌ Siri Shortcuts (introduced in iOS 12). Shortcuts have interesting potential, but they are also limited. They can only be triggered manually, and user consent is needed to send messages with images. They cannot read conversation history.</p>
  </li>
  <li>
    <p>✅ AppleScript support via Messages.app for the Mac.</p>
  </li>
</ul>

<p>The Messages.app on the Mac has existed before the introduction of iMessage. It originally only supported AIM, Google Talk, and Jabber accounts. It was just Apple’s instant messenger client. Of course it has evolved and now only supports iMessage, but the base code base has remained the same. For that reason (I’m assuming), it still (had) full AppleScript support.</p>

<p>AppleScript is basically a local RPC framework for automating actions on the Mac. Its syntax is very… strange (English like)? It’s not so much an elegant programming language, but it is good for its use as an automation language. There is also a technology called JXA which allows you to use AppleScript actions from JavaScript instead, but it is not very well documented and I haven’t spent much time with it. But I think all the AppleScript in this project could be replaced with JXA.</p>

<p>For my purposes, two pieces of functionality were key in the Message App’s AppleScript dictionary:</p>

<p><img class="fullwidthimg defaultimg" src="/assets/incominghandler.png" alt="" /><br />
<span class="caption">Message Handler</span></p>

<p>This is triggered when a message is received. It provides the content of the message as well information on the sender. From then we can do routing on it, and if needed…</p>

<p><img class="fullwidthimg defaultimg" src="/assets/sendhandler.png" alt="" /><br />
<span class="caption">Send Action</span></p>

<p>Send a message to a specified recipient. This even supports attachments, and <a href="https://stackoverflow.com/questions/44852939/send-imessage-to-group-chat/44998688#44998688">sending to group chats</a> making it extremely useful to me. These two pieces were all I need to implement my chat bot.</p>

<h1 id="the-first-implementation">The first implementation</h1>
<p><img class="fullwidthimg defaultimg" src="/assets/JaredArchitecture.001.jpeg" alt="" /><br />
<span class="caption">v1</span></p>

<p>The first take (which I have not open sourced because of how sloppy it is), was functional but not elegant. Everything was implemented in one AppleScript handler file. It did not call into any other languages or frameworks.</p>

<p><img class="fullwidthimg defaultimg" src="/assets/MessagesPreferencesBefore.png" alt="" /><br />
<span class="caption">Installation</span></p>

<p>Because everything is in one file, it became quite bloated quickly. In addition, AppleScript syntax is very verbose and not well catered towards writing scalable software. It was really intended for small macro automations.</p>

<ul>
  <li>Single threaded - Whenever the handler is processing a message, it is unable to handle new incoming messages. This caused messages to not get handled if they were routed at the same time, and errors to assert causing the whole system to stop functioning until it was manually restarted.</li>
  <li>Errors - If any error was thrown and not caught, it could cause an error dialog to pop, which shut the whole thing down as mentioned. These errors could be timeouts (spending more than 60 seconds processing), or just inconsistent system errors. As such, I devised a method that would automatically dismiss any error alerts, but this was not 100% effective either.</li>
  <li>Limited Scope - AppleScript as a language is limiting as it lacks many standard language functionality and frameworks. I was able to cut some corners by using <a href="http://www.mousedown.net/mouseware/JSONHelper.html">some</a> <a href="http://www.mousedown.net/mouseware/TwitterScripter.html">AppleScript helper apps</a> that exposed things like REST calls, but this was obtuse and didn’t work for all the features that I wanted to implement.</li>
</ul>

<p>And thus brought v2, a full rewrite…</p>

<h1 id="the-better-implementation">The better implementation</h1>
<p><img class="fullwidthimg defaultimg" src="/assets/JaredArchitecture.002.jpeg" alt="" /><br />
<span class="caption">v2</span></p>

<p>This (current) version uses a native app written in Swift that exposes an AppleScript interface. The Applescript handler in messages is very simple and just passes data to the Swift app. This solves several issues of the initial implementation.</p>

<ul>
  <li>Multi-threading - All incoming requests on the Swift side are put in a background thread via a GCD Dispatch queue. This allows processing to take as long as needed for each message, and allows for other things such as sending delays.</li>
  <li>Frameworks - Swift is a well supported, native language. You’ll be able to find libraries (built in or not) for most things, and it allows for interop with C and Obj C, among other languages.</li>
  <li>Plugins - The original version had all routing in one AppleScript file, which became difficult to maintain after adding many commands. The Swift version added a plugin framework with a rules engine for routing. Additional commands could be added just by building them into a plugin module and placing them in a plugin directory. The plugins are loaded in via NSBundle modularization.</li>
  <li>UI - Because it is running as an app, Jared can now provide a cocoa UI for enabling/disabling the service, as well as configuration options.</li>
</ul>

<p><img class="fullwidthimg defaultimg" src="/assets/JaredUI.png" alt="" /><br />
<span class="caption">The new UI</span></p>

<p>These improvements allowed for much more flexibility. Many bottlenecks were removed, and it also allowed for adding things like database persistence for various state parameters, background processing jobs for scheduling, etc.</p>

<p>A simple send script is called for sending outgoing messages. It is called by making an <a href="https://ss64.com/osx/osascript.html">osascript</a> shell call from Swift.</p>

<p>This solution was great, until Apple made a change in macOS High Sierra 10.13.4.</p>

<p><img class="fullwidthimg defaultimg" src="/assets/missingsetting.png" alt="" /><br />
<span class="caption">Wait where’d it go.. 🤔🤔🤔</span></p>

<p>They removed the option to specify an AppleScript handler in Messages.app. I would like to think I caused this, but more realistically an engineer at Apple probably discovered this menu option one day and asked why the functionality exists at all.</p>

<p>The message send action still exists, but now there is no way to receive the hooks for incoming messages, which brings us back a few steps.</p>

<h1 id="working-around-apple">Working around Apple</h1>
<p><img class="fullwidthimg defaultimg" src="/assets/JaredArchitecture.003.jpeg" alt="" /><br />
<span class="caption">Third time’s the charm</span></p>

<p>In <code class="language-plaintext highlighter-rouge">~/Library/Messages</code> there is a messages.db SQLite database that contains the contents of all message history. It gets updated when new messages are received/sent. Suprisingly, it is a standard SQLite database, and it is not encrypted. So, as a workaround to the AppleScript handler, we can instead query the database on a set interval (5s)? For all new records since the last query. This will allow us to batch process all new messages received.</p>

<p>In addition, there are lots of fields in the database that are available in the database, that were not available via AppleScript, such as read receipts, and more. Although it will be difficult/impossible to query on changes to fields that do not trigger update of a date field.</p>

<p>This solution works around the removal of the AppleScript handler preference, while still keeping most of the codebase the same.</p>

<p>This is not fully implemented yet, as I’ve been working on it on a branch which hasn’t been merged yet. Progress can be tracked on <a href="https://github.com/ZekeSnider/Jared/issues/20">this issue</a>. Credit to Github user mezeipetister for the idea of repeatedly querying the database. Unfortunately I’ve neglected this project too much, but I hope to complete this fix in the near future, so that we can get Jared working on Mojave 😀. I will likely write another update post once that is done.</p>

<h1 id="ps-private-frameworks">PS: Private Frameworks</h1>
<p>You may ask, isn’t there some private API call you can use to bypass AppleScript for sending messages. Well, there is in theory, but I haven’t been able to get any of them to work properly (read: at all). I’ve used <a href="https://github.com/nygard/class-dump">class-dump</a> to retrieve header files, and tried to use methods of the Messages private framework, but haven’t had any luck.</p>

<p>This is something I should probably spend some more time on, but it’s pretty tedious and unsatisfying. A hundred different things could be preventing the method calls from working, but because it’s a private API, it’s very difficult to diagnose it. If anyone has anyone to share on this, please let me know!</p>

<p>So there you have it, the complete story of Jared thus far. If you want to try it out, <a href="https://github.com/zekesnider/jared">check it out on GitHub</a> and give it a star. Hit me up by Twitter or email if you have any questions!</p>]]></content><author><name>GitHub User</name><email>your-email@domain.com</email></author><category term="side-projects" /><category term="instagram" /><summary type="html"><![CDATA[I’ve realized that I’ve never written about my side project Jared here, and its background, so I figured now is a good time.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://zekesnider.com/jared.png" /><media:content medium="image" url="https://zekesnider.com/jared.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>