<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Toni Granados&apos; Site</title><description>This is my personal blog where I write about code, UI exploration and whatever is on my mind.</description><link>https://tngranados.com/</link><item><title>New tool: `newest-files`</title><link>https://tngranados.com/blog/newest-files/</link><guid isPermaLink="true">https://tngranados.com/blog/newest-files/</guid><description>I&apos;ve released a new tool to help AI agents (and you) find the newest files in a git repo</description><pubDate>Fri, 28 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;AI agents have gotten very good as of late and they&apos;ve completely transformed how I work. There are still many things they don&apos;t do that well, but for certain things, they excel. One of those things they are great at is applying an existing pattern to a new thing.&lt;/p&gt;
&lt;p&gt;This kind of task is easily accomplished by providing the example in the context for the LLM, but sometimes finding that example might be tricky, especially for big, old, established codebases in which there might be multiple ways of achieving the same thing, including old or deprecated code.&lt;/p&gt;
&lt;p&gt;In big repos, the newest files often represent the most relevant patterns, conventions, and ideas. That’s exactly what an AI model should imitate.&lt;/p&gt;
&lt;p&gt;To help with this, I&apos;ve built &lt;code&gt;newest-files&lt;/code&gt;, a simple CLI tool that prints the list of the newest files in a git repo. You can use a glob to filter the files and even see the commit and PR where they were introduced, to gather even more context on how they were built in the first place.&lt;/p&gt;
&lt;p&gt;Agents can call this tool before generating code, pulling in real, relevant and up-to-date examples from your repo instead of relying on guesses. It helps them enhance their context on their own and do what they do best: &lt;strong&gt;context-aware copy-paste&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The tool is available in &lt;a href=&quot;https://brew.sh&quot;&gt;Homebrew&lt;/a&gt;, so you can install it by just running:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;brew tap tngranados/newest-files
brew install newest-files
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or check out the &lt;a href=&quot;https://github.com/tngranados/newest-files&quot;&gt;GitHub repo&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I&apos;ve also included some instructions explaining how to use the tool and its purpose that you can feed directly into your AI agent. Check them out &lt;a href=&quot;https://github.com/tngranados/newest-files/blob/main/llms.txt&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here is an example using it in the &lt;a href=&quot;https://github.com/discourse/discourse&quot;&gt;Discourse&lt;/a&gt; repo:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ newest-files &apos;app/jobs/**/*.rb&apos;

Created          │ Author               │ File                                                       │ Commit
───────────────────────────────────────────────────────────────────────────────────────────────────────────────
2025-10-09 00:33 │ Natalie Tay          │ app/jobs/regular/process_localized_cooked.rb               │ 38e64200
2025-10-03 16:14 │ Ted Johansson        │ app/jobs/scheduled/notify_admins_of_problems.rb            │ 911502eb
2025-08-21 14:18 │ Ted Johansson        │ app/jobs/scheduled/clear_expired_impersonations.rb         │ b24a3d81
2025-08-20 12:55 │ Juan David Martínez… │ app/jobs/regular/site_setting_update_default_categories.rb │ 0d721b47
2025-08-20 12:55 │ Juan David Martínez… │ app/jobs/regular/site_setting_update_default_tags.rb       │ 0d721b47
2025-07-23 11:58 │ Blake Erickson       │ app/jobs/regular/check_video_conversion_status.rb          │ af3abb54
2025-07-23 11:58 │ Blake Erickson       │ app/jobs/regular/convert_video.rb                          │ af3abb54
2025-02-11 10:26 │ Bianca Nenciu        │ app/jobs/scheduled/calculate_scores.rb                     │ 87f88459
2025-02-11 10:26 │ Bianca Nenciu        │ app/jobs/scheduled/clean_up_bookmarks.rb                   │ 87f88459
2025-02-11 10:26 │ Bianca Nenciu        │ app/jobs/scheduled/clean_up_drafts.rb                      │ 87f88459

Showing 10 newest files matching &apos;app/jobs/**/*.rb&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In one glance, you get the newest patterns for jobs, super helpful when adding a new one or showing an AI agent how they should look today, not five years ago.&lt;/p&gt;
&lt;p&gt;This is the first time I release a tool I built for myself to the public, but I&apos;ve been using it for a while now with great success and thought it might be useful for others. Hopefully you find it useful too.&lt;/p&gt;
&lt;p&gt;If you try it, I’d love feedback or ideas for improvements.&lt;/p&gt;
</content:encoded></item><item><title>Pull Down Drawer in SwiftUI</title><link>https://tngranados.com/blog/pull-down-drawer-swiftui/</link><guid isPermaLink="true">https://tngranados.com/blog/pull-down-drawer-swiftui/</guid><description>Creating a fluid, great felling pull down drawer using only SwiftUI with interactive, cancellable animations and haptic feedback.</description><pubDate>Fri, 11 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I&apos;m exploring how far can we go using SwiftUI to build components that follows the directives set by the great &lt;a href=&quot;https://developer.apple.com/wwdc18/803&quot;&gt;Designing Fluid Interfaces&lt;/a&gt; presentation from WWDC18 (if you haven&apos;t seen it, go ahead, it as relevant now as it was then).&lt;/p&gt;
&lt;p&gt;My first experiment is this drawer or sheet that pulls down from the top.&lt;/p&gt;
&lt;p&gt;&amp;lt;div class=&quot;flex justify-center&quot;&amp;gt;
&amp;lt;video autoplay loop muted playsinline poster=&quot;/videos/pull-down-drawer.webp&quot;&amp;gt;
&amp;lt;source src=&quot;/videos/pull-down-drawer.mp4&quot; type=&quot;video/mp4&quot; /&amp;gt;
&amp;lt;source src=&quot;/videos/pull-down-drawer.webm&quot; type=&quot;video/webm&quot; /&amp;gt;
Your browser does not support the video tag.
&amp;lt;/video&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;It&apos;s very simple, yet feels great to the touch.&lt;/p&gt;
&lt;p&gt;The full view looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import SwiftUI

struct PullDownDrawer: View {
    // Define constants for different heights and the threshold for state change
    let closedHeight: CGFloat = 100
    let openHeight: CGFloat = 500
    let stateChangeThreshold = 0.3

    // Track the current height and the height when gesture is inactive
    @State private var currentHeight: CGFloat = 100
    @State private var idleHeight: CGFloat = 100

    // Calculate the normalized height (0 to 1)
    private var normalizedHeight: CGFloat { (currentHeight - closedHeight) / (openHeight - closedHeight) }
    private var isOpen: Bool { idleHeight == openHeight }

    // Track if the state change will occur during the gesture
    var wouldChangeState: Bool {
        // Dragging up when it&apos;s open
        (isOpen &amp;amp;&amp;amp; normalizedHeight &amp;lt; 1 - stateChangeThreshold) ||
        // Dragging down when it&apos;s closed
        (!isOpen &amp;amp;&amp;amp; normalizedHeight &amp;gt; stateChangeThreshold) ||
        // The drawer can also be closed when dragging past the maximum height, kinda like closing a blind
        (isOpen &amp;amp;&amp;amp; normalizedHeight &amp;gt; 1 + stateChangeThreshold)
    }
    @State private var willChangeState = false

    // Define the drag gesture
    var gesture: some Gesture {
        DragGesture()
            .onChanged { value in
                let newHeight = idleHeight + value.translation.height

                if newHeight &amp;gt; openHeight {
                    // When dragging the drawer lower than the maximum height, simulate an increasing drag by reducing
                    // the extra height that we are applying with the drag
                    currentHeight = min(openHeight + pow(abs(newHeight - openHeight), 0.9), openHeight * 1.5)
                } else if newHeight &amp;lt; closedHeight {
                    // Similarly, increase drag when trying dragging below the minimum height
                    currentHeight = max(closedHeight - pow(abs(newHeight + closedHeight), 0.6), closedHeight / 1.65)
                } else {
                    currentHeight = newHeight
                }

                // Generate feedback when passing the state change threshold. The willChangeState flag is used to prevent
                // feedback from being generated multiple times.
                if wouldChangeState {
                    if !willChangeState {
                        UIImpactFeedbackGenerator(style: .light).impactOccurred()
                        willChangeState = true
                    }
                } else {
                    willChangeState = false
                }
            }
            .onEnded { value in
                // Using a bouncy animation, just change the height to the open or close height depending on the current
                // state and whether wouldChangeState is true. If changing state, we generate a heavier feedback that
                // feels great next to the lighter one used to indicate that the drawer were going to change state when
                // releasing the gesture, this one feels like a confirmation of the action
                withAnimation(.bouncy) {
                    if wouldChangeState {
                        currentHeight = isOpen ? closedHeight : openHeight
                        UIImpactFeedbackGenerator(style: .medium).impactOccurred()
                    } else {
                        currentHeight = isOpen ? openHeight : closedHeight
                    }

                    idleHeight = currentHeight
                }
            }
    }

    var body: some View {
        BackgroundView()
            .overlay(alignment: .top) {
                RoundedRectangle(cornerRadius: 18)
                    .fill(.ultraThinMaterial)
                    .ignoresSafeArea(edges: .top)
                    .frame(height: currentHeight)

                    .preferredColorScheme(.dark)
                    .gesture(gesture)
            }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Everything is annotated in the code, but as a summary, what we are doing here is taking a &lt;a href=&quot;https://developer.apple.com/documentation/swiftui/draggesture&quot;&gt;&lt;code&gt;DragGesture&lt;/code&gt;&lt;/a&gt; and use it&apos;s &lt;code&gt;translation.height&lt;/code&gt; to update the height of the drawer, which is just a view overlayed over a the background view.&lt;/p&gt;
&lt;p&gt;What makes it feels fluid and great to the touch is a combination of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Being an interactive animation that you can control with your fingers&lt;/li&gt;
&lt;li&gt;The haptic feedback that gives you an indication of what&apos;s happening&lt;/li&gt;
&lt;li&gt;The spring animation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Overall, I&apos;m a bit surprised how easy this was to make and I&apos;m eager to keep exploring new interactions based on the &lt;a href=&quot;https://developer.apple.com/wwdc18/803&quot;&gt;Designing Fluid Interfaces&lt;/a&gt; presentation.&lt;/p&gt;
&lt;p&gt;PS: The &lt;code&gt;BackgroundView&lt;/code&gt; with the animated gradients used in this example is made using a Metal shader. Apple introduced a &lt;a href=&quot;https://developer.apple.com/documentation/swiftui/view/coloreffect(_:isenabled:)&quot;&gt;simpler way&lt;/a&gt; to use Metal shaders within SwiftUI. I&apos;m exploring shaders right now as well and I might write a post about that in the future.&lt;/p&gt;
</content:encoded></item><item><title>Testing SwiftUI&apos;s ViewModels</title><link>https://tngranados.com/blog/testing-swiftui-viewmodel/</link><guid isPermaLink="true">https://tngranados.com/blog/testing-swiftui-viewmodel/</guid><description>Using Combine&apos;s Publishers, you can write tests that wait for changes in an ObservableObject. I&apos;ll show you a XCTestCase extension that will streamline the process and simplify your unit tests.</description><pubDate>Mon, 26 Jun 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When it comes to testing &lt;code&gt;ObservableObject&lt;/code&gt;s, we can leverage their capabilities to observe when a &lt;code&gt;@Published&lt;/code&gt; property changes. This comes in handy during testing as we can suspend the test and wait until the property assumes the value we are expecting.&lt;/p&gt;
&lt;p&gt;To do this, we use the &lt;code&gt;Combine&lt;/code&gt; &lt;code&gt;Publisher&lt;/code&gt; that &lt;code&gt;@Published&lt;/code&gt; adds to the property. We subscribe to it and test whether the new values of the property satisfy a given condition. Once the condition is met, we can fulfill an Expectation that we had set previously.&lt;/p&gt;
&lt;p&gt;I&apos;ve written an extension to &lt;code&gt;XCTestCase&lt;/code&gt; that introduces a couple of functions to simplify this process:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@MainActor
extension XCTestCase {
    func until&amp;lt;T&amp;gt;(
        _ publisher: T,
        satisfies conditions: [@Sendable (T.Output) -&amp;gt; Bool],
        timeout: TimeInterval = 1
    ) async where T: Publisher {
        let expectation = XCTestExpectation()
        var mutableConditions = conditions

        let cancellable = publisher.sink { _ in } receiveValue: { value in
            if mutableConditions.first?(value) ?? false {
                _ = mutableConditions.remove(at: 0)
                if mutableConditions.isEmpty {
                    expectation.fulfill()
                }
            }
        }

        await fulfillment(of: [expectation], timeout: timeout)
        cancellable.cancel()
    }

    func until&amp;lt;T&amp;gt;(
        _ publisher: T,
        satisfies condition: @escaping @Sendable (T.Output) -&amp;gt; Bool,
        timeout: TimeInterval = 1
    ) async where T: Publisher {
        await until(publisher, satisfies: [condition], timeout: timeout)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To use them, you just need to make your test async and await the functions to finish:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    func testViewModel() async {
        let viewModel = ViewModel()

        let task = Task { await viewModel.addItem() }

        // Check that `isLoading` is updating
        await until(viewModel.$isLoading) { $0 == true}
        XCTAssertTrue(viewModel.isLoading)

        // Wait until task finishes and `isLoading` is back to false
        await task.value
        await until(viewModel.$isLoading) { $0 == false}

        XCTAssertEqual(viewModel.items.count, 1)
        XCTAssertFalse(viewModel.isLoading)
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For reference, this is the &lt;code&gt;ViewModel&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class ViewModel: ObservableObject {
    @Published var items: [Int] = []
    @Published var isLoading: Bool = false

    func addItem() async {
        self.isLoading = true

        try! await Task.sleep(for: .seconds(1))

        self.items.append(self.items.count + 1)
        self.isLoading = false
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Using this techniques, you can easily and consistently test very common SwiftUI patterns. I hope you find them useful. Happy testing!&lt;/p&gt;
</content:encoded></item><item><title>Keep your Mac awake when using Claude Code</title><link>https://tngranados.com/blog/preventing-mac-sleep-claude-code/</link><guid isPermaLink="true">https://tngranados.com/blog/preventing-mac-sleep-claude-code/</guid><description>Prevent Mac from going to sleep while Claude Code is doing its thing, with just a couple of simple hooks</description><pubDate>Sun, 20 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;It&apos;s been a while since I last posted here, but it&apos;s as good a time as any to pick this back up. Today, I want to share a handy tip for those of you using Claude Code.&lt;/p&gt;
&lt;p&gt;Like most people in tech lately, I&apos;ve been using and experimenting with LLMs and AI Agents. Cursor, Copilot, Cline, Roo Code… I&apos;ve tried many things, but for the last few months, there&apos;s been one clear winner: Claude Code.&lt;/p&gt;
&lt;p&gt;I first tried &lt;a href=&quot;https://www.anthropic.com/claude-code&quot;&gt;Claude Code&lt;/a&gt; shortly after it was released in February, but I didn&apos;t pay too much attention to it after I burned through $10 worth of tokens in 15 minutes. It did work pretty well, but at that cost, it just wasn&apos;t justifiable for my use. I came back a couple of months ago when Anthropic introduced Claude Max (and later Pro) plans that allow for flat fee usage.&lt;/p&gt;
&lt;p&gt;I&apos;ve been using it often since then. Although I&apos;m not much of a vibecoder, it does help me with the &lt;a href=&quot;https://en.wikipedia.org/wiki/Writer%27s_block&quot;&gt;blank page syndrome&lt;/a&gt; I often struggle with when starting a new project or a bigger feature in an existing one. Sometimes I even fully refactor Claude&apos;s output after using it, but I still find it worth prompting first.&lt;/p&gt;
&lt;p&gt;Anyway, one of the main annoyances I&apos;ve been dealing with is that when Claude works for a long time, my Mac may go to sleep and pause the agent. I often leave the agent running while I go make a coffee or get a snack, and it&apos;s very annoying to return to the computer only to find that it hasn&apos;t been doing anything in the meantime.&lt;/p&gt;
&lt;p&gt;When Claude Code was updated with hooks support, I thought I could finally address this by just running &lt;code&gt;caffeinate&lt;/code&gt; when it starts working and stopping it when it finishes. However, there was no hook for when it started working—until recently, that is. The &lt;code&gt;UserPromptSubmit&lt;/code&gt; hook was added in version 1.0.54.&lt;/p&gt;
&lt;p&gt;I&apos;ve built these two scripts to be run as hooks that will prevent the Mac from sleeping &lt;strong&gt;only&lt;/strong&gt; while Claude is running.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;prevent_sleep.sh&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash

# Prevent Mac from sleeping while Claude Code is running (for upto 1 hour)
# Kill any previously running caffeinate process started by this script

if [ -f /tmp/claude_caffeinate.pid ]; then
    old_pid=$(cat /tmp/claude_caffeinate.pid)
    if ps -p &quot;$old_pid&quot; &amp;gt; /dev/null 2&amp;gt;&amp;amp;1; then
        # Ensure the process is caffeinate (full command line check)
        if ps -p &quot;$old_pid&quot; -o args= | grep -q &apos;^caffeinate&apos;; then
            kill &quot;$old_pid&quot; 2&amp;gt;/dev/null
        fi
    fi
    rm -f /tmp/claude_caffeinate.pid
fi

nohup caffeinate -i -t 3600 &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;
echo $! &amp;gt; /tmp/claude_caffeinate.pid
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;allow_sleep.sh&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash

# Re-enable Mac sleep by killing the caffeinate process and cleanup the PID file

if [ -f /tmp/claude_caffeinate.pid ]; then
    kill $(cat /tmp/claude_caffeinate.pid) 2&amp;gt;/dev/null
    rm /tmp/claude_caffeinate.pid
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then added the hooks to &lt;code&gt;.claude/settings.json&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;hooks&quot;: {
    &quot;Stop&quot;: [
      {
        &quot;hooks&quot;: [
          {
            &quot;type&quot;: &quot;command&quot;,
            &quot;command&quot;: &quot;$HOME/.claude/hooks/allow-sleep.sh&quot;
          }
        ]
      }
    ],
    &quot;UserPromptSubmit&quot;: [
      {
        &quot;hooks&quot;: [
          {
            &quot;type&quot;: &quot;command&quot;,
            &quot;command&quot;: &quot;$HOME/.claude/hooks/prevent-sleep.sh&quot;
          }
        ]
      }
    ]
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I&apos;ve found this to work very well, hope you find it useful as well.&lt;/p&gt;
</content:encoded></item></channel></rss>