Dave Stein
Dave Stein

Reputation: 9344

How can I pass an anonymous method into a closure that captures context in Swift?

I am trying to pass an anonymous function into a method that uses a closure. I have read Swift: Pass data to a closure that captures context and actually use its referenced question (How to use instance method as callback for function which takes only func or literal closure) for part of the solution.

The problem is that these solutions are all working off of methods on a class.

This is the code I'm attempting to do:

func startListeningForEvents(onNameReceived: @escaping (String) -> Void) {
    selfPtr = Unmanaged.passRetained(self)

    self.tap = CGEvent.tapCreate(tap: CGEventTapLocation.cghidEventTap,
        place: CGEventTapPlacement.tailAppendEventTap,
        options: CGEventTapOptions.listenOnly,
        eventsOfInterest: CGEventMask(CGEventType.leftMouseUp.rawValue),
        callback: { proxy, type, event, refcon in
            let mySelf = Unmanaged<AccController>.fromOpaque(refcon!).takeUnretainedValue()
            let name = mySelf.onEventTap(proxy: proxy, type: type, event: event, refcon);

            // This is what I can't figure how to access
            onNameReceived(name);
            return nil;

        },
        userInfo: selfPtr.toOpaque());

}

In the above example. onEventTap is going to happen no matter who calls this class. I want the caller to choose what happens when a name is successfully grabbed. I get the error A C function pointer cannot be formed from a closure that captures context.

Upvotes: 1

Views: 2138

Answers (2)

Dave Stein
Dave Stein

Reputation: 9344

I marked Rob's answer as the one seeing as it better answers the original question. The solution I ended up using was this:

class MyClass {

  typealias MyFunctionDefinition = (String) -> Void
  private var onNameReceived: MyFunctionDefinition!
  init(callback: @escaping MyFunctionDefinition) {
    self.onNameReceived = callback;
  }

  // ... the code from my question above
        callback: { proxy, type, event, refcon in
            let mySelf = Unmanaged<AccController>.fromOpaque(refcon!).takeUnretainedValue()
            let name = mySelf.onEventTap(proxy: proxy, type: type, event: event, refcon);

            // This is what I can't figure how to access
            self.onNameReceived(name);
            return nil;

        },

}

I am able to just pass the callback in the constructor but I imagine other people might want to solve for the same thing in a way where it's done via a callback in the call.

Upvotes: 0

rob mayoff
rob mayoff

Reputation: 386038

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.

Upvotes: 1

Related Questions