<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Neat Software • Blog]]></title><description><![CDATA[Articles, learning and how-tos about development with Swift, SwiftUI, macOS, iOS and other Apple platforms from Neat Software Co.]]></description><link>https://blog.neat.software</link><image><url>https://cdn.hashnode.com/uploads/logos/62386bd0e6506757acb792c8/9683bc4f-561f-40de-8eba-cdd356b586e3.png</url><title>Neat Software • Blog</title><link>https://blog.neat.software</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 15 Apr 2026 03:20:40 GMT</lastBuildDate><atom:link href="https://blog.neat.software/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Best Mac Time Tracker for Freelancers]]></title><description><![CDATA[If you bill by the hour, time tracking isn't optional — it's how you get paid accurately. But finding a Mac time tracker that's actually pleasant to use is harder than it should be. Most are either to]]></description><link>https://blog.neat.software/best-mac-time-tracker-for-freelancers</link><guid isPermaLink="true">https://blog.neat.software/best-mac-time-tracker-for-freelancers</guid><dc:creator><![CDATA[Neat Software]]></dc:creator><pubDate>Sat, 21 Mar 2026 12:41:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/62386bd0e6506757acb792c8/264d2e9e-0ee8-4560-9120-bed15bad646c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you bill by the hour, time tracking isn't optional — it's how you get paid accurately. But finding a Mac time tracker that's actually pleasant to use is harder than it should be. Most are either too simple to be useful, too complex to bother with, or designed for teams when you're flying solo.</p>
<p>After years of building <a href="https://tim.neat.software/?ref=blog">Tim for Mac</a> — a time tracker used by thousands of freelancers on Mac — and talking to the people who use it, here's an honest breakdown of what to look for and how the main options stack up.</p>
<h2>What Freelancers Actually Need in a Time Tracker</h2>
<p>Before diving into apps, it's worth being clear about what actually matters for freelance work:</p>
<ul>
<li><p><strong>Fast to start and stop</strong> — if it takes more than a click or two, you'll forget to use it</p>
</li>
<li><p><strong>Menu bar access</strong> — you should be able to see what's running at a glance without switching apps</p>
</li>
<li><p><strong>Projects and clients</strong> — you need to separate time by client so you can invoice accurately</p>
</li>
<li><p><strong>Idle detection</strong> — catches the times you walked away and forgot to stop the timer</p>
</li>
<li><p><strong>Exporting</strong> — CSV, JSON, or something you can turn into an invoice</p>
</li>
<li><p><strong>Stays out of your way</strong> — the best time tracker is one you don't have to think about</p>
</li>
</ul>
<p>What most freelancers <em>don't</em> need: team features, Jira integrations, automatic website tracking, or a $20/month subscription for features they'll never use.</p>
<h3>Tim — Best for Mac-Native Simplicity</h3>
<p>Tim lives in your menu bar and gets out of your way. You set up projects (called Groups) and tasks, then start and stop timers with a click. Idle time detection prompts you to clean up time when you've been away from your keyboard.</p>
<p>What makes it stand out for freelancers:</p>
<ul>
<li><p>Genuinely Mac-native — keyboard shortcuts, menu bar, the works</p>
</li>
<li><p>Add notes to time entries, which is invaluable for billing ("reviewed contract, client call, revised proposal")</p>
</li>
<li><p>Filterable export for clean reporting by client or date range</p>
</li>
<li><p><strong>One-time purchase option</strong></p>
</li>
</ul>
<p>The most common feedback from users: it hits the sweet spot between "too simple" and "too complex." It won't generate invoices directly or sync to QuickBooks, but for capturing and reporting time honestly, it's hard to beat.</p>
<p><strong>Price:</strong> Free tier, Pro upgrade \(24.99 lifetime or \)4.99 monthly</p>
<h3>Toggl Track — Best Free Option</h3>
<p>Toggl is the safe default recommendation and for good reason. It's free for individuals, works across Mac, iPhone, and browser, and has solid reporting. The Mac app is well-designed and it syncs everywhere.</p>
<p>The tradeoffs: it's a subscription for anything beyond the basics, it sends your data to their servers, and the interface — while polished — has more friction than something like Tim for purely local, simple tracking.</p>
<p><strong>Price:</strong> Free tier available; paid plans from ~$9/month</p>
<h3>Timing — Best for Automatic Tracking</h3>
<p>Timing tracks what you're doing automatically — it records which apps and websites you use and lets you categorize time after the fact. If you're the type who forgets to start timers, this approach is genuinely useful.</p>
<p>The downside: it feels less like <em>tracking</em> and more like <em>reviewing surveillance footage of yourself</em>. It's also subscription-only (~$8/month) and can be overwhelming to categorize retroactively.</p>
<p><strong>Price:</strong> From ~$8/month</p>
<h3>Clockify — Best for Teams</h3>
<p>Clockify has a generous free plan and good team features. For solo freelancers who just need something free with web access, it works. It's not particularly Mac-native but it's solid.</p>
<p><strong>Price:</strong> Free; paid plans for more features</p>
<h2>The One-Time Payment vs. Subscription Question</h2>
<p>This comes up constantly in time tracker discussions, and it matters more than most apps will admit. A time tracker is a tool you run every day, indefinitely. Paying $8-20/month forever for that adds up fast.</p>
<p>Apps with a one-time price (like Tim) align better with how freelancers actually think about software spend. You buy it once, it works, done. No recurring billing to justify, no features locked behind a higher tier.</p>
<p>If subscription pricing doesn't bother you, Toggl or Timing are both good options. If you'd rather pay once and move on, Tim is the obvious choice in this category.</p>
<h2>What to Actually Try</h2>
<p>Here's the honest advice: <strong>download the free version of two or three apps and use them for a real work week.</strong> Time trackers are deeply personal — what feels frictionless to one person feels clunky to another.</p>
<p>If you work in a predictable pattern (same clients, same types of tasks), Tim or Toggl will serve you well. If your days are unpredictable and you forget to track, give Timing a shot.</p>
<p>The worst outcome is spending months with a tool that has enough friction that you stop using it. A slightly worse app you actually use beats a slightly better app you don't.</p>
<hr />
<p><em>Tim is made by</em> <a href="https://neat.software?ref=blog"><em>Neat Software</em></a> <em>— a small indie shop building focused Mac and iOS apps. You can try Tim for free on the</em> <a href="https://apps.apple.com/app/id1449619230?pt=515396&amp;ct=blog"><em>Mac App Store</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[SwiftUI: Fix toolbar buttons not fully tappable in iOS 26]]></title><description><![CDATA[In iOS 26, you may find that some of your toolbar items can only be tapped exactly on the icon itself rather than the whole padded button. To fix this, use a Label for your buttons instead of just an ]]></description><link>https://blog.neat.software/swiftui-fix-toolbar-buttons-not-fully-tappable-in-ios-26</link><guid isPermaLink="true">https://blog.neat.software/swiftui-fix-toolbar-buttons-not-fully-tappable-in-ios-26</guid><category><![CDATA[SwiftUI]]></category><category><![CDATA[iOS]]></category><dc:creator><![CDATA[Neat Software]]></dc:creator><pubDate>Mon, 08 Dec 2025 20:19:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/62386bd0e6506757acb792c8/d2d11055-c966-4f9f-9ed6-5cd6072912d1.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In iOS 26, you may find that some of your toolbar items can only be tapped exactly on the icon itself rather than the whole padded button. To fix this, use a <code>Label</code> for your buttons instead of just an <code>Image</code>. This is also superior than only an image anyway, as it adds text labeling and accessibility.</p>
<pre><code class="language-swift">ToolbarItem {
    Button(action: {}, label: {
        Label("Like", systemImage: "heart").labelStyle(.iconOnly)
    })
}
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Swift: Go to App's Notification Settings on macOS]]></title><description><![CDATA[To open System Settings on macOS to the Notifications panel and to a specific app's notification configuration in Swift:
let notificationsPath = "x-apple.systempreferences:com.apple.Notifications-Settings.extension"
let bundleId = Bundle.main.bundleI...]]></description><link>https://blog.neat.software/swift-go-to-apps-notification-settings-on-macos</link><guid isPermaLink="true">https://blog.neat.software/swift-go-to-apps-notification-settings-on-macos</guid><category><![CDATA[Swift]]></category><category><![CDATA[macOS]]></category><dc:creator><![CDATA[Neat Software]]></dc:creator><pubDate>Wed, 14 Aug 2024 02:50:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/ogrGCU5uSxc/upload/f41a22bd763cb9f82ba0ba4c76d43b15.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>To open System Settings on macOS to the Notifications panel and to a specific app's notification configuration in Swift:</p>
<pre><code class="lang-swift"><span class="hljs-keyword">let</span> notificationsPath = <span class="hljs-string">"x-apple.systempreferences:com.apple.Notifications-Settings.extension"</span>
<span class="hljs-keyword">let</span> bundleId = <span class="hljs-type">Bundle</span>.main.bundleIdentifier
<span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> url = <span class="hljs-type">URL</span>(string: <span class="hljs-string">"\(notificationsPath)?id=\(bundleId ?? "</span><span class="hljs-string">")"</span>) {
    <span class="hljs-type">NSWorkspace</span>.shared.<span class="hljs-keyword">open</span>(url)
}
</code></pre>
]]></content:encoded></item><item><title><![CDATA[SwiftUI Vertical Divider]]></title><description><![CDATA[Using the standard Divider view and setting its width to 1 will create a vertical divider: Divider().frame(width: 1)
Example in action:
struct VerticalDividerExample: View {
    var body: some View {
        HStack(spacing: 20) {
           Text("Lef...]]></description><link>https://blog.neat.software/swiftui-vertical-divider</link><guid isPermaLink="true">https://blog.neat.software/swiftui-vertical-divider</guid><category><![CDATA[SwiftUI]]></category><dc:creator><![CDATA[Neat Software]]></dc:creator><pubDate>Tue, 10 Jan 2023 22:01:58 GMT</pubDate><content:encoded><![CDATA[<p>Using the standard <code>Divider</code> view and setting its width to <code>1</code> will create a vertical divider: <code>Divider().frame(width: 1)</code></p>
<p>Example in action:</p>
<pre><code class="lang-swift"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">VerticalDividerExample</span>: <span class="hljs-title">View</span> </span>{
    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">HStack</span>(spacing: <span class="hljs-number">20</span>) {
           <span class="hljs-type">Text</span>(<span class="hljs-string">"Left"</span>)
           <span class="hljs-type">Divider</span>().frame(width: <span class="hljs-number">1</span>)
           <span class="hljs-type">Text</span>(<span class="hljs-string">"Right"</span>)
        }
        .padding()
        .frame(width: <span class="hljs-number">200</span>, height: <span class="hljs-number">75</span>)
    }
}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1673387934176/50d88bbd-ad79-46ab-ae86-e76fd429c40e.png" alt class="image--center mx-auto" /></p>
<p>Setting its width to anything greater than 1 will also produce the vertical effect but it will not render greater than one pixel wide and instead just add padding on each side.</p>
<p>We use vertical dividers in the stats view in our <a target="_blank" href="https://tim.neat.software">time tracking app, Tim</a>.</p>
<p><a target="_blank" href="https://tim.neat.software"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1673388728854/269b0f35-caec-41bf-8df6-514a7e697cca.png" alt class="image--center mx-auto" /></a></p>
]]></content:encoded></item><item><title><![CDATA[SwiftUI Picker with Optional Binding]]></title><description><![CDATA[Let's say you have a Picker in SwiftUI and want the ability to set its value to nil. In order for the optional binding to work correctly, you can tag each item with the value casted as the optional type.
struct ContentView: View {
    @State var sele...]]></description><link>https://blog.neat.software/swiftui-picker-with-optional-binding</link><guid isPermaLink="true">https://blog.neat.software/swiftui-picker-with-optional-binding</guid><category><![CDATA[SwiftUI]]></category><category><![CDATA[Swift]]></category><category><![CDATA[iOS]]></category><category><![CDATA[macOS]]></category><dc:creator><![CDATA[Neat Software]]></dc:creator><pubDate>Wed, 14 Dec 2022 22:12:32 GMT</pubDate><content:encoded><![CDATA[<p>Let's say you have a <code>Picker</code> in SwiftUI and want the ability to set its value to <code>nil</code>. In order for the optional binding to work correctly, you can <code>tag</code> each item with the value casted as the optional type.</p>
<pre><code class="lang-swift"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ContentView</span>: <span class="hljs-title">View</span> </span>{
    @<span class="hljs-type">State</span> <span class="hljs-keyword">var</span> selection: <span class="hljs-type">String?</span>

    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">Picker</span>(<span class="hljs-string">"Select:"</span>, selection: $selection) {
            <span class="hljs-type">Text</span>(<span class="hljs-string">"None"</span>).tag(<span class="hljs-literal">nil</span> <span class="hljs-keyword">as</span> <span class="hljs-type">String?</span>)
            <span class="hljs-type">Divider</span>()
            <span class="hljs-type">Text</span>(<span class="hljs-string">"Red"</span>).tag(<span class="hljs-string">"R"</span> <span class="hljs-keyword">as</span> <span class="hljs-type">String?</span>)
            <span class="hljs-type">Text</span>(<span class="hljs-string">"Green"</span>).tag(<span class="hljs-string">"G"</span> <span class="hljs-keyword">as</span> <span class="hljs-type">String?</span>)
            <span class="hljs-type">Text</span>(<span class="hljs-string">"Blue"</span>).tag(<span class="hljs-string">"B"</span> <span class="hljs-keyword">as</span> <span class="hljs-type">String?</span>)
        }
    }
}
</code></pre>
<p>This is great for things like user preferences, which we use it extensively in all of our <a target="_blank" href="https://neat.software/apps">apps at Neat Software</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Adding System Sound Effects to a macOS App in Swift]]></title><description><![CDATA[When creating a macOS app, oftentimes sound feedback is a nice feature to add polish.  We can even let the user pick their own sounds. To fit in with the theme of the OS, sometimes it's best to use the sounds already built into the system.
Unfortunat...]]></description><link>https://blog.neat.software/adding-system-sound-effects-to-a-macos-app-in-swift</link><guid isPermaLink="true">https://blog.neat.software/adding-system-sound-effects-to-a-macos-app-in-swift</guid><category><![CDATA[Swift]]></category><category><![CDATA[SwiftUI]]></category><category><![CDATA[macOS]]></category><category><![CDATA[Xcode]]></category><dc:creator><![CDATA[Neat Software]]></dc:creator><pubDate>Fri, 22 Jul 2022 14:37:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/7B-ivTcS3Lg/upload/v1648039819371/LWEypvREl.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When creating a macOS app, oftentimes sound feedback is a nice feature to add polish.  We can even let the user pick their own sounds. To fit in with the theme of the OS, sometimes it's best to use the sounds already built into the system.</p>
<p>Unfortunately, AppKit does not offer an easy API for accessing the system sounds.  However, we can find and read them from the file system:</p>
<pre><code class="lang-swift"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">getSystemSoundFileEnumerator</span><span class="hljs-params">()</span></span> -&gt; <span class="hljs-type">FileManager</span>.<span class="hljs-type">DirectoryEnumerator?</span> {
    <span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> libraryDirectory = <span class="hljs-type">NSSearchPathForDirectoriesInDomains</span>(.libraryDirectory, .systemDomainMask, <span class="hljs-literal">true</span>).first,
          <span class="hljs-keyword">let</span> soundsDirectory = <span class="hljs-type">NSURL</span>(string: libraryDirectory)?.appendingPathComponent(<span class="hljs-string">"Sounds"</span>),
          <span class="hljs-keyword">let</span> soundFileEnumerator = <span class="hljs-type">FileManager</span>.<span class="hljs-keyword">default</span>.enumerator(at: soundsDirectory, includingPropertiesForKeys: <span class="hljs-literal">nil</span>) <span class="hljs-keyword">else</span> { <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span> }
    <span class="hljs-keyword">return</span> soundFileEnumerator
}
</code></pre>
<p>Next, we need to register the sound files with the system audio services so we can use them as sound effects in the app. Let's create a simple struct to store the sound name and registered sound id:</p>
<pre><code class="lang-swift"><span class="hljs-keyword">import</span> AudioToolbox

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">SoundEffect</span>: <span class="hljs-title">Hashable</span> </span>{
    <span class="hljs-keyword">let</span> id: <span class="hljs-type">SystemSoundID</span>
    <span class="hljs-keyword">let</span> name: <span class="hljs-type">String</span>

    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">play</span><span class="hljs-params">()</span></span> {
        <span class="hljs-type">AudioServicesPlaySystemSoundWithCompletion</span>(id, <span class="hljs-literal">nil</span>)
    }
}

<span class="hljs-class"><span class="hljs-keyword">extension</span> <span class="hljs-title">SoundEffect</span> </span>{
    <span class="hljs-keyword">static</span> <span class="hljs-keyword">let</span> systemSoundEffects: [<span class="hljs-type">SoundEffect</span>] = {
        <span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> systemSoundFiles = getSystemSoundFileEnumerator() <span class="hljs-keyword">else</span> { <span class="hljs-keyword">return</span> [] }
        <span class="hljs-keyword">return</span> systemSoundFiles.<span class="hljs-built_in">compactMap</span> { item <span class="hljs-keyword">in</span>
            <span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> url = item <span class="hljs-keyword">as</span>? <span class="hljs-type">URL</span>, <span class="hljs-keyword">let</span> name = url.deletingPathExtension().pathComponents.last <span class="hljs-keyword">else</span> { <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span> }
            <span class="hljs-keyword">var</span> soundId: <span class="hljs-type">SystemSoundID</span> = <span class="hljs-number">0</span>
            <span class="hljs-type">AudioServicesCreateSystemSoundID</span>(url <span class="hljs-keyword">as</span> <span class="hljs-type">CFURL</span>, &amp;soundId)
            <span class="hljs-keyword">return</span> soundId &gt; <span class="hljs-number">0</span> ? <span class="hljs-type">SoundEffect</span>(id: soundId, name: name) : <span class="hljs-literal">nil</span>
        }.sorted(by: { $<span class="hljs-number">0</span>.name.compare($<span class="hljs-number">1</span>.name) == .orderedAscending })
    }()
}
</code></pre>
<p>Now we can use the SoundEffects anywhere in the app. Here is an example using SwiftUI to display them in a Picker and trigger the sound whenever the selection changes:</p>
<pre><code class="lang-swift"><span class="hljs-keyword">import</span> SwiftUI

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">SoundEffectPicker</span>: <span class="hljs-title">View</span> </span>{
    @<span class="hljs-type">Binding</span> <span class="hljs-keyword">var</span> selection: <span class="hljs-type">SoundEffect?</span>

    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">Picker</span>(selection: $selection, label: <span class="hljs-type">Text</span>(<span class="hljs-string">"Sound:"</span>)) {
            <span class="hljs-type">Text</span>(<span class="hljs-string">"None"</span>).tag(<span class="hljs-literal">nil</span> <span class="hljs-keyword">as</span> <span class="hljs-type">SoundEffect?</span>)
            <span class="hljs-type">ForEach</span>(<span class="hljs-type">SoundEffect</span>.systemSoundEffects, id: \.<span class="hljs-keyword">self</span>) { sound <span class="hljs-keyword">in</span>
                <span class="hljs-type">Text</span>(sound.name).tag(sound <span class="hljs-keyword">as</span> <span class="hljs-type">SoundEffect?</span>)
            }
        }.onChange(of: selection) { sound <span class="hljs-keyword">in</span>
            sound?.play()
        }
    }
}
</code></pre>
<p>One important note to mention: <code>SoundEffect.systemSoundEffects</code> needs to be invoked at some point when your app runs so the sounds get registered with AudioServices.  For example, if you only used the Picker in a preferences window and that may not be open the next time the app starts up, the sounds wouldn't be registered.  You could simply add it to your app delegate on launch to be sure:</p>
<pre><code class="lang-swift"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">applicationDidFinishLaunching</span><span class="hljs-params">(<span class="hljs-number">_</span>: Notification)</span></span> {
   <span class="hljs-number">_</span> = <span class="hljs-type">SoundEffect</span>.systemSoundEffects
}
</code></pre>
<p>You can see how we integrated this in a real app to add sound effect preferences in our
<a target="_blank" href="https://tim.neat.software">time tracking app: Tim</a></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1658520485269/gr4gQrxhb.png" alt="Screen Shot 2022-07-22 at 4.06.59 PM.png" /></p>
<p>A full demo app of this code can be found on GitHub: <a target="_blank" href="https://github.com/neatsoftware/SoundEffectsDemo">https://github.com/neatsoftware/SoundEffectsDemo</a></p>
]]></content:encoded></item></channel></rss>