7

I'm trying to implement a stopwatch based on the MVC model.

The stopwatch uses the NSTimer with the selector -(void) tick being called every timeout.

I've tried to make the stopwatch as a model for reusability but I've run into some design problems regarding how to update the view controller for each tick.

First I created a protocol with the tick method and made the view controller its delegate. The view controller then updates the views based on the timer's properties at each tick. elapsedTime is a readonly NSTimeInterval.

It works, but I'm thinking it might be bad design. I'm an Objective-C/Cocoa Touch beginner. Should I be using something like KVO? Or is there a more elegant solution for the model to notify the view controller that elapsedTime has changed?

Toon Krijthe
  • 52,876
  • 38
  • 145
  • 202
Jach0
  • 115
  • 7
  • 1
    Nice first question! Welcome to SO! – jscs Nov 21 '11 at 21:31
  • What exactly is the relationship between the timer and the view controller? Is the timer owned by the VC? – jscs Nov 21 '11 at 21:32
  • Thank you :) The Timer is owned by the VC, yes. I've implemented an IntervalTimer that inherits from Timer and then the VC owns the IntervalTimer instead - the IntervalTimer is actually the one giving me a bit of trouble. – Jach0 Nov 22 '11 at 09:13

3 Answers3

5

The timer is a good way to make sure that you update your user interface periodically, but don't use it to keep track of time. NSTimer can drift, and any small errors can accumulate if you use a timer to accumulate seconds.

Instead, use NSTimer to trigger a method that updates your UI, but get the real time using NSDate. NSDate will give you millisecond resolution; if you really need better than that, consider this suggestion to use Mach's timing functions. So, using NSDate, your code might be something like this:

- (IBAction)startStopwatch:(id)sender
{
    self.startTime = [NSDate date];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:0.1 
                                                  target:self
                                                selector:@selector(tick:)
                                                userInfo:repeats:YES];
}

- (void)tick:(NSTimer*)theTimer
{
    self.elapsedTime = [self.startTime timeIntervalSinceNow];
    [self updateDisplay];
}

- (IBAction)stopStopwatch:(id)sender
{
    [self.timer invalidate];
    self.timer = nil;
    self.elapsedTime = [self.startTime timeIntervalSinceNow];
    [self updateDisplay];
}

Your code might be a little more sophisticated if you allow restarting, etc., but the important thing here is that you're not using NSTimer to measure total elapsed time.

You'll find additional helpful information in this SO thread.

Community
  • 1
  • 1
Caleb
  • 124,013
  • 19
  • 183
  • 272
2

I would recommend against KVO for this problem. It introduces a lot of complexity (and several annoying gotchas) for little benefit here. KVO is important in cases where you need to ensure absolutely minimal overhead. Apple uses it a lot in cases for low-level, high-performance objects like layers. It is the only generally-available solution that offers zero-overhead when there is no observer. Most of the time, you don't need that. Handling KVO correctly can be tricky, and the bugs it can create are annoying to track down.

There's nothing wrong with your delegate approach. It's correct MVC. The only thing you need to really worry about is that NSTimer doesn't make strong promises about when it's called. A repeating timer is even allowed to skip in some cases. To avoid that problem, you generally want to calculate elapsedTime based on the current time rather than by incrementing it. If the timer can pause, then you need to keep an accumulator and a "when did I last start" date.

If you need higher-accuracy or lower-cost timers, you can look at dispatch_source_set_timer(), but for a simple human-targeted stopwatch, NSTimer is fine, and an excellent choice for a simple project.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • I'm aware of the problem with the skipping timer and using incrementation. I'm using NSDate and in setElapsedTime I compare NSDates (+ an offset when pause is used) – Jach0 Nov 22 '11 at 09:05
  • I'm trying to implement an interval countdown timer that inherits from the stopwatch and has some extra attributes including workInterval and restInterval, but the labels in my ViewController get's updated a bit odd. I thought it might have something to do with the sequential calls between the ViewController's tick, the Timers tick and the IntervalTimers tick. – Jach0 Nov 22 '11 at 09:11
0

Lately, I have been using using blocks instead of plain old @selector's. It creates better and code and keeps the logic on the same location.

There's no native blocks support in NSTimer, but I used a category from https://gist.github.com/250662/d4f99aa9bde841107622c5a239e0fc6fa37cb179

Without the return selector, you keep the code in one spot:

__block int seconds = 0;
    NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:1 
                                                 repeats:YES 
                                              usingBlock:^(NSTimer *timer) {

                                                seconds++;
                                                // Update UI

                                                if (seconds>=60*60*2) {
                                                  [timer invalidate];
                                                }




}];  
aporat
  • 5,922
  • 5
  • 32
  • 54