Search code examples
iosswifttimercallbackselector

How to properly fire/call a "selector" in Swift?


Question Summary:

If you have a Swift class that takes a selector as an argument in its initializer, how do you manually "fire/call" that selector?

Full Question:

Consider the following attempt at making a custom timer in Swift:

let TIME_INTERVAL = 0.1

class ValueAnimator : NSObject {
    private var timer = Timer()

    private let maxRep: Int
    private var currentRepIndex: Int = 0

    private var selector: Selector

    init(durationInSeconds: Int, selector: Selector) {
        print("VALUEANIMATOR INIT")
        self.maxRep = Int(Double(durationInSeconds) / TIME_INTERVAL)
        self.selector = selector

    }

    func start() {
        timer = Timer.scheduledTimer(timeInterval: TIME_INTERVAL, target: self, selector: (#selector(timerCallback)), userInfo: nil, repeats: true)
    }

    @objc func timerCallback() {
        currentRepIndex += 1

        perform(selector) // <-------- this line causes crash, "unrecognized selector sent to instance 0x600001740030"

        print ("VA timer called!, rep: \(currentRepIndex)")
        if currentRepIndex == maxRep {
            timer.invalidate()
            print("VA timer invalidated")
        }

    }


}

The usage of this "ValueAnimator" would be similar to a normal Timer/NSTimer, in that you pass a "selector" as an argument and that selector is called each time the ValueAnimator fires:

[In Parent Class]:

// { ...

   let valueAnimatorTest = ValueAnimator(durationInSeconds: 10, selector: #selector(self.temp))
        valueAnimatorTest.start()
    }

    @objc func temp() {
        print("temp VA callback works!") // this doesn't happen :(
    }

I'm trying to implement the same thing and as I understand, the line:

perform(selector)

should fire the selector in the parent class, but instead I get the error: "unrecognized selector sent to instance 0x600001740030"

I'm in a bit over my head here. I have tried googling the error, but everyone seems to be talking about how to use a selector from the parent-side (how to use Timer.scheduledTimer(), etc.) but I already know how to do that successfully.

I've also tried various tweaks to the code (changing public/private, scope of variables, and different forms of the performSelector() function)... but can't figure out the proper way to make the selector fire... or the unrelated mistake I've made if there is one.

Thanks for any help.


Solution

  • By calling perform(selector) it's like you're calling self.perform(selector) (self is implied), and by doing so the current instance of the ValueAnimator class is the object that actually performs the selector. When that happens, it tries to call a method called temp() of the ValueAnimator class, but as it doesn't exist the app is crashing.

    You can verify that if you add a temp() method in the ValueAnimator:

    @objc func temp() {
        print("Wrong method!!!")
    }
    

    If you run now you'll have no crash and the "Wrong Selector!!!" message will appear on the console.

    The solution to your problem is to pass the object that should run the selector method along with the selector to the initialisation of the ValueAnimator object.

    In the ValueAnimator class declare the following property:

    private var target: AnyObject
    

    Update the init method so it can get the target as an argument:

    init(durationInSeconds: Int, selector: Selector, target: AnyObject) {
        ...
    
        self.target = target
    }
    

    Also update the timerCallback():

    @objc func timerCallback() {
        ...
    
        _ = target.perform(selector)
    
        ...
    }
    

    Finally, when you initialise a ValueAnimator instance pass the object that the selector belongs to as well:

    let valueAnimatorTest = ValueAnimator(durationInSeconds: 10, selector: #selector(self.temp), target: self)
    

    Run again and the proper temp() method will be executed this time.

    I hope it helps.