0

I'm setting up timers to execute code once each one finished. However, it seems that the timing of NSTimer is not completely precise, after a while, the timers seem to finish a little too early.

I've set up the following to test the deviation.

(The start button is done in the Storyboard and linked to.)

import UIKit

class ViewController: UIViewController {

  @IBOutlet weak var startButton: UIButton!

  //var mainTimer: NSTimer!
  var counter: NSTimeInterval = 0.0

  @IBAction func startButtonPressed(sender: AnyObject) {
    startMainTimer()
    startTimers()
  }

  func startMainTimer() {
    let mainTimer = NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: #selector(ViewController.count), userInfo: nil, repeats: true)
    NSRunLoop.mainRunLoop().addTimer(mainTimer, forMode: NSRunLoopCommonModes)
  }

  func count() {
    counter += 0.01
  }

  func startTimers() {
    var lastTimeInterval: NSTimeInterval = 0.0

    for _ in 0..<50 {
      let timeInterval = lastTimeInterval + 1.0
      lastTimeInterval = timeInterval

      // Not setting up a repeating timer as intervals would be different in my original project where the deviation will be even worse.
      let timer = NSTimer.scheduledTimerWithTimeInterval(timeInterval, target: self, selector: #selector(ViewController.printTime), userInfo: nil, repeats: false)
      NSRunLoop.mainRunLoop().addTimer(timer, forMode: NSRunLoopCommonModes)
    }
  }

  func printTime() {
    print(counter)
  }
}

After while, the timers will be early a tenth of a second or even more. Should I be using NSDate for this to have better timing, or would that be overly complicated? How would the above look with NSDate?

Any help / pointers much appreciated!

nontomatic
  • 2,003
  • 2
  • 24
  • 38
  • Have a look at the NSTimer documentation: *"... the effective resolution of the time interval for a timer is limited to on the order of 50-100 milliseconds."* – And see http://stackoverflow.com/questions/31375361/format-realtime-stopwatch-timer-to-the-hundredth-using-swift for an alternative – Martin R Apr 01 '16 at 13:52

1 Answers1

1

It will never be perfect, but you can stop it from compounding by scheduling the next call at the end of the target selector. Calculate a new interval each time based on the current time and when you want it to trigger.

EDIT: I grabbed some code from a very old project to give the idea (that's why it's in Obj-C) -- You want something like this but not exactly:

- (void)constantIntervalTimer {
   static const NSTimeInterval secondsBetweenMessageSend = 1.0;

   if ( !self.timerShouldStop ) {
      NSDate *startTime = [NSDate date];

      // your code here, it should take less than a
      // second to run or you need to increase the timer interval

      // figure out the right time to preserve the interval
      NSTimeInterval waitTime = secondsBetweenMessageSend -
           [[NSDate date] timeIntervalSinceDate:startTime];

      // in case your code runs in more than the interval,
      // we just call back immediately
      if (waitTime < 0.0) 
         waitTime = 0.0;

      [NSTimer scheduledTimerWithTimeInterval: waitTime
            target:self selector:@selector(constantIntervalTimer) 
            userInfo:nil repeats:NO];
    }
}

Once you call this message, it will keep calling itself every second until you set a property called timerShouldStop to YES. You must declare and define this property and set it to NO in your init for this message to work.

Lou Franco
  • 87,846
  • 14
  • 132
  • 192
  • Thank you for the answer, Lou! Would this mean using NSDate to get the current time and set the execution time? I guess I would have to check the current time constantly in a loop. I'd have to look into NSDate then, I've never used it before. Would I still use NSTimer at all? – nontomatic Apr 01 '16 at 14:02