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
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) }
}
}