navigationController keeps running after pop - iphone

Please help me understand the mystery of navigationController. I have a HomeViewController which gets invoked from the didFinishLaunchingWithOptions. From HomeViewController user press a button and my code is
-(IBAction)showMap:(id)sender
{
MapViewController *mapViewController = Nil;
mapViewController = [[MapViewController alloc] initWithNibName:#"MapView-iPad" bundle:nil];
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
[appDelegate.navigationController pushViewController:mapViewController animated:YES];
}
When a user wants to go back from MapViewController, I use the code
-(IBAction)goBackToHome:(id)sender
{
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
[appDelegate.navigationController popViewControllerAnimated:YES];
}
I was under impression that once I am out of MapViewController, all my resources get freed associated with MapViewController. To verify that I put this code inside initWithNibName in MapViewController.
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
[NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:#selector(printMessage) userInfo:nil repeats:YES];
}
return self;
}
-(void) printMessage
{
NSLog(#"I am inside Map View Controller");
}
To my surprise, even after popping out the MapViewController, the printMessage still continues. Please help me understand what is going on and why MapViewController is still running. Is there any way I can verify the MapViewController is freed up ?

Take a look at the documentation for NSTimer. In particular, the scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: method. The documentation notes that
target
The object to which to send the message specified by aSelector when the timer fires. The target object is retained by the timer and released when the timer is invalidated.
This means that your timer is causing the MapViewController to be retained. Without that timer holding onto a reference to the controller, it should indeed be deallocated when you pop it from the navigation controller.

MapViewController is still being retained: The timer retains the ViewController, and the timer is retained by the run loop it is scheduled on. To get your MapViewController to be released you have to remove the timer from the run loop by using [yourTimer invalidate]. One way to do this is to keep a weak reference to your timer in your MapViewController:
#property (weak) NSTimer *timer;
When you create the timer, assign it to the property:
self.timer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:#selector(printMessage) userInfo:nil repeats:YES];
Now you can invalidate the timer when you no longer need it, e.g. in viewWillDisappear:
- (void)viewWillDisappear {
[self.timer invalidate];
[super viewWillDisappear];
}

Related

when should I dismiss the timer?

I have a timer which will be initialized on viewDidLoad, and be released on viewDidUnload, but I found the viewDidUnload nerver be called and each time the view is presented to user the viewDidLoad is called, so I have many timers running simultaneously... which is not what I want.
I just want to stop&release the timer when user navigate away from this view, how can I do that?
Don't initialize it on view did load, instead initialize it in view will apper and stop it in view will dissapear, that way it will stop when you navigate away and restart once you come back to that view.
I had the exact same problem you are having. This is the code just in case you need it:
- (void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
NSLog(#"View will appear");
myTimer = [NSTimer timerWithTimeInterval:5.0 target:self selector:#selector(pageCycler) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer: myTimer forMode: NSDefaultRunLoopMode];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
NSLog(#"View will dissapear");
[myTimer invalidate];
myTimer=nil;
}

switching from view 1 to view 2 without pressing any buttons

How to switch from view1 to view 2 without having any button pressed. In my views i have is uiimageview and uitextview
With the NSTimer i m trying to do this
in the viewdidload method by using the following code:
In the firstviewcontroller.h file
#interface FirstViewController : UIViewController
{
NSTimer *SwitchingTimer;
}
In the firstviewcontroller.m file
- (void)viewDidLoad
{
[super viewDidLoad];
SwitchingTimer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:#selector(SwitchView) userInfo:nil repeats:YES];
}
-(void)SwitchViews:(id)sender
In the secondviewcontroller.m file
-(void) SwitchView
{
SecondViewController *SecondView = [[SecondViewController alloc]
initWithNibName:#"SecondViewController" bundle:nil];
SecondView.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
[self presentModalViewController:SecondView animated:YES];
[SwitchingTimer invalidate];
self.SwitchingTimer = nil;
}
but nothing is happening. Can someone please tell me what i m missing in my code. Will appreciate help.
Thanks in advance.
There are a few issues in your code that are worth mentioning though I am not sure if those will provide you a solution.
Why do you want to repeat the timer every 2 seconds. I think you just want to switch to next view only once and if so then dont repeat the timer. So no need to invalidate the timer.
Your code for the SwitchView method is leaking memory. Please make sure that the SecondView is released after presenting the modal view(in case you are not using ARC).
Please follow the standard naming conventions. For eg: methods and variables should start with lowercase.
Regarding your issue please make sure that the nib name is correct and you are getting a valid object for the second view controller. You can check by using NSLog. Also ensure that the method Switchview is called. Try putting a break point and verify that it is called.
Another Option
If you just want to switch the view only once you can go for another option which does not make use of the NSTimer. For this, you can use performSelector:withObject:afterDelay:. This is just another option for the scenario I mentioned above.
You need to add it to the run loop:
- (void)viewDidLoad
{
[super viewDidLoad];
SwitchingTimer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:#selector(SwitchView) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer: SwitchingTimer forMode:NSDefaultRunLoopMode];
}
Also, you could rename -(void)SwitchView to -(void)switchViewTimerDidFire:(NSTimer *)timer. From the Documentation:
[...] The selector must have the following signature:
- (void)timerFireMethod:(NSTimer*)theTimer
The timer passes itself as the argument to this method.

Set Timer from another class to fire in another view

EDIT: Added the NSRunLoop from my code, as mentioned by Deepak below. This was originally in my code and forgot to add as it was commented out.
I have 2 classes: MainViewController, and ConfigViewController. The user switches to the ConfigView and uses a UIDatePicker / UIButton combo to set a Date/Time. Upon grabbing the correct time from the UIDatePicker object, I setup a NSTimer to fire as per the following code:
ConfigViewController.m
-(IBAction)setAlarmDate:(id)sender {
//Instantiate to get access to doAlarm:
MainViewController *mvc = [[MainViewController alloc] init];
dateFormat = [[NSDateFormatter alloc] init];
[dateFormat setDateStyle:NSDateFormatterMediumStyle];
[dateFormat setTimeStyle:NSDateFormatterShortStyle];
NSString *target = [NSString stringWithFormat:#"%#",[dateFormat stringFromDate:datePicker.date]];
alarmDate = [datePicker date];
mvc.fireTimer = [[NSTimer alloc]
initWithFireDate:alarmDate interval:1 target:mvc
selector:#selector(doAlarm:) userInfo:nil repeats:YES];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addTimer:mvc.timer forMode:NSDefaultRunLoopMode];
[self dismissModalViewControllerAnimated:NO];
}
The doAlarm: method is as follows:
MainViewController.m
- (void)doAlarm:(NSTimer *)timer {
NSLog(#"Called doAlarm:");
UIImage *ac = [UIImage imageNamed:#"alarmclock.png"];
[self.alarmview setImage:ac];
[self.alarmview setHidden:NO];
[self.view addSubview:alarmview];
[self.view bringSubviewToFront:alarmview];
}
However, when I set the alarm date, the timer fails to fire. I think the following problems are afoot:
1) I am instantiating a new instance of the MainViewController class, setting the Timer going and then passing control back to the "original" instance of MainViewController when I dismiss the MVC. At this point, probably the "new" instance of MainViewController is nothing but a dangling pointer, and is never referenced again anyway, hence no segfault.
2) doAlarm: references self.view, which is supposed to be the MainViewController.view, but as it's instantiated in the scope of ConfigViewController, the alarm image would never be seen anyway...
I imagine my theories are a bit unfounded, but with my current level of knowledge, they make sense to me.
Any light you can shed on the above would be smashing.
Many thanks!
swisscheese.
If you go to the documentation for initWithFireDate:interval:target:selector:userinfo:repeats:, you will notice that while the timer is initialized it is not scheduled yet. You will need to add it a NSRunLoop to make it work. Do this after you have initialized an alarm as above.
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
You can also consider using the scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: which does that for you.
There are of course few other things here.
New MainViewController object
You guessed it right. You are creating an unrelated object which will just leak. To solve this you can maintain a weak reference to the MainViewController object. You can do this as follows -
#class MainViewController; // Forward declaration
#interface ConfigViewController {
...
MainViewController *mainViewController;
}
...
#property (nonatomic, assign) MainViewController *mainViewController;
...
#end
in ConfigViewController.m:
#import "MainViewController.h"
#implementation ConfigViewController
#synthesize mainViewController;
...
- (IBAction)setAlarmDate:(id)sender {
dateFormat = [[NSDateFormatter alloc] init];
[dateFormat setDateStyle:NSDateFormatterMediumStyle];
[dateFormat setTimeStyle:NSDateFormatterShortStyle];
alarmDate = [datePicker date];
mainViewController.fireTimer = [[NSTimer alloc]
initWithFireDate:alarmDate interval:1 target:mainViewController
selector:#selector(doAlarm:) userInfo:nil repeats:YES];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addTimer:mainViewController.timer forMode:NSDefaultRunLoopMode];
mainViewController = nil;
[self dismissModalViewControllerAnimated:NO];
}
...
#end
Timer
I am not sure if you need a repeating timer here. There is an alternative - performSelector:withObject:afterDelay:. This will invoke a method on an object this is called after the desired delay. Does what it calls itself. It will be something like this -
[mvcReference performSelector:#selector(doAlarm)
withObject:nil
afterDelay:[datePicker.date timeIntervalSinceNow]];
doAlarm:
I did not get what you wanted to know here but you are right. self is a reference to the MainViewController object. Unless you retained the view controller that you displayed modally, it would have been dismissed and deallocated by then.

Don't call "dealloc" method when I use "layoutSubviews" or "didMoveToSuperview"

I have a XIB with UIView.
This UIView is associated (in IB) with custom class of UIView - Page1.
File's Owner of this is MainMenuController (UIViewController, of course).
I use this XIB when init controller with "initWithNibName" and add it into navController.viewControllers.
In Page1.m I write:
- (void)didMoveToSuperview
{
NSLog(#"Page 1 did move to superview");
mainTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:#selector(refreshDateAndTime) userInfo:nil repeats:YES];
}
-(void) refreshDateAndTime {
NSLog(#"second");
}
- (void)dealloc
{
NSLog(#"Page1 dealloc called");
[mainTimer invalidate];
mainTimer = nil;
[mainTimer release];
[super dealloc];
}
When I start timer "mainTimer" by this code, method "dealloc" isn't called and object isn't unload from memory and timer is running.
If I comment lines in "didMoveToSuperview" block dealloc called and all is OK.
Why?
Dealloc will only be called when your object has no retains left (ie, after your final release).
When you create your timer, you are telling it that its target is self. To avoid complications later on (ie, having self be released from memory and your timer still active, it therefore retains self. Timers will retain their targets.
This means there is one extra retain on your view, which means dealloc won't be called (it's still retained by the timer).
Basically, don't use dealloc to invalidate your timer - use something else (perhaps a method that is triggered when your timer is no longer required).

UIView and NSTimer not releasing memory

I have a line of UILabel text in a UIView which is regularly updated via NSTimer. This code is supposed to write a status item near the bottom of the screen every so often. The data comes from outside of its control.
My app runs out of memory really fast because it seems the UILabel is not being released. It seems that dealloc is never called.
Here is a very compressed version of my code (error checking etc removed for clarity.):
File:SbarLeakAppDelegate.h
#import <UIKit/UIKit.h>
#import "Status.h"
#interface SbarLeakAppDelegate : NSObject
{
UIWindow *window;
Model *model;
}
#end
File:SbarLeakAppDelegate.m
#import "SbarLeakAppDelegate.h"
#implementation SbarLeakAppDelegate
- (void)applicationDidFinishLaunching:(UIApplication *)application
{
model=[Model sharedModel];
Status * st=[[Status alloc] initWithFrame:CGRectMake(0.0, 420.0, 320.0, 12.0)];
[window addSubview:st];
[st release];
[window makeKeyAndVisible];
}
- (void)dealloc
{
[window release];
[super dealloc];
}
#end
File:Status.h
#import <UIKit/UIKit.h>
#import "Model.h"
#interface Status : UIView
{
Model *model;
UILabel * title;
}
#end
File:Status.m
This is the where the problem lies. UILabel just does not seem to be released, and quite possible the string as well.
#import "Status.h"
#implementation Status
- (id)initWithFrame:(CGRect)frame
{
self=[super initWithFrame:frame];
model=[Model sharedModel];
[NSTimer scheduledTimerWithTimeInterval:.200 target:self selector:#selector(setNeedsDisplay) userInfo:nil repeats:YES];
return self;
}
- (void)drawRect:(CGRect)rect
{
title =[[UILabel alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 320.0f, 12.0f)];
title.text = [NSString stringWithFormat:#"Tick %d", [model n]] ;
[self addSubview:title];
[title release];
}
- (void)dealloc
{
[super dealloc];
}
#end
File: Model.h (this and the next are the data sources, so included only for completeness.) All it does is update a counter every second.
#import <Foundation/Foundation.h>
#interface Model : NSObject
{
int n;
}
#property int n;
+(Model *) sharedModel;
-(void) inc;
#end
File: Model.m
#import "Model.h"
#implementation Model
static Model * sharedModel = nil;
+ (Model *) sharedModel
{
if (sharedModel == nil)
sharedModel = [[self alloc] init];
return sharedModel;
}
#synthesize n;
-(id) init
{
self=[super init];
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:#selector(inc) userInfo:nil repeats:YES];
return self;
}
-(void) inc
{
n++;
}
#end
The problem is that you are never removing the UILabel from the Status UIView. Let's take a look at your retain counts in drawRect:
(void)drawRect:(CGRect)rect {
title =[[UILabel alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 320.0f, 12.0f)];
Here, you have created a UILabel with alloc, which creates an object with a retain count of 1.
[self addSubview:title];
[title release];
Adding the UILabel to Status view increases title's retain count to 2. The following release results in a final retain count of 1. Since the object is never removed from its superview, the object is never deallocated.
Essentially, you are adding one UILabel on top of another, each time the timer is fired, until memory runs out.
As suggested below, you should probably create the UILabel once when the view loads, and just update the UILabel's text with [model n].
As a housekeeping note, you might also want to make sure that you are properly deallocating any left over objects in your dealloc methods. 'model' and 'title' should be released in Status' dealloc, just as 'model' should be in SbarLeakAppDelegate.
Hope this helps.
Edit [1]:
It sounds like you have the memory issue pretty well handled at this point. I just wanted to suggest another alternative to the two timers you are using.
The timer you have running in your Status object fires every .2 seconds. The timer which actually increments the 'model' value, n, fires only once each second. While I believe you are doing this to ensure a more regular "refresh rate" of the Status view, you are potentially re-drawing the view 4 or 5 times per second without the data changing. While this is may not be noticeable because the view is fairly simple, you might want to consider something like NSNotification.
With NSNotification, you can have the Status object "observe" a particular kind of notification that will be fired by the Model whenever the value 'n' changes. (in this case, approximately 1 per second).
You can also specify a callback method to handle the notification when it is received. This way, you would only call -setNeedsDisplay when the model data was actually changed.
There are 2 problems with your code.
Problem 1
In -drawRect you add a subview to the view hierarchy every time the view is drawn. This is wrong for 2 reasons:
Every time the view is drawn, the number of subviews increases by 1
You are modifying the view hierarchy at draw time - this is incorrect.
Problem 2
Timers retain their targets. In the initializer for your Status object, you create a timer which targets self. Until the timer is invalidated, there is a retain cycle between the timer and the view, so the view will not be deallocated.
If the approach of using a timer to invalidate the view is really the correct solution to your problem, you need to take explicit steps to break the retain cycle.
One approach to doing this is to schedule the timer in -viewDidMoveToWindow: when the view is being put in a window [1], and invalidate the timer when the view is being removed from a window.
[1] Having the view invalidate itself periodically when it isn't displayed in any window is otherwise pointless.
Instead of calling -setNeedsDisplay with your NSTimer in the view controller, why not create a method that calls "title.text = [NSString stringWithFormat:#"Tick %d", [model n]] ;"? That way instead of re-creating the label every time the timer fires you can just update the value displayed.