Search code examples
iosswift2nstimer

Swift - NSTimer passing wrong value for userInfo param


I recently took it upon myself to learn the Swift programming language and I love it. To help me learn, I have been working on a couple of simple apps using XCode 7. One of those apps is a flashlight and one of the features is a strobe light. I decided on using the NSTimer class to schedule the flashlight toggle functionality to be called at a set time interval, thus giving a nice strobe effect. My first crack at the timer code is as follows :-

NSTimer.scheduledTimerWithTimeInterval(0.5, target: self.torch, selector: "toggleTorch:", userInfo: false, repeats: true)

As you can see here, I am using my own custom made Torch object to handle the flash toggle functionality. I pass the torch object I have specified as an instance variable as the target param and then I call the following function in the Torch class, which I pass as selector :-

func toggleTorch(adjustingBrightness: Bool) -> Bool {
    let device = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)

    defer {
        device.unlockForConfiguration()
    }

    guard device.hasTorch else {
        return false
    }
    do {
        try device.lockForConfiguration()

        if  !adjustingBrightness && device.torchMode == AVCaptureTorchMode.On {
            device.torchMode = AVCaptureTorchMode.Off
        }
        else if !adjustingBrightness && device.torchMode == AVCaptureTorchMode.Off {
            try device.setTorchModeOnWithLevel(brightnessLevel)
        }               
        else if adjustingBrightness && device.torchMode == AVCaptureTorchMode.On {
            try device.setTorchModeOnWithLevel(brightnessLevel)
        }
    } catch {
        return false
    }

    return true
}

The problem I am encountering is that when the timer calls the toggleTorch method, it is passing the value of true as the parameter even though I have specifically passed false to the timer as the userInfo param.

So I scratch my head and think "Ah" let's try using a default value of false in the selector/method parameter and pass nil in the timer userInfo param. That way I can rely on the default value of false being invoked every time. So my new selector signature looks as follows:-

func toggleTorch(adjustingBrightness: Bool = false) -> Bool

And the timer that now calls it has changed to:-

NSTimer.scheduledTimerWithTimeInterval(0.5, target: self.torch, selector: "toggleTorch:", userInfo: nil, repeats: true)

However, despite the default parameter and no parameter passing going on via the timer, the value being passed through is still true. So I tinker some more and decide to see if using my ViewController as the target object will make a difference. I place a new selector in my ViewController like so:-

func toggleTorch() {
    torch.toggleTorch(false)
}

I then update my timer to call this new method within the ViewController :-

NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: "toggleTorch", userInfo: nil, repeats: true)

'Hey Presto' it now works fine and the correct boolean value of false is being passed because I have passed the value myself. Why is this happening? For info, I'v been learning Swift for about three wks so am no expert


Solution

  • NSTimer does not pass the userInfo information as the parameter to the selector. It passes itself as the parameter so that you know which timer was called.

    You should set it up as follows:

    NSTimer.scheduledTimerWithTimeInterval(0.5, target: self.torch, selector: "strobeTimerFired:", userInfo: nil, repeats: true)
    

    And have a method as follows in your Torch object:

    func strobeTimerFired(timer: NSTimer) {
        toggleTorch(false)
    }
    

    In this case you don't need to access the timer object in the strobeTimerFired method, but it's good practise to include it anyway (it can be skipped). You could add additional checking to ensure the timer that triggered the method is the one you expected for example.

    If you did need to use the userInfo for something, (don't think you'd need it in this case), you could pass an object in the original scheduledTimerWithTimeInterval call as the userInfo parameter, and then you would access that in the strobeTimerFired method by accessing: timer.userInfo.