Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Observer pattern for stopwatch

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?

like image 419
Jach0 Avatar asked Nov 21 '11 21:11

Jach0


2 Answers

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.

like image 169
Caleb Avatar answered Sep 30 '22 23:09

Caleb


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.

like image 32
Rob Napier Avatar answered Sep 30 '22 22:09

Rob Napier