Search code examples
iosswiftcocoa-touchnstimerfoundation

NSTimer stops when view controller is not the selected tab or not showing


I have a strange problem with my countdown timer. It fires off normally when my start button is hit, and is reinstantiated correctly when I close the app and relaunch it again. However, when I select a different tab and stay there for a while, it stops counting down, then resumes counting down from where it left off when I show the countdown tab again.

For example, if the timer is now at 00:28:00 (format is HH:MM:SS), select some other tab, stay there for 5 minutes, and then go back to the timer tab, it's only at the 27:52 mark. When I close the app (double tap the home button, swipe up my app) and reopen it, it starts off at a more reasonable 22:50 mark.

I've posted the relevant code from the class to show how I'm setting up the timer, but a summary of what it does:

  • I have plus (+) and minus (-) buttons somewhere that, when tapped, call recalculate().
  • recalculate() fires off a CalculateOperation.
  • A CalculateOperation computes for the starting HH:MM:ss based on the addition/removal of a new record. The successBlock of a CalculateOperation executes in the main thread.
  • A CalculateOperation creates the NSTimer in the successBlock if the countdownTimer hasn't been created yet.
  • The NSTimer executes decayCalculation() every 1 second. It reduces the calculation.timer by 1 second by calling tick().

Code:

class CalculatorViewController: MQLoadableViewController {

    let calculationQueue: NSOperationQueue // Initialized in init()
    var calculation: Calculation?
    var countdownTimer: NSTimer?

    func recalculate() {
        if let profile = AppState.sharedState.currentProfile {
            // Cancel all calculation operations.
            self.calculationQueue.cancelAllOperations()

            let calculateOperation = self.createCalculateOperation(profile)
            self.calculationQueue.addOperation(calculateOperation)
        }
    }

    func decayCalculation() {
        if let calculation = self.calculation {
            // tick() subtracts 1 second from the timer and adjusts the
            // hours and minutes accordingly. Returns true when the timer
            // goes down to 00:00:00.
            let timerFinished = calculation.timer.tick()

            // Pass the calculation object to update the timer label
            // and other things.
            if let mainView = self.primaryView as? CalculatorView {
                mainView.calculation = calculation
            }

            // Invalidate the timer when it hits 00:00:00.
            if timerFinished == true {
                if let countdownTimer = self.countdownTimer {
                    countdownTimer.invalidate()
                }
            }
        }
    }

    func createCalculateOperation(profile: Profile) -> CalculateOperation {
        let calculateOperation = CalculateOperation(profile: profile)

        calculateOperation.successBlock = {[unowned self] result in
        if let calculation = result as? Calculation {
            self.calculation = calculation

            /* Hide the loading screen, show the calculation results, etc. */

            // Create the NSTimer.
            if self.countdownTimer == nil {
                self.countdownTimer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("decayCalculation"), userInfo: nil, repeats: true)
            }
        }
    }

        return calculateOperation
    }

}

Solution

  • Well, if I leave the app in some other tab and not touch the phone for a while, it eventually goes to sleep, the app resigns active, and enters the background, which stops the timer.

    The solution was to set my view controller as a listener to the UIApplicationWillEnterForegroundNotification and call recalculate to correct my timer's countdown value.