24

I am just about finished with my app and beta testing found a bug in the stopwatch portion... The stopwatch uses an nstimer to do the counting and has a table for storing laps, but when the lap table is scrolled the watch stops or pauses and does not make up for the lost time.

This was stalling was eliminated by using:

startingTime = [[NSDate date] timeIntervalSince1970];

to calculate the elapsed time.

but I am still using the NSTimer to trigger every 0.1 secs and that means that the scrolling still stalls the timer even though the elapsed time will be updated correctly in the end... and comparing this to the Apple stopwatch it makes me wonder if that stopwatch has a separate thread just for the elapsed time counting. Does anyone know if that is how it is done?

Now, using the time since the Epoch is working well in one sense, but it complicates the matter of starting, stopping, & restarting the stopwatch

when the watch is stopped the time is stored and used to calculate an offset for when the watch is restarted, but there seems to be some latency introduced and the time jumps ahead visibly when the watch is restarted.

Any thoughts toward the root cause or a solution would be greatly appreciated.

bbum
  • 162,346
  • 23
  • 271
  • 359
echobravo
  • 393
  • 1
  • 3
  • 11

2 Answers2

48

bbum's answer provides a better way to design your application, but if you want your timer to fire regardless of whether the user is manipulating the UI or not you'll need to add it to the tracking mode of the runloop.

Assuming that you're developing for the iPhone, that mode is UITrackingRunLoopMode. If you're developing for the Mac there is a similarly named NSEventTrackingRunLoopMode.

NSRunLoop *runloop = [NSRunLoop currentRunLoop];
NSTimer *timer = [NSTimer timerWithTimeInterval:0.1 target:self selector:@selector(myTimerAction:) userInfo:nil repeats:YES];
[runloop addTimer:timer forMode:NSRunLoopCommonModes];
[runloop addTimer:timer forMode:UITrackingRunLoopMode];
Ashley Clark
  • 8,813
  • 3
  • 35
  • 35
  • 1
    That still won't guarantee accuracy of overall timing. Timers aren't guaranteed to fire at exactly their configured interval. Worse, if each timer firing adds to the overall interval, the amount of error will increase with each timer firing! – bbum Jan 04 '10 at 01:06
  • I didn't intend to argue that they would guarantee anything other than the timers would fire while controls were tracking. Storing the date at the lap start and using that to calculate elapsed time is far more accurate (and simpler IMO) as you had stated in your answer. – Ashley Clark Jan 05 '10 at 00:06
  • +1 Worked for me. bbum's answer didn't make any sense to me. – bentford Jul 30 '11 at 22:17
  • I wonder if there are any caveats to allowing a selector to be fired during tracking. e.g. should UI layout be avoiding during this time? I guess it's not as dangerous as being called from a different thread though... – Joseph Humfrey Dec 12 '12 at 14:28
  • There are very good reasons NOT to give timers to priority of UI tracking. Obviously -- UI tracking will be hurt. Scrolling, zooming, panning and other continuous touch gestures won't be as smooth as they're meant to be. If you only want to record the time or do something very lightweight when the timer fires, that's OK. The timer fires on the same thread and runloop as the UI tracking events, so it is safe to manipulate the UI --- but it is a very bad idea, as I mention before. It simply halts UI tracking. – Motti Shneor Jun 25 '14 at 11:20
22

If the event loop isn't running, any timers will not fire until the event loop can run again. Even if the event loop isn't block, the timer isn't guaranteed to fire at exactly its configured interval. If your timings were based entirely on timers firing, the amount of error will grow over time.

You need to keep track of the duration separately from the firing of the timers. Each time a timer fires, recalculate your duration and redisplay.

For a start/pause/restart/stop type of setup, you generally want to:

  • grab the time upon start (either as an NSDate instance or as an NSTimeInterval value)

  • upon pause or stop, grab the time upon pause/stop. Subtract the start time from this time and you have the interval's duration

  • upon restart, grab the time upon restart but also keep around the already elapsed duration

  • upon pause/stop, grab the time at pause/stop and add the already elapsed duration

In general, doing all of this with NSTimeInterval values -- which are just doubles -- is easiest. However, if you need to keep track of the actual moment in time when the events happened, use NSDate instances instead.

bbum
  • 162,346
  • 23
  • 271
  • 359