← Back to Writings

SnapText: How a Simple Window Close Crashed My MacOS App

A deep dive into debugging a mysterious crash in a macOS menu bar app built with Swift 6 and AppKit.

January 23, 2026 Updated May 28, 2026
SwiftmacOSDebuggingAppKitNSWindowMenu Bar AppMemory ManagementSnapTextSwift 6

Introduction

This was my first foray into native macOS development. SnapText is a menu bar application designed to be a lightweight productivity tool: it allows users to capture a region of their screen and instantly extracts text using OCR (Optical Character Recognition).

What started as a fun project to learn Swift 6 and AppKit quickly turned into a weeks-long debugging nightmare involving memory management, run loops, and the unforgiving nature of the Objective-C runtime lurking beneath Swift’s modern syntax.

The Crash

Everything seemed perfect during the initial build. The capture worked, the OCR was fast, and the UI looked native. But once I started testing specifically the closing behavior, the app began to self-destruct.

After a successful capture (or sometimes just pressing Escape), the app would vanish with this terrifying error:

Program crashed: Bad pointer dereference at 0x00005e00d59682d8

Thread 0 crashed:
  0 objc_release + 16 in libobjc.A.dylib
  1 objc_autoreleasePoolPop + 244 in libobjc.A.dylib
  2 _CFAutoreleasePoolPop + 32 in CoreFoundation
  3 -[NSAutoreleasePool drain] + 136 in Foundation
  4 -[NSApplication run] + 416 in AppKit

Symptoms

  1. First draw didn’t work - User had to click twice to start drawing a selection box
  2. App crashed after OCR - The menu bar icon would disappear and hotkeys stopped working
  3. Crash happened during autorelease pool drain - At the end of the run loop

The Investigation

Initial Hypothesis: SwiftUI Gesture Issues

The app used SwiftUI’s DragGesture embedded in an NSHostingView for the selection overlay. I suspected the “first draw” issue was due to SwiftUI’s gesture recognizer not being ready when the window appeared.

Attempted fix: Added delays before activating the window. Result: Still crashed.

Second Hypothesis: Deferred Window Closing

The capture callback was closing the window immediately:

private func handleCapture(rect: CGRect, screen: NSScreen) {
    overlayWindow?.close()  // ← Closing while still in callback!
    overlayWindow = nil
    // ... process OCR
}

Attempted fix: Defer window closing to the next run loop:

DispatchQueue.main.async {
    window?.close()
}

Result: Still crashed.

Third Hypothesis: SwiftUI App Lifecycle Conflict

The app used SwiftUI as the entry point (@main struct SnapTextApp: App) but created AppKit windows dynamically. I suspected SwiftUI’s lifecycle was conflicting with my window management.

Attempted fix: Replaced SwiftUI @main with pure AppKit:

@main
struct SnapTextMain {
    static func main() {
        let app = NSApplication.shared
        let delegate = AppDelegate()
        app.delegate = delegate
        app.setActivationPolicy(.accessory)
        app.run()
    }
}

Result: Still crashed.

Fourth Hypothesis: Vision Framework Memory Issues

The OCR used Apple’s Vision framework. I suspected autoreleased objects from Vision weren’t being properly retained.

Attempted fix: Wrapped OCR in explicit autoreleasepool and copied all strings:

return try autoreleasepool {
    // ... Vision OCR code
    for obs in observations {
        lines.append(String(candidate.string))  // Force copy
    }
    return String(text)  // Force copy
}

Result: Still crashed.

Fifth Hypothesis: Combine @Published Issues

The HistoryManager used Combine’s @Published which triggers observers on every change.

Attempted fix: Replaced Combine with NotificationCenter:

static let didUpdateNotification = Notification.Name("HistoryManagerDidUpdate")

func addItem(text: String) {
    // ... add item
    NotificationCenter.default.post(name: Self.didUpdateNotification, object: nil)
}

Result: Still crashed.

The Breakthrough: Escape Key Also Crashes!

I discovered that pressing Escape to cancel (without any OCR) also caused the crash. This ruled out OCR, clipboard, history, and toast as the cause.

The only common code path was: window close.

The Root Cause

The crash was caused by NSWindow deallocation while there were still pending references in the autorelease pool.

What Happens When You Close an NSWindow

  1. window.close() is called
  2. Window deallocates its contentView and subviews
  3. All associated objects are released
  4. BUT - some objects were already added to the autorelease pool
  5. At the end of the run loop, macOS drains the autorelease pool
  6. It tries to release an object that was already freed
  7. CRASH: Use-after-free / Double-free

Why macOS 26 / Swift 6?

Older macOS versions were more forgiving of these memory issues. Swift 6 with strict concurrency checking and macOS 26’s updated runtime exposed this latent bug.

The Solution: Object Pooling

Instead of creating and destroying windows, we use object pooling - create once, reuse forever.

Before (Broken)

func startCapture() {
    let window = CaptureOverlayWindow(...)  // Create
    overlayWindow = window
}

func handleCapture() {
    overlayWindow?.close()  // Destroy → CRASH!
    overlayWindow = nil
}

After (Fixed)

private var overlayWindow: ReusableCaptureWindow?  // Singleton

func startCapture() {
    if overlayWindow == nil {
        overlayWindow = ReusableCaptureWindow()  // Create once
    }
    overlayWindow?.startCapture(on: screen) { result in ... }
}

// In ReusableCaptureWindow:
func finishCapture(with result: CaptureResult) {
    orderOut(nil)  // Hide, don't close!
    completionHandler?(result)
}

Key Changes

  1. Never call close() on windows that will be reused
  2. Use orderOut(nil) to hide the window
  3. Reset state instead of recreating the window
  4. Apply to all dynamic windows (capture overlay AND toast)

Files Changed

FileChange
SnapTextApp.swiftPure AppKit entry point (no SwiftUI lifecycle)
CaptureCoordinator.swiftReusable capture window, OCR on background thread
ToastManager.swiftReusable toast window
HistoryManager.swiftNotificationCenter instead of Combine
HotkeyManager.swiftDispatchQueue instead of Task
Logger.swiftThread-safe logging

Lessons Learned

1. Window Lifecycle is Critical

In macOS apps, window creation and destruction must be handled carefully. The autorelease pool can hold references to objects that are deallocated prematurely.

2. Object Pooling for Transient UI

For windows that appear and disappear frequently (overlays, toasts, popups), consider keeping them alive and just hiding/showing them.

3. Swift 6 Exposes Hidden Bugs

Strict concurrency checking in Swift 6 revealed memory management issues that were latent in older versions.

4. Crash Location ≠ Crash Cause

The crash happened in objc_autoreleasePoolPop, but the cause was premature window deallocation happening earlier in the run loop.

5. Isolate the Problem

By discovering that even “Escape to cancel” caused the crash, I eliminated 90% of the code as suspects and focused on window lifecycle.

Best Practices for macOS Menu Bar Apps

// DO: Reuse windows
class ToastManager {
    private var toastWindow: NSWindow?  // Create once, reuse
    
    func show(message: String) {
        if toastWindow == nil {
            toastWindow = createWindow()
        }
        toastWindow?.orderFront(nil)  // Show
    }
    
    func hide() {
        toastWindow?.orderOut(nil)  // Hide, don't close
    }
}

// DON'T: Create and destroy windows
func show(message: String) {
    let window = NSWindow(...)  // New window each time
    window.orderFront(nil)
    
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        window.close()  // Potential crash!
    }
}

Conclusion

This debugging journey took me through multiple hypotheses before discovering the root cause. The key insight was that window deallocation in macOS is not instantaneous and can conflict with the autorelease pool’s lifecycle.

By switching to an object pooling pattern where windows are never deallocated, I eliminated the crash entirely and made the app more reliable.

Have a crash you can't trace?

I help indie devs and small teams debug AppKit and Swift Concurrency issues that don't show up cleanly in stack traces. Reach out if that's your situation.

Hire me