Introduction

Like many developers, I found myself constantly compressing files. Need to submit a scanned document for administrative stuff? Compress it. Want to attach a demo video to a Pull Request? Compress it. File too large for an email? You guessed it, compress it.

At first, I was using those online compression websites. You know the drill: upload your file, wait for it to process, download it back, rename it, move it to the right folder… It works, but it’s slow and kind of annoying when you’re doing it multiple times a day. Plus, uploading sensitive documents to random websites always felt a bit sketchy.

So I thought, “Hey, I’m a developer. I can probably build something for this.” And that’s how SqueezeBar was born.

SqueezeBar is this little macOS app that sits in your menu bar and makes your files smaller. It’s written in Swift and SwiftUI, and honestly, I learned a lot by making a bunch of mistakes and then fixing them (classic developer move, right?).

If you’re tired of the “upload-wait-download” cycle, give it a try and let me know what you think!

App Store: SqueezeBar

This is basically me writing down everything I figured out while building it. If you’re learning macOS development or just curious about how these things work, hopefully this helps.

Here’s some files I used for testing (using different presets) and the results:

Compression Result

Table of Contents

  1. How Everything Fits Together
  2. Making It Live in the Menu Bar
  3. The “Strategy Pattern” (Fancy Name for Organized Code)
  4. Squishing Images
  5. Squishing Videos with AVFoundation
  6. Squishing PDFs with Ghostscript
  7. Apple’s Sandbox (It’s Not a Fun Sandbox)
  8. Async/Await (Or: How I Stopped Worrying About Callback Hell)
  9. The Math Behind Quality Settings
  10. Making Things Fast
  11. Things That Broke (And How I Fixed Them)
  12. Stuff I Learned

How Everything Fits Together

I used something called MVVM (Model-View-ViewModel). It’s a pattern that SwiftUI really likes. Basically it means keeping your UI separate from your logic, which is good because otherwise everything becomes a giant mess (trust me, I’ve been there).

The Main Ideas

1. MVVM for the UI Stuff

Views ↔ ViewModel ↔ Models

2. Strategy Pattern for Compression

CompressionManager → Strategy Protocol → Concrete Strategies

This sounds complicated but it’s actually pretty simple: each file type (images, videos, PDFs) has its own compression “strategy.” This way, if I want to add support for, say, audio files later, I don’t have to rewrite everything. I just add a new strategy. Past me was looking out for future me!

How the Folders Are Organized

SqueezeBar/
├── App/                 # App startup stuff
├── Views/               # What you see on screen
├── ViewModels/          # The brain of the operation
├── Models/              # Data structures
├── Logic/               # Where the actual work happens
│   └── Strategies/      # Different compression methods
└── Resources/           # Icons and utilities

Making It Live in the Menu Bar

Creating the Little Icon Up Top

The app uses something called NSStatusItem to put itself in your menu bar:

class AppDelegate: NSObject, NSApplicationDelegate {
    private var statusItem: NSStatusItem!
    private var popover: NSPopover!

    func applicationDidFinishLaunching(_ notification: Notification) {
        // Let it show up in both menu bar and dock
        NSApp.setActivationPolicy(.regular)

        // Create the menu bar icon
        statusItem = NSStatusBar.system.statusItem(
            withLength: NSStatusItem.variableLength
        )

        if let button = statusItem.button {
            if let image = NSImage(named: "AppIcon") {
                image.size = NSSize(width: 18, height: 18)
                button.image = image
            }
            button.action = #selector(togglePopover)
        }
    }
}

Why I Made Certain Choices (Some Better Than Others)

Why show up in BOTH menu bar AND dock?

  • Turns out people (including me) keep clicking the dock icon wondering why nothing happens
  • Menu bar is great for quick access
  • Dock is what people expect
  • .regular activation policy lets us do both, so why not?

Popover vs Regular Window?

  • Popover feels more “Mac-like” (I spent way too long thinking about this)
  • It automatically goes away when you click elsewhere
  • Takes up less space
  • Looks cleaner (in my opinion, anyway)

Fixing the Weird Grey Popover Thing

At first, my popover looked all greyish and sad, like it was tired of life. Turns out it wasn’t becoming the “key window” (which is Apple’s way of saying “the window that’s actually paying attention”). Here’s the fix:

private func showPopover() {
    guard let button = statusItem.button else { return }

    // Wake up the app
    NSApp.activate(ignoringOtherApps: true)

    // Show the popover
    popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)

    // Make sure it's actually listening
    DispatchQueue.main.async { [weak self] in
        self?.popover.contentViewController?.view.window?.makeKey()
    }
}

The “Strategy Pattern” (Fancy Name for Organized Code)

The Strategy pattern is just a way to handle different file types without writing one giant messy function. Each file type gets its own “strategy” for compression.

The Basic Rules

protocol CompressionStrategy {
    var supportedTypes: [UTType] { get }

    func compress(
        inputURL: URL,
        outputURL: URL,
        quality: Double
    ) async throws -> CompressionResult
}

Picking the Right Strategy

The CompressionManager uses a dictionary lookup (which is super fast) to find the right compressor:

class CompressionManager {
    private let strategies: [CompressionStrategy] = [
        ImageCompressor(),
        VideoCompressor(),
        PDFCompressor()
    ]

    private lazy var strategyMap: [UTType: CompressionStrategy] = {
        var map: [UTType: CompressionStrategy] = [:]
        for strategy in strategies {
            for type in strategy.supportedTypes {
                map[type] = strategy
            }
        }
        return map
    }()
}

Why lazy initialization?

  • It doesn’t build the map until you actually need it
  • Then it keeps it around for next time
  • Basically, being lazy is sometimes the smart choice (in programming, at least)

Figuring Out What Type of File You Have

Instead of just looking at the file extension (because people can lie about that), we ask the system:

private func getFileType(for url: URL) throws -> UTType {
    let resourceValues = try url.resourceValues(forKeys: [.contentTypeKey])
    guard let type = resourceValues.contentType else {
        throw CompressionError.unsupportedFileType
    }
    return type
}

Why not just check if it ends in “.jpg”?

  • Because someone could rename “virus.exe” to “cute_cat.jpg”
  • The system actually checks the file’s real type
  • Much more reliable
  • I learned this the hard way

Squishing Images

Image compression uses Apple’s built-in ImageIO framework, which is way better than anything I could write myself.

Two Ways to Do It

1. The Fast Way (Direct Compression)

private func compressDirectly(
    imageSource: CGImageSource,
    outputURL: URL,
    outputType: CFString,
    quality: Double
) throws {
    let imageDestination = CGImageDestinationCreateWithURL(
        outputURL as CFURL,
        outputType,
        1,
        nil
    )

    var options: [CFString: Any] = [
        kCGImageDestinationLossyCompressionQuality: quality
    ]

    // Don't accidentally rotate the image (been there, done that)
    if let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) {
        if let orientation = properties[kCGImagePropertyOrientation] {
            options[kCGImagePropertyOrientation] = orientation
        }
    }

    CGImageDestinationAddImageFromSource(
        imageDestination,
        imageSource,
        0,
        options as CFDictionary
    )

    CGImageDestinationFinalize(imageDestination)
}

2. The Better Compression Way (Decode-Recode)

private func compressWithDecoding(
    imageSource: CGImageSource,
    outputURL: URL,
    outputType: CFString,
    quality: Double
) throws {
    guard let cgImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) else {
        throw CompressionError.compressionFailed("Could not decode image")
    }

    // If quality is super low, make the image smaller too
    if quality < 0.4 {
        let maxDimension = 2048
        let width = cgImage.width
        let height = cgImage.height

        if width > maxDimension || height > maxDimension {
            let scale = Double(maxDimension) / Double(max(width, height))
            options[kCGImageDestinationImageMaxPixelSize] =
                Int(Double(max(width, height)) * scale)
        }
    }

    CGImageDestinationAddImage(imageDestination, cgImage, options)
}

When to Use Which One

let shouldDecodeImage = isLosslessSource || quality < 0.7

if shouldDecodeImage {
    try compressWithDecoding(...)
} else {
    try compressDirectly(...)
}

The Logic:

  • If it’s a PNG or similar (lossless format), we need to convert it to JPEG
  • If you want really small files (quality < 0.7), decode it first
  • Otherwise, use the faster method
  • This took some trial and error to figure out

Smart Format Switching

private func selectOutputType(for inputType: CFString) -> CFString {
    let typeString = inputType as String

    // PNG, BMP, TIFF → JPEG (way smaller)
    if typeString.contains("png") ||
       typeString.contains("bmp") ||
       typeString.contains("tiff") {
        return UTType.jpeg.identifier as CFString
    }

    // HEIC is already pretty good, leave it alone
    if typeString.contains("heic") || typeString.contains("heif") {
        return inputType
    }

    // When in doubt, JPEG it
    return UTType.jpeg.identifier as CFString
}

When Compression Makes Things Worse

Sometimes you can’t make a file smaller (it’s already compressed well). In that case, just use the original:

if compressedSize >= originalSize {
    try? FileManager.default.removeItem(at: outputURL)

    // Hard link is faster than copying
    // (it's like creating a shortcut that thinks it's the real file)
    do {
        try FileManager.default.linkItem(at: inputURL, to: outputURL)
    } catch {
        try? FileManager.default.copyItem(at: inputURL, to: outputURL)
    }

    return CompressionResult(
        originalSize: originalSize,
        compressedSize: originalSize
    )
}

Why “hard link” instead of copy?

  • Doesn’t actually copy the data
  • Almost instant
  • Same file, two names pointing to it
  • Falls back to copying if it doesn’t work (like if they’re on different drives)

Squishing Videos with AVFoundation

Video compression uses AVAssetExportSession, which is Apple’s fancy way of saying “we’ll use your Mac’s video chip to do this fast.”

How It Works

class VideoCompressor: CompressionStrategy {
    var supportedTypes: [UTType] {
        [.movie, .video, .mpeg4Movie, .quickTimeMovie]
    }

    func compress(
        inputURL: URL,
        outputURL: URL,
        quality: Double
    ) async throws -> CompressionResult {
        let asset = AVURLAsset(url: inputURL)

        // Pick a preset based on quality
        let preset = selectPreset(for: quality)

        guard let exportSession = AVAssetExportSession(
            asset: asset,
            presetName: preset
        ) else {
            throw CompressionError.compressionFailed("Cannot create export session")
        }

        exportSession.outputURL = outputURL
        exportSession.outputFileType = .mp4

        await exportSession.export()

        // Did it work?
        guard exportSession.status == .completed else {
            throw CompressionError.compressionFailed("Export failed")
        }
    }
}

Choosing Quality Presets

AVFoundation doesn’t let you pick exact quality numbers, so you pick from presets:

private func selectPreset(for quality: Double) -> String {
    switch quality {
    case 0.8...1.0:
        return AVAssetExportPresetHighestQuality
    case 0.5..<0.8:
        return AVAssetExportPresetMediumQuality
    default:
        return AVAssetExportPresetLowQuality
    }
}

Why only three options?

  • Apple decided these are the ones you get
  • Hardware encoders work best with specific settings
  • At least they’re consistent
  • I’m not bitter about the lack of control (okay, maybe a little)

Squishing PDFs with Ghostscript

PDF compression uses Ghostscript, which is this really powerful (and slightly intimidating) tool that’s been around forever.

Why Ghostscript?

  • It’s the industry standard for PDF stuff
  • Really good at making PDFs smaller
  • Doesn’t mess up the PDF structure
  • Free to use
  • I just bundle it with the app so you don’t need to install anything

Using Ghostscript

class PDFCompressor: CompressionStrategy {
    func compress(
        inputURL: URL,
        outputURL: URL,
        quality: Double
    ) async throws -> CompressionResult {
        let gsPath = try getGhostscriptPath()
        let pdfSettings = selectPDFSettings(for: quality)

        let process = Process()
        process.executableURL = URL(fileURLWithPath: gsPath)
        process.arguments = [
            "-sDEVICE=pdfwrite",
            "-dCompatibilityLevel=1.4",
            "-dPDFSETTINGS=/\(pdfSettings)",
            "-dNOPAUSE",
            "-dQUIET",
            "-dBATCH",
            "-sOutputFile=\(outputURL.path)",
            inputURL.path
        ]

        try process.run()
        process.waitUntilExit()
    }
}

Quality Levels

Ghostscript has pre-defined settings with weird names:

private func selectPDFSettings(for quality: Double) -> String {
    switch quality {
    case 0.8...1.0:
        return "prepress"  // For professional printing
    case 0.5..<0.8:
        return "ebook"     // Good balance
    default:
        return "screen"    // Tiny files, screen-only quality
    }
}

Finding Ghostscript in the App Bundle

private func getGhostscriptPath() throws -> String {
    guard let gsPath = Bundle.main.path(
        forResource: "gs",
        ofType: nil,
        inDirectory: "Resources/Binaries"
    ) else {
        throw CompressionError.missingDependency("Ghostscript not found")
    }
    return gsPath
}

Apple’s Sandbox (It’s Not a Fun Sandbox)

macOS has this security feature called “sandboxing” which is great for users but kind of annoying for developers. Basically, your app can’t just access files willy-nilly.

Security-Scoped Bookmarks (Sounds Scarier Than It Is)

To remember which folder you’re allowed to save to, you need to create “bookmarks”:

class AppSettings: ObservableObject {
    @Published var outputFolderURL: URL? {
        didSet {
            saveOutputFolder()

            if let newURL = outputFolderURL {
                isAccessingSecurityScope =
                    newURL.startAccessingSecurityScopedResource()
            }
        }
    }

    private func saveOutputFolder() {
        guard let url = outputFolderURL else { return }

        do {
            let bookmark = try url.bookmarkData(
                options: .withSecurityScope,
                includingResourceValuesForKeys: nil,
                relativeTo: nil
            )
            UserDefaults.standard.set(bookmark, forKey: "outputFolderBookmark")
        } catch {
            print("Failed to save bookmark: \(error)")
        }
    }

    private func loadOutputFolder() {
        guard let bookmark = UserDefaults.standard.data(
            forKey: "outputFolderBookmark"
        ) else { return }

        do {
            var isStale = false
            let url = try URL(
                resolvingBookmarkData: bookmark,
                options: .withSecurityScope,
                relativeTo: nil,
                bookmarkDataIsStale: &isStale
            )

            if !isStale {
                outputFolderURL = url
            }
        } catch {
            print("Failed to resolve bookmark: \(error)")
        }
    }
}

Making Sure You Have Access

Before compressing, check if you’re still allowed to use that folder:

func ensureAccess() -> Bool {
    guard let url = outputFolderURL else { return false }

    if isAccessingSecurityScope {
        return true
    }

    if url.startAccessingSecurityScopedResource() {
        isAccessingSecurityScope = true
        return true
    }

    return false
}

Cleanup (Important!)

If you don’t stop accessing the resource, bad things happen (memory leaks, angry system):

deinit {
    if let url = outputFolderURL, isAccessingSecurityScope {
        url.stopAccessingSecurityScopedResource()
        isAccessingSecurityScope = false
    }
}

Async/Await (Or: How I Stopped Worrying About Callback Hell)

Swift’s modern concurrency features make things so much easier than the old callback style. Instead of callback pyramids, you get nice linear code.

How Compression Works Asynchronously

func compressFile(settings: AppSettings) async {
    guard let inputURL = droppedFileURL else { return }

    await MainActor.run {
        isCompressing = true
        errorMessage = nil
    }

    let inputAccessing = inputURL.startAccessingSecurityScopedResource()

    do {
        let quality = try calculateQuality(settings: settings, inputURL: inputURL)

        let result = try await compressionManager.compress(
            inputURL: inputURL,
            outputFolder: outputFolder,
            quality: quality
        )

        await MainActor.run {
            lastResult = result
            statusMessage = formatSuccessMessage(result: result)
        }

        // Clean up after 3 seconds (feels natural)
        try? await Task.sleep(for: .seconds(3))

        await MainActor.run {
            self.droppedFileURL = nil
            self.statusMessage = ""
        }
    } catch {
        await MainActor.run {
            errorMessage = formatErrorMessage(error)
        }
    }

    if inputAccessing {
        inputURL.stopAccessingSecurityScopedResource()
    }

    await MainActor.run {
        isCompressing = false
    }
}

Important Patterns

1. MainActor for UI Updates

await MainActor.run {
    self.isCompressing = false
}

UI stuff MUST happen on the main thread. This makes sure it does.

2. Cleanup with Defer

defer {
    if inputAccessing {
        inputURL.stopAccessingSecurityScopedResource()
    }
}

This runs no matter what, even if there’s an error.

3. Sleeping

try? await Task.sleep(for: .seconds(3))

Wait without freezing the UI. Magic!

The Math Behind Quality Settings

This was probably the hardest part to figure out. Users can say “I want this 10MB file to be 5MB” but how do you know what quality setting will get you there?

The Problem

Users might want:

  1. Direct Quality: “Use 70% quality”
  2. Target Size: “Make it 5 MB”
  3. Percentage: “Make it half the size”

For options 2 and 3, I need to guess what quality setting will work.

Image Quality Formula

After a lot of testing (and some files coming out the wrong size), I found this formula works pretty well:

finalSize ≈ baselineSize + compressionFactor × quality^exponent

More specifically:

size = 0.15 + 0.85 × quality^1.8

To solve for quality:

let adjustedRatio = max(0.15, targetRatio)
let normalizedRatio = (adjustedRatio - 0.15) / 0.85
let quality = pow(normalizedRatio, 1.0 / 1.8)

What do these magic numbers mean?

  • 0.15: Even at minimum quality, files have some overhead (headers, metadata, etc.)
  • 0.85: The part that’s actually compressible
  • 1.8: This I found through trial and error (it’s different for each format)
  • I’m not a mathematician, I just pressed buttons until it worked

Video Quality Mapping

Videos are simpler because you only have three presets:

if targetRatio > 0.75 {
    quality = 0.85  // High quality
} else if targetRatio > 0.5 {
    quality = 0.55  // Medium quality
} else {
    quality = 0.25  // Low quality
}

PDF Quality Mapping

Similar story for PDFs:

if targetRatio > 0.7 {
    quality = 0.85  // prepress
} else if targetRatio > 0.4 {
    quality = 0.65  // ebook
} else {
    quality = max(0.1, 0.4 + (targetRatio - 0.1) * 0.3)  // screen
}

Calculating Target Size

private func calculateQuality(
    settings: AppSettings,
    inputURL: URL
) throws -> Double {
    switch settings.compressionMode {
    case .targetSize:
        guard let resourceValues = try? inputURL.resourceValues(
            forKeys: [.fileSizeKey, .contentTypeKey]
        ),
        let fileSize = resourceValues.fileSize,
        let contentType = resourceValues.contentType else {
            return 0.5
        }

        let targetBytes = Double(settings.targetSizeMB * 1024 * 1024)
        let originalBytes = Double(fileSize)

        // If target is bigger than original, just use high quality
        if targetBytes >= originalBytes * 0.95 {
            return 0.95
        }

        let targetRatio = targetBytes / originalBytes

        // Use the right formula for this file type
        return calculateQualityForRatio(targetRatio, type: contentType)
    }
}

Making Things Fast

Small optimizations add up, especially when you’re processing large files.

1. Better File Size Reading

Before (slow):

let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
let size = attributes[.size] as? Int64

After (fast):

let resourceValues = try url.resourceValues(forKeys: [.fileSizeKey])
let size = Int64(resourceValues.fileSize ?? 0)

Why it’s better:

  • Goes straight to the system
  • No dictionary overhead
  • Type-safe
  • Faster when you only need one thing

2. Cached Strategy Map

Before (had to search every time):

for strategy in strategies {
    if strategy.supportedTypes.contains(where: { fileType.conforms(to: $0) }) {
        return strategy
    }
}

After (instant lookup):

private lazy var strategyMap: [UTType: CompressionStrategy] = {
    // Build once
}()

// O(1) lookup
if let strategy = strategyMap[fileType] {
    return strategy
}

3. Timestamp-Based File Names

Before (slow when many files):

var counter = 1
while FileManager.default.fileExists(atPath: path) {
    path = "\(basename).\(counter).\(ext)"
    counter += 1
}

After (instant):

let timestamp = Int(Date().timeIntervalSince1970)
let path = "\(basename).compressed.\(timestamp).\(ext)"

Why it’s better:

  • No checking if file exists
  • Almost instant
  • Collisions are super rare
  • Works great even with lots of files
// Try hard link first (basically free)
do {
    try FileManager.default.linkItem(at: inputURL, to: outputURL)
} catch {
    // If that doesn't work, copy it
    try? FileManager.default.copyItem(at: inputURL, to: outputURL)
}

5. Background Queue for System Stuff

func openResultFolder() {
    guard let result = lastResult else { return }

    DispatchQueue.global(qos: .userInitiated).async {
        let folderURL = result.compressedURL.deletingLastPathComponent()
        NSWorkspace.shared.open(folderURL)
    }
}

Don’t block the UI for file operations!

Things That Broke (And How I Fixed Them)

Problem 1: The Grey, Sad-Looking Popover

What happened: The popover looked all washed out and didn’t respond to clicks.

Why: It wasn’t becoming the “key window” (Apple’s term for “the window that’s actually active”).

Fix:

DispatchQueue.main.async { [weak self] in
    self?.popover.contentViewController?.view.window?.makeKey()
}

Problem 2: The “Publishing During View Updates” Warning

What happened: Scary warning messages in the console.

Why: I was updating @Published properties during the drop event, which SwiftUI doesn’t like.

Fix:

DispatchQueue.main.async {
    // All @Published updates go here
}

Problem 3: Files Disappeared After Restarting

What happened: After restarting the app, it couldn’t save files anymore.

Why: Lost permission to access the folder.

Fix: Security-scoped bookmarks (see the sandbox section).

Problem 4: PDF Quality Was All Over the Place

What happened: Sometimes PDFs would be huge, sometimes tiny, same settings.

Why: Ghostscript only has three settings, not fine-grained control.

Fix: Map quality ranges to the right preset, with some interpolation for the “screen” mode.

Problem 5: “Compressed” Files Getting Bigger

What happened: Already-compressed files got bigger after “compression.”

Why: Re-encoding overhead was more than any compression gain.

Fix: Compare sizes and just use the original if compression makes it worse.

Problem 6: App Store Rejected My Archive Because of Ghostscript

What happened: When I tried to upload to the App Store, validation failed with:

gs does not have sandbox entitlements enabled

Why: So here’s the thing - when you bundle executable binaries (like Ghostscript) inside a sandboxed macOS app, Apple requires EVERY executable to be properly signed with sandbox entitlements. I was signing the gs binary with the hardened runtime flag, but I completely forgot to give it entitlements. Apple was like “nope, not gonna let you through.”

Even worse, Xcode’s automatic file synchronization was creating TWO copies of the Ghostscript binary in different folders, and both needed to be signed. Fun times!

Fix: I had to create a separate entitlements file just for the Ghostscript binary:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.security.app-sandbox</key>
    <true/>
    <key>com.apple.security.inherit</key>
    <true/>
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
    <true/>
    <key>com.apple.security.cs.disable-library-validation</key>
    <true/>
</dict>
</plist>

The key part is com.apple.security.inherit - this tells macOS that the binary should inherit all the file access permissions from the parent app. So I don’t need to duplicate all the file access entitlements; the gs binary just gets whatever SqueezeBar can access.

Then I updated my build script to sign the binary WITH these entitlements:

codesign --force \
         --sign "${EXPANDED_CODE_SIGN_IDENTITY}" \
         --timestamp \
         --options runtime \
         --entitlements "$GS_ENTITLEMENTS" \
         "$DEST_GS"

The --entitlements flag was the missing piece. After that, validation passed and I could finally submit to the App Store.

Lesson learned: If you’re bundling ANY executable in a sandboxed app (helper tools, scripts, binaries), they ALL need to be signed with proper entitlements. Don’t make the same mistake I did!

Stuff I Learned

1. SwiftUI + AppKit: Best Friends Forever (Kind Of)

SwiftUI is great, but macOS menu bar stuff still needs AppKit. Luckily, bridging them is pretty easy:

popover.contentViewController = NSHostingController(rootView: MainPopoverView())

2. Security-Scoped Resources Are a Must

For sandboxed apps, you MUST handle bookmarks properly:

  • Create them with .withSecurityScope
  • Start accessing before use
  • Stop accessing when done
  • Handle it when they go stale

This took me way too long to get right.

3. Strategy Pattern Is Pretty Great

Adding new file types is super easy:

  1. Make a new strategy class
  2. Implement the protocol
  3. Add it to the array

No need to touch any existing code!

4. Async/Await Makes My Brain Happy

Compared to the old callback style:

  • Code reads top to bottom (like normal)
  • Errors automatically propagate up
  • Way easier to understand
  • My code reviews got shorter

5. Apple’s Built-In Stuff Is Really Good

ImageIO, AVFoundation, PDFKit give you:

  • Hardware acceleration (fast!)
  • Support for tons of formats
  • Metadata preservation
  • Great performance

Most of the time, you don’t need third-party libraries.

6. Estimating Compression Is Hard

Compression isn’t linear. It varies by format. The only way to get good formulas is to test with real files.

I still don’t have perfect formulas (turns out I’m not a wizard).

7. Small Optimizations Add Up

Using resourceValues instead of attributes, hard links instead of copies, cached maps… all these little things make the UI feel snappier.

Users notice when things feel slow, even if it’s just a few milliseconds.

8. Make It Feel Nice to Use

Things like auto-cleanup after 3 seconds, showing instant feedback, and having smart defaults make the app feel more polished.

Also, never underestimate the power of good defaults. Most people will just use what you give them.

Wrapping Up

Building SqueezeBar was a fun journey into macOS development. SwiftUI makes UI easy, Swift’s async/await makes concurrency less scary, and Apple’s frameworks are really powerful.

The Strategy pattern kept my code clean and easy to extend. Security-scoped bookmarks were annoying but necessary. And async/await saved me from callback hell.

Most importantly, I learned that sweating the small stuff (fast file operations, instant feedback, automatic cleanup) makes the difference between “yeah it works” and “this feels nice to use.”

What’s Next

Some ideas for future versions (if I get around to it):

  • Process multiple files at once
  • Support for audio and archive files
  • Save common settings as presets
  • Show before/after preview
  • Better progress bar (the current one is kind of basic)

The code is organized well enough that adding these should be pretty straightforward. Famous last words, right?


Repository: github.com/dimaswisodewo/SqueezeBar

Author: Meynabel Dimas Wisodewo

Built with: Swift 5, SwiftUI, Xcode 26.1.1

Platform: macOS 12.0+


This is a real project I built. All the code examples are from the actual SqueezeBar app. I hope this helps someone out there who’s trying to build something similar. Or at least makes you chuckle at my mistakes.