MEB
MEB

Reputation: 53

Swift CFRunLoopTimerCreate - how to get "self" in timer callback

How do I get access to the class "self" instance to call class instance methods given the following code. If I try self.callSomeClassIntance(), as shown, I get a "A C function pointer cannot be formed fro a closure that captures context" error from the compiler. I trying info.callSomeClassInstance(), but this will give a "no member callSomeClassInstance" error. Code will fire time correctly if the one line of code xxxx.callSomeClassIntance() is removed.

import Foundation

class Foo {
    func callSomeClassIntance() {}

    func start() {
        let runLoop : CFRunLoopRef = CFRunLoopGetCurrent();
        var context = CFRunLoopTimerContext(version: 0, info: unsafeBitCast(self, UnsafeMutablePointer<Void>.self), retain: nil, release: nil, copyDescription: nil)

        let timer : CFRunLoopTimerRef = CFRunLoopTimerCreate(kCFAllocatorDefault, 0.1, 3.0, 0, 0, cfRunloopTimerCallback(), &context);

        CFRunLoopAddTimer(runLoop, timer, kCFRunLoopCommonModes);

        CFRunLoopRun()
    }

    func cfRunloopTimerCallback() -> CFRunLoopTimerCallBack {

        return { (cfRunloopTimer, info) -> Void in
            print("Fire timer...")
            // need self context here to call class instance methods
            self.callSomeClassIntance()
        }

    }
}

Upvotes: 3

Views: 1196

Answers (2)

Martin R
Martin R

Reputation: 540105

As in How to use instance method as callback for function which takes only func or literal closure, the CFRunLoopTimerCallBack must be a global function or a closure which does not capture context. In particular, that closure cannot capture self and therefore must convert the void pointer from info in the context back to an instance pointer.

You don't necessarily need the cfRunloopTimerCallback() function, a closure can be passed directly as an argument:

class Foo {
    func callSomeClassIntance() {}

    func start() {
        let runLoop = CFRunLoopGetCurrent();
        var context = CFRunLoopTimerContext()
        context.info = UnsafeMutablePointer<Void>(Unmanaged.passUnretained(self).toOpaque())

        let timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0.1, 3.0, 0, 0, {
            (cfRunloopTimer, info) -> Void in

            let mySelf = Unmanaged<Foo>.fromOpaque(COpaquePointer(info)).takeUnretainedValue()
            mySelf.callSomeClassIntance()
        }, &context);

        CFRunLoopAddTimer(runLoop, timer, kCFRunLoopCommonModes);
        CFRunLoopRun()
    }
}

Here I have used Unmanaged for the conversions between instance pointer and void pointer. It looks more complicated but emphasizes that unretained references are passed around. unsafeBitCast() as in @nhgrif's answer can be used alternatively.

You can also define functions similar to the Objective-C __bridge, compare How to cast self to UnsafeMutablePointer<Void> type in swift.

Upvotes: 3

nhgrif
nhgrif

Reputation: 62072

We don't need to capture self because we're already passing it in.

When you create the context for your timer, you're putting self into a format that allows the C code to deal with it, a void pointer:

unsafeBitCast(self, UnsafeMutablePointer<Void>.self)

This code returns a void pointer to self. And that's what you're passing in for the info argument when you create your context.

Whatever you pass for the info argument when you create your context is what is used to pass in for the info argument of the CFRunLoopTimerCallback function. So, we need to apply the inverse operation (unsafeBitCast(info, Foo.self)) to that info argument:

func cfRunloopTimerCallback() -> CFRunLoopTimerCallBack { 
    return { _, info in
        let grabSelf = unsafeBitCast(info, Foo.self)
        grabSelf.callSomeClassIntance()
    }
}

Upvotes: 2

Related Questions