I just decided to run a test upon suspecting some behavior:
In my view controller I have a new game method which does this:
-(void) newClassicGame {
if (classicGame!=nil) {
[classicGame saveStats];
}
classicGameController = nil;
classicGameController = [[RegularGameViewController alloc] initWithPowerMode:NO ];
classicGame = nil;
classicGame = [[RegularGame alloc] initWithViewController:classicGameController];
classicGameController.game = classicGame; // game property is __weak to avoid
//retain cycles.
[classicGame startGame];
}
and to test if objects get destroyed with ARC, in the startGame method of classicGame, I put this:
-(void) startGame
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(#"once");
[NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:#selector(repeat) userInfo:nil repeats:YES];
});
//other stuff..
}
-(void) repeat {
NSLog(#"I repeat");
}
for those who are not familiar with dispatch_once method, it's just a method that makes sure that the block gets called exactly once, and no more, during the life time of an application.
I start my app, hit new game and game starts - I see the log "I repeat" repeating every 3 seconds".. Then I go back to the menu and hit new game again. Strangely, the log keeps on repeating, indicating that my classicGame instance is not completely destroyed and the timer is still running somewhere. Any ideas what is happening here?
The timer that you create retains its target (self, the game), so your game object won't be deallocated at least until the timer is invalidated.
Related
I'm working on a very simple iPhone game that involves choosing the right colored button as many times in a row based on a randomized voice prompt. I have it set up so that if the button is one color and gets clicked, it always goes to a hard-coded color every time (e.g. if you click red, it always turns blue). The color change method is set up in an IBOutlet. I have a timer set up in a while loop, and when the timer ends it checks if the player made the right selection. The problem is that the button color change does not occur until after the timer runs out, and this causes a problem with the method used to check the correct answer. Is there a way to make this color change happen instantly? From what I've searched I know it has something to do with storyboard actions not occurring until after code executes, but I haven't found anything with using a timer. Here is a section of the method that calls the timer if the answer is correct:
BOOL rightChoice = true;
int colorNum;
NSDate *startTime;
NSTimeInterval elapsed;
colorNum = [self randomizeNum:middle];
[self setTextLabel:colorNum];
while (rightChoice){
elapsed = 0.0;
startTime = [NSDate date];
while (elapsed < 2.0){
elapsed = [startTime timeIntervalSinceNow] * -1.0;
NSLog(#"elapsed time%f", elapsed);
}
rightChoice = [self correctChoice:middleStatus :colorNum];
colorNum = [self randomizeNum:middle];
}
One of two things stood out
You're using a while loop as a timer, don't do this - the operation is synchronous.
If this is run on the main thread, and you code doesn't return, your UI will update. The mantra goes: 'when you're not returning you're blocking.'
Cocoa has NSTimer which runs asynchronously - it is ideal here.
So let's get to grips with NSTimer (alternatively you can use GCD and save a queue to an ivar, but NSTimer seems the right way to go).
Make an ivar called timer_:
// Top of the .m file or in the .h
#interface ViewController () {
NSTimer *timer_;
}
#end
Make some start and stop functions. How you call these is up to you.
- (void)startTimer {
// If there's an existing timer, let's cancel it
if (timer_)
[timer_ invalidate];
// Start the timer
timer_ = [NSTimer scheduledTimerWithTimeInterval:5.0
target:self
selector:#selector(onTimerFinish:)
userInfo:nil
repeats:NO];
}
- (void)onTimerFinish:(id)sender {
NSLog(#"Timer finished!");
// Clean up the timer
[timer_ invalidate];
timer_ = nil;
}
- (void)stopTimer {
if (!timer_)
return;
// Clean up the timer
[timer_ invalidate];
timer_ = nil;
}
And now
Put your timer test code in the onTimerFinish function.
Make an ivar that stores the current choice. Update this ivar when a choice is made and make the relevant changes to the UI. Call stopTimer if the stop condition is met.
In the onTimerFinished you can conditionally call and startTimer again if you desire.
Hope this helps!
I am trying to create an explosion on the iphone screen which gets bigger fast, then goes away. Why is this timer not stopping?
NSTimer *explosion = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(explosion) userInfo:nil repeats:YES];
-(void)explosion {
image.image = [UIImage imageNamed:#"explosion.png"];
expsize = expsize + 2.5;
image.frame = CGRectMake(image.frame.origin.x, image.frame.origin.y, expsize, expsize);
if (expsize > 60) {
NSLog(#"%f",expsize);
[explosion invalidate];
explosion = nil;
}
}
You are most likely invalidating the wrong timer.
You create a local variable named explosion that has the same name as the instance variable.
Avoid declaring instance variables and local variables with the same name!
I'd suggest that you use the form of selector that the NSTimer doc calls for: - (void)timerFireMethod:(NSTimer*)theTimer. The you can invalidate "theTimer" and be sure you're invalidating the right one.
Also, of course, if "explosion" is declared as a property, then there will be two methods in the class named "explosion", and no real clue as to which one is getting called.
It's hard to be certain, because it's not clear whether this is exactly your code, but you have two variables named explosion, and one of them has an NSTimer assigned to it; the other (which seems to be an ivar) is nil.
// Variable local to whatever method this is in
NSTimer *explosion = [NSTimer scheduledTimerWithTimeInterval:0.1...
if (expsize > 60) {
NSLog(#"%f",expsize);
// Other variable named "explosion" does not exist.
// This is an ivar? Has not been set.
[explosion invalidate];
Assuming you've got explosion declared as a property (and there is no reason not to), you should fix this by using the setter when you create the timer:
[self setExplosion:[NSTimer scheduledTimerWithTimeInterval:...]];
Now the ivar has the timer instance and you can use it to invalidate the timer.
Also, note that your timer's method is incorrect; it must take one parameter which is a pointer to the timer. You can also use this pointer to invalidate the timer when it fires.
- (void) fireExplosion: (NSTimer *)tim {
//...
if( expsize > 60 ){
[tim invalidate];
//...
}
}
Finally, you have one last naming problem; if your property is called explosion, the convention in Cocoa is that the accessor should have the same name, but you have used explosion for the method that your timer calls. This could cause hard-to-track problems later. You should rename the timer method as I have here, using a verb indicating that something is happening.
If you are declaring explosion how you posted in your example then you are shadowing your instance variable explosion. As a word of advice you should use a naming convention for instance variables such as an underscore prefix. Now keeping track of the timer is not required if you only invalidate it after it fires. You could just take an extra parameter on the explosion method that would be the timer explosion:(id)timer. Otherwise you can do the following.
#interface X : NSObject
{
NSTimer *_explosion;
}
#end
And when you go to declare it in your code do the following
...
[_explosion invalidate];
[_explosion release];
//There is a whole 'nother debate on whether or not to retain a scheduled timer
//but I am a stickler for ownership so remember to release this in dealloc
_explosion = [[NSTimer scheduledTimerWithTimeInterval:0.1
target:self
selector:#selector(explosion)
userInfo:nil
repeats:YES] retain];
...
-(void)explosion {
image.image = [UIImage imageNamed:#"explosion.png"];
expsize = expsize + 2.5;
image.frame = CGRectMake(image.frame.origin.x, image.frame.origin.y, expsize, expsize);
if (expsize > 60) {
NSLog(#"%f",expsize);
[_explosion invalidate];
[_explosion release];
_explosion = nil;
}
}
I've read up a lot about NSTimers, but I must be doing something very wrong with them, because it's practically all the leaks that show up in the Leaks Instrument. The "Responsible Frame" column says -[NSCFTimer or +[NSTimer(NSTimer).
So here's how I have an NSTimer set up in my main menu. I shortened it up to just show how the timer is setup.
.h -
#interface MainMenu : UIView {
NSTimer *timer_porthole;
}
#end
#interface MainMenu ()
-(void) onTimer_porthole:(NSTimer*)timer;
#end
.m -
(in initWithFrame)
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
timer_porthole = [[NSTimer scheduledTimerWithTimeInterval:.05
target:self
selector:#selector(onTimer_porthole:)
userInfo:nil
repeats:YES] retain];
}
return self;
}
When leaving the view, it kills the timers:
-(void) kill_timers{
[timer_porthole invalidate];
timer_porthole=nil;
}
And of course, dealloc:
- (void)dealloc {
[timer_porthole invalidate];
[timer_porthole release];
timer_porthole = nil;
[super dealloc];
}
Don't call retain on your NSTimer!
I know it sounds counter-intuitive but when you create the instance it's automatically registered with the current (probaby main) threads run loop (NSRunLoop). Here's what Apple have to say on the subject...
Timers work in conjunction with run
loops. To use a timer effectively, you
should be aware of how run loops
operate—see NSRunLoop and Threading
Programming Guide. Note in particular
that run loops retain their timers, so
you can release a timer after you have
added it to a run loop.
Once scheduled on a run loop, the
timer fires at the specified interval
until it is invalidated. A
non-repeating timer invalidates itself
immediately after it fires. However,
for a repeating timer, you must
invalidate the timer object yourself
by calling its invalidate method.
Calling this method requests the
removal of the timer from the current
run loop; as a result, you should
always call the invalidate method from
the same thread on which the timer was
installed. Invalidating the timer
immediately disables it so that it no
longer affects the run loop. The run
loop then removes and releases the
timer, either just before the
invalidate method returns or at some
later point. Once invalidated, timer
objects cannot be reused.
Quotes are sourced from Apple's NSTimer class reference.
So your instantiation becomes...
timer_porthole = [NSTimer scheduledTimerWithTimeInterval:.05
target:self
selector:#selector(onTimer_porthole:)
userInfo:nil
repeats:YES];
And now that you're no longer holding the reference to the instance you wont want the release call in your dealloc method.
I've seen you already accepted an answer but there are two things here that I wanted to rectify:
It's not needed to retain a scheduled timer but it doesn't do any harm (as long as you release it when it's no longer needed). The "problematic" part of a timer/target relationship is that...
a timer retains its target. And you've decided to set that target to self.
That means — retained or not — the timer will keep your object alive, as long as the timer is valid.
With that in mind, let's revisit your code from bottom to top:
- (void)dealloc {
[timer_porthole invalidate]; // 1
[timer_porthole release];
timer_porthole = nil; // 2
[super dealloc];
}
1 is pointless:
If timer_porthole was still a valid timer (i.e. scheduled on a runloop) it would retain your object, so this method wouldn't be called in the first place...
2 no point here, either:
This is dealloc! When [super dealloc] returns, the memory that your instance occupied on the heap will be freed. Sure you can nil out your part of the heap before it gets freed. But why bother?
Then there is
-(void) kill_timers{
[timer_porthole invalidate];
timer_porthole=nil; // 3
}
3 given your initializer (and as others have pointed out) you are leaking your timer here; there should be a [timer_porthole release] before this line.
PS:
If you think it all over, you'll see that retaining the timer (at least temporarily) creates a retain-cycle. In this particular case that happens to be a non-issue which is resolved as soon as the timer is invalidated...
You missed [timer_porthole release]; call in your kill_timers method. If you call kill_timers before dealloc method is called, you set timer_porthole to nil, but you did not release it.
I have a Class with a NSTimer *myTimer; variable. At some point I do:
myTimer = [NSTimer scheduledTimerWithTimeInterval:20 target:self selector:#selector(doStuff) userInfo:nil repeats: YES];
further, I have a method:
- (void)doStuff
{
if(myTimer)
{
//do stuff
}
}
and I stop my timer when the class is released through:
- (void)dealloc
{
if (myTimer) { //if myTimer==nil it already has been stopped in the same way
[myTimer invalidate];
myTimer = nil;
}
}
Now, the problem is that when I release the class the timer goes on and on and on firing the event anyway. Am I doing something wrong? It seems the dealloc method is never called, otherwise myTimer would be nil and even if the selector is fired it would not go into the if(myTimer)
This will never work, because timers retain their target, which means your dealloc method will never get invoked until after you've invalidated the timer.
For more info, see the NSTimer documentation and this blog post on "Dangerous Cocoa Calls"
Have you tried the handy debugger tools at your disposal? What happens if you set a breakpoint in your dealloc method? Also, you should post more context around your creation. Is it possible you're creating the timer more than once, thereby replacing the original (but not invalidating it) and leaving it out there to fire at will?
So my menu calls a game with this piece of code:
game = [[Game alloc] init];
[self presentModalViewController:memoryTest animated:FALSE];
A UIViewController then appears with a countdown. The player can go back to the menu DURING the countdown. However when I try this, the countdown keeps running and eventually the game starts, even thought the UIViewController has been dismissed (therefore the UIView has disappeared) in the backToMenu method.
[self.parentViewController dismissModalViewControllerAnimated:FALSE];
I've tried to release the game object in the viewDidAppear method of the menu, but no luck. I thought of having a "quit" BOOL value, so the countdown can check wether or not the player has quit the game, but there must be a better way to release an object AND stop all method calls inside it.
Thanks for helping me out.
CountDown method:
- (void)countDown {
SoundEffect *sound = [[SoundEffect alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"tick" ofType: #"wav"]];
[sound playAndRelease];
if(self.countDownStep > 0) {
feedback.image = [UIImage imageNamed:[NSString stringWithFormat:#"countdown%d.png",self.countDownStep]];
feedback.hidden = FALSE;
[self performSelector:#selector(countDown) withObject:nil afterDelay:0.8];
}
else {
[self displayMessage:self.startMessage];
[self.game performSelector:#selector(start) withObject:nil afterDelay:0.8];
[self performSelector:#selector(hide) withObject:nil afterDelay:0.8];
}
self.countDownStep--;
}
In the viewcontroller's viewWillDisappear, check if the timer is running. If it is, then just invalidate it.
[timerObject invalidate] will stop the timer
How do you handle the countdown? It seems like you want to explicitly void that countdown in the viewWillDisappear method for the UIViewController you mentioned.
I was assuming you would use something like NSTimer. In fact, that might still not be a bad idea. NSTimer can handle re-running your countdown method every 0.8 seconds. If you decrement the value to 0, your countdown has expired. If your view disappears, invalidate the timer in viewWillDisappear.