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
ud2instruction, 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
nilindeinit. - 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