dave
dave

Reputation: 1180

Subview UINavigationController Leak ARC

I'm experiencing a memory leak (the UINavigationController and its root View Controller are both being leaked) when presenting and dismissing a UINavigationController in a subview. My method of presentation of the navigation controller seems a bit non-standard, so I was hoping someone in the SO community might be able to help.

1. Presentation

The Navigation Controller is presented as follows:

-(void) presentSubNavigationControllerWithRootViewControllerIdentifier:(NSString *)rootViewControllerIdentifier inStoryboardWithName:(NSString *)storyboardName {

    // grab the root view controller from a storyboard
    UIStoryboard * storyboard = [UIStoryboard storyboardWithName:storyboardName bundle:nil];
    UIViewController * rootViewController = [storyboard instantiateViewControllerWithIdentifier:rootViewControllerIdentifier];

    // instantiate the navigation controller
    UINavigationController * nc = [[UINavigationController alloc] initWithRootViewController:rootViewController];

    // perform some layout configuration that should be inconsequential to memory management (right?)
    [nc setNavigationBarHidden:YES];
    [nc setEdgesForExtendedLayout:UIRectEdgeLeft | UIRectEdgeRight | UIRectEdgeBottom];
    nc.view.frame = _navControllerParentView.bounds;

    // install the navigation controller (_navControllerParentView is a persisted IBOutlet)
    [_navControllerParentView addSubview:nc.view];

    // strong reference for easy access
    [self setSubNavigationController:nc];
}

At this point, my expectation is that the only "owner" of the navigation controller is the parent view controller (in this case, self). However, when dismissing the navigation controller as shown below, it is not deallocated (and as a result its rootViewController is also leaked, and so on down the ownership tree).

2. Dismissal

Dismissal is pretty simple, but it seems not to be sufficient for proper memory management:

-(void) dismissSubNavigationController {

    // prevent an orphan view from remaining in the view hierarchy
    [_subNavigationController.view removeFromSuperview];

    // release our reference to the navigation controller
    [self setSubNavigationController:nil];
}

Surely something else is "retaining" the navigation controller as it is not deallocated. I don't think it could possibly be the root view controller retaining it, could it?

Some research has suggested that retainCount is meaningless, but FWIW I've determined that it remains at 2 after dismissal, where I would expect it to be zero.

Is there an entirely different / better method of presenting the subNavigationController? Maybe defining the navigation controller in the storyboard would have greater benefit than simply eliminating the need for a few lines of code?

Upvotes: 0

Views: 982

Answers (2)

rdelmar
rdelmar

Reputation: 104082

It is best practice when adding a controller's view as a subview of another controller's view, that you make that added view's controller a child view controller; that is, the controller whose view your adding it to, should implement the custom container controller api. An easy way to set this up is to use a container view in the storyboard which gives you an embedded controller automatically (you can select that controller and, in the edit menu, choose embed in Navigation controller to get the UI you're trying to make). Normally, this embedded view controller would be added right after the parent controller's view is loaded, but you can suppress that by implementing shouldPerformSegueWithIdentifier:sender:. I created a simple test app with this storyboard,

enter image description here

The code in ViewController to suppress the initial presentation, and the button methods to subsequently present and dismiss it is below,

@implementation ViewController

-(BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender {
    if ([identifier isEqualToString:@"Embed"]) { // The embed segue in IB was given this identifier. This method is not called when calling performSegueWithIdentifier:sender: in code (as in the button method below)
        return NO;
    }else{
        return  YES;
    }
}


- (IBAction)showEmbed:(UIButton *)sender {

    [self performSegueWithIdentifier:@"Embed" sender:self];
}


- (IBAction)dismissEmbed:(UIButton *)sender {
    [[self.childViewControllers.firstObject view] removeFromSuperview];
    [self.childViewControllers.firstObject willMoveToParentViewController:nil];
    [self.childViewControllers.firstObject removeFromParentViewController];
}

@end

The navigation controller and any of its child view controllers are properly deallocated when the Dismiss button is touched.

Upvotes: 1

rounak
rounak

Reputation: 9387

The navigationController property on a UIViewController is retain/strong, which is presumably the other strong reference.

So try popping all view controllers from the navigation controller and see if that works.

Upvotes: 1

Related Questions