20

I'm trying to add a child view controller to a UIViewController contained in a UINavigationController with this code:

- (void)buttonTapped:(id)sender
{
    MyChildController *viewController = [self.storyboard instantiateViewControllerWithIdentifier:@"MyChild"];
    [self addChildViewController:viewController];
    [self.view addSubview:viewController.view];
    [viewController didMoveToParentViewController:self];


    viewController.view.alpha = 0.0f;
    [UIView animateWithDuration:0.4 animations:^{
        viewController.view.alpha = 1.0f;
    }];
}

But this is the result:

Image Result

As you can see the UINavigatioBar and the UIToolbar are still on top of the child view controller. How can I put the child view controller on top of all? I've already tried to replace the code with:

[self.navigationController addChildViewController:viewController];
    [self.navigationController.view addSubview:viewController.view];
    [viewController didMoveToParentViewController:self.navigationController];

But in this way the viewDidAppear:animated of the viewController doesn't get called. I don't know, why.

peterh
  • 11,875
  • 18
  • 85
  • 108
Fred Collins
  • 5,004
  • 14
  • 62
  • 111

3 Answers3

34

@Sam's comment is correct. You need to call beginApperanceTransition:animated: and endAppearanceTransition for viewDidAppear to be triggered. The reason why UINavigationController does not call viewDidAppear when you add a child view controller is because it has overridden its container composition methods to prevent the programmer from adding a child view controller in strange places. In your case, it doesn't want your child view to cover up the navigation bar. The correct usage of a navigation controller is to have children appear under the navigation bar. Nonetheless, you can still force upon this non-standard UI by manually telling the child when it is appearing and when it has finished appearing.

Add a child to UINavigationController

MyChildViewController* child = [[MyChildViewController alloc] init];
[self.navigationController addChildViewController:child];
child.view.frame = self.navigationController.view.bounds;
[self.navigationController.view addSubview:child.view];
child.view.alpha = 0.0;
[child beginAppearanceTransition:YES animated:YES];
[UIView
    animateWithDuration:0.3
    delay:0.0
    options:UIViewAnimationOptionCurveEaseOut
    animations:^(void){
        child.view.alpha = 1.0;
    }
    completion:^(BOOL finished) {
        [child endAppearanceTransition];
        [child didMoveToParentViewController:self.navigationController];
    }
];

Remove a child from UINavigationController

[child willMoveToParentViewController:nil];
[child beginAppearanceTransition:NO animated:YES];
[UIView
    animateWithDuration:0.3
    delay:0.0
     options:UIViewAnimationOptionCurveEaseOut
    animations:^(void){
        child.view.alpha = 0.0;
    }
    completion:^(BOOL finished) {
        [child endAppearanceTransition];
        [child.view removeFromSuperview];
        [child removeFromParentViewController];
    }
];
Community
  • 1
  • 1
Pwner
  • 3,714
  • 6
  • 41
  • 67
  • 1
    I think child be calling `didMoveToParentViewController:` when its being removed.. Something like, `[child didMoveToParentViewController:nil]` – Mohammad Abdurraafay May 11 '14 at 08:37
  • 2
    `beginAppearanceTransition` should be called before `addSubview`, and `endAppearanceTransition` should be called after `removeFromSuperview`, otherwise 'unbalanced calls' errors might occur. – thijsonline Mar 21 '18 at 12:58
8

In your first view controller, do something like this:

- (IBAction)buttonClick:(id)sender
{
    SecondViewController *secondView = [self.storyboard instantiateViewControllerWithIdentifier:@"SecondViewController"];
    UIImage *blurryImage = [UIImage imageNamed:@"foo.jpeg"];
    secondView.imageView.image = blurryImage;
    [self.navigationController addChildViewController:secondView];
    secondView.view.frame = self.navigationController.view.frame;
    [self.navigationController.view addSubview:secondView.view];
}

Then in your second view controller, add the getter for your imageview:

-(UIImageView *)imageView
{
    if( _imageView == nil )
    {
        _imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 320, 548)];
        [self.view addSubview:_imageView];
    }
    return _imageView;
}
JonahGabriel
  • 3,066
  • 2
  • 18
  • 28
  • I'm trying to use the viewcontroller-container approach because I need to blur the background view of the modal view controller that is also semi-transparent. The effect is the same as notification view that you pull down from the top of the screen. The background is semi-transparent and it's also blur/frosted glass. Why if I add a child view controller to `self.navigationController` the `viewDidAppear` method is not called? – Fred Collins Oct 08 '13 at 23:21
  • Hmm...not quite sure...have you tried calling [viewController viewWillAppear:NO]? – JonahGabriel Oct 08 '13 at 23:38
  • 1
    Yes and it does nothing. I need to call `[viewController viewDidAppear:NO]` just after the line `[viewController didMoveToParentViewController:self.navigationController]`. Why the hell the methods are not called? Otherwise if I add the child view controller to `self` and not `self.navigationController` it works but as showed in the above image the view is 'inside' the `navigationController`. – Fred Collins Oct 08 '13 at 23:44
  • So I just coded up a simple case of your problem and it looks like viewDidLoad is getting called. Why not put your logic in there? I am imagining you are deallocating your view when it gets hidden right? – JonahGabriel Oct 08 '13 at 23:51
  • Thanks for the time Jonah. Yea the `viewDidLoad` is getting called. I'm using `viewDidAppear` because I need to do stuff about making the view blurred (actually it doesn't work now). By the way thanks for the code! ;) Do you know how to make the view of the second view controller look like blurred/frosted glass? – Fred Collins Oct 09 '13 at 00:02
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/38833/discussion-between-jonah-at-godaddy-and-fred-collins) – JonahGabriel Oct 09 '13 at 00:06
  • 4
    In order for `viewWillAppear:` and `viewDidAppear:` to be called, you need to send the message `-beginAppearanceTransition:animated:` to the view controller responsible for the view you added. Then you do the animation, if any. And in the animation completion block, send `-endAppearanceTransition` to the view controller that appeared. NOTE: Do not call `[viewController viewDidAppear:NO]` - you should never invoke these methods yourself. – Sam Nov 12 '13 at 10:41
  • this is the perfection to me – Kernelzero Dec 16 '15 at 08:24
2

@Pwner's answer Swift version:

Add child to UINavigaitonController

let child = MyChildViewController()
self.navigationController?.addChildViewController(child)
guard let navigationController = navigationController else {
    return
}
child.view.frame = navigationController.view.bounds
child.beginAppearanceTransition(true, animated: true)
self.navigationController?.view.addSubview(child.view)
self.view.alpha = 0
UIView.animate(withDuration: 0.3, animations: {
    child.view.alpha = 1.0
}, completion: { _ in
    guard let navigationController = self.navigationController else {
        return
    }
    child.endAppearanceTransition()
    child.didMove(toParentViewController: navigationController)
})

Remove a child from UINavigationController

child.willMove(toParentViewController: nil)
child.beginAppearanceTransition(false, animated: true)
UIView.animate(withDuration: 0.3, animations: {
    child.view.alpha = 0.0
}, completion: { _ in
    guard let navigationController = self.navigationController else {
        return
    }
    child.view.removeFromSuperview()
    child.endAppearanceTransition()
    child.removeFromParentViewController()
})
Ivan Smetanin
  • 1,999
  • 2
  • 21
  • 28