0

So I have a view controller with a timer. It has one button. When the view loads for the very first time, the button should say "start."

When it is tapped, "start"->"pause".

Another tap, "pause"->"resume".

Another tap, "resume"->"pause".

Since I want the timer to be accurate, I detached it from the main thread (I think I chose the right method, I would appreciate some clarification). But it seems like detaching it from the thread actually calls the method...which makes the button start with "pause" instead of start. How do I fix this?

By the way, default value (loads with) for testTask.showButtonValue is 1.

- (void)viewDidLoad {
    [super viewDidLoad];

    [NSThread detachNewThreadSelector:@selector(startTimer:) toTarget:self withObject:nil];

    if (testTask.showButtonValue == 1) {
        [startButton setTitle:@"Start" forState:UIControlStateNormal];
    } else if (testTask.showButtonValue == 2) {
        [startButton setTitle:@"Pause" forState:UIControlStateNormal];
    } else if (testTask.showButtonValue == 3){
        [startButton setTitle:@"Resume" forState:UIControlStateNormal];
    }
}
-(IBAction)startTimer:(id)sender{
    if (testTask.showButtonValue == 1) {
        [startButton setTitle:@"Pause" forState:UIControlStateNormal];
        timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
        testTask.showButtonValue = 2;
    } else if (testTask.showButtonValue == 2) {
        [startButton setTitle:@"Resume" forState:UIControlStateNormal];
        [timer invalidate];
        timer = nil;
        testTask.showButtonValue = 3;
    } else if (testTask.showButtonValue == 3){
        [startButton setTitle:@"Pause" forState:UIControlStateNormal];
        timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
        testTask.showButtonValue = 2;
    }
}
-(void)timerAction:(NSTimer *)t
{
    if(testTask.timeInterval == 0)
    {
        if (self.timer)
        {
            [self timerExpired];
            [self.timer invalidate];
            self.timer = nil;
        }
    }
    else
    {
        testTask.timeInterval--;
    }
    NSUInteger seconds = (NSUInteger)round(testTask.timeInterval);
    NSString *string = [NSString stringWithFormat:@"%02u:%02u:%02u",
                        seconds / 3600, (seconds / 60) % 60, seconds % 60];
    timerLabel.text = string;
    NSLog(@"%f", testTask.timeInterval);
}
EvilAegis
  • 733
  • 1
  • 9
  • 18

2 Answers2

1

Calling detachNewThreadSelector will create a new thread and perform the selector mentioned, immediately afer the call.

For fixing your issue change your methods like:

- (void)viewDidLoad
{
    [super viewDidLoad];

    [NSThread detachNewThreadSelector:@selector(startTimer:) toTarget:self withObject:nil];
}
-(IBAction)startTimer:(id)sender
{
    if (testTask.showButtonValue == 1) {
        [startButton setTitle:@"Start" forState:UIControlStateNormal];
        timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
        testTask.showButtonValue = 3;
    } else if (testTask.showButtonValue == 2) {
        [startButton setTitle:@"Resume" forState:UIControlStateNormal];
        [timer invalidate];
        timer = nil;
        testTask.showButtonValue = 3;
    } else if (testTask.showButtonValue == 3){
        [startButton setTitle:@"Pause" forState:UIControlStateNormal];
        timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
        testTask.showButtonValue = 2;
    }
}
Midhun MP
  • 103,496
  • 31
  • 153
  • 200
  • Isn't UIKit being used in a thread other than mainthread in this code? – hfossli Jul 30 '13 at 21:04
  • This worked, but before I make it the correct answer, you did delete part of the code in viewDidLoad :O. When the user presses back and goes to the view again, it doesn't show the correct label for the button :/. I'm not sure how to fix that.. – EvilAegis Jul 30 '13 at 21:07
  • Hmm..it seems like if I start it and then press back, and then go to the view again, it actually gets detached to the thread TWICE (or more), which makes the timer go down 2x as fast :O!! Hm... – EvilAegis Jul 30 '13 at 21:10
  • @hfossli: yes, you are correct. UI elements should not be manipulated in other threads. – Midhun MP Jul 31 '13 at 04:40
1

I suggest going about this in a different way:

@interface SNDViewController ()
@property (weak, nonatomic) IBOutlet UIButton *startButton;
@property (weak, nonatomic) IBOutlet UILabel *timerLabel;
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, assign) NSTimeInterval accumulativeTime;
@property (nonatomic, assign) NSTimeInterval currentReferenceTime;
@end

@implementation SNDViewController

EDIT: when the view loads, initialise self.accumulativeTime and update the timer label. The initialisation of accumulativeTime variable should really be done in the appropriate init* method, e.g. initWithNibName:bundle:. It is at this point that you would read the timer value from core data.

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.accumulativeTime = 300;
    [self updateTimerLabelWithTotalTime:self.accumulativeTime];
}

- (IBAction)changeTimerState:(UIButton *)sender
{
    if (self.timer == nil) {
        self.currentReferenceTime = [NSDate timeIntervalSinceReferenceDate];
        [self.timer invalidate];
        self.timer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(updateTimer:) userInfo:nil repeats:YES];
    } else {
        //Pause the timer and track accumulative time.
        NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate];
        [self.timer invalidate];
        self.timer = nil;
        NSTimeInterval timeSinceCurrentReference = now - self.currentReferenceTime;

EDIT: subtract timeSinceCurrentReference from accumalitiveTime, because this is a countdown timer. Also added a comment for saving to core data if necessary.

        self.accumulativeTime -= timeSinceCurrentReference;
        [self updateTimerLabelWithTotalTime:self.accumulativeTime];

        //Optionally save self.accumulativeTime to core data for future use.
    }

    NSString *buttonTitle = (self.timer != nil) ? @"Pause" : @"Resume";
    [self.startButton setTitle:buttonTitle forState:UIControlStateNormal];
}

You don't need to store any unnecessary state, such as the showButtonValue variable. Instead you can base your decision of whether to pause or resume on whether or not self.timer == nil. If there is no timer running, then grab the current reference time and start a new timer. The timer is scheduled to fire every 0.01 seconds, which will hopefully make it accurate to 0.1 seconds. You never need to change the button's title to "Start". It is either "Pause" or "Resume". When the timer is paused by the user, we dispose of self.timer and update the timer label with the most accurate time.

- (void)updateTimer:(id)sender
{
    NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate];
    NSTimeInterval timeSinceCurrentReference = now - self.currentReferenceTime;

EDIT: subtract timeSinceCurrentReference from self.accumulativeTime to get the totalTime (i.e. totalTime decreases as time goes by).

    NSTimeInterval totalTime = self.accumulativeTime - timeSinceCurrentReference;

    if (totalTime <= 0) {
        totalTime = 0;
        [self.timer invalidate];
        self.timer = nil;

        //What to do when we reach zero? For example, we could reset timer to 5 minutes:
        self.accumulativeTime = 300;
        totalTime = self.accumulativeTime;
        [self.startButton setTitle:@"Start" forState:UIControlStateNormal];
    }

    [self updateTimerLabelWithTotalTime:totalTime];
}

Each time the timer is fired, we get the total time by finding the difference between now and self.currentReferenceTime and add it to self.accumulativeTime.

- (void)updateTimerLabelWithTotalTime:(NSTimeInterval)totalTime
{
    NSInteger hours = totalTime / 3600;
    NSInteger minutes = totalTime / 60;
    NSInteger seconds = totalTime;
    NSInteger fractions = totalTime * 10;

    self.timerLabel.text = [NSString stringWithFormat:@"%02u:%02u:%02u.%01u", hours, minutes % 60, seconds % 60, fractions % 10];
}

@end

The method - (IBAction)changeTimerState:(UIButton *)sender is called by the UIButton on the "Touch Up Inside" event (UIControlEventTouchUpInside).

You don't need to do anything in viewDidLoad.

Also, and importantly, this is all done on the main thread. If anything gets in the way of the main thread updating the timer label, then the text visible to the user may be inaccurate, but when it is updated, you can be sure that it will be accurate again. It depends on what else your app is doing. But since all UI updates must be done on the main thread there is really no avoiding this.

Hope this helps. Let me know if anything is unclear.

(Xcode project available here: https://github.com/sdods3782/TVTTableViewTest)

Sam
  • 5,892
  • 1
  • 25
  • 27
  • The thing is, we need to use the testTask.timeInterval property to set the countdown timer instead of what you are doing right now because it is stored in Core Data and the timeInterval needs to be saved. How would I add that? – EvilAegis Jul 30 '13 at 22:48
  • Also, what do you mean by "You never need to change the button's title to "Start". It is either "Pause" or "Resume"". It should be Start when the view is loaded for the first time :O. – EvilAegis Jul 30 '13 at 22:51
  • Lastly, what do you mean by this: "The timer is scheduled to fire every 0.01 seconds, which will hopefully make it accurate to 0.1 seconds." Will this cause a great deal of memory usage? – EvilAegis Jul 30 '13 at 22:53
  • I mean while the app is running, you don't need to reset the button title to "Start" so either set it up in the nib, or set it in viewDidLoad. It doesn't need to be set in any of the processing methods. – Sam Jul 31 '13 at 07:15
  • Firing the timer every 0.01 seconds certainly wouldn't use up more memory. It will use a tiny bit more CPU time, but for what the `updateTimer:` method is doing, I wouldn't worry about it. – Sam Jul 31 '13 at 07:17
  • At what point are you saving the testTask.timeInterval to core data? Is it only when you pause the timer? If so, then you only need to set it when the timer is paused, and you can read from it into one of the local variables (i.e. `self.accumulativeTime`) on viewDidLoad (or awakeFromNib, which should be used for any non-view-related initialisation. Note that my example above is a count-up (stopwatch-style) timer, I'll take a look later to convert it to countdown timer. – Sam Jul 31 '13 at 07:21
  • Please see my edits - it is now a countdown timer with the value initialised to 300 seconds. You could read this initial value from core data and save it back to core data when you're finished. – Sam Jul 31 '13 at 08:45
  • i bascially got rid of the detachNewThreadSelector and just made it to check if the timer existed or not – EvilAegis Aug 05 '13 at 02:27