Question

I need to start an NSTimer arbitrarily that will repeat every N seconds until I stop it. It's not repeating.

I've tried SO solutions described here, here and here. None of the the accepted answers are working.

Here's what I've done. My UIViewController is calling:

[AppDelegate doStartMessageRefresh];

In AppDelegate.m:

#define MESSAGE_REFRESH_INTERVAL_SECONDS    3.0
@property (nonatomic, retain, strong) NSTimer *messageRefreshTimer;
- (void) doInvokerForDoStartMessageRefresh
{
    NSLog(@"%s", __FUNCTION__);
    NSLog(@"%@", [self.messageRefreshTimer fireDate]);
}
- (void) doStartMessageRefresh {
    NSLog(@"%s", __FUNCTION__);
    /*
    self.messageRefreshTimer = [NSTimer scheduledTimerWithTimeInterval:MESSAGE_REFRESH_INTERVAL_SECONDS
                                                                target:self
                                                              selector:@selector(doInvokerForDoStartMessageRefresh)
                                                              userInfo:nil
                                                               repeats:YES];
    */
    self.messageRefreshTimer = [NSTimer timerWithTimeInterval:MESSAGE_REFRESH_INTERVAL_SECONDS
                                                       target:self
                                                     selector:@selector(doInvokerForDoStartMessageRefresh)
                                                     userInfo:Nil
                                                      repeats:YES];
    // [self.messageRefreshTimer fire];
    //[[NSRunLoop currentRunLoop] addTimer:self.messageRefreshTimer forMode:NSRunLoopCommonModes];
    [[NSRunLoop currentRunLoop] addTimer:self.messageRefreshTimer forMode:NSDefaultRunLoopMode];
}

The only log line that's printing out is:

2014-05-02 16:34:20.248 myappnamehere[9977:6807] -[AppDelegate doStartMessageRefresh]

If I uncomment the line [self.messageRefreshTimer fire]; it runs once.

If I formulate the selector like: @selector(doInvokerForDoStartMessageRefresh:) and manually fire, I get an unrecognized selector sent

Was it helpful?

Solution 3

Using GCD solved my problem. I wrapped the body of doStartMessageRefresh() in dispatch_async(dispatch_get_main_queue(), ^{...}); and NSTimer is now running and calling doInvokerForDoStartMessageRefresh() every N sec.

For whatever reason, this code works:

- (void) doInvokerForDoStartMessageRefresh:(NSTimer *) theTimer
{
    NSLog(@"%s", __FUNCTION__);
    NSLog(@"%@", [self.messageRefreshTimer fireDate]);
    NSLog(@"%@", [theTimer fireDate]);
}
- (void) doStartMessageRefresh {
    NSLog(@"%s", __FUNCTION__);

    dispatch_async(dispatch_get_main_queue(), ^{
        self.messageRefreshTimer = [NSTimer scheduledTimerWithTimeInterval:MESSAGE_REFRESH_INTERVAL_SECONDS
                                                                    target:self
                                                                  selector:@selector(doInvokerForDoStartMessageRefresh:)
                                                                  userInfo:nil
                                                                   repeats:YES];
     });
}

I'd still like to know why GCD was required here.

OTHER TIPS

Your method signature is wrong. It should be doInvokerForDoStartMessageRefresh: and doInvokerForDoStartMessageRefresh:(NSTimer *)timer, instead of doInvokerForDoStartMessageRefresh. Note that the : is part of the method name! Without the colon it's a totally different method.

Here is how you setup a repeating timer:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  [NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(timer:) userInfo:nil repeats:YES];

  return YES;
}

- (void)timer:(NSTimer *)timer
{
  NSLog(@"%@", [NSDate date]);
}

Also, if you want to store a timer in a property, instead of doing this:

@property (nonatomic, retain, strong) NSTimer *messageRefreshTimer;

Just do this:

@property NSTimer *messageRefreshTimer;

You noted that if you dispatch your timer to the main queue, it worked:

dispatch_async(dispatch_get_main_queue(), ^{
    self.messageRefreshTimer = [NSTimer scheduledTimerWithTimeInterval:MESSAGE_REFRESH_INTERVAL_SECONDS
                                                                target:self
                                                              selector:@selector(refreshMessage:)
                                                              userInfo:nil
                                                               repeats:YES];
});

This would suggest that you're calling this method from some thread other than the main thread, or more accurately, one without a run loop. Perhaps you called it from the completion block of some asynchronous method that doesn't use the main queue for its completion handler. Perhaps you had manually dispatched it to some background queue.

Regardless, NSTimer should be scheduled on a thread with a run loop, and dispatching this to the main run loop is an easy way to do that. Another approach is to create, but not schedule, the timer (using timerWithTimeInterval) and then manually add the timer to the main run loop:

self.messageRefreshTimer = [NSTimer timerWithTimeInterval:MESSAGE_REFRESH_INTERVAL_SECONDS
                                                   target:self
                                                 selector:@selector(refreshMessage:)
                                                 userInfo:nil
                                                  repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.messageRefreshTimer forMode:NSDefaultRunLoopMode];

Note, I've simplified the method name of the selector, and you'd have a method like:

- (void)refreshMessage:(NSTimer *)timer
{
    // refresh your message here
}

If, however, you want to run a timer on a thread other than the main thread, you can also create a GCD timer source, which doesn't require a runloop to function:

dispatch_queue_t  queue = dispatch_queue_create("com.domain.app.messageRefreshTimer", 0);
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), MESSAGE_REFRESH_INTERVAL_SECONDS * NSEC_PER_SEC, 1ull * NSEC_PER_SEC);

dispatch_source_set_event_handler(timer, ^{
    [self refreshMessage];
});

dispatch_resume(timer);

I'd only use this final approach if you deliberate did not want to run the timer on the main queue.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top