It started, like most things I build, from frustration.
I was managing dozens of screenshots, product mockups, and compressed images every day. Some I needed to convert. Some I needed to shrink. Some I needed to rename and upload without breaking my workflow. But every tool I tried was either bloated, ugly, overengineered, paywalled, or just not built with care for the Mac user.
I didn’t want a full-blown photo editor. I just wanted something sharp, thoughtful, native, and fast.
So I built it.
The learning curve
I’ve built a few tools before, but this was my first time building a macOS app in Swift. Within the first hour, I was neck-deep in questions:
- What’s the difference between Swift and SwiftUI?
- When do I use AppKit vs Foundation?
- Is the CLI handler part of the app, or a separate script?
What helped was treating the whole thing like a learning artifact—and using AI to get answers to common errors. Slowly, the pieces started to click.
The technical stack
- Frontend: SwiftUI for the dashboard — clean, reactive, and minimal
- Backend: AppKit for image manipulation (
NSBitmapImageRep
andNSImage
) - Persistence: A settings.json file in ~/Library/Application Support for user defaults
- Extras:
SDWebImage
+WebPCoder
to support WebP formats- Combine for state management inside
AppSettings.swift
- macOS Shortcuts for right-click integration
Total coding time: ~18 hours
Laying the foundation
ImageToolApp is a SwiftUI macOS app. It runs standalone or silently via CLI. Under the hood, it uses SDWebImage
and WebP support via SDWebImageWebPCoder
. Nothing fancy — just practical tools wired together with logic.
The GUI was designed for quick edits. Pick a file, choose a format, set a compression target. But from the beginning, I wanted it to act more like a system extension — triggered via right-click shortcuts, not launched like a full-blown app.
So I gave it two brains: one for GUI, one for CLI. The first visible, the second invisible. Both had to play nice.
The invisible brain: CLI logic and fallback UX
I wanted to compress or convert images via a custom Finder Shortcut. Right-click a PNG, hit “Convert,” and walk away.
The app reads CommandLine.arguments, parses the request, and if all goes well, compresses the image in place. If something goes wrong — like the file’s already in the target format, or the target size is too aggressive — it gracefully launches the GUI with the file preloaded and a visible explanation.
That fallback mattered. The last thing you want is a broken Shortcut and no clue why it failed.
Here’s a sample scenario:
CLI Input | Saved Default | Outcome |
–convert webp | jpeg | File converted to WebP |
–compress 20 | N/A | File compressed to 20% quality |
No args | webp, 60% | Defaults applied |
PNG → PNG | Same format | Launches GUI with warning |
Target size too small | 10KB | Launches GUI with explanation |
This logic made the tool feel intelligent. It either worked quietly or failed gracefully — no dead ends, no mystery bugs.
UI principles: minimalism, motion, memory
For the visible side of the app, I leaned into macOS norms. One window. Snap-to-content layout. Minimal UI.
No popups unless absolutely needed. All settings save instantly — no “Save” button. The app uses Combine
to observe state and write changes directly to disk.
Drag-and-drop is fully supported, with a visual indicator that animates when files are hovered. If you drop multiple files, it processes everything in one go.
I avoided modals and confirmation boxes. The UI responds in real time. If compression fails, it explains why and what to try instead.
It also tries to remember everything — including your last window size. Close the app and reopen later, and it feels exactly like you left it.
Engineering quirks and small wins
Some parts were surprisingly tricky.
Handling background CLI runs without spawning a visible window required gating the WindowGroup with a launchUI flag. Failing to do this resulted in blank windows or double launches.
Same-format conversions had to be caught and warned against. Otherwise, users would get no result and assume something broke.
I added a simple toast system that reports compression results like this:
“Saved 63% — output file is 293KB”
These little confirmations go a long way. They help users trust the tool without needing to inspect every output manually.
Batch processing was another key feature. Drag three or more files into the app and it processes each one, and shows a single toast with the folder link. Internally, this used a DispatchGroup to sync processing and return cleanly.
What changed during development
This started as a small helper app. But as I refined the interaction model, it grew into something more systemic — a tool that respected context.
- CLI vs GUI
- Single file vs batch
- Success vs error
In every case, the tool made a choice that tried to help. If it failed, it explained. If it succeeded, it confirmed. If nothing needed doing, it stayed quiet.
Developer ergonomics and install quirks
For testing, I wired up logic to support both Xcode previews and terminal-triggered runs. That helped catch weird permission issues, especially around sandboxed file access.
I also discovered that you can copy the build/Debug/ImageToolApp.app directly into /Applications and it works — including Finder Services and CLI. You’ll get Gatekeeper warnings, but for personal tools, it’s good enough.
One unexpected bug: images dropped via Services would sometimes report 0KB. This turned out to be a sandboxing quirk and was fixed with better file permission handling in Xcode.
Another one: toasts containing buttons broke layout unless wrapped with HStack
and .buttonStyle(.plain)
. Simple fix, but not obvious at first.
Where it goes next
This version of ImageToolApp is solid for day-to-day use. It compresses, converts, and remembers your choices.
But there’s still more to do:
- Smarter format suggestions based on image content
- Per-file overrides during batch processing
- Native support for more formats like AVIF
- Auto-detection of optimal compression settings
The foundation is in place. And now that it’s working, I’m finding more reasons to use it — which is usually a good sign.
Check out the GitHub repo here.