7

I am trying to use NSTimer to create a Stop-watch style timer that increments every 0.1 seconds, but it seems to be running too fast sometimes ..

This is how I've done it:

Timer =[NSTimer scheduledTimerWithTimeInterval: 0.1 target:self selector:@selector(updateTimeLabel) userInfo:nil repeats: YES];

and then:

-(void)updateTimeLabel
{
    maxTime=maxTime+0.1;
    timerLabel.text =[NSString stringWithFormat:@"%.1f Seconds",maxTime];
}

This will display the value of the timer in the Label, and I can later utilize maxTime as the time when the Timer is stopped ...

THe problem is that it runs very inaccurately.

Is there a method where I can make sure that NSTimer fires strictly every 0.1 seconds accurately ? I know that NSTimer isn't accurate , and I'm asking for a tweak to make it accurate.

THanks

user2308343
  • 107
  • 2
  • 7
  • In what method do you create the timer? – rob mayoff Jul 01 '13 at 21:57
  • 2
    If you are timing events, you should call `CFAbsoluteTimeGetCurrent()` when you start your timer, then call it again at the end of the interval. Subtract the 2 values and you will get the elapsed time in seconds. – nielsbot Jul 01 '13 at 22:10
  • i.e. the actual time elapsed is independent of the time you display. so you can update your display approximately every 0.1s, but it doesn't really matter if your updates are off a little bit. Then when your timer is paused, update the display again using the method in my last comment. – nielsbot Jul 01 '13 at 22:11
  • Also, if you want your display to update at least every 0.1s, I would set my timer to update every 0.05s. – nielsbot Jul 01 '13 at 22:12
  • Rob - the timer is created in the *.h file, and initialized with viewdidLoad – user2308343 Jul 01 '13 at 22:19
  • niels - no, as you see i'm displaying the time live in a label – user2308343 Jul 01 '13 at 22:19
  • I posted code that illustrates what you want. – nielsbot Jul 01 '13 at 22:19
  • The repeats should be quite accurate, if you hold your mouth right. – Hot Licks Jul 01 '13 at 22:43
  • "THe problem is that it runs very accurately." -- That's a problem? – Hot Licks Jul 01 '13 at 22:46
  • @user2308343 You say your timer is initialized in `viewDidLoad`. That's ok. My main point is that you don't want to try to `invalidate` the timer in `dealloc`. You generally do that in `viewWillDisappear`. And for the sake of symmetry, you'd therefore want to schedule the timer in `viewDidAppear` rather than `viewDidLoad`. (If you, for example, push to another scene, you'll get `viewWillDisappear`, but when you pop back, `viewDidLoad` will _not_ get called again, hence the common pattern to do it in `viewWillAppear`.) – Rob Jul 01 '13 at 23:45

4 Answers4

10

According to the NSTimer documentation, it is not meant to be accurate.

Because of the various input sources a typical run loop manages, the effective resolution of the time interval for a timer is limited to on the order of 50-100 milliseconds. If a timer’s firing time occurs during a long callout or while the run loop is in a mode that is not monitoring the timer, the timer does not fire until the next time the run loop checks the timer. Therefore, the actual time at which the timer fires potentially can be a significant period of time after the scheduled firing time.

You may want to use the dispatch_after function from GCD, which is suggested by the official documentation for this exact purpose (creating a timer).

If you want to perform a block once after a specified time interval, you can use the dispatch_after or dispatch_after_f function.


By the way, I agree with Caleb's answer. You probably are going to solve your problems if you don't accumulate error like your doing right now. If you store the start date and recalculate the time at every iteration using the -timeIntervalSince: method, you're gonna end up with an accurate UI update, regardless of the timer precision.

Gabriele Petronella
  • 106,943
  • 21
  • 217
  • 235
  • Gabriele - I don't need 'Time-since' , because i need to display the time continuously to the user in a label ... – user2308343 Jul 01 '13 at 22:20
  • 2
    You need it in order not to accumulate error. If you recompute the time at every "iteration" the error is not accumulated. Otherwise you're summing up every single imprecision of `NSTimer`, ending up with a very imprecise measure. – Gabriele Petronella Jul 01 '13 at 22:22
  • 4
    NSTimer isn't "imprecise" if you use repeat. – Hot Licks Jul 01 '13 at 22:44
  • @HotLicks: Do you have any reference for this claim? Why would the repeat option negate the error accumulation? – Darren Black Aug 31 '16 at 12:39
5
maxTime=maxTime+0.1;

This is the wrong way to go. You don't want to use a timer to accumulate the elapsed time because you'll be accumulating error along with it. Use the timer to periodically trigger a method that calculates the elapsed time using NSDate, and then updates the display. So, change your code to do something instead:

maxTime = [[NSDate date] timeIntervalSince:startDate];
Caleb
  • 124,013
  • 19
  • 183
  • 272
  • but I'm supposing that the NSTimer trigger is the thing that's happening every 0.1 seconds , and it is increasing maxTime -- maxTime follows the timer and doesn't set it – user2308343 Jul 01 '13 at 22:16
  • 4
    I understand, but the timer may be off by 50ms one way or the other. That's not a large error for many things, but it'll add up quickly. Nobody cares whether you update the display *exactly* every 0.1 second, but when you *do* get around to updating the display, it should be as accurate as possible. You achieve that by calculating based on a the difference between the current time and a fixed start time, thus avoiding the accumulation of error. – Caleb Jul 01 '13 at 22:21
5

Here's a class you can use to do what you want:

@interface StopWatch()
@property ( nonatomic, strong ) NSTimer * displayTimer ;
@property ( nonatomic ) CFAbsoluteTime startTime ;
@end

@implementation StopWatch

-(void)dealloc
{
    [ self.displayTimer invalidate ] ;
}

-(void)startTimer
{
    self.startTime = CFAbsoluteTimeGetCurrent() ;
    self.displayTimer = [ NSTimer scheduledTimerWithTimeInterval:0.05 target:self selector:@selector( timerFired: ) userInfo:nil repeats:YES ] ;
}

-(void)stopTimer
{
    [ self.displayTimer invalidate ] ;
    self.displayTimer = nil ;

    CFAbsoluteTime elapsedTime = CFAbsoluteTimeGetCurrent() - self.startTime ;
    [ self updateDisplay:elapsedTime ] ;
}

-(void)timerFired:(NSTimer*)timer
{
    CFAbsoluteTime elapsedTime = CFAbsoluteTimeGetCurrent() - self.startTime ;
    [ self updateDisplay:elapsedTime ] ;
}

-(void)updateDisplay:(CFAbsoluteTime)elapsedTime
{
    // update your label here
}

@end

The key points are:

  1. do your timing by saving the system time when the stop watch is started into a variable.
  2. when the the stop watch is stopped, calculate the elapsed time by subtracting the stop watch start time from the current time
  3. update your display using your timer. It doesn't matter if your timer is accurate or not for this. If you are trying to guarantee display updates at least every 0.1s, you can try setting your timer interval to 1/2 the minimum update time (0.05s).
nielsbot
  • 15,922
  • 4
  • 48
  • 73
  • Hi Niels ... Seems to work, but the label is displaying a very large value , even though i used the relative value for elapsed time ... any idea why ? – user2308343 Jul 01 '13 at 22:36
  • +1 But a few thoughts: 1. You can't `invalidate` the timer in `dealloc`, because the strong reference of the timer will prevent `dealloc` from ever getting called. You've got a strong reference cycle here. You should probably create timer in `viewWillAppear` and `invalidate` in `viewWillDisappear`. 2. I might also suggest a `CADisplayLink` rather than a `NSTimer`, but the idea is the same. – Rob Jul 01 '13 at 22:37
  • In other words (As i understood) ... I don't really need to pass 'elapsedTime' to the display method, since it is fired accurately now ? My initial updateTimeLabel would still work since the update is fine using the absolute difference in timerFired – user2308343 Jul 01 '13 at 22:45
  • 1
    @user2308343 I think the take-home message of nielsbot's answer is that you shouldn't be maintaining your own counter. You shouldn't be relying on the precision of timers. You should call your timer with a frequency high enough to get the desired UX, but calculate the elapsed time using differences between the start `CFAbsoluteTimeGetCurrent()` and the current value. – Rob Jul 01 '13 at 22:58
  • what @Rob said. Also @Rob: I wrote a simple program to test this and `-dealloc` is called. I think using `CADisplayLink()` might be CPU overkill, but you will get a more accurate on-screen display, right? – nielsbot Jul 01 '13 at 23:06
  • @user2308343 check your display code for bugs. how are you formatting the time value for display? I tested this code and it gives the correct result for me. – nielsbot Jul 01 '13 at 23:12
  • @nielsbot On the `CADisplayLink`, two observations: 1. If you use `NSRunLoopCommonModes` you can have it continue running while other stuff is going on. For example, if you're scrolling a tableview at the same time, standard timer will stop until the scrolling is done. 2. If you're worried about computational load, you can set the `frameInterval` to a higher value. But it's not called until it's ready to redraw the screen, so I wouldn't be so worried about computational overhead. He'd should only be updating his label if the time string changed, anyway (even if using timer). – Rob Jul 01 '13 at 23:35
  • @nielsbot On the `invalidate` in `dealloc`, this pattern of trying to invalidate a repeating timer in `dealloc` is a common retain cycle. (The `dealloc` method is called when there are no more strong references, so trying to resolve the timer's `strong` reference in a `dealloc` method that isn't called until there are no more strong references will not work.) In my test, `dealloc` does not get called. (Don't get me wrong. Otherwise I quite like your answer.) – Rob Jul 01 '13 at 23:48
2

NSTimer is not guaranteed to be accurate, although in practice it usually is (if you're not doing anything else on your main thread...). However, it's perfectly reasonable for updating a display... just don't use the callback to calculate your timer. Save the current time when you start your timer, and get the difference between now and when you started every time the timer fires. Then it doesn't really matter how accurately NSTimer is firing, it only impacts how many times a second your on screen display updates.

escrafford
  • 2,373
  • 16
  • 19