Crashing while assigning nil to UIViewController after removing view and popping - iphone

I have a sequence in which I need to pop a UIViewcontroller from outside the class. From the server I will get an log out event, and I need to pop the view controller if it is open. So I have done this to find the top view controller and pop it:
UIViewController *top_view_ctrlr = [self.navigationController topViewController];
if (top_view_ctrlr.view != nil) {
[top_view_ctrlr.view removeFromSuperview];
}
// Popping only the top view controller.
[top_view_ctrlr.navigationController popToViewController:dash animated:NO];
Then I am setting nil for all the view controllers created, e.g.:
if (history != nil) {
history = nil; // Here history is an view controller. Because it is not on top, I am setting only nil here to release all its memory.
}
As I am using ARC, I guess I don't need to worry much about memory release inside history. However, it's crashing with:
[history_class tableView:cellForRowAtIndexPath:]: message sent to deallocated instance.
What could be the problem? Why it is crashing when I set the object to nil?

Why do you remove the topViewController's view from its superview? You should never need to do that in a navigation controller, popToViewController: does that automatically, and removing a view "behind the navigation controller's back" is probably causing the crash.

I think I know what you plan to achieve, but I fear you lack some fundamental understanding of how to use UIViewControllers and UINavigationControllers, that's why I recommend reading this first:
View Controller Programming Guide for iOS
I'm sure it will help you with your current problem and in the future.

Related

self.navigationController pushViewController not adding controller to stack

I am on a viewController called vcA and I do this:
[self.navigationController pushViewController:vcB animated:YES];
and it works. vcB is pushed. Inside vcB's viewDidAppear I do this:
NSArray *controllers = self.navigationController.viewControllers;
and controllers contains just one object, vcA !!!! (what?)
Why is vcB being added to the controllers array? Is there anything that can prevent that from happening?
thanks.
I banged my head on the wall for a week to solve this problem involving pushing a view controller on the navigation controller stack.
The problem was this: I was on a navigation controlled based app and I had 3 view controllers, vA, vB and vC.
I was pushing vB from vA using this:
[self.navigationController pushViewController:vB animated:YES];
it worked, but, when I tried to push vC from vB, the self.navigationController property of vB was nil and vB was not on the navigation controller stack. What? Yes, I pushed vB on the navigation stack and vB was not being added to it, but even so, vB was showing correctly.
I discovered that inside vB's viewDidLoad, self.navigationController was not nil and that vB was on the navigation controller stack, but as soon as vB's viewDidLoad ended, vB was removed from the navigation controller stack and its navigationController property was set to nil. At that time, vB was a kind of ghost control, outside the navigation controller stack.
I don't need to mention that from vB I was unable to get back to vA or to push vC.
How that happen?
simple, I was pushing vB from inside a block... something like:
void (^ block1)() = ^(){
[self.navigationController pushViewController:vB animated:YES];
};
What was happening because a UIKit function (the push thing) was being executed in a block that was probably running on a thread that was not the main thread.
The resolution to that was to wrap the push in a dispatch to the main thread, using this:
void (^ block1)() = ^(){
dispatch_async(dispatch_get_main_queue(),
^{
[self.navigationController pushViewController:vB animated:YES];
});
};
The app had another minor problem with a wrong outlet connected to a viewController, but this was the main problem. This is the kind of problem hard to spot, for being subtle. I discovered the problem when I added another button to the navigation bar and that button did not appear. So, I suspect that something involving UIKit was happening, or not happening in that case. The big problem here is that this code was not crashing, no error message, nothing, just working bad, but working.
I believe this has to do with the timing of your checking. During the pushViewController method, the next view controller's viewWillAppear: method (and others, such as the one you're checking) are in the process of getting called. I believe after all those method are called, the view controller is considered on the navigation stack AFTER the animation is done completing.

viewDidLoad is in fact called every time there is a segue transition

I have seen a lot of posts on stack overflow stating that the viewDidLoad method of controllers is only called the first time the controller is accessed and not necessarily every time but always at least once.
This is not what I am seeing at all! I put together a simple test to highlight this:
https://github.com/imuz/ViewDidLoadTest
It seems for navigation controller segues and modal views viewDidLoad is always called. The only time it is not called is when switching between tabs.
Every explanation of viewDidLoad I can find contradicts this:
When is viewDidLoad called?
UIViewController viewDidLoad vs. viewWillAppear: What is the proper division of labor?
http://www.manning-sandbox.com/thread.jspa?threadID=41506
And apples own documentation indicate that a view is only unloaded when memory is low.
I am currently doing initialisation in viewDidLoad making the assumption that it is called with every segue transition.
Am I missing something here?
Phillip Mills' answer is correct. This is just an enhancement of it.
The system is working as documented.
You are seeing viewDidLoad because the view controller being pushed onto the navigation controller is a new instance. It must call viewDidLoad.
If you investigate a little further, you would see that each of those view controllers are deallocated when they are popped (just put a breakpoint or NSLog in dealloc). This deallocation has nothing to do with the view controller container... it does not control the life of the controller it uses... it is just holding a strong reference to it.
When the controller is popped off the navigation controller stack, the nav controller releases its reference, and since there are no other references to it, the view controller will dealloc.
The navigation controller only holds strong references to view controllers that are in its active stack.
If you want to reuse the same controller, you are responsible for reusing it. When you use storyboard segues, you relinquish that control (to a large extent).
Let's say you have a push segue to view controller Foo as the result of tapping some button. When that button is tapped, "the system" will create an instance of Foo (the destination view controller), and then perform the segue. The controller container now holds the only strong reference to that view controller. Once it's done with it, the VC will dealloc.
Since it creates a new controller each time, viewDidLoad will be called each time that controller is presented.
Now, if you want to change this behavior, and cache the view controller for later reuse, you have to do that specifically. If you don't use storyboard segues, it's easy since you are actually pushing/popping the VC to the nav controller.
If, however, you use storyboard segues, it's a bit more trouble.
There are a number of ways to do it, but all require some form of hacking. The storyboard itself is in charge of instantiating new view controllers. One way is to override instantiateViewControllerWithIdentifier. That is the method that gets called when a segue needs to create a view controller. It's called even for controllers that you don't give an identifier to (the system provides a made-up unique identifier if you don't assign one).
Note, I hope this is mostly for educational purposes. I'm certainly not suggesting this as the best way to resolve your problems, whatever they may be.
Something like...
#interface MyStoryboard : UIStoryboard
#property BOOL shouldUseCache;
- (void)evict:(NSString*)identifier;
- (void)purge;
#end
#implementation MyStoryboard
- (NSMutableDictionary*)cache {
static char const kCacheKey[1];
NSMutableDictionary *cache = objc_getAssociatedObject(self, kCacheKey);
if (nil == cache) {
cache = [NSMutableDictionary dictionary];
objc_setAssociatedObject(self, kCacheKey, cache, OBJC_ASSOCIATION_RETAIN);
}
return cache;
}
- (void)evict:(NSString *)identifier {
[[self cache] removeObjectForKey:identifier];
}
- (void)purge {
[[self cache] removeAllObjects];
}
- (id)instantiateViewControllerWithIdentifier:(NSString *)identifier {
if (!self.shouldUseCache) {
return [super instantiateViewControllerWithIdentifier:identifier];
}
NSMutableDictionary *cache = [self cache];
id result = [cache objectForKey:identifier];
if (result) return result;
result = [super instantiateViewControllerWithIdentifier:identifier];
[cache setObject:result forKey:identifier];
return result;
}
#end
Now, you have to use this storyboard. Unfortunately, while UIApplication holds onto the main storyboard, it does not expose an API to get it. However, each view controller has a method, storyboard to get the storyboard it was created from.
If you are loading your own storyboards, then just instantiate MyStoryboard. If you are using the default storyboard, then you need to force the system to use your special one. Again, there are lots of ways to do this. One simple way is to override the storyboard accessor method in the view controller.
You can make MyStoryboard be a proxy class that forwards everything to UIStoryboard, or you can isa-swizzle the main storyboard, or you can just have your local controller return one from its storyboard method.
Now, remember, there is a problem here. What if you push the same view controller on the stack more than once? With a cache, the exact same view controller object will be used multiple times. Is that really what you want?
If not, then you now need to manage interaction with the controller containers themselves so they can check to see if this controller is already known by them, in which case a new instance is necessary.
So, there is a way to get cached controllers while using default storyboard segues (actually there are quite a few ways)... but that is not necessarily a good thing, and certainly not what you get by default.
I believe the Apple documentation is describing a situation where the view controller is not being deallocated. If you use a segue, then you are causing the instantiation of a new destination controller and, being a new object, it needs to load a view.
In xib-based apps, I have sometimes cached a controller object that I knew I might re-use frequently. In those cases, they behaved in keeping with the documentation in terms of when a view had to be loaded.
Edit:
On reading the links you included, I don't see any contradiction in them. They, too, are talking about things that happen during the lifespan of a view controller object.
It is called every time the controller's view is loaded from scratch (i.e. requested but not yet available). If you deallocate the controller and the view goes along with it, then it will be called again the next time you instantiate the controller (for example when you create the controller to push it modally or via segue). View controllers in tabs are not deallocated because the tab controller keeps them around.

ARC UINavigationController stack not getting deallocated when presented as modal view controller

First: I ported my App to ARC and everything seemed to work. But now I discovered a problem: I have a UINavigationController that is presented modally with some UIViewControllers on its stack. But when I dismiss the modal view controller, the view controllers from the stack don't seem to be deallocated. Here is what I do:
UIViewController* root = [[UIViewController alloc] init];
UINavigationController* navi = [[UINavigationController alloc] initWithRootViewController:root];
[self presentModalViewController:navi animated:TRUE];
Then from the root I push some more view controllers, but that doesn't really matter. The fact is when I later call
[self dismissModalViewControllerAnimated:TRUE];
root doesn't get deallocated. Of course in my code root is a subclass of UIViewController, and I track dealloc and viewDidUnload, but nothing gets called.
Any ideas?
What's inside your navigation controller? It could be that something else (perhaps a view controller inside your navigation controller) is the culprit, which is leading up the chain meaning the navigation controller doesn't get released.
Either way, the code you posted is correct, so if your navigation controller isn't being released after calling dismissModalViewController it would suggest that something else still has an active reference to it or one of its dependencies. I know that doesn't answer your question, but you will probably have to hunt around.
Since you aren't showing actual code, it's hard to tell what is going on with your root view controller.
But, with ARC, if you have a strong pointer to an object, it won't get released. I suspect that you are holding on to this controller after adding it to your navigation controller.
But, without seeing your code, I can't tell.

iPhone - error loading view controller

I load view controllers all the time in the following format:
-(void)loadSelectUser {
MyViewController *nextController = [[MyViewController alloc] initWithStyle:UITableViewStyleGrouped];
MyAppDelegate *delegate = [[UIApplication sharedApplication] delegate];
[delegate.navigationController pushViewController:nextController animated:YES];
[nextController release];
}
And I've never had an issue with that. But right now I'm dealing with the issue that the next view doesn't load completely. The navigation bar shows up and the viewDidLoad and the numberOfSectionsInTableView methods are both called. That is it. The table doesn't show up, it still shows the previous view.
I imagine this means there is a memory leak or something not connected properly. Is this the right path to be looking? If so, what is your best suggestion for debugging this issue. My code has no error messages so I'm not sure where to start. I load the view properly in a different controller, but for some reason it doesn't do it after this particular view*.
*This view happens to do a lot data manipulation with downloading objects, saving them and such. But again, it looks like it is all working properly. What would mess up the navigation controller loading the next view completely?
Oh, and just to mess things up more, some times, it works properly. But I run it one more time and it doesn't do it again.
Update: TechZen comment about the proper way to push a new view controller seemed to help a little. There is a higher rate of it working, unless I am pushing a tableviewcontroller. Depending on the action my view will push a UITableViewController or a UIViewController with a nib file. The second usually (not always) works.
Also, in a different view I am adding a modal view. But when I try to dismiss it using [self dismissModalViewControllerAnimated:YES]; it doesn't always work. Again it's hit or miss on it. Anyone have an idea of what would be causing the transition of windows to be finicky?
Calling the app delegate to get the navigation controller is unnecessary and risky. Any view controller on a navigation controller stack has a populated navigationController property so you can just use self.navigationController.
It's risky to call the app delegate's navigation controller because you have no guarantee that you will get the same navigation controller as the one that currently holds the view controller calling the push. You could in theory end up with two overlapping and conflict navigation controllers.
Switch the code to self.navigationController and see if that fixes the problem.

What happens to the root view in UINavigationViewController when you push on a new view?

Specifically, what am I supposed to do with the view that is now hidden after pushing on a new view controller?
In my situation I have a view with animations going on, that continue to execute after the view is off screen.
Is there some accepted convention?
Do I remove the View Controller and View from memory?
Does Cocoa Touch have a convenient method to "Pause" a view (and controller) and remove it from memory, and bring it back into existence when needed (after a pop)?
Do I have to archive it myself and then un-archive it?
Are there any examples you can point me to?
Thanks
Another possible solution is to implement two of the following methods:
– viewWillAppear:
– viewDidAppear:
– viewWillDisappear:
– viewDidDisappear:
You could potentially stop your animation in viewWillDisappear or viewDidDisappear and then restart it in viewWillAppear or viewDidAppear. You could also store any necessary state information about the animation before you stop it.
Under low memory conditions, your controller's 'view' property will automatically be set to nil, if it's not on screen. Then the view will automatically load again when it is needed - and viewDidLoad should get called at that time.
If your view controller is retaining any subviews of the top-level view, then you may want to override the view controller's setView: method, check if the view is being set to nil, and if so, release subviews that you were retaining. Otherwise, the top-level view may never get deallocated.
- (void)setView:(UIView *)view
{
[super setView:view];
if (view == nil)
{
// Release other views
// self.someView = nil;
}
}
Whenever you call pushViewController, the current viewcontroller is stored in an array by navigation controller (this can be accessed using the viewControllers property of navcontroller).
Think of it as a stack. As you call pushViewController, a new viewcontroller is added to the stack on top of the current viewcontroller. But your rootviewcontroller is still in memory. When you call popViewController, that viewcontroller is removed from the stack and released.
So if you want to stop your animation when the view disappears, use the viewWillDisappear method as suggested by Andy.