List and detail controller - iphone

If I have 2 controllers; List and detail controller, what is the correct way to handle memory management for these 2 controllers?
I mean at what point should release be called on them?
Also in case my list controller is dynamic (i.e. data gets called from ext web service) and some data is passed to detail controller, where exactly should I write the code to retrieve/display the data in detail controller. I mean should it be viewDidLoad or viewWillAppear ?
Any examples would be great.

There is no single answer. But my answer is.... viewWillAppear
Take a detail view which is almost never used. So maybe you decide to create one each time it's used and destroy it after every use.
Take another detail view which may be used frequently. You decide create it once and just re-use it. Maybe you even destroy it on low-memory warnings and re-create it the next time it's used. In this case, you can't depend upon viewDidLoad being called each time the
Using viewWillAppear makes my code more consistent and makes it easier to make a change when I realize that detail view is being called a lot more than I expect. I should re-use it instead of creating it every time.
As to when you should release them... that really depends on what perspective. How often is it used? How much memory does it take to simply exist? How much work does it take to re-create?

From my experience, the best way to istantiate the detail view controller is:
MyDetailController * dc = [[MyDetailController alloc] initWithMyObject: anObject];
[self.navigationController pushViewController: dc animated: YES];
[dc release];
where anObject is the piece of your downloaded data you want to present in the detail view controller.
I assume your list controller is the main one of your app, which should be instantiate as follows:
- (BOOL) application: (UIApplication *) application didFinishLaunchingWithOptions: (NSDictionary *) launchOptions {
_baseWindow = [[UIWindow alloc] initWithFrame: [[UIScreen mainScreen] bounds]];
_listVC = [MyListController new];
[_baseWindow addSubview: _listVC.view];
[_baseWindow makeKeyAndVisible];
return YES;
}
And released:
- (void) dealloc {
[_listVC release]; _listVC = nil;
[_baseWindow release]; _baseWindow = nil;
[super dealloc];
}
Remember that -viewDidLoad is called after -loadView, which in turn is called when someone tries to access the -view property of the view controller.
Thus you may want to prepare your view in -viewDidLoad and do some additional tasks in -viewWillAppear or -viewDidAppear. Please note that -viewWillAppear (and similar methods) is called EVERY TIME that view controller's view is shown on screen. That is, for example, if you push another VC from detail vc and then pop, -viewDidAppear will be called again.

Related

viewwillappear and viewdidappear not called

this question is very frequent, but I am not able to solve it with any answers available.
I am working on iOS 5.1. My navigation controller is one tab amongst tab bar view controllers. There's a tableview, in which selecting of a row pushes new view controllers.
This problem occurs Only on selecting of the second row and only sometimes. It's not regular.
The Pushed view comes blank - viewWillAppear/viewDidAppear are not being called. On clicking the back button of the navigation bar - the root view's viewWillAppear/viewDidAppear are also not being called, making it blank.
I am pushing the view on select of first row/second row in exactly the same way. But the problem occurs only on the second row.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
switch (indexPath.row) {
case 0:
AViewController *aObj = [[AViewController alloc] init];
aObj.homeObj = self;
[self.navigationController pushViewController:aObj animated:YES];
[aObj release];
break;
case 1:
BViewController *bVCObj = [[BViewController alloc] init];
bVCObj.homeObj = self;
[self.navigationController pushViewController:bVCObj animated:YES];
[bVCObj release];
break;
default:
break;
}
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
I have tried this and this but in vain.
viewDidLoad is being called on pushing the BViewController, However, viewWillAppear and viewDidAppear is not being called. Following is my viewDidLoad:
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.backgroundColor = [UIColor blackColor];
NSLog(#"nav stack: %#", [self.navigationController viewControllers]);
NSLog(#"nav stack: %#", [[self.navigationController visibleViewController] description]);
//some initialization and call of methods
}
It's not regular. Sometimes I get this scenario, and this continues until I close the app from the background and restart it. But sometimes it works just fine. I am just pushing my view controller to the nab stack.
As I mentioned in the comment, It's a regular navigation controller in tab bar controller.
How are you defining your views for AViewController and BViewController? Generally you'd use initWithNibName, e.g.
AViewController *aObj = [[AViewController alloc] initWithNibName:#"mynibname" bundle:nil]`
As Carl pointed out, you can apparently use just init (though I don't see this documented in the UIViewController Class Reference), but then the system will be very particular about the name of your NIB file. The documentation does say, though, that you can use initWithNibName and pass a nil for the NIB name, in which case it will try to find it for you. Personally, if you're having inconsistent results, though, I'd try using initWithNibName and explicitly pass the name of your NIB, and see if that rectifies the situation.
Or are you building your view programmatically with loadView in your two controllers? Then you need to show us those loadView routines (not to be confused with viewDidLoad).
But according to the documentation, you need to either specify your NIB or use loadView. See the View Management discussion in the UIViewController Class Reference.
Update:
Given your feedback, I have a couple of thoughts:
Needless to say, the problem is apparently not related to the above code. You need to broaden you search and show us more code. Perhaps show us your viewDidLoad of B?
Generally when you don't get these sorts of events, it's because the view controller hierarchy has gotten out of sync with the view hierarchy. The most common way that people do this is if they've done something like "[addSubview someNewController.view]" at some point. If you're using a view controller in any context either than (a) your app delegate's initial configuration; (b) presentViewController (or dismiss); or (c) pushViewController (or pop), then you might want to share what you've done.
As andreamazz pointed out, your comment, "My navigation controller is inside a view controller of the tab bar controller," is a little disturbing if one reads it literally. You can put navigation bar in a view controller's view, but you can't put a navigation controller in a view controller (unless you're doing view controller containment, which is a whole different beast). Equally concerning is where, in another one of your questions, you said, "Embedding a UINavigationController or UITabBarController (my case) in a UIViewController somehow interrupts with the calling of these methods." Thing is, you don't embed nav controllers in other view controllers (unless it is, itself, a container controller such as a tab view controller), but rather its the other way around. But if you literally mean that you have a controller that contains a nav controller, you have to show us how you're doing that (proper view controller containment?) because that's highly unusual.
It's unusual, but I've had projects get corrupted, ending up in weird states. At a minimum, I might suggest "Product" - "Clean" and rebuild. If problem persists, and you've isolated the problem to to B's NIB, then temporarily rename the it and build a quick and dirty one from scratch.

How to dealloc uiviewcontroller/unload views on logout action

I think this is a pretty common usecase as I have seen it in several apps. But after spending couple of days, I am still struggling with this. I have a structure like the following:
UITabBarController
-- UINavigationController1
---- UITableViewController1
-- UINavigationController2
---- UITableViewController2
Now I have a logout button on UITableViewController2. When I click on that logout button I want all and any viewcontroller to be deallocated, all view unloaded. Basically start fresh like launching the app. I basically want the viewDidLoad on each of those UITableViewController called again.
I tried the following method to be called in my appdelegate when the logout action on UITableViewController2 is taken.
-(void) logout {
for (UINavigationController* ctrl in self.tabBarController.viewControllers) {
[ctrl popToRootViewControllerAnimated:NO];
ctrl.visibleViewController.view = nil;
}
[self.tabBarController.view removeFromSuperview];
[self.window addSubview:self.tabBarController.view];
}
But alas, it does not seem to work?
Any ideas how such a thing is accomplished? Also I see different behaviors in iOS4 vs iOS5 with the visibleViewController. I am not using any modal viewcontroller here. Any gotchas?
Update: I am not using ARC
thanks
mbh
Your for-loop will release and thus dealloc any view controllers that you have pushed onto the respective UINavigationController roots (depending on how many tabs you have), i.e. as these will not have a superview when you pop back to the root of each navigation controller, these are dealloc-ed automatically. These are your UITableViewControllers taken care of.
As for the respective UINavigationControllers, you would need your tabbar-controller to release the old instance. IMHO, this should be done for you when you release the UITabBarController.
This then leaves the UITabBarController itself. I don't think it can be done tbh. Your code will only remove the view, but not dealloc the tabbar controller itself. And as Krishna K points out, you need at least one view controller to reload all others.
Putting the code into the appdelegate makes sense, but you need to ensure that your logout() will not cause a retain on the UITableViewController2 as well as the UITabbarController as it's called from UITableViewController2 somewhere.
One idea to explore, does your AppDelegate hold an instance to the TabBar-Controller which you could release and create a new instance after removing the view from self.window?
// manually create UITabBarController - AppDelegate holds instance
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
mytabcontroller=[[UITabBarController alloc] initWithNibName:#"foo" bundle:nil];
}
- (void) logout {
[self.tabBarController.view removeFromSuperview];
[mytabcontroller release];
mytabcontroller=[[UITabBarController alloc] initWithNibName:#"foo" bundle:nil];
[self.window addSubview:self.tabBarController.view];
}
But as I said, there might be caveats with memory management at this point.
You need to release your view controllers. When their release method is called, that method should include statements to release all of its object's resources (and also dealloc its superclass).
Your rootViewController for both Navigation controllers are their respective TableView controllers. So I don't think popToRootViewController would do anything.
You probably need to reset the data and refresh the views instead of deallocating the views.

viewWillAppear does not run when using addSubView!

I'm stuck! I can't see why viewWillAppear doesn't run in my code but viewDidLoad runs. If I understand it correctly viewDidLoad runs once on the first instance and viewWillAppear runs every time a view is added to the stack of views to display.
I see others have had this issue but some how their solutions of calling viewWillAppear directly causes my app to crash. Other solutions were related to Navigation Controller and pushingView's but thats not what i'm using either! What am I missing?
Thanks in advance for your help! :)
See below:
View Controller #1 - Currently being displayed on screen
-(IBAction)someButtonPressed:(id)sender{
NSLog(#"FirstViewController - someButtonPressed");
SecondViewController *secondViewController = [[SecondViewController alloc] initWithNibName:#"SecondViewController" bundle:nil];
secondViewController.myLocation = self.myLocation;
secondViewController.myDatabase = self.myDatabase;
[self.view addSubview:secondViewController.view];
//[secondViewController viewWillAppear:YES];
}
SecondViewController:
- (void)viewWillAppear:(BOOL)animated {
NSLog(#"SecondViewController - viewWillAppear");
[super viewWillAppear:animated];
// updating ivars with data
}
// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad {
NSLog(#"SecondViewController - viewDidLoad");
[super viewDidLoad];
}
If I understand it correctly viewDidLoad runs once on the first instance and viewWillAppear runs every time a view is added to the stack of views to display.
-viewDidLoad is called every time a UIViewController's view is loaded. That may be many times during a single controller's life as the view may be unloaded to free up memory when it is not visible and reloaded, triggering another call to -viewDidLoad, when needed.
-viewWillAppear: is called when a UIViewController's view becomes visible. However UIKit assumes that UIViewController's views will fill their window. Nesting UIViewControllers' views is an example of abusing UIViewControllers and will result in unexpected behavior. As you have seen.
See About Custom View Controllers in the View Controller Programming Guide for iOS:
Each custom view controller object you create is responsible for managing all of the views in a single view hierarchy. In iPhone applications, the views in a view hierarchy traditionally cover the entire screen, but in iPad applications they may cover only a portion of the screen. The one-to-one correspondence between a view controller and the views in its view hierarchy is the key design consideration. You should not use multiple custom view controllers to manage different portions of the same view hierarchy. Similarly, you should not use a single custom view controller object to manage multiple screens worth of content.
If you wrote a custom UIViewController Container you might have overwritten the following method, which leads to your described behavior.
- (BOOL)shouldAutomaticallyForwardAppearanceMethods{
return NO;
}
In this case you have to manually handle beginAppearanceTransition/endAppearanceTransition.
See Apples View Controller Containment article
viewWillAppear: is called when a view controller is displayed in one of the normal ways (e.g. by selecting a tab in a UITabBarController, by pushing onto a UINavigationController, by being popped back to in a UINavigationController, by being presented with presentModalViewController:animated, by being uncovered after dismissModalViewControllerAnimated:, etc). Just displaying a view with addSubview: does not call the method.
It is possible to correctly call viewWillAppear: manually, but in general it's better to use one of the normal ways mentioned above.
Just try this.. I got it working :)
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSLog(#"SecondViewController - viewWillAppear");
// updating ivars with data
}
When you push view or present a view controller by pushViewController:animated or presentModelViewController:animated:, they will call viewWillAppear:animated:, and else method for you. But if you addSubview: manually, you need to call those method by self.

How to recreate UIViewController stack?

I'm writing a 'load' feature for my current iPhone app.
Practically what I want to do is get the user back where s/he left off. To do so I want to recreate all the UIViewControllers appeared before.
My problem is the UIViewControllers won't appear if I simply call [[self navigationController] pushViewController:newController animated:YES];.
The same code works fine when invoked as an event handler, like after touching a button. But if I do same without an even (for ex. in viewDidLoad) the new view controller's loadView method won't get called.
My actual code (with some pseudo elements):
- (void)viewDidLoad {
[super viewDidLoad];
if (loading)
[self onSomeEvent];
}
- (void)onSomeEvent {
UIViewController *newController = //init....;
[[self navigationController] pushViewController:newController animated:YES];
[newController release];
}
I guess viewDidLoad is not the right place to do such a call, but then what is?
I'm not sure that spreading your "load" feature all accross your controllers is the best way to achieve it.
I would rather put it in the init of your application, in the applicationDidFinishLauching part. Then you have a centralized place where you restore the previous state (easier to handle IMHO).
Let's suppose you want to implement some more advanced restore feature like:
displaying a splash UIView with an activity indicator to indicate the user that you're restoring previous state
restore the stack of your controllers in the navigation controller
remove the splash
it's easier to have all this code in the applicationDidFinishLauching : it's all managed at one point.
Morever, when restoring the old state to your navigation controller, you can avoid using the transitions and use animated:NO instead of YES when pushing your controllers. It will be easier to handle from your perspective and the restore time may be decreased if you remove time needed to achieve transitions.

How do I have a view controller run updating code when it is brought to the top of the stack of views?

I have a viewController (Planner) that loads two view controllers (InfoEditor and MonthlyPlan) when the application starts. MonthlyPlan is hidden behind InfoEditor (on load).
So my question is when I exchange InfoEditor for MonthlyPlan (MonthlyPlan gets brought to the top) how can I have data on the MonthlyPlan view be updated. An NSLog in viewDidLoad is being called when the application starts (which makes sense.) NSLogs in viewDidAppear and viewWillAppear aren't doing anything.
Any ideas?
Thanks!
-- Adding more details --
I'm creating the view hierarchy myself. A simple viewController that is just loading two other viewControllers. The two child viewControllers are loaded at the same time (on launch of application.) To exchange the two views I'm using this code:
[self.view exchangeSubviewAtIndex:1 withSubviewAtIndex:0];
The exchanging of the views is fine. The part that is missing is just some way of telling the subview, you're in front, update some properties.
There's a lack of details here. How are you "exchanging" the two views?
If you were using a UINavigationController as the container then viewWillAppear/viewDidAppear would be called whenever you push/pop a new viewController. These calls are made by the UINavigationController itself. If you ARE using a UINavigationController then make sure you have the prototypes correct for these functions.
- (void)viewWillAppear:(BOOL)animated
If you are trying to implement a view hierarchy yourself then you may need to make these calls yourself as part of activating/deactivating the views. From the SDK page of viewWillAppear;
If the view belonging to a view
controller is added to a view
hierarchy directly, the view
controller will not receive this
message. If you insert or add a view
to the view hierarchy, and it has a
view controller, you should send the
associated view controller this
message directly.
Update:
With the new details the problem is clear: This is a situation where you must send the disappear/appear messages yourself as suggested by the SDK. These functions are not called automagically when views are directly inserted/removed/changed, they are used by higher-level code (such as UINavigationController) that provides hierarchy support.
If you think about your example of using exchangeSubView then nothing is disappearing, one view just happens to cover the other wholly or partially depending on their regions and opacity.
I would suggest that if you wish to swap views then you really do remove/add as needed, and manually send the viewWillAppear / viewWillDisappear notifications to their controllers.
E.g.
// your top level view controller
-(void) switchActiveView:(UIViewController*)controller animated:(BOOL)animated
{
UIController* removedController = nil;
// tell the current controller it'll disappear and remove it
if (currentController)
{
[currentController viewWillDisapear:animated];
[currentController.view removeFromSuperView];
removedController = currentController;
}
// tell the new controller it'll appear and add its view
if (controller)
{
[controller viewWillAppear:animated];
[self.view addSubView:controller.view];
currentController = [controller retain];
}
// now tell them they did disappear/appear
[removedController viewDidDisappear: animated];
[currentController viewDidAppear: animated];
[removedController release];
}
I would just add an updataData method to each subview and call it at the same time you bring it to the front. You would need to add a variable to your root view controller to track the active subView:
[self.view exchangeSubviewAtIndex:1 withSubviewAtIndex:0];
if (subView1IsActive) [subView1Controller updateData];
else [subView2Controller updateData];