{
    "version": "https://jsonfeed.org/version/1",
    "title": "Zeke Snider",
    "home_page_url": "https://zekesnider.com/",
    "feed_url": "https://zekesnider.com/feed.json",
    "description": "My personal website\n",
    "icon": "https://zekesnider.com/apple-touch-icon.png",
    "favicon": "https://zekesnider.com/favicon.ico",
    "expired": false,
    
    "author":  {
        "name": "GitHub User",
        "url": null,
        "avatar": null
    },
    
"items": [
    
        {
            "id": "https://zekesnider.com/customizing-swiftui-list-selection/",
            "title": "Customizing SwiftUI List Selection",
            "summary": null,
            "content_text": "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.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.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.If you’re just interested in the source code you can check it out here. 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 Lyrigraphy on the App Store to try the finished product in the lyrics screen!System viewThe default list implementation does 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.ApproachSince I would essentially need to implement this multi-row selection from scratch, let’s break down what the tap and drag gesture actually entails.  First a hold gesture needs to be made (~0.5s duration)  Until the user releases, a drag gesture is recognized  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  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.  When the user releases, we should add (or remove) items from the selection list and update the visual state accordingly.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.ImplementationThese are the APIs that ended up being essential for my implementation:  DragGesture sequenced with LongPressGesture for gesture recognizers  highPriorityGesture for applying the gesture  CoordinateSpace and GeometryProxy to retrieve coordinates within the scroll view  Using PreferenceKeys with GeometryReader to pass coordinate data back up to the containing view  ScrollViewReader to control scroll stateIt’s worth noting that you should not 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.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.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.struct LinePreferenceData: Equatable {    let index: Int    let minY: Double    let maxY: Double    let globalMinY: Double    let globalMaxY: Double        init(index: Int, bounds: CGRect, globalBounds: CGRect) {        self.index = index        self.minY = bounds.minY        self.maxY = bounds.maxY        self.globalMinY = globalBounds.minY        self.globalMaxY = globalBounds.maxY    }}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 LinePreferenceData for reconciliation.struct LinePreferenceKey: PreferenceKey {    typealias Value = [LinePreferenceData]        static var defaultValue: [LinePreferenceData] = []        static func reduce(value: inout [LinePreferenceData], nextValue: () -&gt; [LinePreferenceData]) {        value.append(contentsOf: nextValue())    }}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..background() {    GeometryReader { geometry in        Rectangle()            .fill(Color.clear)            .preference(key: LinePreferenceKey.self,                        value: [LinePreferenceData(index: lyric.id,                                                    bounds: geometry.frame(in: .named(\"container\")),                                                    globalBounds: geometry.frame(in: .global))])    }}It is critical that the .coordinateSpace 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.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.var drag: some Gesture {    LongPressGesture(minimumDuration: 0.5)        .sequenced(before: DragGesture(minimumDistance: 0, coordinateSpace: .named(\"container\")))        .updating($isDragging, body: { value, state, transaction in            switch value {            case .first(true):                break            case .second(_, let drag):                guard let start = drag?.startLocation else { return }                let end = drag?.location ?? start                                if isDraggingSelected == nil {                    let dragStartIndex = getStartIndex(from: start.y)                    isDraggingSelected = dragStartIndex.map { viewModel.selectedLyricIndexes.contains($0) } ?? false                }                                handleDragChange(start: start, end: end)            default:                return            }        })        .onEnded { value in            switch value {            case .first(true):                // Long press succeeded                isSelecting = true            case .second(true, _):                // Drag ended                updateSelectionRange()                isSelecting = false                isDraggingSelected = nil            default:                break            }        }}And finally, implement the method to handle update events to the gesture.private func handleDragChange(start: CGPoint, end: CGPoint) {    let minY = min(start.y, end.y)    let maxY = max(start.y, end.y)        // Clear the pending selection before recalculating    pendingSelection.removeAll()        // Find all lines that intersect with the drag range    let selectedLines = lineData.filter { line in        return !(line.maxY &lt; minY || line.minY &gt; maxY)    }    let currentLine = lineData.first { line in        return end.y &gt;= line.minY &amp;&amp; end.y &lt;= line.maxY    }        pendingSelection = Set(selectedLines.map { $0.index })        if let currentLine {        handleAutoScroll(currentY: currentLine.globalMaxY, index: currentLine.index)    }}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 asyncAfter to isolate scrolling so that it doesn’t continually re-trigger.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 full code is a bit messy, but figured it would be worth publishing in case it’s useful to anyone to reference.Appendix: a note on LLMsAs 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.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.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 very 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.My general iteration steps were to:  Provide simple instructions to the model on my goal with my existing code  Review output, incorporate relevant functionality into my implementation  Re-prompt with new issues or bugs with the new implementation  Manually correct bugs and continue to iterateA 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.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.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.",
            "content_html": "<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>",
            "url": "https://zekesnider.com/customizing-swiftui-list-selection/",
            "image": "CustomMultiSelectList.png",
            
            
            
            
            "date_published": "2025-03-01T19:00:00+00:00",
            "date_modified": "2025-03-01T19:00:00+00:00",
            
                "author":  {
                "name": "GitHub User",
                "url": null,
                "avatar": null
                }
                
            
        },
    
        {
            "id": "https://zekesnider.com/swift-data-transction-history/",
            "title": "Using SwiftData Transaction History to Update Widgets",
            "summary": null,
            "content_text": "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.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:  The app subscribes to data model changes (local change data capture)  Filter changes to only those relevant to active widgets  Trigger reload of those widgetsJust interested in the sample code? Check it out here.Widget UpdatesApple has provided documentation 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.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.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.It’s worth noting that WidgetKit’s WidgetCenter 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.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.SwiftData Transaction HistoryApple recently added transaction history API to SwiftData in iOS 18. This allows you to easily query chronological transactions that were made to your data store. This WWDC talk 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.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 NSPersistentStoreRemoteChange, which notifies for both remote and local changes to the Core Data database.So to recap the approach before getting into the code:  Wait for NSPersistentStoreRemoteChange notifications  Poll SwiftData transaction history since the last history token  Filter events to those relevant to widgets  Reload relevant widget kinds  Delete old transactions, store new history tokenImplementationFirst, let’s start by adding a ModelActor for performing these operations:@ModelActor final actor DataMonitor {    func subscribeToModelChanges() async {        for await _ in NotificationCenter.default.notifications(            named: .NSPersistentStoreRemoteChange        ).map({ _ in () }) {            await processNewTransactions()        }    }...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.func processNewTransactions() async {    let tokenData = UserDefaults.standard.data(forKey: \"historyToken\")            var historyToken: DefaultHistoryToken? = nil    if let tokenData {        historyToken = try? JSONDecoder().decode(DefaultHistoryToken.self, from: tokenData)    }        let transactions = findTransactions(after: historyToken)    let (updatedModelIds, newHistoryToken) = findUpdatedModelIds(in: transactions)    if let newHistoryToken {        let newTokenData = try? JSONEncoder().encode(newHistoryToken)        UserDefaults.standard.set(newTokenData, forKey: \"historyToken\")    }    if let historyToken {        try? deleteTransactions(before: historyToken)    }        await maybeUpdateWidgets(relevantTo: updatedModelIds)}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.private func findTransactions(after token: DefaultHistoryToken?) -&gt; [DefaultHistoryTransaction] {    var historyDescriptor = HistoryDescriptor&lt;DefaultHistoryTransaction&gt;()    if let token {        historyDescriptor.predicate = #Predicate { transaction in            (transaction.token &gt; token)        }    }    var transactions: [DefaultHistoryTransaction] = []    do {        transactions = try modelContext.fetchHistory(historyDescriptor)    } catch {        logger.error(\"Error while fetching history transactions \\(error, privacy: .public)\")    }    return transactions}private func deleteTransactions(before token: DefaultHistoryToken) throws {    var descriptor = HistoryDescriptor&lt;DefaultHistoryTransaction&gt;()    descriptor.predicate = #Predicate {        $0.token &lt; token    }    let context = ModelContext(modelContainer)    try context.deleteHistory(descriptor)}With the transactions in hand, I now need to convert the DefaultHistoryTransaction 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 id field to determine if they’re relevant to my widget.private func findUpdatedModelIds(in transactions: [DefaultHistoryTransaction]) -&gt; (Set&lt;UUID&gt;, DefaultHistoryToken?) {    let taskContext = ModelContext(modelContainer)    var updatedModelIds: Set&lt;UUID&gt; = []    for transaction in transactions {        for change in transaction.changes {            let transactionModifiedID = change.changedPersistentIdentifier            let fetchDescriptor = FetchDescriptor&lt;SongArtworkViewModel&gt;(predicate: #Predicate { model in                model.persistentModelID == transactionModifiedID            })            let fetchResults = try? taskContext.fetch(fetchDescriptor)            guard let matchedModel = fetchResults?.first else {                continue            }            switch change {            case .insert(_ as DefaultHistoryInsert&lt;SongArtworkViewModel&gt;):                break            case .update(_ as DefaultHistoryUpdate&lt;SongArtworkViewModel&gt;):                updatedModelIds.update(with: matchedModel.id)            case .delete(_ as DefaultHistoryDelete&lt;SongArtworkViewModel&gt;):                updatedModelIds.update(with: matchedModel.id)            default: break            }        }    }    return (updatedModelIds, transactions.last?.token)}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.private func maybeUpdateWidgets(relevantTo modelIds: Set&lt;UUID&gt;) async {    let configurations = try? await WidgetCenter.shared.currentConfigurations()    guard let configurations else { return }    let relevantConfigurationKinds = configurations.filter { configuration in        let config = configuration.widgetConfigurationIntent(of: SongConfigurationAppIntent.self)        guard let config else {            return false        }                if config.mode == .random {            return true        }                guard let entityId = config.specificSong?.id else {            return false        }                return modelIds.contains(entityId)    }.map { $0.kind }        Array(Set(relevantConfigurationKinds)).forEach { kind in        WidgetCenter.shared.reloadTimelines(ofKind: kind)    }}With all the logic completed, I could then add a modifier to my app so that this occurs on startup..task {    Task {        let monitor = DataMonitor(modelContainer: ModelContainer.sharedModelContainer)        await monitor.subscribeToModelChanges()    }}Build and run and everything would work for sure. …Right?SwiftData/DataUtilities.swift:1305: Fatal error: Unexpected class type: CodableColorWomp 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 @Attribute annotations with.transformable(by: ). 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.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.While working on this, I realized I also could just use the simple solution of always reloading all widgets whenever I receive the NSPersistentStoreRemoteChange 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 Core Spotlight.If you’re interested in trying this architecture for yourself, you check out my sample code.Takeaways:  For WidgetKit refresh granularity, it is better to vend multiple kinds of widgets  The SwiftData Transaction History APIs can be used in conjunction with the NSPersistentStoreRemoteChange notification to trigger off of new updates to your data model  SwiftData’s Transaction History APIs may have issues with very complex data models  This approach would also be useful for other problem spaces like exposing your SwiftData to SpotlightThanks for reading!",
            "content_html": "<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>",
            "url": "https://zekesnider.com/swift-data-transction-history/",
            
            
            
            
            
            "date_published": "2025-02-12T04:14:00+00:00",
            "date_modified": "2025-02-12T04:14:00+00:00",
            
                "author":  {
                "name": "GitHub User",
                "url": null,
                "avatar": null
                }
                
            
        },
    
        {
            "id": "https://zekesnider.com/swiftui-device-previews/",
            "title": "SwiftUI Device Previews",
            "summary": null,
            "content_text": "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.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.Unfortunately I ran into some provisioning errors when I tried to set this up.\"This app cannot be installed because its integrity could not be verified\"NSLocalizedRecoverySuggestion=Failed to install embedded profileThis StackOverflow post 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.",
            "content_html": "<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>",
            "url": "https://zekesnider.com/swiftui-device-previews/",
            
            
            
            
            
            "date_published": "2024-12-07T16:14:00+00:00",
            "date_modified": "2024-12-07T16:14:00+00:00",
            
                "author":  {
                "name": "GitHub User",
                "url": null,
                "avatar": null
                }
                
            
        },
    
        {
            "id": "https://zekesnider.com/2024-canvassing/",
            "title": "2024 Canvassing",
            "summary": null,
            "content_text": "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.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.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.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.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.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.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.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.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!",
            "content_html": "<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>",
            "url": "https://zekesnider.com/2024-canvassing/",
            
            
            
            
            
            "date_published": "2024-11-21T14:53:52+00:00",
            "date_modified": "2024-11-21T14:53:52+00:00",
            
                "author":  {
                "name": "GitHub User",
                "url": null,
                "avatar": null
                }
                
            
        },
    
        {
            "id": "https://zekesnider.com/everett-parks-project/",
            "title": "Everett Parks Reviews Project",
            "summary": null,
            "content_text": "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.If you’re just interested in the park reviews, you can check it out here. This post details the background of the project and some of the benign details of how I created the write up.The preparationThis project started off with some research a spreadsheet to track and categorize each park. The city parks website 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.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.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 the most efficient path, but the time loss was not substantial as the city of Everett is not very large.The adventureOn 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.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.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.With the photos and notes taken, it was time to move on to documenting our memories!The write upFor 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!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.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:  List all the parks in a paginated view  Have a gallery image view for each  Show where each park is on a map  (Ideally) show where all the parks are on a map viewPaginationThis was the most straightforward. Just needed to migrate to jekyll-paginate-v2 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.ImagesThe 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.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 Photoswipe. I used a fairly simple _include that wraps a specific set of images in a gallery.But there were a few complications with this approach:  Image dimensionsPhotoswipe 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.  Image sizingWhen 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 srcset property which will dynamically load whichever asset dimension is needed. Luckily, there is already a great Jekyll plugin, jekyll_picture_tag which takes care of this. I installed it, wrapped my images tags with it, and everything worked perfectly!…Except when I went to deploy and test my changes in AWS Amplify. Because the library uses a dependency called libvips 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 custom dockerfile 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.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.MapsFor each park I wanted to include a screenshot of where it is on a map. The Apple Maps Web Snapshots 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 _includes 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 so I decided to open source it.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 CircleCI and Codecov. 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.I experimented with using Cursor and and Claude 3.5 Sonnet 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.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.Writing the contentWith 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.ConclusionIn 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.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 :).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 similar challenge in San Francisco sometime.",
            "content_html": "<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>",
            "url": "https://zekesnider.com/everett-parks-project/",
            
            
            
            
            
            "date_published": "2024-10-22T03:39:52+00:00",
            "date_modified": "2024-10-22T03:39:52+00:00",
            
                "author":  {
                "name": "GitHub User",
                "url": null,
                "avatar": null
                }
                
            
        },
    
        {
            "id": "https://zekesnider.com/automatic-reference-counting-with-self/",
            "title": "Automatic Reference Counting with `self`",
            "summary": null,
            "content_text": "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.Jared contains the ability to load .bundle 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 Telegraph.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 deinit statement. Setting a breakpoint, the deinit was never called.After unloading the server module, I ran an experiment by mashing the reload plugins button repeatedly.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.Looking closer into the pertinent part of the memory graph: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:var routes: [Route] = []init() {    let reload = Route(name:\"/reload\", comparisons: [.startsWith: [\"/reload\"]],      call: self.reload, description: localized(\"reloadDescription\"))        routes = [reload]}func reload(_ message: Message) -&gt; Void {}After some research, it was evident that the issue lied in the reference to a self 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 strong self reference, it was causing any module to never 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.This behavior is explicitly called out in the Swift documentation:  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.As a simple solution, I simply provided the callback as a weak self reference instead:let reload = Route(name:\"/reload\", comparisons: [.startsWith: [\"/reload\"]],  call: {[weak self] in self?.reload($0)},  description: localized(\"reloadDescription\"))The weak reference allows you to reference self, without keeping a strong hold on it. This prevents a strong reference cycle, and allows my Module 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.tl;dr: You should be very careful when using self in callbacks. It is very likely that you should use a reference to weak self instead of a strong reference.",
            "content_html": "<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>",
            "url": "https://zekesnider.com/automatic-reference-counting-with-self/",
            
            
            
            
            
            "date_published": "2020-08-12T05:39:20+00:00",
            "date_modified": "2020-08-12T05:39:20+00:00",
            
                "author":  {
                "name": "GitHub User",
                "url": null,
                "avatar": null
                }
                
            
        },
    
        {
            "id": "https://zekesnider.com/best-of-the-decade/",
            "title": "Best of the decade",
            "summary": null,
            "content_text": "This is the stuff that I enjoyed the most since 2010.Music      Melodrama - Lorde        1989 - Taylor Swift        E•MO•TION - Carly Rae Jepsen        Norman Fucking Rockwell - Lana Del Rey        Pure Heroine - Lorde        Gone Now - Bleachers        Ghost Stories - Coldplay        Ultraviolence - Lana Del Rey        Every Open Eye - Chvrches        Days Are Gone - HAIM        How Big, How Blue, How beautiful - Florence and the Machine        Nothing’s Real - Shura        BADLANDS - Halsey  Videogames      Persona 4 Golden        The Last of Us        Persona 5        Shin Megami Tensei IV        Tetris 99 / Tetris Effect  Movie  Lady BirdTV Show  LostThe last episode aired in 2010 so it counts",
            "content_html": "<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>",
            "url": "https://zekesnider.com/best-of-the-decade/",
            "image": "the-greatest.jpg",
            
            
            
            
            "date_published": "2020-01-01T02:15:40+00:00",
            "date_modified": "2020-01-01T02:15:40+00:00",
            
                "author":  {
                "name": "GitHub User",
                "url": null,
                "avatar": null
                }
                
            
        },
    
        {
            "id": "https://zekesnider.com/homekit-servo-blinds/",
            "title": "Homekit Servo Blinds",
            "summary": null,
            "content_text": "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.Best thing I’ve done all year. #HomeKit #homebridge pic.twitter.com/4uWfUkBkt0&mdash; Zeke (@ZekeSnider) April 18, 2019The Equipment  Raspberry Pi 3          Alternatively, you can use a Raspberry Pi Zero with a headboard soldered on (or solder directly)        360° servo          Make sure you get a 360° servo.        Jumper wires  Screwdriver extension          This one is overkill for this use case, but I figure they’d be useful to have anyway.        Command strip  Wand grip          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.      (Note: the amazon links are affiliate links.)The hardware\t  \tThe original blindsThe 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.screwing into the servoYou’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.now how to attach to the wallI 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.modern problems require modern solutionsCommand strips were a good solution for me. I’m sure this is super amateur hour, but it worked for my use case.servo attached to wallI then plugged in my raspberry pi to the wall (running raspbian), and wired up the servo to the pi using the wiring guide here.The softwareBecause my ultimate goal was to connect to homekit, I installed homebridge, and set it up to run in the background. Then I stumbled upon the homebridge-minimal-http-blinds plugin, 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.I ended up writing it in Node, and using pigpio to control the servo. You can see (and use) the source here. The nice thing about this solution is it allowed me to implement percentage based setting of the blinds.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 on every iteration of the control loop.After much tinkering via HTTP requests to determine the correct config parameters, 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 new accessory. Afterwards it was all good to go.I’m very happy with my final product and am currently working on implementing on the other blinds in my apartment!",
            "content_html": "<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\">\t<img class=\"sidebyimage\" src=\"/assets/originalBlinds.jpeg\" alt=\"The original blinds\" />  \t<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>",
            "url": "https://zekesnider.com/homekit-servo-blinds/",
            "image": "blinds.png",
            
            
            
            
            "date_published": "2019-04-21T19:15:40+00:00",
            "date_modified": "2019-04-21T19:15:40+00:00",
            
                "author":  {
                "name": "GitHub User",
                "url": null,
                "avatar": null
                }
                
            
        },
    
        {
            "id": "https://zekesnider.com/running-homebridge-in-background/",
            "title": "How to run homebridge in the background",
            "summary": null,
            "content_text": "A no BS guide to running homebridge in the background on a unix system (requires systemd). Works 70% of the time every time.Add user to run homebridge undersudo useradd --system homebridgeCreate directory for itsudo mkdir /var/lib/homebridgeOwn the directory permissionssudo chown -R homebridge:homebridge /var/lib/homebridge sudo chmod 777 -R /var/lib/homebridgeCopy your home directory’s config (if you don’t have one already, edit it instead)sudo cp ~/.homebridge/config.json /var/lib/homebridge/config.jsonCopy your home directory’s persist directory (if it exists)sudo cp -R ~/.homebridge/persist /var/lib/homebridge/persistDetermine where homebridge is aliasedwhich homebridgeEdit the systemd servicesudo nano /etc/systemd/system/homebridge.servicePaste in the contents of homebridge.service from here. Make sure to replace /usr/local/bin/homebridge with where homebridge is actually installed.Exit nanocontrol + x, Ysudo nano /etc/default/homebridge Paste in the contents of homebridge from here.Reload systemd configssudo systemctl daemon-reloadEnable and start the servicesudo systemctl enable homebridgesudo systemctl start homebridgeCheck on its statussudo systemctl status homebridgeIf something is wrong, check the logs.journalctl -u homebridgeAt this point if there are any issues, live vicariously via google and stackoverflow.🏝",
            "content_html": "<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>",
            "url": "https://zekesnider.com/running-homebridge-in-background/",
            
            
            
            
            
            "date_published": "2019-04-20T19:15:40+00:00",
            "date_modified": "2019-04-20T19:15:40+00:00",
            
                "author":  {
                "name": "GitHub User",
                "url": null,
                "avatar": null
                }
                
            
        },
    
        {
            "id": "https://zekesnider.com/the-making-of-jared/",
            "title": "The Making of Jared",
            "summary": null,
            "content_text": "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.BackgroundFor 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).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.There are only a few hooks available to iMessage:      ❌ 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.        ❌ 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.        ✅ AppleScript support via Messages.app for the Mac.  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.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.For my purposes, two pieces of functionality were key in the Message App’s AppleScript dictionary:Message HandlerThis 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…Send ActionSend a message to a specified recipient. This even supports attachments, and sending to group chats making it extremely useful to me. These two pieces were all I need to implement my chat bot.The first implementationv1The 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.InstallationBecause 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.  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.  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.  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 some AppleScript helper apps that exposed things like REST calls, but this was obtuse and didn’t work for all the features that I wanted to implement.And thus brought v2, a full rewrite…The better implementationv2This (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.  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.  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.  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.  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.The new UIThese 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.A simple send script is called for sending outgoing messages. It is called by making an osascript shell call from Swift.This solution was great, until Apple made a change in macOS High Sierra 10.13.4.Wait where’d it go.. 🤔🤔🤔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.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.Working around AppleThird time’s the charmIn ~/Library/Messages 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.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.This solution works around the removal of the AppleScript handler preference, while still keeping most of the codebase the same.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 this issue. 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.PS: Private FrameworksYou 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 class-dump to retrieve header files, and tried to use methods of the Messages private framework, but haven’t had any luck.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!So there you have it, the complete story of Jared thus far. If you want to try it out, check it out on GitHub and give it a star. Hit me up by Twitter or email if you have any questions!",
            "content_html": "<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>",
            "url": "https://zekesnider.com/the-making-of-jared/",
            "image": "jared.png",
            
            
            
            
            "date_published": "2018-11-21T05:15:40+00:00",
            "date_modified": "2018-11-21T05:15:40+00:00",
            
                "author":  {
                "name": "GitHub User",
                "url": null,
                "avatar": null
                }
                
            
        },
    
        {
            "id": "https://zekesnider.com/a-blog-post/",
            "title": "A Blog Post",
            "summary": null,
            "content_text": "It’s been 364 days since my last post, so I’m getting right in under that 1 year mark so that I can still declare that my blog is regularly updated. Here are some boring random tidbits that somehow constitute a blog post.Also, I am hereby committing to posting a technical blog post about the status of Jared within the next week!!      Once you start using a clipboard manager on your computer and adapt your work-flow around it, you can never go back.        You can un-subscribe from junk mail by paying $2 for a 10 year membership at the Direct Marketing Association (DMA). You can also un-subcribe from credit card offers in the mail. (More info from the FTC here).        If you live in California it’s worth investing in some N95 masks.        If you post really long ridiculous captions on Instagram of a product and tag it with #ad, everyone will believe you’ve been sponsored by Coke (even though you have like 50 followers) and ask you about it the next time they see you.        I’ll soon (maybe) finally get CarPlay in my car. This will be a truly incredible day 2 years in the making. I don’t even know what else in life I’m waiting for at that point.        I’ve been made aware that a HomeKit supported essential oil diffuser is available, and this is so ridiculous that I might just need to own one.        There are many trade offs to using Sublime vs a full featured IDE and I don’t think I’ll ever be fully happy with either.        Dark Mode on Mojave is nice.        Not having to use Windows ever ever is a great blessing that I will never take for granted. Not having to use Windows APIs is an even bigger blessing.        👏👏 House Majority Leader Nancy Pelosi 👏👏  ",
            "content_html": "<p>It’s been 364 days since my last post, so I’m getting right in under that 1 year mark so that I can still declare that my blog is regularly updated. Here are some boring random tidbits that somehow constitute a blog post.</p><p>Also, I am hereby committing to posting a technical blog post about the status of <a href=\"https://github.com/zekesnider/jared\">Jared</a> within the next week!!</p><ul>  <li>    <p>Once you start using a <a href=\"https://tapbots.com/pastebot/\">clipboard manager</a> on your computer and adapt your work-flow around it, you can never go back.</p>  </li>  <li>    <p>You can un-subscribe from junk mail by paying $2 for a 10 year membership at the Direct Marketing Association (DMA). You can also un-subcribe from credit card offers in the mail. (<a href=\"https://www.consumer.ftc.gov/articles/0262-stopping-unsolicited-mail-phone-calls-and-email\">More info from the FTC here</a>).</p>  </li>  <li>    <p>If you live in California it’s worth investing in some <a href=\"https://www.amazon.com/gp/product/B008MCV1HY/ref=oh_aui_detailpage_o01_s00?ie=UTF8&amp;psc=1\">N95 masks</a>.</p>  </li>  <li>    <p>If you post really long ridiculous captions on Instagram of a product and tag it with #ad, everyone will believe you’ve been sponsored by Coke (even though you have like 50 followers) and ask you about it the next time they see you.</p>  </li>  <li>    <p>I’ll soon (maybe) finally get CarPlay in my car. This will be a truly incredible day 2 years in the making. I don’t even know what else in life I’m waiting for at that point.</p>  </li>  <li>    <p>I’ve been made aware that a <a href=\"https://www.macrumors.com/review/vocolinc-flowerbud/\">HomeKit supported essential oil diffuser</a> is available, and this is so ridiculous that I might just need to own one.</p>  </li>  <li>    <p>There are many trade offs to using Sublime vs a full featured IDE and I don’t think I’ll ever be fully happy with either.</p>  </li>  <li>    <p>Dark Mode on Mojave is nice.</p>  </li>  <li>    <p>Not having to use Windows ever ever is a great blessing that I will never take for granted. Not having to use Windows APIs is an even bigger blessing.</p>  </li>  <li>    <p>👏👏 House Majority Leader Nancy Pelosi 👏👏</p>  </li></ul>",
            "url": "https://zekesnider.com/a-blog-post/",
            
            
            
            
            
            "date_published": "2018-11-12T17:15:40+00:00",
            "date_modified": "2018-11-12T17:15:40+00:00",
            
                "author":  {
                "name": "GitHub User",
                "url": null,
                "avatar": null
                }
                
            
        },
    
        {
            "id": "https://zekesnider.com/coldplay-favorites/",
            "title": "Coldplay Favorites",
            "summary": null,
            "content_text": "I’ve been thinking about this a lot since their amazing Seattle show, so I made a playlist of my favorite Coldplay songs.Check it out:Apple MusicSpotify",
            "content_html": "<p>I’ve been thinking about this a lot since their amazing Seattle show, so I made a playlist of my favorite Coldplay songs.</p><p>Check it out:</p><p><a href=\"https://itunes.apple.com/us/playlist/coldplay-favorites/idpl.u-VkVKcBo6PJG\">Apple Music</a><a href=\"https://open.spotify.com/user/1246039606/playlist/2Vc0dUgUlgR1esSDJTzNqK\">Spotify</a></p>",
            "url": "https://zekesnider.com/coldplay-favorites/",
            "image": "coldplayOG.jpg",
            
            
            
            
            "date_published": "2017-10-03T05:15:40+00:00",
            "date_modified": "2017-10-03T05:15:40+00:00",
            
                "author":  {
                "name": "GitHub User",
                "url": null,
                "avatar": null
                }
                
            
        },
    
        {
            "id": "https://zekesnider.com/favorite-ios-apps/",
            "title": "My Favorite iOS Apps",
            "summary": null,
            "content_text": "Here’s my highly anticipated list of favorite iOS apps. A follow up to my favorite Mac apps.1PasswordMy favorite password manager is excellent on iOS. A must have for me.OvercastOvercast is by far my favorite podcast app on iOS. According to the settings panel, Overcast’s smart speed feature has saved me 55 hours of listening time. The app is very nicely designed, features smart speed &amp; voice boost, and has a robust sync service backing it. If you are using another podcast app, I highly recommend giving Overcast a try.TweetbotTweetbot is a beautifully designed third party twitter client. The default twitter app is gross, I only use it to access polls or other features which Tweetbot is unable to add due to lack of API support from Twitter.InstagramPerhaps this goes without saying, but I really enjoy using Instagram.AutomaticI have an automatic dingus in my car, and the app is great at letting me view past routes, and interesting statistics and such.PCalcIf you’re looking for a more advanced calculator than the system default (or you need an iPad calculator), PCalc is excellent. It is feature rich and is frequently updated with any new features added to the OS.LyftThe better ride sharing app. (Uber is a terrible company)CashThe fastest and easiest way to send money to friends or family. We’re done using those other apps.Dark SkyUp to the minute weather notifications that really work. Getting a notification on your wrist when rain is about to arrive is often very useful. The app also provides more detailed weather info than available in the default weather app. I still use the default app as well, but Dark Sky is a great backup.WorkflowThe best way to automate common tasks on iOS. A really powerful tool. So good it got bought out by Apple. My favorite workflow: one that quick-plays Melodrama by Lorde.MidoriMy favorite Japanese dictionary on iOS.Day OneDay One is how I journal. I’ve written in detail about it in my Mac Apps post, and the iOS app has feature parity. It’s delightful.Stack ExchangeI enjoy browsing stack exchange sites, and the app is surprisingly native and feature rich. Recommended if you’re a frequent visitor to any stack exchange sites.The Washington PostMy favorite news site has a solid iOS app. I prefer scrolling through in the WaPo app instead of Apple’s News app because of how nicely it is laid out.",
            "content_html": "<p>Here’s my <em>highly</em> anticipated list of favorite iOS apps. A follow up to my <a href=\"/favorite-mac-apps/\">favorite Mac apps</a>.</p><h3 id=\"1password\"><a href=\"https://1password.com\">1Password</a></h3><p>My favorite password manager is excellent on iOS. A must have for me.</p><h3 id=\"overcast\"><a href=\"https://overcast.fm/\">Overcast</a></h3><p>Overcast is by far my favorite podcast app on iOS. According to the settings panel, Overcast’s smart speed feature has saved me 55 hours of listening time. The app is very nicely designed, features smart speed &amp; voice boost, and has a robust sync service backing it. If you are using another podcast app, I highly recommend giving Overcast a try.</p><h3 id=\"tweetbot\"><a href=\"https://tapbots.com/tweetbot/\">Tweetbot</a></h3><p>Tweetbot is a beautifully designed third party twitter client. The default twitter app is gross, I only use it to access polls or other features which Tweetbot is unable to add due to lack of API support from Twitter.</p><h3 id=\"instagram\"><a href=\"https://www.instagram.com\">Instagram</a></h3><p>Perhaps this goes without saying, but I really enjoy using Instagram.</p><h3 id=\"automatic\"><a href=\"https://www.automatic.com\">Automatic</a></h3><p>I have an automatic dingus in my car, and the app is great at letting me view past routes, and interesting statistics and such.</p><h3 id=\"pcalc\"><a href=\"http://www.pcalc.com\">PCalc</a></h3><p>If you’re looking for a more advanced calculator than the system default (or you need an iPad calculator), PCalc is excellent. It is feature rich and is frequently updated with any new features added to the OS.</p><h3 id=\"lyft\"><a href=\"https://www.lyft.com\">Lyft</a></h3><p>The better ride sharing app. (<a href=\"http://www.slate.com/blogs/browbeat/2017/04/24/here_are_some_more_terrible_things_uber_has_been_doing.html\">Uber</a> is a <a href=\"https://thenextweb.com/opinion/2017/04/22/uber-in-a-nutshell/\">terrible</a> <a href=\"https://www.vice.com/en_us/article/exm7za/all-the-reasons-why-uber-is-the-worst-1118\">company</a>)</p><h3 id=\"cash\"><a href=\"https://cash.me\">Cash</a></h3><p>The fastest and easiest way to send money to friends or family. We’re done using those other apps.</p><h3 id=\"dark-sky\"><a href=\"https://darksky.net/app\">Dark Sky</a></h3><p>Up to the minute weather notifications that really work. Getting a notification on your wrist when rain is about to arrive is often very useful. The app also provides more detailed weather info than available in the default weather app. I still use the default app as well, but Dark Sky is a great backup.</p><h3 id=\"workflow\"><a href=\"https://workflow.is\">Workflow</a></h3><p>The best way to automate common tasks on iOS. A really powerful tool. So good it got bought out by Apple. My favorite workflow: one that quick-plays Melodrama by Lorde.</p><h3 id=\"midori\"><a href=\"http://www.midoriapp.com\">Midori</a></h3><p>My favorite Japanese dictionary on iOS.</p><h3 id=\"day-one\"><a href=\"http://dayoneapp.com\">Day One</a></h3><p>Day One is how I journal. I’ve written in detail about it in my Mac Apps post, and the iOS app has feature parity. It’s delightful.</p><h3 id=\"stack-exchange\"><a href=\"https://itunes.apple.com/us/app/stack-exchange/id871299723?mt=8\">Stack Exchange</a></h3><p>I enjoy browsing stack exchange sites, and the app is surprisingly native and feature rich. Recommended if you’re a frequent visitor to any stack exchange sites.</p><h3 id=\"the-washington-post\"><a href=\"https://itunes.apple.com/us/app/the-washington-post/id938922398?mt=8\">The Washington Post</a></h3><p>My favorite news site has a solid iOS app. I prefer scrolling through in the WaPo app instead of Apple’s News app because of how nicely it is laid out.</p>",
            "url": "https://zekesnider.com/favorite-ios-apps/",
            "image": "iOS.jpeg",
            
            
            
            
            "date_published": "2017-10-03T04:15:40+00:00",
            "date_modified": "2017-10-03T04:15:40+00:00",
            
                "author":  {
                "name": "GitHub User",
                "url": null,
                "avatar": null
                }
                
            
        },
    
        {
            "id": "https://zekesnider.com/favorite-mac-apps/",
            "title": "My Favorite Mac Apps",
            "summary": null,
            "content_text": "Here’s a list of my favorite (third party) Mac apps.1Password1Password is a must app for anybody, in my opinion. It is an easy to use and well designed application that makes it super easy to manage unique passwords for all of your online accounts. It autofills in all browsers, and stores everything else important in your life like Pin codes, credit card numbers, etc. It also supports 2 factor auth codes. It’s a lifeline for me. And it syncs easily between all my devices. 5/5.Day OneTerrific journaling App. Makes it easy to log your life, and syncs with the iOS and watchOS apps as well. Really well designed, and makes it easy to get writing, format it how you like, and attach images or locations. I think journaling is really important, and Day One fills an important need. There are pros and cons to digital journling, but a few things I like:      Easy tagging / searching.It’s so easy to find old posts by tags, locations, date, etc. Much easier than searching through physical pages. It’s also fun to look at the different views such as the map with all your journal posts.        Password protectionYou can use a passcode (or TouchID) to secure your journal. That way nobody, even with physical access can access my journal. And with end to end encryption, even if Day One servers are hacked, my data is safe. Since my journal contains my most personal thoughts, this is very important.        BackupThere are backups of my journal on Day One’s server, as well as my iCloud backup, Time Machine, Backblaze, etc. There’s little risk of me losing my journal with so many replications of it. With a physical journal, there’s only one.        ConvienienceI can type out an entry whenever I want. Sometimes I’ll create a quick entry from my watch with the time and location, then fill it in later. Always accessible.  I get the appeal of a physical journal, but for me, writing digitally is way easier. It’s personal preference. Also my hand would get so tired when I write long winded journals…IINAAn up and coming open source video player. Plays nearly every format and the UI is more native and prettier than VLC. If you’re a old time VLC user because of format compatibility, give IINA a try.TweetbotA fantastic Twitter client for macOS. Way better than using the website if you’re a regular Twitter user. Has feature parity with the iOS version and the UI design is great.DaisyDiskMy preferred way to see how the disk space on my mac is being used. Allows me to find and delete unneeded files when I need to make space.1BlockerA native content blocker for macOS &amp; safari. Because it uses Apple’s content blocker API, it’s faster and has better privacy by design that other ad blockers. All the rules and filters sync with the iOS app as well.SketchA vector design app that is mostly used for user interfaces. Great for designers, but I also like prototyping in it, as it is sometimes faster and easier than messing with interface builder.PixelmatorMy favorite drop in Photoshop replacement. No subscription, fairly cheap (~$15), and fully native.Affinity DesignerI’ll admit I’m terrible with vectors. But this looks like a great app. I’ve tried it a few times…ScreensI use screens to connect to my work iMac when I need to work from home. And it does the job well. Syncs with the iOS app as well.TransmitDeveloped by Panic, one of my favorite software companies. This is definitely one of the best designed apps on the platform. A nearly perfect FTP/SFTP client. If you need to upload files of any sort to a web server, this is how to get it done.Sublime TextA nice text editor.PawPaw is terrific for simulating HTTP(S) requests to test the web services you’re developing, or play with public APIs. I use this everyday at work. Postman is the more well known cross platform alternative, but I like Paw way better.Sequel ProThe best way to the contents of your database. I use it during development, but I’m sure it could also be used in production. It’s fast, and just works.TowerThe best git client I have used on the Mac. Some things are still a little unwiedly and sometimes I’ll have to go back to command line git, but for most operations Tower gets the job done well. If you’re looking for a free solution, Source Tree is fine as well.",
            "content_html": "<p>Here’s a list of my favorite (third party) Mac apps.</p><h3 id=\"1password\"><a href=\"https://1password.com\">1Password</a></h3><p>1Password is a must app for anybody, in my opinion. It is an easy to use and well designed application that makes it super easy to manage unique passwords for all of your online accounts. It autofills in all browsers, and stores everything else important in your life like Pin codes, credit card numbers, etc. It also supports 2 factor auth codes. It’s a lifeline for me. And it syncs easily between all my devices. 5/5.</p><h3 id=\"day-one\"><a href=\"http://dayoneapp.com\">Day One</a></h3><p>Terrific journaling App. Makes it easy to log your life, and syncs with the iOS and watchOS apps as well. Really well designed, and makes it easy to get writing, format it how you like, and attach images or locations. I think journaling is really important, and Day One fills an important need. There are pros and cons to digital journling, but a few things I like:</p><ul>  <li>    <p>Easy tagging / searching.<br />It’s so easy to find old posts by tags, locations, date, etc. Much easier than searching through physical pages. It’s also fun to look at the different views such as the map with all your journal posts.</p>  </li>  <li>    <p>Password protection<br />You can use a passcode (or TouchID) to secure your journal. That way nobody, even with physical access can access my journal. And with end to end encryption, even if Day One servers are hacked, my data is safe. Since my journal contains my most personal thoughts, this is very important.</p>  </li>  <li>    <p>Backup<br />There are backups of my journal on Day One’s server, as well as my iCloud backup, Time Machine, Backblaze, etc. There’s little risk of me losing my journal with so many replications of it. With a physical journal, there’s only one.</p>  </li>  <li>    <p>Convienience<br />I can type out an entry whenever I want. Sometimes I’ll create a quick entry from my watch with the time and location, then fill it in later. Always accessible.</p>  </li></ul><p>I get the appeal of a physical journal, but for me, writing digitally is way easier. It’s personal preference. Also my hand would get so tired when I write long winded journals…</p><h3 id=\"iina\"><a href=\"https://github.com/lhc70000/iina\">IINA</a></h3><p>An up and coming open source video player. Plays nearly every format and the UI is more native and prettier than VLC. If you’re a old time VLC user because of format compatibility, give IINA a try.</p><h3 id=\"tweetbot\"><a href=\"https://tapbots.com/tweetbot/mac/\">Tweetbot</a></h3><p>A fantastic Twitter client for macOS. Way better than using the website if you’re a regular Twitter user. Has feature parity with the iOS version and the UI design is great.</p><h3 id=\"daisydisk\"><a href=\"https://daisydiskapp.com\">DaisyDisk</a></h3><p>My preferred way to see how the disk space on my mac is being used. Allows me to find and delete unneeded files when I need to make space.</p><h3 id=\"1blocker\"><a href=\"https://1blocker.com\">1Blocker</a></h3><p>A native content blocker for macOS &amp; safari. Because it uses Apple’s content blocker API, it’s faster and has better privacy by design that other ad blockers. All the rules and filters sync with the iOS app as well.</p><h3 id=\"sketch\"><a href=\"https://www.sketchapp.com\">Sketch</a></h3><p>A vector design app that is mostly used for user interfaces. Great for designers, but I also like prototyping in it, as it is sometimes faster and easier than messing with interface builder.</p><h3 id=\"pixelmator\"><a href=\"http://www.pixelmator.com\">Pixelmator</a></h3><p>My favorite drop in Photoshop replacement. No subscription, fairly cheap (~$15), and fully native.</p><h3 id=\"affinity-designer\"><a href=\"https://affinity.serif.com/en-us/\">Affinity Designer</a></h3><p>I’ll admit I’m terrible with vectors. But this looks like a great app. I’ve tried it a few times…</p><h3 id=\"screens\"><a href=\"https://edovia.com/screens-mac/\">Screens</a></h3><p>I use screens to connect to my work iMac when I need to work from home. And it does the job well. Syncs with the iOS app as well.</p><h3 id=\"transmit\"><a href=\"https://panic.com/transmit/\">Transmit</a></h3><p>Developed by Panic, one of my favorite software companies. This is definitely one of the best designed apps on the platform. A nearly perfect FTP/SFTP client. If you need to upload files of any sort to a web server, this is how to get it done.</p><h3 id=\"sublime-text\"><a href=\"https://www.sublimetext.com\">Sublime Text</a></h3><p>A nice text editor.</p><h3 id=\"paw\"><a href=\"https://paw.cloud\">Paw</a></h3><p>Paw is terrific for simulating HTTP(S) requests to test the web services you’re developing, or play with public APIs. I use this everyday at work. Postman is the more well known cross platform alternative, but I like Paw way better.</p><h3 id=\"sequel-pro\"><a href=\"https://www.sequelpro.com\">Sequel Pro</a></h3><p>The best way to the contents of your database. I use it during development, but I’m sure it could also be used in production. It’s fast, and just works.</p><h3 id=\"tower\"><a href=\"https://www.git-tower.com/mac/\">Tower</a></h3><p>The best git client I have used on the Mac. Some things are still a little unwiedly and sometimes I’ll have to go back to command line git, but for most operations Tower gets the job done well. If you’re looking for a free solution, Source Tree is fine as well.</p>",
            "url": "https://zekesnider.com/favorite-mac-apps/",
            "image": "macos.png",
            
            
            
            
            "date_published": "2017-08-31T04:15:40+00:00",
            "date_modified": "2017-08-31T04:15:40+00:00",
            
                "author":  {
                "name": "GitHub User",
                "url": null,
                "avatar": null
                }
                
            
        },
    
        {
            "id": "https://zekesnider.com/blogging-in-2017/",
            "title": "Blogging in 2017",
            "summary": null,
            "content_text": "I’ve always enjoyed blogging, but have been very lazy recently about getting around to it. And part of that is because I haven’t really had a good home for all my content. I’ve been using Medium because it’s easy and fast. The design is pretty good and has suited my needs. But I really want a place that’s my very own on the WWW. One that’s not owned by any corporation, that I have (almost) complete control over.I still think Medium is a great platform, and I will probably continue to cross-post my important pieces there. There’s still a lot of insightful content I read on Medium. However, I would like to have a place I completely manage and own, so that’s why we’re here. I think it’s important that the web doesn’t become too centralized on large services. I post enough of my content on Twitter, Instagram, etc. Some of it should be hosted by me.Also Medium has added dickbars and some other questionable design decisions that take away from the most import thing: content.Choosing a blogging engineWhen deciding what blogging engine to use, I was mainly debating between Wordpress and Ghost. Wordpress is a battle tested CMS that is used very widely. I’ve used it a lot in the past and have done some contracting work with it. Wordpress is great at what it does.But it does come with its fair share of bloat. And upkeep with installing security upgrades, patching PHP, managing the server, installing plugins, etc. And because of its wide range of functionality, I find that theming it can be overly complicated. I wanted to keep it simple for my blog, and I just thought that managing a Wordpress install and theme would be overkill.So I also took a look at Ghost, which is a new up and comer in the area. It’s built using Node.js and Express, which is a stack I am fond of. I spun up a $10 digital ocean droplet to take Ghost for a spin. I liked it, but again seemed overkill for what I wanted. Also it seems a lot of open source themes I was trying out weren’t working due to lack of updates for the latest versions of Ghost.Ghost overall seemed very promising, but again overkill. A lot of the themes I was looking at weren’t striking my fancy. And hosting it on Digital Ocean would incur additional costs over the shared hosting I’m already paying for at dreamhost.JekyllThis brings us to Jekyll, which is what I ended up going with. Jekyll is an awesome tool which allows you to generate a completely static site using Markdown. The templates are easily modifiable, and you have complete control over what goes into your site.Let me just say, I love Jekyll. I am a total convert. The default theme is awesome, minimal, and was easy to customize to my liking. I am very satisfied with how the look of my blog ended up. I wanted something minimal and easy to read, with nice colors, and a unique style so that it doesn’t look like a generic template.I am a huge promponent of Markdown, so the writing format is a huge plus for me. I want to write my posts in Markdown no matter what engine I’m using. I really like how Jekyll formats and organizes the project files as well.Stepping back down to a completely static site is actually very nice. The current WWW has so many sites with insane bloat, it’s nice to keep my site to a fast loading lean machine. Just content, that’s it. My site does not have a single line of JavaScript. Hopefully it can stay that way. No need for AMP here, just natively fast loading pages.Importing PostsI only had 4 posts on Medium, so this wasn’t a huge undertaking. I decided to do it manually because of how few posts I have. There’s no importer plugin that I know of. I would of written one myself if I had more posts, but it just wouldn’t be worth it with the number I have.In a few hours I was done. Most of my issues were relating to layout of images. Jekyll doesn’t have many images helpers built in, so I added some which mimick the responsive layout functionality of Medium’s image embeds. I still have some tweaking to do, but I think it’s pretty good for the most part. Because I implemented them with _includes, they will be easy to modify later.JSON FeedJSON Feed is a very new (May 2017) syndication format designed by Manton Reece and Brent Simmons. I really like the idea of it, and wanted to support it on my blog for the novelty of it if possible.Luckily, there is already an open source project which implements JSON feed for Jekyll. With some minor modifications, my JSON Feed was ready to go. You can check it out here! Even if there’s not much that parses JSON feed yet, I like the idea of supporting it.Open Graph / Twitter CardsI wanted to add the meta tags used by Twitter/Facebook to create rich previews. Also because they are used by iMessage, and I really wanted the previews to look nice in there. This was fairly easy to accomplish by modifying my header template. There are some great articles out there already which helped out a lot.With some minor modification and tweaking in Facebook’s open graph validator, my rich content previews were set.BeautifulHostingJekyll has awesome integration with Github Pages, which allows for free and easy hosting of your jekyll site with built in source control. This is a excellent option for most, especially considering the price (free) and ease of use.Unfortunately, Github Pages does not current support HTTPS for custom domains. They support HTTPS for *.github.io domains which is great, but I really want to host my site on my own domain (zekesnider.com). So I decided to just copy over the files to my Dreamhost shared hosting instance.I might set up a better automatic commit pull workflow in the future, but for now I just copy over the _site directory using Transmit. I already had a letsencrypt cert setup, so I customized the .htaccess file, and I was set! If GitHub adds support for HTTPS on custom domains in the future, I will probably switch over to that.I would like to improve the deployment workflow in the future, just so I can easily update my blog on the go from my iPad (or iPhone). Hoping this is possible with some combination of Working Copy, Coda, and better server tooling. This is a nice to have, hopefully the frequency of my postings will necessitate this in the future.AnalyticsI didn’t want any ad tracking JavaScript on my site at all. In fact, the site currently has no JavaScript on it all. But I also wanted to have a general access log so I know how many people are visiting my site.Dreamhost Site Statistics seemed like the best solution to this problem (considering it’s built into the hosting). I have yet to fully configure it, but I might do another post on this once I get some actionable data built up.ConclusionSo there you have it. With Jekyll I have a very fast loading, nice looking site with no ad tracking and 0 lines of JavaScript. It fits my needs very well, and I hope that I can keep the blogging habbit for this redesign to be worth my effort. Thanks for reading, and stay tuned for more! You can check out the source for my blog here if you like.",
            "content_html": "<p>I’ve always enjoyed blogging, but have been very lazy recently about getting around to it. And part of that is because I haven’t really had a good home for all my content. I’ve been using <a href=\"https://medium.com\">Medium</a> because it’s easy and fast. The design is pretty good and has suited my needs. But I really want a place that’s my very own on the WWW. One that’s not owned by any corporation, that I have (almost) complete control over.</p><p>I still think Medium is a great platform, and I will probably continue to cross-post my important pieces there. There’s still a lot of insightful content I read on Medium. However, I would like to have a place I completely manage and own, so that’s why we’re here. I think it’s important that the web doesn’t become too centralized on large services. I post enough of my content on Twitter, Instagram, etc. Some of it should be hosted by me.</p><p>Also Medium has added <a href=\"https://daringfireball.net/2017/06/medium_dickbars\">dickbars</a> and some other questionable design decisions that take away from the most import thing: content.</p><h1 id=\"choosing-a-blogging-engine\">Choosing a blogging engine</h1><p>When deciding what blogging engine to use, I was mainly debating between Wordpress and Ghost. Wordpress is a battle tested CMS that is used very widely. I’ve used it a lot in the past and have done some contracting work with it. Wordpress is great at what it does.</p><p>But it does come with its fair share of bloat. And upkeep with installing security upgrades, patching PHP, managing the server, installing plugins, etc. And because of its wide range of functionality, I find that theming it can be overly complicated. I wanted to keep it simple for my blog, and I just thought that managing a Wordpress install and theme would be overkill.</p><p>So I also took a look at Ghost, which is a new up and comer in the area. It’s built using Node.js and Express, which is a stack I am fond of. I spun up a $10 digital ocean droplet to take Ghost for a spin. I liked it, but again seemed overkill for what I wanted. Also it seems a lot of open source themes I was trying out weren’t working due to lack of updates for the latest versions of Ghost.</p><p>Ghost overall seemed very promising, but again overkill. A lot of the themes I was looking at weren’t striking my fancy. And hosting it on Digital Ocean would incur additional costs over the shared hosting I’m already paying for at dreamhost.</p><h1 id=\"jekyll\">Jekyll</h1><p>This brings us to Jekyll, which is what I ended up going with. Jekyll is an awesome tool which allows you to generate a completely static site using <a href=\"https://daringfireball.net/projects/markdown/syntax\">Markdown</a>. The templates are easily modifiable, and you have complete control over what goes into your site.</p><p>Let me just say, I <em>love</em> Jekyll. I am a total convert. The default theme is awesome, minimal, and was easy to customize to my liking. I am very satisfied with how the look of my blog ended up. I wanted something minimal and easy to read, with nice colors, and a unique style so that it doesn’t look like a generic template.</p><p>I am a huge promponent of Markdown, so the writing format is a huge plus for me. I want to write my posts in Markdown no matter what engine I’m using. I really like how Jekyll formats and organizes the project files as well.</p><p>Stepping back down to a completely static site is actually very nice. The current WWW has so many sites with insane bloat, it’s nice to keep my site to a fast loading lean machine. Just content, that’s it. My site does not have a single line of JavaScript. Hopefully it can stay that way. No need for AMP here, just natively fast loading pages.</p><h1 id=\"importing-posts\">Importing Posts</h1><p>I only had 4 posts on Medium, so this wasn’t a huge undertaking. I decided to do it manually because of how few posts I have. There’s no importer plugin that I know of. I would of written one myself if I had more posts, but it just wouldn’t be worth it with the number I have.</p><p>In a few hours I was done. Most of my issues were relating to layout of images. Jekyll doesn’t have many images helpers built in, so I added some which mimick the responsive layout functionality of Medium’s image embeds. I still have some tweaking to do, but I think it’s pretty good for the most part. Because I implemented them with _includes, they will be easy to modify later.</p><h1 id=\"json-feed\">JSON Feed</h1><p><a href=\"https://jsonfeed.org\">JSON Feed</a> is a very new (May 2017) syndication format designed by Manton Reece and Brent Simmons. I really like the idea of it, and wanted to support it on my blog for the novelty of it if possible.</p><p>Luckily, there is already an <a href=\"https://github.com/vallieres/jekyll-json-feed\">open source project</a> which implements JSON feed for Jekyll. With some minor modifications, my JSON Feed was ready to go. You can check it out <a href=\"/feed.json\">here</a>! Even if there’s not much that parses JSON feed yet, I like the idea of supporting it.</p><h1 id=\"open-graph--twitter-cards\">Open Graph / Twitter Cards</h1><p>I wanted to add the meta tags used by Twitter/Facebook to create rich previews. Also because they are used by iMessage, and I really wanted the previews to look nice in there. This was fairly easy to accomplish by modifying my header template. There are some <a href=\"http://davidensinger.com/2013/04/adding-open-graph-tags-to-jekyll/\">great</a> <a href=\"http://davidensinger.com/2013/04/supporting-twitter-cards-with-jekyll/\">articles</a> out there already which helped out a lot.</p><p>With some minor modification and tweaking in Facebook’s open graph validator, my rich content previews were set.</p><p><img class=\"fullwidthimg defaultimg\" src=\"/assets/RichiMessage.png\" alt=\"\" /><br /><span class=\"caption\">Beautiful</span></p><h1 id=\"hosting\">Hosting</h1><p>Jekyll has awesome integration with <a href=\"https://pages.github.com\">Github Pages</a>, which allows for free and easy hosting of your jekyll site with built in source control. This is a excellent option for most, especially considering the price (free) and ease of use.</p><p>Unfortunately, <a href=\"https://github.com/isaacs/github/issues/156\">Github Pages does not current support HTTPS for custom domains</a>. They support HTTPS for *.github.io domains which is great, but I really want to host my site on my own domain (zekesnider.com). So I decided to just copy over the files to my Dreamhost shared hosting instance.</p><p>I might set up a better automatic commit pull workflow in the future, but for now I just copy over the _site directory using Transmit. I already had a letsencrypt cert setup, so I customized the .htaccess file, and I was set! If GitHub adds support for HTTPS on custom domains in the future, I will probably switch over to that.</p><p>I would like to improve the deployment workflow in the future, just so I can easily update my blog on the go from my iPad (or iPhone). Hoping this is possible with some combination of Working Copy, Coda, and better server tooling. This is a nice to have, hopefully the frequency of my postings will necessitate this in the future.</p><h1 id=\"analytics\">Analytics</h1><p>I didn’t want any ad tracking JavaScript on my site at all. In fact, the site currently has no JavaScript on it all. But I also wanted to have a general access log so I know how many people are visiting my site.</p><p><a href=\"https://help.dreamhost.com/hc/en-us/articles/216510258-Panel-statistics-overview\">Dreamhost Site Statistics</a> seemed like the best solution to this problem (considering it’s built into the hosting). I have yet to fully configure it, but I might do another post on this once I get some actionable data built up.</p><h1 id=\"conclusion\">Conclusion</h1><p>So there you have it. With Jekyll I have a very fast loading, nice looking site with no ad tracking and 0 lines of JavaScript. It fits my needs very well, and I hope that I can keep the blogging habbit for this redesign to be worth my effort. Thanks for reading, and stay tuned for more! You can check out the source for my blog <a href=\"https://github.com/ZekeSnider/ZekeSniderDotCom\">here</a> if you like.</p>",
            "url": "https://zekesnider.com/blogging-in-2017/",
            "image": "jekyll.png",
            
            
            
            
            "date_published": "2017-08-19T05:15:40+00:00",
            "date_modified": "2017-08-19T05:15:40+00:00",
            
                "author":  {
                "name": "GitHub User",
                "url": null,
                "avatar": null
                }
                
            
        },
    
        {
            "id": "https://zekesnider.com/why-i-like-vinyl/",
            "title": "Why I Like Vinyl",
            "summary": null,
            "content_text": "Recently I, a 20 year old, have taken a liking to vinyl records. And not because it’s an old medium and I want to be a hipster. I am not “nostalgic” about CDs, casset tapes, or other antiquated mediums. There are solid reasons for why I specifically like this antiquated medium.The age of streaming\t  \tWhen I listen to music, 90% of the time I am streaming it. I use Apple Music and really enjoy the service. I’m not arguing that Vinyl is the best way to listen to music. The convenience and pricing of streaming services are king, and will stay that way. If anything, I would argue that streaming is the best platform for most consumers. It offers a the most flexibility, and tons of variety for a low price.And you can listen anytime, anywhere. Personally I have my whole library downloaded offline, which is a another plus of most streaming services. You can still have the benefit of an offline cache with the flexibility of an extremely large catalog.Physical collectionsAnd yet, even with streaming’s flexibility and ease of use, I still would like to have a physical collection of my very favorite albums. The albums that I love and could listen to literally hundreds of times without getting tired of them. I like the physical collection because it gives me a better connection with my media, rather than just a expose of album art in iTunes.When I buy games, I tend to buy the physical versions whenever possible for the same reason. The physical collection will also always be in my possession as long as it’s not damaged or lost. I’m not sure if Apple Music / Spotify / streaming service of your choice will still be active in 30 years. And, if you decide to stop paying for the streaming service, the music is not longer yours to keep.So, if I want a physical collection of my music, I’m left with 2 options for the most part: CD and vinyl. There are several problems with CD which prevent it from being a medium that I enjoy collecting.It is a digital format, with no connection to the music on disc. It’s boring, scratches easily, and you’re just going to import it to your computer and play from there for the most part. There is nothing unique or special about it. It exists because of the technology at the time of its inception.Also, the art is tiny, the jewel cases are usually bad quality and scratch/crack easily. It’s just not an appealing collector’s item. I do own lots of albums that are only on CD, and they are fine. But if it’s available on vinyl, because…HUGE ALBUM ARTIt puts CDs to shameThe fact that vinyl is a huge and awkward disc format means the outer sleeve is a huge print of the album artwork. This is AWESOME, and honestly probably my favorite thing about vinyl. I love album art and the vinyl sleeves are just a great way to display them. It’s cool to see my favorite albums jumbo size.They can also be hanged or displayed in different ways which is great.My signed copy of E•MO•TION: Side B framedThe analog connectionVinyl is not a superior format to digital (in my opinion at least). But still, the feeling you get dropping the needle on the record to start the album is absolutely great. It brings a sort of connection to the music that you don’t get when tapping the album on your phone.Just the fact that you’re playing from physical grooves in a disc that are recreating the audio of the song is great. Sure it’s clumsy and inconvenient to swap discs and they can only store a few minutes of content, but the analog connection is very special.Whole AlbumsIn the age of playlisting, singles, and random mixes, I think many people have really lost the value in listening to an album all the way through. I think it’s important to listen to an album all the way as its how the artist intended it. Each song also has its own connection to the other around it on the album that you don’t understand when listenting to singles.A album can have a great single on it, but that doesn’t make it a great album. Truly great artists deliver on all tracks on an album, even the ones that can’t top the charts. And I think it’s important to experience the whole collection of work that way. I’m not trying to argue an elitist viewpoint that you should always listen to albums all the way through, but I think cohesive albums are still important. I still enjoy playlisting, but albums are my favorite way to listen to music.Vinyl, by design kind of forces you to listen all the way through the album, because of the clumsiness of switching vinyls and finding where on the record a song starts. While it is annoying to have to switch records 4 times to finish an album, I still enjoy it.QualityMany people say that vinyl is the only way to listen to music because the quality is that music better. And to be honest, I don’t agree. There may be a difference in sound, but it’s hard to notice. I’m not an audiophile per say, so this is not a huge issue for me, it sounds consistent to digital to me, which is OK. It depends on your speaker/headphone set hooked up to your player as well.I will say that some records do sound different or “warmer” to me on vinyl, although this may depend on the album or it might just be placebo. Regardless, the sound from vinyl is definitely not a step down in my experience.“Pure Heroine” by Lorde is one of my favorite sounds on vinyl.You can play an album on your phone anytime, but there’s just something special about handling the vinyl record, placing it on the platter and starting it. It provides a connection with the music that doesn’t exist in other mediums.Vinyl is an antiquated, awkward to handle, and expensive format. But I still really like it.",
            "content_html": "<p>Recently I, a 20 year old, have taken a liking to vinyl records. And not because it’s an old medium and I want to be a hipster. I am not “nostalgic” about CDs, casset tapes, or other antiquated mediums. There are solid reasons for why I specifically like this antiquated medium.</p><h2 id=\"the-age-of-streaming\">The age of streaming</h2><div class=\"sidebyimagecontainer\">\t<img class=\"sidebyimage\" src=\"/assets/AppleMusicForYou.jpeg\" alt=\"\" />  \t<span class=\"caption\"></span></div><p>When I listen to music, 90% of the time I am streaming it. I use Apple Music and really enjoy the service. I’m not arguing that Vinyl is the best way to listen to music. The convenience and pricing of streaming services are king, and will stay that way. If anything, I would argue that streaming is the best platform for most consumers. It offers a the most flexibility, and tons of variety for a low price.And you can listen anytime, anywhere. Personally I have my whole library downloaded offline, which is a another plus of most streaming services. You can still have the benefit of an offline cache with the flexibility of an extremely large catalog.</p><h2 id=\"physical-collections\">Physical collections</h2><p>And yet, even with streaming’s flexibility and ease of use, I still would like to have a physical collection of my very favorite albums. The albums that I love and could listen to literally hundreds of times without getting tired of them. I like the physical collection because it gives me a better connection with my media, rather than just a expose of album art in iTunes.</p><p>When I buy games, I tend to buy the physical versions whenever possible for the same reason. The physical collection will also always be in my possession as long as it’s not damaged or lost. I’m not sure if Apple Music / Spotify / streaming service of your choice will still be active in 30 years. And, if you decide to stop paying for the streaming service, the music is not longer yours to keep.So, if I want a physical collection of my music, I’m left with 2 options for the most part: CD and vinyl. There are several problems with CD which prevent it from being a medium that I enjoy collecting.</p><p>It is a digital format, with no connection to the music on disc. It’s boring, scratches easily, and you’re just going to import it to your computer and play from there for the most part. There is nothing unique or special about it. It exists because of the technology at the time of its inception.</p><p>Also, the art is tiny, the jewel cases are usually bad quality and scratch/crack easily. It’s just not an appealing collector’s item. I do own lots of albums that are only on CD, and they are fine. But if it’s available on vinyl, because…</p><h2 id=\"huge-album-art\">HUGE ALBUM ART</h2><p><img class=\"fullwidthimg defaultimg\" src=\"/assets/1989Art.jpeg\" alt=\"\" /><br /><span class=\"caption\">It puts CDs to shame</span></p><p>The fact that vinyl is a huge and awkward disc format means the outer sleeve is a huge print of the album artwork. This is AWESOME, and honestly probably my favorite thing about vinyl. I love album art and the vinyl sleeves are just a great way to display them. It’s cool to see my favorite albums jumbo size.</p><p>They can also be hanged or displayed in different ways which is great.</p><p><img class=\"fullwidthimg defaultimg\" src=\"/assets/EmotionSideB.jpeg\" alt=\"\" /><br /><span class=\"caption\">My signed copy of E•MO•TION: Side B framed</span></p><h2 id=\"the-analog-connection\">The analog connection</h2><p>Vinyl is not a superior format to digital (in my opinion at least). But still, the feeling you get dropping the needle on the record to start the album is absolutely great. It brings a sort of connection to the music that you don’t get when tapping the album on your phone.Just the fact that you’re playing from physical grooves in a disc that are recreating the audio of the song is great. Sure it’s clumsy and inconvenient to swap discs and they can only store a few minutes of content, but the analog connection is very special.</p><h2 id=\"whole-albums\">Whole Albums</h2><p>In the age of playlisting, singles, and random mixes, I think many people have really lost the value in listening to an album all the way through. I think it’s important to listen to an album all the way as its how the artist intended it. Each song also has its own connection to the other around it on the album that you don’t understand when listenting to singles.</p><p>A album can have a great single on it, but that doesn’t make it a great album. Truly great artists deliver on all tracks on an album, even the ones that can’t top the charts. And I think it’s important to experience the whole collection of work that way. I’m not trying to argue an elitist viewpoint that you should always listen to albums all the way through, but I think cohesive albums are still important. I still enjoy playlisting, but albums are my favorite way to listen to music.</p><p>Vinyl, by design kind of forces you to listen all the way through the album, because of the clumsiness of switching vinyls and finding where on the record a song starts. While it is annoying to have to switch records 4 times to finish an album, I still enjoy it.</p><h2 id=\"quality\">Quality</h2><p>Many people say that vinyl is the only way to listen to music because the quality is that music better. And to be honest, I don’t agree. There may be a difference in sound, but it’s hard to notice. I’m not an audiophile per say, so this is not a huge issue for me, it sounds consistent to digital to me, which is OK. It depends on your speaker/headphone set hooked up to your player as well.</p><p>I will say that some records do sound different or “warmer” to me on vinyl, although this may depend on the album or it might just be placebo. Regardless, the sound from vinyl is definitely not a step down in my experience.</p><p><img class=\"fullwidthimg defaultimg\" src=\"/assets/PureHeroine.jpeg\" alt=\"\" /><br /><span class=\"caption\">“Pure Heroine” by Lorde is one of my favorite sounds on vinyl.</span></p><hr /><p>You can play an album on your phone anytime, but there’s just something special about handling the vinyl record, placing it on the platter and starting it. It provides a connection with the music that doesn’t exist in other mediums.Vinyl is an antiquated, awkward to handle, and expensive format. But I still really like it.</p>",
            "url": "https://zekesnider.com/why-i-like-vinyl/",
            "image": "spinningvinyl.jpg",
            
            
            
            
            "date_published": "2017-05-18T02:13:40+00:00",
            "date_modified": "2017-05-18T02:13:40+00:00",
            
                "author":  {
                "name": "GitHub User",
                "url": null,
                "avatar": null
                }
                
            
        },
    
        {
            "id": "https://zekesnider.com/using-an-ecobee-thermostat-with-a-millivolt-heating-system/",
            "title": "Using an Ecobee Thermostat with a Millivolt Heating System",
            "summary": null,
            "content_text": "We’ve always been using an old simple thermostat for use with the house’s stove which uses a millivolt connection. This is the only heating appliance in the house, and there’s no AC (perks of living in Washington). But recently, I decided that it was time to upgrade to a smart thermostat.\t  \tIt's working!I really wanted HomeKit support on the thermostat, so I ended up going with the Ecobee 3 Lite. It is a cheaper version of the Ecobee 3 that skips the remote sensors and some other premium features. I figured the lite would be perfect for my smaller use case, and because there’s only one source of heat so the remote sensors wouldn’t be all that useful.Unfortunately, the Ecobee (and Nest and most other competitors as well I believe) don’t support millivolt heaters natively. They only support the HVAC wiring system. Millivolt does not contain wiring for power, so that has to be managed as well. I was still determined install this thing, after some research on the internet and expirementation with electronics parts off Amazon, I was able to get it working!Now I’m just going to preface this with the fact that I’m a software engineer not a electrical engineer. So I’m not guarenteeing that everything here is 100% correct, just sharing the solution that worked for me. There was a few posts on forums and Q&amp;A sites giving some hints on how this works but I couldn’t find a comprehensive guide. So I figured I’d write one in case anyone else is trying to connect their Ecobee (or nest, etc.) to a millivolt stove.The Parts  24V Transformer  Relay  Electrical Wire Connection ScrewsAnd also you’ll need some wiring, which you may have laying around. Be sure to have enough slack for when you install it in the wall.Wiring DiagramA professional drawingPlease excuse the amateur iPad sketch, but this should give a good idea of how it’s wired up. Here’s a legend if any of the colors are ambiguous  Relay Orange -&gt; Heater  Relay Yellow -&gt; Heater  Relay White/Yellow -&gt; Ecobee W(1)  White / Blue joins Transformer ouput #1-&gt; Ecobee C  Transformer output output #2 -&gt; Ecobee Rc  Relay White / Black: unused  Relay Blue: unused\t  \tWired in.\t  \tConnections #1\t  \tConnections #2A note on the transfomers, originally we had tried using this Heath Zenith transformer but it only worked for about 5 minutes before exibiting some strange smells and overheating. We may have just received a faulty product, but we decided to just get one that plugs directly into the socket, then wire into the wall from there. A little more clunky, but it works for us. It’s easier because you don’t need to worry about mounting the transformer in the wall.Fortunately Amazon refunded us for the broken transformer, so not much harm done. Very very glad it did not damage the ecobee. These are the thrills of experimentation I suppose. The new transformer works like a charm.HomeKit access right from notification centerAfter getting it all set up, the Ecobee is very nice! We still haven’t properly mounted it in the wall, but we’re trying it out in the temporary setup until then. The homekit functionality is great and everything is working as expected. Overall I would recommend the Ecobee to anyone using a millivolt system or not. Hopefully this guide helped someone, let me know if you have any improvements / comments.",
            "content_html": "<p>We’ve always been using an old simple thermostat for use with the house’s stove which uses a millivolt connection. This is the only heating appliance in the house, and there’s no AC (perks of living in Washington). But recently, I decided that it was time to upgrade to a smart thermostat.</p><div class=\"sidebyimagecontainer\">\t<img class=\"sidebyimage\" src=\"/assets/WorkingEcobee.jpeg\" alt=\"It's working!\" />  \t<span class=\"caption\">It's working!</span></div><p>I really wanted HomeKit support on the thermostat, so I ended up going with the <a href=\"https://www.amazon.com/Ecobee3-Thermostat-Wi-Fi-Works-Amazon/dp/B01K48T09Y/ref=sr_1_2?ie=UTF8&amp;qid=1481598019&amp;sr=8-2&amp;keywords=ecobee\">Ecobee 3 Lite</a>. It is a cheaper version of the <a href=\"https://www.amazon.com/Ecobee3-Thermostat-Sensor-Generation-Amazon/dp/B00ZIRV39M/ref=sr_1_1?ie=UTF8&amp;qid=1481598019&amp;sr=8-1&amp;keywords=ecobee\">Ecobee 3</a> that skips the remote sensors and some other premium features. I figured the lite would be perfect for my smaller use case, and because there’s only one source of heat so the remote sensors wouldn’t be all that useful.</p><p>Unfortunately, the Ecobee (and Nest and most other competitors as well I believe) don’t support millivolt heaters natively. They only support the HVAC wiring system. Millivolt does not contain wiring for power, so that has to be managed as well. I was still determined install this thing, after some research on the internet and expirementation with electronics parts off Amazon, I was able to get it working!</p><p>Now I’m just going to preface this with the fact that I’m a software engineer not a electrical engineer. So I’m not guarenteeing that everything here is 100% correct, just sharing the solution that worked for me. There was a few posts on <a href=\"http://www.doityourself.com/forum/thermostatic-controls/465126-help-installing-nest-millivolt-system-using-24v-transformer.html#b\">forums</a> and <a href=\"http://diy.stackexchange.com/questions/69345/how-can-i-wire-my-wifi-thermostat-to-control-my-millivolt-fireplace\">Q&amp;A sites</a> giving some hints on how this works but I couldn’t find a comprehensive guide. So I figured I’d write one in case anyone else is trying to connect their Ecobee (or nest, etc.) to a millivolt stove.</p><hr /><h2 id=\"the-parts\">The Parts</h2><ul>  <li><a href=\"https://www.amazon.com/gp/product/B004VMVDTA/ref=oh_aui_detailpage_o03_s00?ie=UTF8&amp;psc=1\">24V Transformer</a></li>  <li><a href=\"https://www.amazon.com/gp/product/B000LESCI2/ref=oh_aui_detailpage_o06_s00?ie=UTF8&amp;psc=1\">Relay</a></li>  <li><a href=\"https://www.amazon.com/XtremepowerUS-Electrical-Connection-Connector-Assortment/dp/B00MI72RFY/ref=sr_1_1?ie=UTF8&amp;qid=1481598721&amp;sr=8-1&amp;keywords=electrical+wire+screw\">Electrical Wire Connection Screws</a></li></ul><p>And also you’ll need some wiring, which you may have laying around. Be sure to have enough slack for when you install it in the wall.</p><h2 id=\"wiring-diagram\">Wiring Diagram</h2><p><img class=\"fullwidthimg defaultimg\" src=\"/assets/EcobeeDiagram.png\" alt=\"\" /><br /><span class=\"caption\">A professional drawing</span></p><p>Please excuse the amateur iPad sketch, but this should give a good idea of how it’s wired up. Here’s a legend if any of the colors are ambiguous</p><ul>  <li>Relay Orange -&gt; Heater</li>  <li>Relay Yellow -&gt; Heater</li>  <li>Relay White/Yellow -&gt; Ecobee W(1)</li>  <li>White / Blue joins Transformer ouput #1-&gt; Ecobee C</li>  <li>Transformer output output #2 -&gt; Ecobee Rc</li>  <li>Relay White / Black: unused</li>  <li>Relay Blue: unused</li></ul><div class=\"sidebyimagecontainer\">\t<img class=\"sidebyimage\" src=\"/assets/EcobeeWiredIn.jpeg\" alt=\"Wired in.\" />  \t<span class=\"caption\">Wired in.</span></div><div class=\"sidebyimagecontainer\">\t<img class=\"sidebyimage\" src=\"/assets/EcobeeWired1.jpeg\" alt=\"Connections #1\" />  \t<span class=\"caption\">Connections #1</span></div><div class=\"sidebyimagecontainer\">\t<img class=\"sidebyimage\" src=\"/assets/EcobeeWired2.jpeg\" alt=\"Connections #2\" />  \t<span class=\"caption\">Connections #2</span></div><p>A note on the transfomers, originally we had tried using this <a href=\"https://www.amazon.com/gp/product/B000BQY88I/ref=oh_aui_detailpage_o06_s01?ie=UTF8&amp;psc=1\">Heath Zenith transformer</a> but it only worked for about 5 minutes before exibiting some strange smells and overheating. We may have just received a faulty product, but we decided to just get one that plugs directly into the socket, then wire into the wall from there. A little more clunky, but it works for us. It’s easier because you don’t need to worry about mounting the transformer in the wall.Fortunately Amazon refunded us for the broken transformer, so not much harm done. Very very glad it did not damage the ecobee. These are the thrills of experimentation I suppose. The new transformer works like a charm.</p><p><img class=\"fullwidthimg defaultimg\" src=\"/assets/EcobeeHomekit.png\" alt=\"\" /><br /><span class=\"caption\">HomeKit access right from notification center</span></p><p>After getting it all set up, the Ecobee is very nice! We still haven’t properly mounted it in the wall, but we’re trying it out in the temporary setup until then. The homekit functionality is great and everything is working as expected. Overall I would recommend the Ecobee to anyone using a millivolt system or not. Hopefully this guide helped someone, let me know if you have any improvements / comments.</p>",
            "url": "https://zekesnider.com/using-an-ecobee-thermostat-with-a-millivolt-heating-system/",
            "image": "ecobee.jpeg",
            
            
            
            
            "date_published": "2016-12-13T02:13:40+00:00",
            "date_modified": "2016-12-13T02:13:40+00:00",
            
                "author":  {
                "name": "GitHub User",
                "url": null,
                "avatar": null
                }
                
            
        },
    
        {
            "id": "https://zekesnider.com/project-diva-x-review/",
            "title": "Project Diva X Review",
            "summary": null,
            "content_text": "For those new to the series, X is the third project diva game on the Playstation Vita. The two others are f and F2nd, which are the same game for the most part with different song lists and minor changes. The basic gameplay is a rhythm game based around the playstation shape buttons, hitting the right notes in time with the music. You can watch a few gameplay videos or try to the demo to find out if it clicks for you or not pretty quickly.This review is written from the perspective of a fan of the series, who has platinumed both f and F 2nd. Prior to X’s release I was still frequently playing f and F 2nd, and often going to the arcade to play that version. X made a lot of changes to the classic formula, thus being called X and not F 3rd. I’m all for brining some new ideas to the series, but for the most part the changes made in X are step backs. There is a lot more reliance on equips and RNG luck over rhythm game skill. I think this is taking the series in a direction I don’t really want to see. So let’s get right into it…This game introduces a new game mode called “area quest” which replaces free play more for the most part. Free play still exists but it’s mostly an afterthought, you won’t be playing that mode if you want to progress on the game’s extremely long grind. Free play is required for 2 trophies but besides that, is not required for anything at all and there’s no way to progress on the game’s objectives in free play.X removes diva points, which are usually earned in songs and used to purchase modules, presents, accessories, etc. Instead everything in X is a random drop at the end of a song. Entirely random. Yes, this is awful.Roll the diceIf you clear the chance time section of a song, you are granted a random module. And the modules, which are normally cosmetic now serve functional purposes in area quest mode. And by no means are you guaranteed a new module every time. You’ll find once you get a few in your library you’ll frequently get the same modules again, and this is extremely frustrating if you want to go for 100% module completion of modules. In past games you were able to buy modules at will from the shop.Area quest mode is very different from normal free play, you have a voltage meter which raises the percentage of return on correct notes. There’s “voltage raise” notes outlined rarely which will raise it more than other notes. There’s no life bar which means you can’t fail by missing so many notes in the middle of a song, you just won’t pass the requirement at the end.The elementsAlso they’ve separated out the modules and songs into 5 groups: cute, cool, neutral, chaos &amp; beauty. If you equip a module and accessories of the matching element you can gain boosts of up to 60% on your score. If you don’t have the maximum 60% boost you’re SOL on some of the harder challenges. Which is making the game more about RNG and equips than actual skill.When I briefly tried playing Love Live! on iOS I was turned off because the game was not very skill based, I would still get rank C scores even with full combos in that game because I didn’t have the right cards. And that’s why I never got into it. I fear that this series is heading in that direction (possibly because it’s more profitable) which worries me.The module skills are a nice idea to make the game into an “RPG” but fail badly at being balanced or interesting at all. Once you get the right modules the only ones that are worthwhile are “new module up Lv4” which increases the chances of obtaining a new module, and “voltage note up Lv5” which increases the frequency of the notes that bump your voltage up. The lower level modules and other skills become entirely useless, and these 2 skills will be the only useful ones in your arsenal for the most part.The 60% bonusThey’ve also gave a “purpose” to the accessories which are just cosmetic accessories in previous games. However they fail to have any real purpose, as once you’ve obtained the best accessory combination for each element that grants the 40% boost there is no point to using any other accessories. I saved load outs for each of the magic combinations and never looked back. There’s a module skill for increasing accessory drop rates but it’s already easy to obtain all of them normally so that’s another useless skill.In the past diva games, there has been a “Diva Room” mode which is usually my least favorite part, but required to 100% it. You could give presents to the vocaloids and play games to raise their affection level. In X this mode is absolutely awful. They raised the max affection level for each vocaloid to 10 from 5, making it an INSANE grind to get max level which is required for several trophies. You can no longer play games with the vocaloids, you can only give presents. Now you can spam presents as much as you want but they give so little affection per present it will take forever to get to max level that way.The only way to effectively raise affection is fulfilling a vocaloid’s request for a present, for example if they want a something sweet you can give them a cookie which would increase the affection much more than regularly giving a cookie. This may not sound bad, however you cannot start a request at will, they are prompted randomly at the end of a song. There’s no rule or behavior for it, but it will randomly prompt you sometimes at the end of a song. Randomly. Some people online critisize the requests for being hard to understand if you don’t understand Japanse, but I won’t mark the game down for this because well, it’s in Japanese…Weebs can’t decipher what she wantsX also introduces another mode called “live quest” (not area quest), and medleys which are one of the few additions I actually like. They allow you to chain together several shortened versions of songs into one live show. You can create a custom mix of 3 of your favorites and play them as a show basically. This is quite cool but again suffers from the same equips and RNG of everything else in the game. One time I got a perfect (full combo) on a live quest and still failed because I didn’t have the proper module or accessory so my multiplier was not high enough. This is maddening.This game also only contains 24 songs (and 6 medleys). F has 44, F2nd has 40. This is a step back in the one major thing I care about in a rhythm game, the song collection. In addition, of those 24 songs, 20 feature Miku as the main singer and 22 have Miku in them. I like Miku but there should be more variety in vocaloids, as there’s 6 of them. Another disappointment, and there’s not even a Rin &amp; Len song in this game.Just a stageThe PVs in this game are also lackluster, they are all on a stage with no story. Past games featured full story scenes movies in the background of the song which are quite cool and tell the story of the song. In this game there is none of that, literally all of the songs are just dancing on a stage. I could excuse this if the song selection was wider, but in my opinion this is just a disappointment with only 24 songs with no quality PVs.Several other features such as skins, edit mode, and loading screen art are also gone. In F and F 2nd you could customize the GUI of the rhythm game with several art presets called skins. I never used this feature that much but there’s no reason to remove it and it’s no longer here. Also previous games had fan art on the loading screens which was nice to look at, this is replaced with a generic loading screen in X. And edit mode which allows you to create custom beat maps doesn’t exist anymore. No idea why all of these things were removed, probably just laziness.I guess I should also mention that they tried to shoehorn in a “story”, which I started skipping all the text of after the first half hour because it’s about as interesting as something a first grader would write for his homework assignment.Amazing and enthralling dialog!For completionists, this game is hell for grinding with all the random drops, and increased max level of 10 in the diva room. I am 85 hours in but still not close to getting the platinum. I should also mention that the area quest mode requires you to play each song with challenges after beating the 4 main difficulties (easy, normal, hard, extreme). These “challenges” do stupid stuff like make the notes waver, make the notes hidden, make the notes go super fast. This is just distracting and not fun, but to 100% the game you have to clear about 5 challenges on each song. The last of which is 3 modifiers on extreme difficulty, not fun at all.The game is fun normally, but how about a mode where you can’t see where the hell the notes are goingAnd to get the right random drops to obtain all the modules you’ll be playing songs over and over over and over and over again. By the end of your PSN platinum experience you will hate even your favorite songs from trying to get the correct module and item drops. Because you have no control over this, if your luck is bad you can be repeatedly screwed with nothing you can do. This is entirely personal opinion, but I also didn’t care for a lot of the songs in the game. In fact I hate a majority of the “chaos” songs. And the game really does force you to play ALL the songs many times for completion.Project Diva X tries to make some changes to the well tested formula to add RPG like elements and expand the game. But it fails badly on most of these ventures and creates something that is reliant on luck, equips, and other factors out of your control. It may be interesting enough to hold the attention of a casual player for a few hours, but for a seasoned veteran of the series, Project Diva X is a few steps in the wrong direction.Final Official IGN Score: 6/10",
            "content_html": "<p>For those new to the series, X is the third project diva game on the Playstation Vita. The two others are f and F2nd, which are the same game for the most part with different song lists and minor changes. The basic gameplay is a rhythm game based around the playstation shape buttons, hitting the right notes in time with the music. You can watch a few gameplay videos or try to the demo to find out if it clicks for you or not pretty quickly.</p><p>This review is written from the perspective of a fan of the series, who has platinumed both f and F 2nd. Prior to X’s release I was still frequently playing f and F 2nd, and often going to the arcade to play that version. X made a lot of changes to the classic formula, thus being called X and not F 3rd. I’m all for brining some new ideas to the series, but for the most part the changes made in X are step backs. There is a lot more reliance on equips and RNG luck over rhythm game skill. I think this is taking the series in a direction I don’t really want to see. So let’s get right into it…</p><p><img class=\"fullwidthimg defaultimg\" src=\"/assets/ProjectDivaXMenu.jpeg\" alt=\"\" /><br /><span class=\"caption\"></span></p><p>This game introduces a new game mode called “area quest” which replaces free play more for the most part. Free play still exists but it’s mostly an afterthought, you won’t be playing that mode if you want to progress on the game’s extremely long grind. Free play is required for 2 trophies but besides that, is not required for anything at all and there’s no way to progress on the game’s objectives in free play.X removes diva points, which are usually earned in songs and used to purchase modules, presents, accessories, etc. Instead everything in X is a random drop at the end of a song. Entirely random. Yes, this is awful.</p><p><img class=\"fullwidthimg defaultimg\" src=\"/assets/ProjectDivaXModuleDrop.png\" alt=\"\" /><br /><span class=\"caption\">Roll the dice</span></p><p>If you clear the chance time section of a song, you are granted a random module. And the modules, which are normally cosmetic now serve functional purposes in area quest mode. And by no means are you guaranteed a new module every time. You’ll find once you get a few in your library you’ll frequently get the same modules again, and this is extremely frustrating if you want to go for 100% module completion of modules. In past games you were able to buy modules at will from the shop.</p><p>Area quest mode is very different from normal free play, you have a voltage meter which raises the percentage of return on correct notes. There’s “voltage raise” notes outlined rarely which will raise it more than other notes. There’s no life bar which means you can’t fail by missing so many notes in the middle of a song, you just won’t pass the requirement at the end.</p><p><img class=\"fullwidthimg defaultimg\" src=\"/assets/ProjectDivaXElements.jpeg\" alt=\"\" /><br /><span class=\"caption\">The elements</span></p><p>Also they’ve separated out the modules and songs into 5 groups: cute, cool, neutral, chaos &amp; beauty. If you equip a module and accessories of the matching element you can gain boosts of up to 60% on your score. If you don’t have the maximum 60% boost you’re SOL on some of the harder challenges. Which is making the game more about RNG and equips than actual skill.</p><p>When I briefly tried playing Love Live! on iOS I was turned off because the game was not very skill based, I would still get rank C scores even with full combos in that game because I didn’t have the right cards. And that’s why I never got into it. I fear that this series is heading in that direction (possibly because it’s more profitable) which worries me.</p><p>The module skills are a nice idea to make the game into an “RPG” but fail badly at being balanced or interesting at all. Once you get the right modules the only ones that are worthwhile are “new module up Lv4” which increases the chances of obtaining a new module, and “voltage note up Lv5” which increases the frequency of the notes that bump your voltage up. The lower level modules and other skills become entirely useless, and these 2 skills will be the only useful ones in your arsenal for the most part.</p><p><img class=\"fullwidthimg defaultimg\" src=\"/assets/ProjectDivaXModules.jpeg\" alt=\"\" /><br /><span class=\"caption\">The 60% bonus</span></p><p>They’ve also gave a “purpose” to the accessories which are just cosmetic accessories in previous games. However they fail to have any real purpose, as once you’ve obtained the best accessory combination for each element that grants the 40% boost there is no point to using any other accessories. I saved load outs for each of the magic combinations and never looked back. There’s a module skill for increasing accessory drop rates but it’s already easy to obtain all of them normally so that’s another useless skill.</p><p>In the past diva games, there has been a “Diva Room” mode which is usually my least favorite part, but required to 100% it. You could give presents to the vocaloids and play games to raise their affection level. In X this mode is absolutely awful. They raised the max affection level for each vocaloid to 10 from 5, making it an INSANE grind to get max level which is required for several trophies. You can no longer play games with the vocaloids, you can only give presents. Now you can spam presents as much as you want but they give so little affection per present it will take forever to get to max level that way.</p><p>The only way to effectively raise affection is fulfilling a vocaloid’s request for a present, for example if they want a something sweet you can give them a cookie which would increase the affection much more than regularly giving a cookie. This may not sound bad, however you cannot start a request at will, they are prompted randomly at the end of a song. There’s no rule or behavior for it, but it will randomly prompt you sometimes at the end of a song. Randomly. Some people online critisize the requests for being hard to understand if you don’t understand Japanse, but I won’t mark the game down for this because well, it’s in Japanese…</p><p><img class=\"fullwidthimg defaultimg\" src=\"/assets/ProjectDivaXRequests.jpeg\" alt=\"\" /><br /><span class=\"caption\">Weebs can’t decipher what she wants</span></p><p>X also introduces another mode called “live quest” (not area quest), and medleys which are one of the few additions I actually like. They allow you to chain together several shortened versions of songs into one live show. You can create a custom mix of 3 of your favorites and play them as a show basically. This is quite cool but again suffers from the same equips and RNG of everything else in the game. One time I got a perfect (full combo) on a live quest and still failed because I didn’t have the proper module or accessory so my multiplier was not high enough. This is maddening.</p><p>This game also only contains 24 songs (and 6 medleys). F has 44, F2nd has 40. This is a step back in the one major thing I care about in a rhythm game, the song collection. In addition, of those 24 songs, 20 feature Miku as the main singer and 22 have Miku in them. I like Miku but there should be more variety in vocaloids, as there’s 6 of them. Another disappointment, and there’s not even a Rin &amp; Len song in this game.</p><p><img class=\"fullwidthimg defaultimg\" src=\"/assets/ProjectDivaXStage.jpeg\" alt=\"\" /><br /><span class=\"caption\">Just a stage</span></p><p>The PVs in this game are also lackluster, they are all on a stage with no story. Past games featured full story scenes movies in the background of the song which are quite cool and tell the story of the song. In this game there is none of that, literally all of the songs are just dancing on a stage. I could excuse this if the song selection was wider, but in my opinion this is just a disappointment with only 24 songs with no quality PVs.</p><p>Several other features such as skins, edit mode, and loading screen art are also gone. In F and F 2nd you could customize the GUI of the rhythm game with several art presets called skins. I never used this feature that much but there’s no reason to remove it and it’s no longer here. Also previous games had fan art on the loading screens which was nice to look at, this is replaced with a generic loading screen in X. And edit mode which allows you to create custom beat maps doesn’t exist anymore. No idea why all of these things were removed, probably just laziness.</p><p>I guess I should also mention that they tried to shoehorn in a “story”, which I started skipping all the text of after the first half hour because it’s about as interesting as something a first grader would write for his homework assignment.</p><p><img class=\"fullwidthimg defaultimg\" src=\"/assets/ProjectDivaXDialog.jpeg\" alt=\"\" /><br /><span class=\"caption\">Amazing and enthralling dialog!</span></p><p>For completionists, this game is hell for grinding with all the random drops, and increased max level of 10 in the diva room. I am 85 hours in but still not close to getting the platinum. I should also mention that the area quest mode requires you to play each song with challenges after beating the 4 main difficulties (easy, normal, hard, extreme). These “challenges” do stupid stuff like make the notes waver, make the notes hidden, make the notes go super fast. This is just distracting and not fun, but to 100% the game you have to clear about 5 challenges on each song. The last of which is 3 modifiers on extreme difficulty, not fun at all.</p><iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/PyS_3hiu-ZU\" frameborder=\"0\" allowfullscreen=\"\"></iframe><p>The game is fun normally, but how about a mode where you can’t see where the hell the notes are going</p><p>And to get the right random drops to obtain all the modules you’ll be playing songs over and over over and over and over again. By the end of your PSN platinum experience you will hate even your favorite songs from trying to get the correct module and item drops. Because you have no control over this, if your luck is bad you can be repeatedly screwed with nothing you can do. This is entirely personal opinion, but I also didn’t care for a lot of the songs in the game. In fact I hate a majority of the “chaos” songs. And the game really does force you to play ALL the songs many times for completion.</p><p>Project Diva X tries to make some changes to the well tested formula to add RPG like elements and expand the game. But it fails badly on most of these ventures and creates something that is reliant on luck, equips, and other factors out of your control. It may be interesting enough to hold the attention of a casual player for a few hours, but for a seasoned veteran of the series, Project Diva X is a few steps in the wrong direction.</p><h1 id=\"final-official-ign-score-610\">Final Official IGN Score: 6/10</h1>",
            "url": "https://zekesnider.com/project-diva-x-review/",
            "image": "ProjectDivaBox.jpeg",
            
            
            
            
            "date_published": "2016-07-06T02:13:40+00:00",
            "date_modified": "2016-07-06T02:13:40+00:00",
            
                "author":  {
                "name": "GitHub User",
                "url": null,
                "avatar": null
                }
                
            
        },
    
        {
            "id": "https://zekesnider.com/applying-to-the-wwdc-scholarship/",
            "title": "Applying to the WWDC scholarship",
            "summary": null,
            "content_text": "I submitted my application to Apple’s WWDC 2016 student scholarship 2 days ago. It seems like every year I really want to do this, but this year I finally did. It feels like a great accomplishment, even if I don’t win. I’ve been learning Swift ever since The Swift Programming Language book dropped at WWDC 2014, but I’ve always struggled with Cocoa frameworks because there’s just so much there to learn.Recently, I’ve become somewhat bored with working on web projects in my free time. Possibly because I’m a web developer by occupation now, or maybe I’m just getting tired with the platform. But either way, I figured the best way to hit the ground running on learning iOS development would be to just build an app and fill in the gaps as I went. This brings us to my app, titled Z Split.Z SplitI started on this project in January, before even considering that I could submit this for the scholarship. It’s a split timer, in the vein of WSplit, Llainfair, and LiveSplit. There are plenty of split timers out there, but no really great ones on iOS or OS X. This was a gap I wanted to fill.I wanted to make a split timer that is elegant, and makes use of native APIs and specifics to the platform. I used Autolayout, Core Data, 3D Touch, Watch Kit, UIKeyCommand, among other technologies.\t  \tCurrent Run ViewI worked with my dad on the design for the app. We decided to go with a dark theme for the app to start out with. I usually prefer dark theme in apps (Tweetbot for one). A light version is definitely coming at some point in the future. Hoping system wide dark mode is coming in iOS 10!In March I attended the try! Swift conference in Tokyo. There were lots of great speakers there, and I learned a lot of great tips there. One of which: protocols/extensions in Swift are really great!I used protocols in my code to simplify two things: gradients on my custom UITableViewCells, and simple CoreData tables. They allowed me to save on repetitive code and slim down my View Controllers so they don’t become gigantic all encompassing monsters.One of the hardest parts of developing the app was probably getting everything with Core Data working properly. It’s a pretty great framework, but the learning curve is quite high for a newbie like myself. And there’s a lot of tricky “gotchas” to manage with a persistence system. For example, at one point my app was taking seconds to load in each view because loading the split/route images from the core data store was blocking the UI thread. And I haven’t even gotten into the more advanced features of background thread management yet.Z Split for Apple WatchDeveloping the Apple Watch extension app was also an interesting experience. I wanted to make it as simple as possible, because otherwise you’ll honestly just reach for your phone because of the loading times on third party apps on the watch. Currently it just acts as a quick status indicator for the status of a run and you can preform actions such as Split, Pause, etc by the force touch menu. In the future I may consider adding more independent functionality to the watch app. But for now, I think it works quite well (as long as your phone is nearby).Git commit summaryThis might not be the best idicator of activity since it includes library commits (I used 1 cocoapod, RSKImageCropper) and Storyboards which rack up line addition/deletions. But it gives a general idea of my activity on the project over time. Even though this project wasn’t conceived after the scholarship was announced, the past 2 weeks were still definitely a giant cram session to get things done on time.&lt;img class=”notfullwidthimage” src=”/assets/ZSplitIcon.png” alt=”Z Split app icon, Z on top of a clock background”  Z Split’s amazing app iconFuture DevelopmentI plan to continue development of Z Split, and submit it to the App Store in the coming weeks. There’s tons of features that I want to add, cramming over the scholarship period basically got me to the minimum required featured set. Additional functionality I want to add includes:  iCloud Sync  Handoff support  More functionality on the Apple Watch app  Sharing functionality for runs  Ability to edit routes after they are created (duplication functionality if you want to change the ordering)  Maps integration for automatic splitting for location based activities  Polish the UX, add some sounds and fancy animations to make things more personable  Add more run control buttons such as undo, skip.  More statistics on the run page. Gold splits, comparisons to best segments, possible time save, delta time save over last segment, etc. Lots to do here.  More advanced statistics on saved run histories. There’s lots of interesting information that could be generated based on previous runs because it’s stored in Core Data. Just gotta write some queries.  Code refactoring  And eventually… An OS X version! (Fingers crossed for UXKit coming to the mac)Also I have a quite a few other ideas for apps to work on, and I will be open sourcing a OS X app I’ve been working on quite soon. Stay tuned. 😉Even if I don’t win the scholarship, this was still an amazing experience, it was a great way to get me motivated!",
            "content_html": "<p>I submitted my application to Apple’s WWDC 2016 student scholarship 2 days ago. It seems like every year I really want to do this, but this year I finally did. It feels like a great accomplishment, even if I don’t win. I’ve been learning Swift ever since The <a href=\"https://itunes.apple.com/us/book/swift-programming-language/id881256329?mt=11\">Swift Programming Language book</a> dropped at WWDC 2014, but I’ve always struggled with Cocoa frameworks because there’s just so much there to learn.</p><p>Recently, I’ve become somewhat bored with working on web projects in my free time. Possibly because I’m a web developer by occupation now, or maybe I’m just getting tired with the platform. But either way, I figured the best way to hit the ground running on learning iOS development would be to just build an app and fill in the gaps as I went. This brings us to my app, titled Z Split.</p><h1 id=\"z-split\">Z Split</h1><p>I started on this project in January, before even considering that I could submit this for the scholarship. It’s a split timer, in the vein of <a href=\"http://www.speedrunslive.com/tools/\">WSplit</a>, <a href=\"http://jenmaarai.com/llanfair/en/\">Llainfair</a>, and <a href=\"http://livesplit.org/\">LiveSplit</a>. There are plenty of split timers out there, but no really great ones on iOS or OS X. This was a gap I wanted to fill.</p><p>I wanted to make a split timer that is elegant, and makes use of native APIs and specifics to the platform. I used Autolayout, Core Data, 3D Touch, Watch Kit, UIKeyCommand, among other technologies.</p><div class=\"sidebyimagecontainer\">\t<img class=\"sidebyimage\" src=\"/assets/ZSplitCurrentView.jpeg\" alt=\"Current Run View\" />  \t<span class=\"caption\">Current Run View</span></div><p>I worked with my <a href=\"https://twitter.com/the_big_cor\">dad</a> on the design for the app. We decided to go with a dark theme for the app to start out with. I usually prefer dark theme in apps (Tweetbot for one). A light version is definitely coming at some point in the future. Hoping system wide dark mode is coming in iOS 10!</p><p>In March I attended the <a href=\"http://www.tryswiftconf.com/en\">try! Swift conference in Tokyo</a>. There were lots of great speakers there, and I learned a lot of great tips there. One of which: protocols/extensions in Swift are really great!</p><p>I used protocols in my code to simplify two things: gradients on my custom UITableViewCells, and simple CoreData tables. They allowed me to save on repetitive code and slim down my View Controllers so they don’t become gigantic all encompassing monsters.</p><p>One of the hardest parts of developing the app was probably getting everything with Core Data working properly. It’s a pretty great framework, but the learning curve is quite high for a newbie like myself. And there’s a lot of tricky “gotchas” to manage with a persistence system. For example, at one point my app was taking seconds to load in each view because loading the split/route images from the core data store was blocking the UI thread. And I haven’t even gotten into the more advanced features of background thread management yet.</p><p><img class=\"fullwidthimg defaultimg\" src=\"/assets/ZSplitAppleWatch.jpeg\" alt=\"\" /><br /><span class=\"caption\">Z Split for Apple Watch</span></p><p>Developing the Apple Watch extension app was also an interesting experience. I wanted to make it as simple as possible, because otherwise you’ll honestly just reach for your phone because of the loading times on third party apps on the watch. Currently it just acts as a quick status indicator for the status of a run and you can preform actions such as Split, Pause, etc by the force touch menu. In the future I may consider adding more independent functionality to the watch app. But for now, I think it works quite well (as long as your phone is nearby).</p><p><img class=\"fullwidthimg defaultimg\" src=\"/assets/ZSplitCommitHistory.png\" alt=\"\" /><br /><span class=\"caption\">Git commit summary</span></p><p>This might not be the best idicator of activity since it includes library commits (I used 1 cocoapod, RSKImageCropper) and Storyboards which rack up line addition/deletions. But it gives a general idea of my activity on the project over time. Even though this project wasn’t conceived after the scholarship was announced, the past 2 weeks were still definitely a giant cram session to get things done on time.</p><p>&lt;img class=”notfullwidthimage” src=”/assets/ZSplitIcon.png” alt=”Z Split app icon, Z on top of a clock background”</p><blockquote>  <p><span class=\"caption\">Z Split’s amazing app icon</span></p></blockquote><h1 id=\"future-development\">Future Development</h1><p>I plan to continue development of Z Split, and submit it to the App Store in the coming weeks. There’s tons of features that I want to add, cramming over the scholarship period basically got me to the minimum required featured set. Additional functionality I want to add includes:</p><ul>  <li>iCloud Sync</li>  <li>Handoff support</li>  <li>More functionality on the Apple Watch app</li>  <li>Sharing functionality for runs</li>  <li>Ability to edit routes after they are created (duplication functionality if you want to change the ordering)</li>  <li>Maps integration for automatic splitting for location based activities</li>  <li>Polish the UX, add some sounds and fancy animations to make things more personable</li>  <li>Add more run control buttons such as undo, skip.</li>  <li>More statistics on the run page. Gold splits, comparisons to best segments, possible time save, delta time save over last segment, etc. Lots to do here.</li>  <li>More advanced statistics on saved run histories. There’s lots of interesting information that could be generated based on previous runs because it’s stored in Core Data. Just gotta write some queries.</li>  <li>Code refactoring</li>  <li>And eventually… An OS X version! (Fingers crossed for UXKit coming to the mac)</li></ul><p>Also I have a quite a few other ideas for apps to work on, and I will be open sourcing a OS X app I’ve been working on quite soon. Stay tuned. 😉</p><p>Even if I don’t win the scholarship, this was still an amazing experience, it was a great way to get me motivated!</p>",
            "url": "https://zekesnider.com/applying-to-the-wwdc-scholarship/",
            "image": "wwdc-submission.png",
            
            
            
            
            "date_published": "2016-05-03T02:13:40+00:00",
            "date_modified": "2016-05-03T02:13:40+00:00",
            
                "author":  {
                "name": "GitHub User",
                "url": null,
                "avatar": null
                }
                
            
        }
    
    ]
}