I am trying to pass an anonymous function into a method that uses a closure.
This is not precisely the case. CGEvent.tapCreate
is a wrapper for the C function CGEventTapCreate
, which takes a callback that is a C function. A C function is not a closure. A closure contains both code to execute and any values that it references. It is said to “close over” those values. A C function can't close over any values; it only has access to the arguments passed to it and to global variables.
To work around this behavior, a C API that takes a callback function usually also takes an opaque pointer (void *
in C parlance) which you can use to pass in whatever extra state you want. In the case of CGEvent.tapCreate
, that is the userInfo
or refcon
argument (the documentation refers to it by both names). You're passing self
as that argument, but you actually want to pass in two values: self
and onNameReceived
.
One way to solve this is by adding a new instance property to hold onNameReceived
for use by the callback, since you can access the instance property through the self
reference you recover from refcon
.
My preferred solution is to wrap the event tap in a class that lets you use a Swift closure as the handler.
class EventTapWrapper {
typealias Handler = (CGEventTapProxy, CGEventType, CGEvent) -> ()
init?(
location: CGEventTapLocation, placement: CGEventTapPlacement,
events: [CGEventType], handler: @escaping Handler)
{
self.handler = handler
let mask = events.reduce(CGEventMask(0), { $0 | (1 << $1.rawValue) })
let callback: CGEventTapCallBack = { (proxy, type, event, me) in
let wrapper = Unmanaged<EventTapWrapper>.fromOpaque(me!).takeUnretainedValue()
wrapper.handler(proxy, type, event)
return nil
}
// I can't create the tap until self is fully initialized,
// since I need to pass self to tapCreate.
self.tap = nil
guard let tap = CGEvent.tapCreate(
tap: location, place: placement,
options: .listenOnly, eventsOfInterest: mask,
callback: callback,
userInfo: Unmanaged.passUnretained(self).toOpaque())
else { return nil }
self.tap = tap
}
private let handler: Handler
private var tap: CFMachPort?
}
With this wrapper, we can just use a regular Swift closure as the tap handler:
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
func startListeningForEvents(onNameReceived: @escaping (String) -> Void) {
self.tap = EventTapWrapper(
location: .cghidEventTap, placement: .tailAppendEventTap,
events: [.leftMouseUp],
handler: { [weak self] (proxy, type, event) in
guard let self = self else { return }
onNameReceived(self.onEventTap(proxy: proxy, type: type, event: event))
})
}
func onEventTap(proxy: CGEventTapProxy, type: CGEventType, event: CGEvent) -> String {
return "hello"
}
private var tap: EventTapWrapper?
}
Please note that this is not a complete example (although it compiles). You also have to enable the tap (with CGEvent.tapEnable
) and add it to a run loop (using CFMachPortCreateRunLoopSource
and then CFRunLoopAddSource
). You also need to remove the source from the run loop in the wrapper's deinit
, lest it try to use the wrapper after the wrapper has been destroyed.