3

This is my code for an iPhone stopwatch. It works as expected and stops and resumes when the buttons are clicked.

When I hit "Stop", however, the timer won't stop running in the background, and when I hit "Start" to resume it, it will update the time and skip to where it is currently instead of resuming from the stopped time.

How can I stop the NSTimer? What is causing this to occur?

@implementation FirstViewController;
@synthesize stopWatchLabel;

NSDate *startDate;
NSTimer *stopWatchTimer;
int touchCount;


-(void)showActivity {

    NSDate *currentDate = [NSDate date];
    NSTimeInterval timeInterval = [currentDate timeIntervalSinceDate:startDate];
    NSDate *timerDate = [NSDate dateWithTimeIntervalSince1970:timeInterval];
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setDateFormat:@"mm:ss.SS"];
    [dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0.0]];
    NSString *timeString=[dateFormatter stringFromDate:timerDate];
    stopWatchLabel.text = timeString;
    [dateFormatter release];
}

- (IBAction)onStartPressed:(id)sender {

    stopWatchTimer = [NSTimer scheduledTimerWithTimeInterval:1/10 target:self selector:@selector(showActivity) userInfo:nil repeats:YES];

    touchCount += 1;
    if (touchCount > 1)
    {
        [stopWatchTimer fire];
    }
    else 
    {
        startDate = [[NSDate date]retain];
        [stopWatchTimer fire];

    }
}

- (IBAction)onStopPressed:(id)sender {
    [stopWatchTimer invalidate];
    stopWatchTimer = nil;
    [self showActivity];
}

- (IBAction)reset:(id)sender; {
    touchCount = 0;
    stopWatchLabel.text = @"00:00.00";
}
jscs
  • 63,694
  • 13
  • 151
  • 195
Grant Wilkinson
  • 1,088
  • 1
  • 13
  • 38

5 Answers5

10

Your calculation of the current display always uses the original start time of the timer, so the display after pausing includes the interval that the timer was paused.

The easiest thing to do would be to store another NSTimeInterval, say secondsAlreadyRun, when the timer is paused, and add that to the time interval you calculate when you resume. You'll want to update the timer's startDate every time the timer starts counting. In reset:, you would also clear out that secondsAlreadyRun interval.

-(void)showActivity:(NSTimer *)tim {

    NSDate *currentDate = [NSDate date];
    NSTimeInterval timeInterval = [currentDate timeIntervalSinceDate:startDate];
    // Add the saved interval
    timeInterval += secondsAlreadyRun;
    NSDate *timerDate = [NSDate dateWithTimeIntervalSince1970:timeInterval];
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setDateFormat:@"mm:ss.SS"];
    [dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0.0]];
    NSString *timeString=[dateFormatter stringFromDate:timerDate];
    stopWatchLabel.text = timeString;
    [dateFormatter release];
}

- (IBAction)onStartPressed:(id)sender {

    stopWatchTimer = [NSTimer scheduledTimerWithTimeInterval:1/10 
                                                      target:self 
                                                    selector:@selector(showActivity:) 
                                                    userInfo:nil 
                                                     repeats:YES];
    // Save the new start date every time
    startDate = [[NSDate alloc] init]; // equivalent to [[NSDate date] retain];
    [stopWatchTimer fire];
}

- (IBAction)onStopPressed:(id)sender {
    // _Increment_ secondsAlreadyRun to allow for multiple pauses and restarts
    secondsAlreadyRun += [[NSDate date] timeIntervalSinceDate:startDate];
    [stopWatchTimer invalidate];
    stopWatchTimer = nil;
    [startDate release];
    [self showActivity];
}

- (IBAction)reset:(id)sender; {
    secondsAlreadyRun = 0;
    stopWatchLabel.text = @"00:00.00";
}

Don't forget to release that startDate somewhere appropriate! Also keep in mind that the documented NSTimer interface is for the method you give it to accept one argument, which will be the timer itself. It seems to work without that, but why tempt fate?

Finally, since you're using that NSDateFormatter so much, you might want to consider making it an ivar or put it in static storage in showActivity:, like so:

static NSDateFormatter * dateFormatter = nil;
if( !dateFormatter ){
    dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setDateFormat:@"mm:ss.SS"];
    [dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0.0]];
}
jscs
  • 63,694
  • 13
  • 151
  • 195
  • Thank you very much, this is a much more efficient way to do it. i have one quick error that hopefully you can help me with, i cant seem to get `secondsAlreadyRun += [NSDate timeIntervalSinceDate:startDate];` to work. what variables do i need or what else is there to do? Thanks! – Grant Wilkinson Nov 12 '11 at 05:24
  • `secondsAlreadyRun` should be an `NSTimeInterval` ivar. What does "can't get it to work" mean exactly? – jscs Nov 12 '11 at 08:22
  • well yes I have `secondsAlreadyRun` as an `NSTimeInterval` ivar but it is returning the error message "invalid operands to binary expression ('NSTimeInterval' (aka 'double') and 'id') and giving the warning "class method '+timeIntervalSinceDate' not found". I must be doing something wrong, thanks for your help – Grant Wilkinson Nov 14 '11 at 17:35
  • Whoops! I compiled this by brain and screwed up that line. There's no class method named `timeIntervalSinceDate:`. The compiler assumes that a method returns `id` when it can't find the declaration. That's why you got that warning. I've fixed that line. – jscs Nov 14 '11 at 19:21
  • Thanks! that fixed it and its working now. One last quick thing is that when i pause the clock on lets say 3.56 seconds when I hit start again it will add the 3 seconds but not the .56 so it will start from 3.00 and not 3.56. i will try so see if i can fix it myself but i was hoping you would maybe know? – Grant Wilkinson Nov 14 '11 at 19:41
  • 1
    `abs` is returning an integer, truncating the `NSTimeInterval`. Use `fabs` or change the right hand side to `[[NSDate date] timeIntervalSinceDate:startDate];` – jscs Nov 14 '11 at 19:47
  • @JoshCaswell, Why its always giving me `-31:-23.-64` ? I'm running it on iPhone5s - iOS 8.3. – Hemang Jul 10 '15 at 12:44
2

So, when the user presses stop, and then start again, you aren't resetting the start time. But when you update the label, you are basing that on the total elapsed time from the original start time to the current time.

So if you run the timer for 10 seconds, stop, wait 10 seconds, and then start again, the timer will show 00:20.00 and start counting again from there.

What you want to do is reset the start time each time the user starts the clock, but then add the elapsed times of all previous runs as well. Or something similar.

BTW, you are leaking the start time every time you reset it now. Minor bug.

EDIT: looks like @Josh Caswell was thinking the same thing, but he types a LOT faster. :)

Firoze Lafeer
  • 17,133
  • 4
  • 54
  • 48
0

You can use NSTimeInterval instead of timer. I have a functional code to pause and stop the timer.

@interface PerformBenchmarksViewController () {

    int currMinute;
    int currSecond;
    int currHour;
    int mins;
    NSDate *startDate;
    NSTimeInterval secondsAlreadyRun;
}

@end

- (void)viewDidLoad
{
    [super viewDidLoad];

    running = false;
}

- (IBAction)StartTimer:(id)sender {

    if(running == false) {

        //start timer
        running = true;
        startDate = [[NSDate alloc] init];
        startTime = [NSDate timeIntervalSinceReferenceDate];
        [sender setTitle:@"Pause" forState:UIControlStateNormal];
        [self updateTime];
    }
    else {
        //pause timer
        secondsAlreadyRun += [[NSDate date] timeIntervalSinceDate:startDate];
        startDate = [[NSDate alloc] init];
        [sender setTitle:@"Start" forState:UIControlStateNormal];
        running = false;
    }
}

- (void)updateTime {

    if(running == false) return;

    //calculate elapsed time
    NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate];
    NSTimeInterval elapsed = secondsAlreadyRun + currentTime - startTime;

    // extract out the minutes, seconds, and hours of seconds from elapsed time:
    int hours = (int)(mins / 60.0);
    elapsed -= hours * 60;
    mins = (int)(elapsed / 60.0);
    elapsed -= mins * 60;
    int secs = (int) (elapsed);

    //update our lable using the format of 00:00:00
    timerLabel.text = [NSString stringWithFormat:@"%02u:%02u:%02u", hours, mins, secs];

    //call uptadeTime again after 1 second
    [self performSelector:@selector(updateTime) withObject:self afterDelay:1];
}

Hope this will help. Thanks

Muhammad Aamir Ali
  • 20,419
  • 10
  • 66
  • 57
0

A timer class I created in Swift for a timer program in which a counter is updated every second from a set time. Answered to illustrate the Swift solution and the NSTimer function.

The timer can be stopped and restarted; it will resume from where it stopped. Events can be intercepted by the delegate for start, stop, reset, end and second events. Just check the code.

import Foundation

protocol TimerDelegate {
  func didStart()
  func didStop()
  func didReset()
  func didEnd()
  func updateSecond(timeToGo: NSTimeInterval)
}

// Inherit from NSObject to workaround Selector bug
class Timer : NSObject {

  var delegate: TimerDelegate?
  var defaultDuration: NSTimeInterval?

  var isRunning: Bool {
    get {
      return self.timer != nil && timer!.valid
    }
  }

  private var secondsToGo: NSTimeInterval = 0

  private var timer: NSTimer?

  init(defaultDuration: NSTimeInterval, delegate: TimerDelegate? = nil) {
    self.defaultDuration = defaultDuration
    self.delegate = delegate
    super.init()
  }

  func start() {
    self.timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: "updateTimer", userInfo: nil, repeats: true)
    self.timer!.tolerance = 0.05

    if delegate != nil { delegate!.didStart() }
  }

  func stop () {
    self.timer?.invalidate()
    self.timer = nil

    if delegate != nil { delegate!.didStop() }
  }

  func reset() {
    self.secondsToGo = self.defaultDuration!

    if delegate != nil { delegate!.didReset() }
  }


  func updateTimer() {
    --self.secondsToGo
    if delegate != nil { delegate!.updateSecond(self.secondsToGo) }

    if self.secondsToGo == 0 {
      self.stop()
      if delegate != nil { delegate!.didEnd() }
    }
  }

}
Yer00n
  • 123
  • 1
  • 8
0

Are you using ARC or not?

If you are using ARC, it looks like you arent using a _strong reference. If you aren't using ARC, it doesn't looking you are retaining a reference to the timer.

I'm posting this from mobile so might be missing something.

EDIT: just noticed you were using release elsewhere, so I'll assume no ARC. You need to retain the timer after setting it to be able to access it later and invalidate.

kcharwood
  • 2,501
  • 19
  • 22