11

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:

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?

Community
  • 1
  • 1
Imran
  • 1,488
  • 1
  • 15
  • 36

3 Answers3

14

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.

Jody Hagins
  • 27,943
  • 6
  • 58
  • 87
  • 1
    Thanks for the detailed answer. Im fine with not caching the controllers. I just wanted to understand the load unload cycle a little more so I could understand exactly what I can and can't put in viewDidLoad. It's much clearer now. – Imran Aug 15 '12 at 15:43
  • 1
    Actually, viewWillUnload and viewDidUnload have been deprecated, so you should not really put any code in there going forward. Handle didReceiveMemoryWarning to free up resources when under memory pressure. – Jody Hagins Aug 15 '12 at 15:53
  • Ahh right in IOS6. Sounds like there is no real point to viewDidLoad anymore, it might as well be a constructor. I take it this means that memory warnings will no longer cause viewDidUnload to be called? This kind of changes things as it means viewDidLoad is only ever called once? I don't have IOS6 at the moment to test. – Imran Aug 15 '12 at 16:24
  • @JodyHagins what's wrong with using the same cached UIViewController? would all the methods be invoked except for viewDidLoad? – Dejell Dec 25 '12 at 12:21
  • @Odelya Yes, but whatever state that you may have in your view controller will be overwritten too. The trick is, therefore, to avoid reusing controllers that are already on the stack, because you will mess up their instance variables. – Sergey Kalinichenko Dec 27 '12 at 18:37
  • @dasblinkenlight so you suggest NOT to use the caching solution? – Dejell Dec 27 '12 at 19:45
  • @Odelya Why, no, I did not suggest that! All I suggest is that you should be very careful if you decide to do it. Jody's implementation is perfect for cases when the same view controller is never pushed onto the stack while another active one exists. If this is the case for your system, you can use this implementation as-is. Otherwise, you'd need to make caching less aggressive - for example, by removing from cache the items being pushed, and returning them to cache only when they are popped from the stack. Of course you'd need to cache multiple items per ID, too. – Sergey Kalinichenko Dec 27 '12 at 19:51
  • @dasblinkenlight how can I know if the same view controller is never pushed to the stack? does the fact that I am using segue points to the case that I need it to prevent the view to be recreated ? – Dejell Dec 27 '12 at 20:05
  • @Odelya You know that a controller is never pushed on the stack while its other copy is on the stack through the way you design your storyboard: if you do not have segues going back, you are safe. This is very often the case, but there are exceptions: for example, if you have an arbitrarily deep hierarchy, and you use multiple copies of the same view controller to traverse it, the controller would have a segue going back to itself. That's the red flag, telling you that caching should be either disabled, or that it should proceed extra carefully and pay attention to controller deallocations. – Sergey Kalinichenko Dec 27 '12 at 20:13
  • I have segue going back - but not to the same controller. It would go back from C to B and another one from C to A – Dejell Dec 27 '12 at 20:27
  • @JodyHagins when I try to navigate back to a controller using your code I get: 'Application tried to present modally an active controller – Dejell Dec 27 '12 at 21:06
  • That means you didn't use it right... or there is a bug in it, which is more likely since I hacked it up as an demonstration example. I am currently pretty busy for the Christmas holidays, but if you are willing to put an example project up somewhere (dropbox maybe), I'd be glad to look at it. – Jody Hagins Dec 27 '12 at 23:12
13

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.

Phillip Mills
  • 30,888
  • 4
  • 42
  • 57
  • Its the bits where they talk about views being unloaded on 'low memory conditions' implying they are left around by default which I haven't seen to be the case. So really it depends on the parent controller implementation. If you use navigation controllers a new instance is created and loaded each time. Not so with tabcontrollers. – Imran Aug 15 '12 at 12:58
  • 1
    A way to test the low memory behavior (on the simulator) is to put up a view controller, cover it with a modal view controller, and use the Hardware->Simulate Memory Warning option. The hidden controller's view should unload, and then reload when the modal one is dismissed. – Phillip Mills Aug 15 '12 at 14:05
  • oic interesting, ill try that. I guess any view that is not currently active is a candidate for unloading then. – Imran Aug 15 '12 at 14:20
  • Yup it does unload, updated my test project: https://github.com/imuz/ViewDidLoadTest. – Imran Aug 15 '12 at 14:29
  • Interestingly though, under normal conditions when you close a modal dialogue or navigate backwards unload is not called. – Imran Aug 15 '12 at 14:30
0

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.

borrrden
  • 33,256
  • 8
  • 74
  • 109