1

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

Community
  • 1
  • 1
randomx
  • 2,357
  • 1
  • 21
  • 30
  • Is the timer a strong property? – Zia May 02 '14 at 23:59
  • This code works fine for me. As with @user2891327 if it's not a strong property you should be seeing a warning. – Ben Flynn May 03 '14 at 00:04
  • Added the @property line to my question above. I added the 'retain' in a shotgun attempt to get it to work. – randomx May 03 '14 at 00:07
  • Small points: You don't need both retain and strong -- if you're using ARC, just use strong. Also "nil" is preferable to "Nil" for userInfo (you want a nil instance, not a nil class). I'd recommend stepping through this code with a debugger. I copied your code verbatim and call it form my view controller and it logs away like a dream every 3 seconds. – Ben Flynn May 03 '14 at 00:13
  • Is it possible there's an app configuration point that blocks NSTimer? Grasping at straws... – randomx May 03 '14 at 00:16
  • wrapping the body of doStartMessageRefresh() in dispatch_async(dispatch_get_main_queue(), ^{...}); works... any idea why? – randomx May 03 '14 at 00:49

3 Answers3

3

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;
Abhi Beckert
  • 32,787
  • 12
  • 83
  • 110
3

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.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
1

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.

randomx
  • 2,357
  • 1
  • 21
  • 30
  • @Rob looks like he's on to it. I can recreate your issue if I call deStartMessageRefresh from a background thread. A cleaner solution might be to use mainRunLoop instead of currentRunLoop. `[[NSRunLoop mainRunLoop] addTimer:self.messageRefreshTimer forMode:NSDefaultRunLoopMode];` – Ben Flynn May 03 '14 at 01:34