EXC_BAD_ACCESS (code=EXC_I386_GPFLT) when using RegisterEventHotKey + SwiftUI AppDelegate

Hi everyone,

I’m working on a macOS SwiftUI app that integrates a global keyboard shortcut using the old Carbon API (RegisterEventHotKey, InstallEventHandler, etc.). Everything works fine initially, but I’m running into a consistent EXC_BAD_ACCESS (code=EXC_I386_GPFLT) crash when the app is reopened, or sometimes even while drawing on my SwiftUI canvas view.

Setup

Here’s the relevant setup (simplified):

private let hotKeySignature: FourCharCode = 0x626c6e6b // 'blnk'
private weak var hotKeyDelegate: AppDelegate?

private func overlayHotKeyHandler(
    _ callRef: EventHandlerCallRef?,
    _ event: EventRef?,
    _ userData: UnsafeMutableRawPointer?
) -> OSStatus {
    guard let appDelegate = hotKeyDelegate else { return noErr }
    return appDelegate.handleHotKey(event: event)
}

final class AppDelegate: NSObject, NSApplicationDelegate {
    private var hotKeyRef: EventHotKeyRef?
    private var eventHandlerRef: EventHandlerRef?

    override init() {
        super.init()
        hotKeyDelegate = self
    }

    func applicationDidFinishLaunching(_ notification: Notification) {
        registerGlobalHotKey()
    }

    func applicationWillTerminate(_ notification: Notification) {
        unregisterGlobalHotKey()
    }

    private func registerGlobalHotKey() {
        var eventType = EventTypeSpec(eventClass: OSType(kEventClassKeyboard),
                                      eventKind: UInt32(kEventHotKeyPressed))
        InstallEventHandler(GetEventDispatcherTarget(),
                            overlayHotKeyHandler,
                            1,
                            &eventType,
                            nil,
                            &eventHandlerRef)

        var hotKeyID = EventHotKeyID(signature: hotKeySignature, id: 1)
        RegisterEventHotKey(UInt32(kVK_Space),
                            UInt32(cmdKey | shiftKey),
                            hotKeyID,
                            GetEventDispatcherTarget(),
                            0,
                            &hotKeyRef)
    }

    private func unregisterGlobalHotKey() {
        if let ref = hotKeyRef {
            UnregisterEventHotKey(ref)
        }
        if let handler = eventHandlerRef {
            RemoveEventHandler(handler)
        }
    }
}

What happens

  • The first run works fine.
  • If I close the app window and reopen (SwiftUI recreates the AppDelegate), the next time the hot key triggers, I get:
    Thread 8: EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
    0x7ff824028ff0 <+240>: callq *%rax
    
  • The debugger stops at a ud2 instruction, which likely means the handler is calling through a freed pointer (hotKeyDelegate).

What I suspect

Because RegisterEventHotKey and InstallEventHandler store global callbacks, they may outlive my SwiftUI AppDelegate instance. When SwiftUI reinitializes the app scene or recreates AppDelegate, the old handler still points to a now-freed object.

What I’ve tried

  • Unregistering the hot key and removing the handler in applicationWillTerminate().
  • Setting the global delegate to nil in deinit.
  • Replacing the weak global with a static weak reference cleared on deallocation.
  • Confirmed no Combine or SwiftUI memory leaks; the crash still occurs exactly when the Carbon handler fires after reopening.

Question

How should a modern SwiftUI macOS app safely integrate a global keyboard shortcut using the Carbon Hot Key APIs?
Is there a recommended way to manage the callback lifetime so it doesn’t call into a deallocated AppDelegate or scene?

Any advice on a modern alternative (e.g., using NSEvent.addGlobalMonitorForEvents or another framework) that can replace this global hotkey mechanism would also be appreciated.

Environment

  • macOS 15.0 Sonoma
  • Xcode 16.0
  • Swift 5.10
  • AppKit + SwiftUI hybrid app

Any advice on a modern alternative (e.g., using NSEvent.addGlobalMonitorForEvents or another framework) that can replace this global hotkey mechanism would also be appreciated.

In a SwiftUI View, to observe and react to keyboard input in a focused view hierarchy you can use the onKeyPress(_:action:) modifier. The would allow you specify key event matching conditions and provides an action to fire when a match is found.

If you're using AppKits NSEvent.addGlobalMonitorForEvents you could use an observable object and SwiftUI would know when theirs an event change. For example:

@Observable
class Manager {
    var eventModifiers = EventModifiers()
    var localEventMonitor: Any?
    var globalEventMonitor: Any?

    init() {
        localEventMonitor = NSEvent.addLocalMonitorForEvents(
            matching: [.flagsChanged],
            handler: { [weak self] event in
                self?.eventModifiers =
                    self?.convertModifierFlags(event.modifierFlags) ?? EventModifiers()
                return event
            }
        )
        globalEventMonitor = NSEvent.addGlobalMonitorForEvents(
            matching: [.flagsChanged],
            handler: { [weak self] event in
                self?.eventModifiers =
                    self?.convertModifierFlags(event.modifierFlags) ?? EventModifiers()
            }
        )
    }

...
    deinit {
        [localEventMonitor, globalEventMonitor]
            .compactMap { $0 }
            .forEach { NSEvent.removeMonitor($0) }
    }
}
EXC_BAD_ACCESS (code=EXC_I386_GPFLT) when using RegisterEventHotKey &#43; SwiftUI AppDelegate
 
 
Q