Yesterday, I tweeted a classical design issue on who should own a view controller.

 

 

The negative replies I received to the tweet were phenomenal that I had to write this post explaining the “why” behind the tweet.

Memory management with pushed view controllers

Things are straight forward when you “push” a view controller to the navigation stack. The UINavigationController retains/takes ownership of your view controller and you can forget about it. viewDidAppear/viewDidDisappear methods are called notifying the controllers to prepare itself.

Memory management with presented view controllers

However, this is not the case when you present a view controller modally.

Memory Management pre ARC

Lets see how memory management worked in the pre-ARC era (read: last year). Before ARC, there are two ways to retain the child view controller. The first (and slightly cumbersome) method is to have a retain property for every child controller, on the parent controller.

Method 1

@interface MasterViewController
@property(nonatomic, retain) ChildViewController *child;
@end

Presenting the view modally requires you to allocate it and then present it.

self.child = [[ChildViewController alloc] initWith…];
self.child.delegate = self;
[self.navigationController presentModalViewController:self.child animated:YES]
…
…

Implement the delegates

-(void) childViewController:(UIViewController*) childController didFinishWithResult {
 
  [self.child dismissModalViewControllerAnimated:YES];
  self.child.delegate = nil;
  self.child = nil;
}

Method 2

The second method is like the classical email sheet, where the didFinishWithResult delegate sends a pointer to self.

-(void) childViewController:(UIViewController*) childController didFinishWithResult {
 
[childController dismissModalViewControllerAnimated:YES];
[childController release];
}

The only advantage of this method is, you don’t need to retain a pointer to the child in the parent view controller.
You allocate the child view controller when you show it and release it when the method returns. However, this method isn’t going to work with ARC. You cannot allocate a pointer in one method and release it in another without the parent specifying a ownership qualifier on that pointer. That means we have only one option, retain a strong reference to the child.

Memory Management with ARC

Memory management with ARC is going to be exactly similar to method 1 that I showed you earlier, except that instead of a retain, you specify a strong ownership qualifier.

@interface MasterViewController
@property(nonatomic, strong) ChildViewController *child;
@end

The presenting and delegate handling remains the same. It gets tricky when you try to implement method 2 with ARC. The problem is, ARC compiler doesn’t allow a pointer to linger around. So your child controller gets deallocated after it’s presented unless you need to “own” the pointer explicitly.

The other easier way is to use blocks. With completion delegates implemented as blocks, this becomes easier. This, I would say, is synonymous to the method 2 which I described before.

ChildViewController *child = [[ChildViewController alloc] initWith…];
 
  __weak __block ChildViewController *weakChild = child; 
 
  child.completionHandler = ^(BOOL result) {
 
	[self dismissModalViewControllerAnimated:YES];
	weakChild = nil;
};
 
[self.navigationController presentModalViewController:child animated:YES]

Back to square one

In both methods, with or without ARC, we dismiss the child controller on the callback method in the parent. Coming back to our original problem, if you dismiss a child controller by calling

[self dismissModalViewControllerAnimated:YES];

The parent has no way to know that the child controller has completed it task and has been dismissed.
Using viewDidAppear on parent to release a child view controller and use boolean logic is, well, BOOL sh!t.

ARC manages memory in the following ways depending on your design.
1. If you captured the child within a completion handler, the memory allocated to the child leaks
2. If your child controller is a local stack variable and there is no completion handler block, the memory allocated to the child gets released prematurely (as soon as the view is presented and results in a crash)
3. If your child controller is owned by the parent with a strong reference, it lingers around till the parent view gets deallocated (if you set it to nil in viewDidUnload of the master).

All these three methods are probably not the right way to do. The only right way is to invoke a completion handler and let the parent dismiss your child safely. This is the reason why I tweeted,

 

Apple follows this pattern throughout their sample code and within their framework. So, I repeat, don’t ever dismiss a child view controller within a child.

Update 1: Method 2 works fine with ARC in this case since presentModalViewController bumps up the retain count. (Thanks Hollance and Johan Kool for pointing this out) However, do note that, there are some methods very similar to this, that are likely to be affected. CLLocationManager’s startUpdatingLocation will not call the delegate if there is no owner for the CLLocationManager object. Same happens to MKReverseGeocoder.

Update 2:When you call dismissModalViewController on a child, the UIKit framework automatically takes care of forwarding the message to the parent view controller.

Mugunth

Follow me on Twitter

  • After reading your post, I realized that even if I always followed this recommendation, the way I do it is to call “[self dismissModalViewControllerAnimated:YES]” on the *parent*, instead of “[self.child dismissModalViewControllerAnimated:YES]”. It always worked as expected.

    Do you know if there’s any difference?

  • Hollance

    While I don’t disagree with the idea that the view controller that presents the new screen should also be the one to dismiss it (in most cases), I’m not so sure that your memory management explanation is correct. Doing the following is just fine:

    – (void)someMethod
    {
        ChildViewController *controller = [[ChildViewController alloc] init …];
        [self presentModalViewController:controller animated:YES];
        [controller release];
    }

    -(void) childViewController:(UIViewController*)childController
    didFinishWithResult:(id)result
    {
         [childController dismissModalViewControllerAnimated:YES];
    }

    The presenting view controller will hold a strong reference to your child view controller until it gets dismissed.

  • The Apple documentation on “dismissModalViewControllerAnimated:” says:

    The presenting view controller is responsible for dismissing the view controller it presented. If you call this method on the presented view controller itself, however, it automatically forwards the message to the presenting view controller.

    So unless you are uncomfortable reading this code, you’re good with “child committing seppuku” without implementing delegates or anything else.

    The delegate approach can be error prone, since if you call the delegate method so that delegate could dismiss you – remember that once you dismissed the controller, the message call will return to the calling method where you can still try to access local variables, which could lead to some bugs and headache afterwards since the child view controller is deallocated.

    • MugunthKumar

      You call a “didFinish” delegate without actually finishing your task and try access more deal located variables?

      • I don’t, my point being that it’s possible for other developers to do that.

  • There is typo error it block deffinition. It has to use weakChild instead of self.

    ChildViewController *child = [[ChildViewController alloc] initWith…];
     
    __weak __block ChildViewController *weakChild = child;
     
    child.completionHandler = ^(BOOL result) {
     
    [weakChild dismissModalViewControllerAnimated:YES]; weakChild = nil;
    };
     
    [self.navigationController presentModalViewController:child animated:YES]

  • Neil Kimmett

    ” it lingers around till the parent view gets deallocated (if you set it to nil in viewDidUnload of the master)”

    viewDidUnload doesn’t get called on deallocation; dealloc does. viewDidUnload gets called in low memory situations:
    “When a low-memory warning occurs, the UIViewController class purges its views if it knows it can reload or recreate them again later. If this happens, it also calls the viewWillUnload and viewDidUnload methods to give your code a chance to relinquish ownership of any objects that are associated with your view hierarchy, including objects loaded from the nib file, objects created in your viewDidLoad method, and objects created lazily at runtime and added to the view hierarchy.”
    (from http://developer.apple.com/library/ios/#documentation/UIKit/Reference/UIViewController_Class/Reference/Reference.html)

  • Featuretower


    1. If you captured the child within a completion handler, the memory allocated to the child leaks “, is this real?? So you mean such block is useless and harmful?

  • I started getting crashes after adding a CALayer to the self-dismissing child view controller.  Upon return, the parent view controller’s gestureRecognizer: shouldReceiveTouch: method inexplicably references the child view controller (now pfft) at [touch.view isKindOfClass:[UIButton button]].  Thinking about going back to Method 1 or maybe trying the blocks technique.

  • Paul Davide

    Great Article. blog post . I am thankful for the information , Does someone know if my business might get access to a blank IRS 1040 – Schedule M form to fill out ?

    • Cornelius Desousa

      Hello Paul, my friend saw a template OREA 502 form here http://goo.gl/SiLy6p