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