简体   繁体   中英

How can I programmatically pause an NSTimer?

I'm using an NSTimer to do some rendering in an OpenGL based iPhone app. I have a modal dialog box that pops up and requests user input. While the user is providing input I'd like to "pause" ie something like this:

[myNSTimer pause];

I'm using this syntax because I've been doing things like:

[myNSTimer invalidate];

when I want it to stop.

How can I programmatically pause the NSTimer?

Just thought of updating minor fixes to kapesoftware 's answer:

NSDate *pauseStart, *previousFireDate;

-(void) pauseTimer:(NSTimer *)timer { 

    pauseStart = [[NSDate dateWithTimeIntervalSinceNow:0] retain];

    previousFireDate = [[timer fireDate] retain];

    [timer setFireDate:[NSDate distantFuture]];
}

-(void) resumeTimer:(NSTimer *)timer {

    float pauseTime = -1*[pauseStart timeIntervalSinceNow];

    [timer setFireDate:[previousFireDate initWithTimeInterval:pauseTime sinceDate:previousFireDate]];

    [pauseStart release];

    [previousFireDate release];
}

From here:

http://discussions.apple.com/thread.jspa?threadID=1811475&tstart=75

"You can store the amount of time that has passed since the timer started... When the timer starts store the date in an NSDate variable. Then when the user switches... use the method timeIntervalSinceNow in the NSDate class to store how much time has passed... note that this will give a negative value for timeIntervalSinceNow. When the user returns use that value to set an appropriate timer.

I don't think there's a way to pause and restart a timer. I faced a similar situation. "

The way I accomplished this was by storing the NSTimer's firing date in a variable and then setting the firing date to INFINITY.

When I pause:

pauseStart = [[NSDate dateWithTimeIntervalSinceNow:0] retain];
previousFiringDate = [timer firingDate];
[timer setFiringDate:INFINITY]

When I unpause I add the amount of time that the timer was paused to the previous firing date:

float pauseTime = -1*[pauseStart timeIntervalSinceNow];
[timer setFireDate:[previousFireDate initWithTimeInterval:pauseTime sinceDate:previousFireDate]];

This made it a lot easier than worrying about invalidating and reinitializing the timer. I had a lot of timers so I just put them all in an array and did this to all of them. I subclassed the NSTimer in order to keep the previousFiringDate associated with that timer.

This was also better than using a BOOL of whether or not it was paused so actions wouldn't take place if the BOOL was set. This is because if something was about to happen before you pause, it won't happen after you unpause it because it will have just been skipped.

The easiest way I managed to do it was like this:

-(void) timerToggle{
    if (theTimer == nil) {
        float theInterval = 1.0/30.0;
        theTimer = [NSTimer scheduledTimerWithTimeInterval:theInterval target:self selector:@selector(animateBall:) userInfo:nil repeats:YES];
    } else {
        [theTimer invalidate];
        theTimer = nil;
    }
}

In my case, I had a variable 'seconds', and another 'timerIsEnabled'. When I wanted to pause the timer, just made the timerIsEnabled as false. Since the seconds variable was only incremented if timerIsEnabled was true, I fixed my problem. Hope this helps.

Here is a category on NSTimer that allows pause and resume functionality.

Header:

#import <Foundation/Foundation.h>

@interface NSTimer (CVPausable)

- (void)pauseOrResume;
- (BOOL)isPaused;

@end

Implementation:

#import "NSTimer+CVPausable.h"
#import <objc/runtime.h>

@interface NSTimer (CVPausablePrivate)

@property (nonatomic) NSNumber *timeDeltaNumber;

@end

@implementation NSTimer (CVPausablePrivate)

static void *AssociationKey;

- (NSNumber *)timeDeltaNumber
{
    return objc_getAssociatedObject(self, AssociationKey);
}

- (void)setTimeDeltaNumber:(NSNumber *)timeDeltaNumber
{
    objc_setAssociatedObject(self, AssociationKey, timeDeltaNumber, OBJC_ASSOCIATION_RETAIN);
}

@end


@implementation NSTimer (CVPausable)

- (void)pauseOrResume
{
    if ([self isPaused]) {
        self.fireDate = [[NSDate date] dateByAddingTimeInterval:[self.timeDeltaNumber doubleValue]];
        self.timeDeltaNumber = nil;
    }
    else {
        NSTimeInterval interval = [[self fireDate] timeIntervalSinceNow];
        self.timeDeltaNumber = @(interval);
        self.fireDate = [NSDate distantFuture];
    }
}

- (BOOL)isPaused
{
    return (self.timeDeltaNumber != nil);
}

@end

Old question, but I just wrote a lightweight utility class to accomplish exactly what OP is looking for.

https://github.com/codeslaw/CSPausibleTimer

Supports repeating timers as well. Hope it comes in handy!

Based off @Thiru and @kapesoftware's answers, I made an Objective-C category on NSTimer using Associated Objects for pausing and resuming the timer.

#import <objc/runtime.h>

@interface NSTimer (PausableTimer)

@property (nonatomic, retain) NSDate *pausedDate;

@property (nonatomic, retain) NSDate *nextFireDate;

-(void)pause;

-(void)resume;

@end

...

@implementation NSTimer (PausableTimer)

static char * kPausedDate = "pausedDate";
static char * kNextFireDate = "nextFireDate";

@dynamic pausedDate;
@dynamic nextFireDate;

-(void)pause {

  self.pausedDate = [NSDate date];

  self.nextFireDate = [self fireDate];

  [self setFireDate:[NSDate distantFuture]];
}

-(void)resume
{
  float pauseTime = -1*[self.pausedDate timeIntervalSinceNow];

  [self setFireDate:[self.nextFireDate initWithTimeInterval:pauseTime sinceDate:self.nextFireDate]];
}

- (void)setPausedDate:(NSDate *)pausedDate
{
  objc_setAssociatedObject(self, kPausedDate, pausedDate, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSDate *)pausedDate
{
  return objc_getAssociatedObject(self, kPausedDate);
}

- (void)setNextFireDate:(NSDate *)nextFireDate
{
  objc_setAssociatedObject(self, kNextFireDate, nextFireDate, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSDate *)nextFireDate
{
  return objc_getAssociatedObject(self, kNextFireDate);
}

It keeps things tidy and means you don't have to create instance variables or properties just to pause and resume your timers.

Well, I find this as the best method to implement the pausing a timer thing. Use a static variable to toggle pause and resume. In pause, invalidate the timer and set it to nil and in the resume section reset the timer using the original code by which it was started.

The following code works on the response to a pause button click.

-(IBAction)Pause:(id)sender
{
    static BOOL *PauseToggle;
    if(!PauseToggle)
    {
        [timer invalidate];
        timer = nil;
        PauseToggle = (BOOL *) YES;
    }
    else
    {
        timer = [NSTimer scheduledTimerWithTimeInterval:0.04 target:self selector:@selector(HeliMove) userInfo:nil repeats:YES];
        PauseToggle = (BOOL *) NO;
    }
}

The code below is for use with a scrollview, that brings content on at regular intervals, and loops back to the front on completion, but the same logic can be used for any pause/restart button.

The timer fires on load (initiateFeatureTimer), and pauses for user-interaction (will begin dragging method), allowing the user to look at content for as long as she likes. When user lifts her finger (will end dragging) the featurePaused boolean is reset, but also, you have the option of adding in a little more of a delay.

This way, the scroller doesn't move off instantly on finger lift, it's more reactive to the user.

//set vars, fire first method on load.
featureDelay = 8; //int
featureInt = featureDelay-1; //int
featurePaused = false; //boolean

-(void)initiateFeatureTimer {
    featureTimer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(moveFeatureScroller) userInfo:nil repeats:true];
    [featureTimer fire];
}
 -(void)moveFeatureScroller {

    if (!featurePaused){ //check boolean
        featureInt++;

         if (featureInt == featureDelay){
             featureInt = 0; //reset the feature int

            /*//scroll logic
            int nextPage = (featurePage + 1) * w;
            if (featurePage == features.count-1){ nextPage = 0; }
            [featureScroller setContentOffset:CGPointMake(nextPage, 0)]; */          
         }
      }
}

-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    featurePaused = true; //pause the timer
}
-(void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
    featurePaused = false; //restart the timer
    featureInt = -3; //if you want to add an additional delay
}

If your fire time interval is tolerant, you can use CADisplayLink instead of NSTimer . Because CADisplayLink support -pause .

displayLink = [[CADisplayLink displayLinkWithTarget:self selector:@selector(fireMethod:)];
displayLink.frameInterval = approximateVaue;//CADisplayLink's frame rate is 60 fps.
    [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

//pause
[displayLink pause];

Here is a more modern solution, wrapping the timer and storing when it stopped.

I also ran into an interesting bug when invoking the pause in the actual timer fire callback, which will cause the timer to keep firing repeatedly after the expression timer.fireDate.timeIntervalSinceNow evaluates to zero, I added a flag in the pause to explicitly avoid this case

import Foundation

class PausableTimer {
    
    let timer: Timer
    var timeIntervalOnPause: TimeInterval?
        
    init(timeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void) {
        timer = Timer(timeInterval: interval, repeats: repeats, block: block)
    }
    
    func invalidate() {
        timer.invalidate()
    }
    
    var paused: Bool {
        timeIntervalOnPause != nil
    }
    
    func toggle() {
        if paused {
            resume()
        } else {
            pause(isFiringTimer: false)
        }
    }
    
    /// Pause the timer
    /// - Parameter isFiringTimer: pass in true if the timer is currently firing, in which case the resume time will not
    /// be relative to next fire date, but instead relative to the time interval. It's improtant that this be correct otherwise there maybe a near
    /// infinite loop firing of the `Timer`
    func pause(isFiringTimer: Bool) {
        guard timer.isValid else {
            return
        }
        timeIntervalOnPause = isFiringTimer || timer.fireDate.timeIntervalSinceReferenceDate == 0 ? timer.timeInterval : timer.fireDate.timeIntervalSinceNow
        timer.fireDate = .distantFuture
    }
    
    /// Resume the timer, if it was not paused has not effect, and causes assertion failure
    func resume() {
        guard timer.isValid else {
            return
        }
        guard let timeIntervalOnPause = timeIntervalOnPause else {
            assertionFailure("Resuming a timer that was never paused \(self) - \(self.timer)")
            return
        }
        let relativeFireDate = Date(timeIntervalSinceNow: timeIntervalOnPause)
        timer.fireDate = relativeFireDate
        self.timeIntervalOnPause = nil
    }
    
}

You cant pause NSTimer as mentioned above. So the time to be captured should not be dependent on Timer firedate,i suggest. Here is my simplest solution:

When creating the timer initialize the starting unit time like:

self.startDate=[NSDate date];
self.timeElapsedInterval=[[NSDate date] timeIntervalSinceDate:self.startDate];//This ``will be 0 //second at the start of the timer.``     

myTimer= [NSTimer scheduledTimerWithTimeInterval:1.0 target:self `selector:@selector(updateTimer) userInfo:nil repeats:YES];

` Now in the update timer method:

  NSTimeInterval unitTime=1;
-(void) updateTimer
 {
 if(self.timerPaused)
       {
           //Do nothing as timer is paused
       }
 else{
     self.timerElapsedInterval=timerElapsedInterval+unitInterval;
    //Then do your thing update the visual timer texfield or something.
    }

 }

I was also in need of pausable NSTimer. After reading this thread and your answers and realizing that only thing I need is to set timer's fireDate to distant future and then back to Now I made category to NSTimer with Pause and Resume methods. So I can simply pause timer by calling [myTimer pause]; and resume by calling [myTimer resume]; methods. Here it is, hope that it will help someone.

Interface:

#import <Foundation/Foundation.h>

@interface NSTimer (Pausable)

-(void)pause;
-(void)resume;

@end

Implementation:

#import "NSTimer+Pausable.h"

@implementation NSTimer (Pausable)

-(void)pause
{
    [self setFireDate:[NSDate dateWithTimeIntervalSinceNow:[NSDate distantFuture]]]; //set fireDate to far future
}

-(void)resume
{
    [self setFireDate:[NSDate date]];
}

@end

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM