0

In a certain view I wish to have a certain method fired when the firing timing is based on an array of NSTimeIntervals (which I keep in as NSArray holding double values).

So I have a method that does the relevant action according to the relevant timing, and within this method I call the same method (recursively) to be fired again in the next scheduled time:

-(void) actionMethod:(int)someDataINeed {

    //do something and then...

    if (someDataINeed == someDefinitionIHaveToBreakTheLoop) {
        return;
    }

    double nextFire = [self aMethodThatCalculatesTheNextFiringTime];
    NSNumber * nextDataINeed = [NSNumber numberWithInt:someDataINeed+1];

    NSTimer * aTimer = [NSTimer scheduledTimerWithTimeInterval:nextFire 
                                        target:self 
                                        selector:@selector(actionMethod:)
                                        userInfo:nextDataINeed 
                                        repeats:NO];
}

Well... it doesn't work (if it did, I guess I wouldn't ask about it...). When I NSLog it, it seems like the time is not running at all and that the timer is called in some sort of loop and not "being fired" according to the timing I had defined. The nextFire double data is correct (according to my definitions).

If I have it wrong, I would appreciate if someone can direct me to how this type of action should be performed.

If I got it right, but simply write it wrong, an eye that catches my "bug" would be appreciated as well...

Ohad Regev
  • 5,641
  • 12
  • 60
  • 84

2 Answers2

1

I'm guessing you are using ARC. You don't store the timer in a strong variable, so it gets released before it can fire. Create an ivar "NSTimer * aTimer", then use that instead of a local variable when you instantiate the timer (I'm waffling here since I cannot remember if the runLoop retains it when its scheduled - probably but leaving this here for completeness, as you should call invalidate on the timer if for any reason you get "popped" etc).

Also your timer sends the message: actionMethod:(NSSTimer *)itself, but your action method expects an int. You need to reconcile that.

EDIT:

Change your actionMethod to this format:

-(void)actionMethod:(id)something
{
  int foo = 0;
  if([something isKindOfClass:[NSNumber class]]) {
    foo = [foo intValue];
  } else
  if([something isKindOfClass:[NSTimer class]]) {
    foo = ... // however you get your value
  } else
     assert(!"Yikes! Wrong class");
  }
  ...

Then you can send it a NSNumber or have the timer call it with the timer. You can pull out the int from the NSNumber, or get a value from the timer:

David H
  • 40,852
  • 12
  • 92
  • 138
  • David, Thanks for the answer. several clarifications - I do use an ivar for the NSTimer, I simplified the code a bit (since in the app there are a few several elements in place). I read Apple's documentation and didn't really understand the concept of "NSTimer invalidating itself". Also, can you say a bit further regarding to what it is I should reconcile in my method? the "int" value uses to catch a relevant object in an NSArray, the method doen't require an NSTimer - it needs an automatic fire and needs to define a new NSTimer for the next fire event. – Ohad Regev Aug 12 '12 at 17:38
  • So when the timer sends you its message, someDataINeed is a pointer to the NSTimer that just fired. Timers retain "self" so you cannot get dealloc'd if a timer is active. Whenever you want to prepare for releasing the current object, you have to send "invalidate" to the timer, which tells it to stop and set its target to nil, releasing it. – David H Aug 12 '12 at 19:40
  • OK, I got the invalidate issue. Regarding the way the NSTimer>userInfo parameter works >> so, what should I change in my actionMethod? I saw in some documentations that I'm supposed to write something like "-(void) actionMethod:(NSTimer*)aTimer". Is that what you meant when you said "You need to reconcile that" (if so, how do I then collect the "int someDataINeed" value?) or should I change the "Target" parameter from "self" to something else (if so, to what?)? If you can clarify that - I will appreciate it very much. – Ohad Regev Aug 13 '12 at 13:37
1

It's not a memory management problem

When you schedule an NSTimer instance, the run loop retains the timer, so there's no need to worry about retaining it yourself (ARC or not).

The timer also retains its target, so you don't have to worry about a situation where the target is deallocated first. However, if the target is a view, it might receive a timer message after it has been removed from the view hierarchy. In that case you will want to ignore the message.

You can handle this two ways:

  1. In your actionMethod, be sure to check that you still want to perform the action. If not, ignore the message and let everything be cleaned up.

  2. Retain a reference to the timer instance, and call [timer invalidate] when you no longer want to receive timer messages.

If you can be sure that the target will always be around and it will always be OK to receive a message, you don't need to do anything. For example, your app delegate object will be around for the lifetime of your application.

NSTimer's target action

You cannot set a timer to call just any method on any object. The timer method MUST take only one parameter, which is the NSTimer instance that sent the message. You have defined actionMethod: to take an int, which will actually compile and execute, but when the timer fires and the method is called, the value of someDataINeed will be the memory address of the NSTimer object. This isn't very useful.

You need to define your method this way:

- (void)actionMethod:(NSTimer *)timer

To get the someDataINeed value, you can save it as an NSNumber in the timer's userInfo. It is there just so you can add some data to the timer.

    NSNumber *number = [timer userInfo];
    int someDataINeed = [number intValue];

When you create the timer, you can wrap the nextDataINeed into an NSNumber and set the userInfo this way:

    currentTimer = [NSTimer scheduledTimerWithTimeInterval:nextFire 
                            target:self 
                            selector:@selector(actionMethod:)
                            userInfo:[NSNumber numberWithInt:nextDataINeed]
                            repeats:NO];

You need to use NSNumber because the userInfo must be an object.

Possible mistake

If applying the above doesn't fix the problem, one thing to check is that aMethodThatCalculatesTheNextFiringTime is returning the time interval since now, and not since a reference date. If you want a timer to fire in three seconds, the value of nextFire must be 3.0. If you accidentally call timeIntervalSinceReferenceDate you'll get a huge number, and the timer will be scheduled to fire several years into the future.

benzado
  • 82,288
  • 22
  • 110
  • 138
  • Thanks benzado - I read several tutorials explaining how to use these methods that get (NSTimer*) as parameter - and your answer is the first time I really got it. U rule! – Ohad Regev Aug 14 '12 at 13:25