I am having trouble with NSTimer. The below code is called from a method and it creates an NSTimer and sets it to the instance variable self.syncDelay. The if statement at the end of the method checking if the timer is valid gets called.
However when I call this method again before the timer runs out, the if statement at the top of the method doesn't get called. Which, I am guessing, is why [self.syncDelay invalidate]; doesn't invalidate the timer.
I have a property defined as: #property (nonatomic, strong) NSTimer *syncDelay;
The functionality I am looking for is the timer being invalidated when the method is called for the second time before the timer runs out and creating a new timer. Thus effectively resetting the timer.
EDIT: When the method gets called for the second the NSTimer is nil. Which is strange as I am keeping a strong pointer to it.
else {
if ([self.syncDelay isValid]) {
NSLog(#"Timer valid at start of method");
}
[self.syncDelay invalidate];
self.syncDelay = nil;
NSInvocation *timerInvocation = [NSInvocation invocationWithMethodSignature:
[self methodSignatureForSelector:#selector(testMethod:)]];
// configure invocation
[timerInvocation setSelector:#selector(testMethod:)];
[timerInvocation setTarget:self];
[timerInvocation setArgument:&className atIndex:2]; // argument indexing is offset by 2 hidden args
self.syncDelay = [NSTimer scheduledTimerWithTimeInterval:10
invocation:timerInvocation
repeats:NO];
if ([self.syncDelay isValid]) {
NSLog(#"Timer valid at end of method");
}
}
Related
In my app, I associate an NSTimer with a block passed to a method; the block is also added to a an array of blocks. When the timer fires, its associated block is called and should be removed from the array. So my setup looks like this:
#interface MyObject : NSObject
#property(strong, nonatomic) NSMutableArray *allBlocks;
- (void)myMethodWithBlock:(void(^)(void))block;
- (void)timerFired:(NSTimer *)timer;
#end
#implementation MyObject
- (id)init
{
self = [super init];
if (self)
{
self.allBlocks = [NSMutableArray array];
}
return self;
}
- (void)myMethodWithBlock:(void(^)(void))block
{
[NSTimer scheduledTimerWithTimeInterval:5.0f
target:self
selector:#selector(timerFired:)
userInfo:block
repeats:NO];
[self.allBlocks addObject:block];
}
- (void)timerFired:(NSTimer *)timer
{
void(^block)(void) = timer.userInfo;
[self.allBlocks removeObject:block];
block();
}
#end
My problem is that when timerFired: is called, the block is (sometimes) not removed. Why?
The problem here is that NSTimer copies the block assigned to userInfo, but the block passed to myMethodWithBlock: is probably an instance of NSStackBlock, which is not equal to its copies.
Let's consider three scenarios, where myObject is an instance of MyObject:
// A
void(^myBlock)(void) = ^{
NSLog(#"1");
};
[myObject myMethodWithBlock:myBlock];
// B
int one = 1;
void(^myBlock)(void) = ^{
NSLog(#"%d", one);
};
[myObject myMethodWithBlock:myBlock];
// C
int one = 1;
[myObject myMethodWithBlock:^{
NSLog(#"%d", one);
};];
In A, the block captures no variables from its context; the block will be an instance of NSGlobalBlock, which simply returns itself when copied.
In B, the block captures the variable one; it will be an instance of NSMallocBlock, which also returns itself when copied.
In C, the block again captures the variable one, but is also not assigned to a variable before being passed to myMethodWithBlock:. In this case, the block is an instance of NSStackBlock, which returns an instance of NSMallocBlock when copied.
The result of this is that in a situation like scenario C, the NSStackBlock will be added to allBlocks, while an NSMallocBlock will be assigned to the timer's userInfo. When the timer fires, removeObject: does nothing since the block assigned to the timer isn't equal to any of the blocks in the array.
The solution is to always copy the block before storing it in the array. This way, the same block instance will be stored in the array and assigned to the timer:
- (void)myMethodWithBlock:(void(^)(void))block
{
block = [block copy];
[NSTimer scheduledTimerWithTimeInterval:5.0f
target:self
selector:#selector(timerFired:)
userInfo:block
repeats:NO];
[self.allBlocks addObject:block];
}
A clearer approach is to mark the blocks with something whose isEqual: behavior is well known and more readable, like an NSNumber...
// keep state so these can be made unique
#property(nonatomic, assign) NSInteger blockIndex;
// change the blocks collection to record blocks' associations with numbers
#property(nonatomic, strong) NSMutableDictionary *allBlocks;
// in myMethod...
NSNumber *nextIndex = [NSNumber numberWithInt:++self.blockIndex];
self.allBlocks[nextIndex] = block;
// pass userInfo:nextIndex when you schedule the timer
Now the timer context never has a block, copied or otherwise. Then, when the timer fires...
- (void)timerFired:(NSTimer *)timer {
NSNumber *index = timer.userInfo;
void(^block)(void) = self.allBlocks[index];
[self.allBlocks removeObjectForKey:index];
block();
}
I think it will be safest to compare apples to apples.
- (void)myMethodWithBlock:(void(^)(void))block
{
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:5.0f
target:self
selector:#selector(timerFired:)
userInfo:block
repeats:NO];
[self.allBlocks addObject:timer.userInfo];
}
- (void)timerFired:(NSTimer *)timer
{
[self.allBlocks removeObject:timer.userInfo];
void(^block)(void) = timer.userInfo;
block();
}
I cannot seem to work this one out. Here is my set up:
I have a function called requestDataWithCompletion:(someBlock)block. I call it when the class is initialised. The function requests certain motion data. I want to do this periodically, therefore, the first time I call this function, I specify some completion code which sets up a timer that re-calls this function periodically. The timer calls it via another function requestDataWithoutCompletion which simply calls the requestDataWithCompletion but with an empty block (so I don't keep creating timers);
- (void) requestDataWithCompletion:(someBlock)block {
// BREAK POINT 1
[self.motionManager queryActivityStartingFromDate:start toDate:[NSDate date] toQueue:self.queue withHandler:^(NSArray *activities, NSError *error) {
// BREAK POINT 2
// do some processing;
block();
}];
}
The block simply creates a timer on the main queue, which periodically recalls this function, but with no completion (since I don't want to keep creating more timers).
block = ^{
dispatch_async(dispatch_get_main_queue(), ^{
self.timer = [NSTimer scheduledTimerWithTimeInterval:timerInterval
target:self selector:#selector(requestDataWithoutCompletion) userInfo:nil repeats:YES];
});
}
- (void) requestDataWithoutCompletion {
[self requestDataWithCompletion^{;}];
}
The amazing thing is that despite this set up, my app is creating timer after timer! I can't understand why.
I have placed break points in requestDataWithCompletion method. One is outside the block submitted to NSOperationQueue to get activity data (BREAKPOINT 1) and one is inside the block submitted to NSOperationQueue. (BREAKPOINT 2). Basically it shows that each time the method is called by the timer, BREAKPOINT 1 has an empty completion block (as it should be) but then strangely BREAKPOINT 2 has the completion block I submitted when I first called the function when initialising the class. Therefore, it continues to create a new timer each time the function is called by the timer. Over time, this means a massive number of timers and then the app crashes!
I have a feeling this is something to do with NSOperationQueue, but I really don't know what it could be.
In your initialisation (or when you first want to get the data and then continue getting it):
self.timer = [NSTimer scheduledTimerWithTimeInterval:timerInterval target:self selector:#selector(requestDataWithoutCompletion) userInfo:nil repeats:YES];
[self.timer fire]; //get the activity data immediately.
- (void) requestDataWithoutCompletion {
[self requestDataWithCompletion:^{}];
}
With your original requestDataWithCompletion: method. (though you could get rid of requestDataWithCompletion: and put it's code directly in requestDataWithoutCompletion if you're not using it elsewhere)
In my viewController, I have a button that calls a method to start a timer in a Timer class. Just like this
main view controller
[self.timer startTimer];
In the timer class, startTimer calls a countdownTime method, both of which you can see below. Very simple. At the end of the countdownTime, method, the time is put in the label in the view like this as I iterate through a loop of the clocks.
Timer *timerblah = self.gameClocks[i];
[self.gameClocks[i] setText: [NSString stringWithFormat:#"%#", self.time]];
In other words, the Timer class has an array property that holds all the clocks and that is connected to the view, hence the ability to set the text.
The problem with this code is that the model (i.e. Timer class) is setting the text in the view. I want the view Controller to get the time from the model and have the text set in the view controller i.e. have the viewController and only the viewController communicate with the view. It is no problem for me to get the array of clocks in the viewController, however, I'm sure how to pass the time back from the countdownTime method to the viewController. It would seem like overkill to set up an NSNotification class to send the time back every second, wouldn't it?
-(void)startTimer{
self.NStimer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:#selector(countdownTime:)
userInfo:nil
repeats:YES];
}
-(void)countdownTime:(NSTimer *)timer
{
#logic to countdown time
self.minutes = self.secondsRemaining / 60;
self.stringMinutes = [NSString stringWithFormat:#"%i", self.minutes];
self.seconds = self.secondsRemaining - (self.minutes * 60);
self.stringSeconds = [NSString stringWithFormat:#"%i", self.seconds];
if (self.seconds < 10) self.stringSeconds = [NSString stringWithFormat:#"0%i", self.seconds];
self.time = [NSString stringWithFormat:#"%#:%#", self.stringMinutes, self.stringSeconds];
self.secondsRemaining -= 1;
if (self.secondsRemaining == 0){
[self stopTimer];
}
#adding to the view (from the model i.e. in Timer.m class)
for(int i = 0; i < [self.gameClocks count]; i++){
Timer *timerblah = self.gameClocks[i];
if (timerblah.systemClock == YES){
[self.gameClocks[i] setText: [NSString stringWithFormat:#"%#", self.time]];
}
}
}
Update
There's a comment suggesting that I use a block to do this. In the timer class, I added a method like this to return a string with the time (the time is set to self.time above)
-(NSString *)passTheTime:(NSString (^)(void))returnBlock
{
return self.time;
}
I then, in view controller, call the method passTheTime on the timer class. I created a property in the view controller that will store the time, so I pass that as a parameter to the block,
[self.timer passTheFoo:^NSString * (self.timeToSet){
}];
a) I'm unsure of what to do in the block here.
b) I'm unsure of whether it was necessary to pass self.timeToSet into the block. How do I connect self.time from the Timer class to self.timeToSet in the view controller
c) there's an error incompatible block pointer type sending NSString *((^)(void)) to parameter of type NSString(^)(void)
d) alternatively, can I pass a block to startTimer, and have that block passed in the selector countdownTime and return the self.time once the calculations in countdownTime are finished?
You can use block to update the label in the view controller class as below
In Timer Class, create a block
TimerClass.h
#import <Foundation/Foundation.h>
typedef void(^TimerCallback)(NSString *time);
#interface TimerClass : NSObject{
TimerCallback timerCallback;
NSTimer *timer;
}
- (void)startTimerWithCallBack:(TimerCallback)callback;
#end
In TimerClass.m , update the code as below
#import "TimerClass.h"
#implementation TimerClass
- (void)startTimerWithCallBack:(TimerCallback)callback{
timerCallback = callback;
timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:#selector(countdownTime:)
userInfo:nil
repeats:YES];
}
-(void)countdownTime:(NSTimer *)timer
{
// countdown logic here
// call the callback method with time as a parameter to the viewcontroller class
// here for demo purpose I am passing random number as a value in callback, you need to pass the time that you want to display here
NSString *str = [NSString stringWithFormat:#"%d",1+arc4random()%10];
timerCallback(str);
}
#end
In ViewController class, call the timer start method as follows
TimerClass *timer = [[TimerClass alloc] init];
[timer startTimerWithCallBack:^(NSString *time) {
lbl.text = time; // display time in label
}];
I have an NSTimer which I want to update a label every second. My code is:
- (IBAction)OnClickEmergencyButton:(id)sender
{
emergencyAlertTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:#selector(emergencyTimer) userInfo:nil repeats:YES];
[emergencyAlertTimer fire];
}
- (void)emergencyTimer
{
int i = 0;
_emergencyAlertTriggerTimerLabel.text = [NSString stringWithFormat:#"%d", ++i];
}
When I ran it, the label displayed "1" initially and then stopped.
I want the label to continuously count up every second, like "1", "2", "3", ...
There is no issue with your timer. The issue is with the variable declaration inside the emergencyTimer, you declared it as a local variable. So each time when the timer fires the variable will be initialized to 0 again. So declare the variable as static, so that it can preserve the value.
Change the method like:
-(void)emergencyTimer
{
static int timeValue = 0;
_emergencyAlertTriggerTimerLabel.text = [NSString stringWithFormat:#"%d",++timeValue];
}
Why static variable and Why not instance variable ?
I didn't used instance variable, for keeping the variable "Scope" safe. If I put it as an instance variable, it can be accessed by other methods of the same class, if there is no need of that functionality, I think using a static variable will be better.
Issue is with this code
int i=0;
Every time when timer method gets called, the integer i gets initialized and label will be displayed as "1" always.
Make this variable global or static to fix your issue.
Remove int i=0; from your timer action method because it will always have a value of zero. You should be using an instance variable (#property) to store the timerCounter and increment that and use it to populate the label.
At some point in time you need to invalidate the timer. This is particularly important to do before you create a new timer and replace the reference to the old timer. Currently, if you press the button twice, you will then have 2 timers running and your label will increment twice a second...
#property (nonatomic, assign) NSInteger timerCounter;
- (IBAction)OnClickEmergencyButton:(id)sender {
[emergencyAlertTimer invalidate];
emergencyAlertTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:#selector(emergencyTimer) userInfo:nil repeats:YES];
[emergencyAlertTimer fire];
}
- (void)emergencyTimer {
_emergencyAlertTriggerTimerLabel.text = [NSString stringWithFormat:#"%d", self.timerCounter];
self.timerCounter++;
}
You should also invalidate the timer when the view is removed from display / deallocated.
Always timer get called emergencyTimer but your value of i won't changed because it's an local variable, scope of i will remain at end of function call. Try this with static variable which remain globally...
-(void)emergencyTimer{
static int i=0; // initialize at first time only..
_emergencyAlertTriggerTimerLabel.text = [NSString stringWithFormat:#"%d",++i];
if ( i == 100)
[ emergencyAlertTimer invalidate] // stop at certain condition
}
Firstly everyone is right you will only display a 0 no matter what you do currently so use an instance variable.
With regards to the only firing once, instead of [NSTimer fire] try this:
emergencyAlertTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:#selector(emergencyTimer) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:emergencyAlertTimer forMode:NSRunLoopCommonModes];
I don't want to create NSTimer object. How do I invalidate timer? I want to invalidate timer in viewWillDisappear.
-(void) viewDidLoad
{
[super viewDidLoad];
[NSTimer scheduledTimerWithTimeInterval:10 target:self selector:#selector(onTimer:) userInfo:nil repeats:YES];
}
A
you have to hold on to the timer you create:
#interface MONObject ()
#property (nonatomic, retain) NSTimer * timerIvar;
#end
#implementation MONObject
...
- (void)viewDidLoad
{
[super viewDidLoad];
self.timerIvar = [NSTimer scheduledTimerWithTimeInterval:10 target:self selector:#selector(onTimer:) userInfo:nil repeats:YES];
}
- (void)invalidateTimer
{
[self.timerIvar invalidate];
self.timerIvar = nil;
}
- (void)viewWillDisappear:(BOOL)animated
{
...
[self invalidateTimer];
}
B
another option would be to invalidate the timer that is passed in the callback, but that won't occur within viewDidUnload:. therefore, it doesn't quite apply in this scenario:
- (void)onTimer:(NSTimer *)pTimer
{
[pTimer invalidate];
}
If you want to be able to cancel the timer, you have to refer to the timer you’re cancelling, and that means you have to keep the pointer to the timer around, see justin’s answer.
Keeping a reference to the timer is the right way to do it, but for the sake of completeness you may also use the -performSelector:withObject:afterDelay: method as a poor man’s timer. That call may be invalidated using +cancelPreviousPerformRequestsWithTarget:. Sample code:
- (void) viewDidLoad
{
[super viewDidLoad];
[self performSelector:#selector(timerTick) withObject:nil afterDelay:10];
}
And then:
- (void) viewWillDisappear
{
[NSObject cancelPreviousPerformRequestsWithTarget:self];
[super viewWillDisappear];
}
But this is not the right way to do it, because there might be other perform-selector requests pending on your object that you would cancel. It’s best to keep your timer around, that way you know exactly what you’re cancelling.
By the way, it’s also probably a bad idea to run a timer in -viewDidLoad. View loading may happen anytime, without any relation to view being displayed.
Maybe this method can help you:
[self performSelector:#selector(onTimer:) withObject:nil afterDelay:10];
If you don't want to hold on to your timer, the NSTimer object will be passed to the timer method (in your case onTimer:), so in that method you could check whether the timer is still needed and invalidate it. However, you will run into trouble if the view comes back before you invalidated the timer, and you create a new one.
By far the best way is to store the timer into an instance variable. It works, no clever tricks, and you'll know six months later what you did. I'd probably write a
#property (readwrite, nonatomic) BOOL hasTimer;
getter returns YES iff the timer is not nil, setter invalidates the timer or creates a new one.