Content falls beneath navigation bar when embedded in custom container view controller.

12,784

Solution 1

Your custom container view controller will need to adjust the contentInset of the second view controller according to your known navigation bar height, respecting the automaticallyAdjustsScrollViewInsets property of the child view controller. (You may also be interested in the topLayoutGuide property of your container - make sure it returns the right value during and after the view switch.)

UIKit is remarkably inconsistent (and buggy) in how it applies this logic; sometimes you'll see it perform this adjustment automatically for you by reaching multiple view controllers down in the hierarchy, but often after a custom container switch you'll need to do the work yourself.

Solution 2

This seems to all be a lot simpler than what people make out.

UINavigationController will set scrollview insets only while laying out subviews. addChildViewController: does not cause a layout, however, so after calling it, you just need to call setNeedsLayout on your navigationController. Here's what I do while switching views in a custom tab-like view:

[self addChildViewController:newcontroller];
[self.view insertSubview:newview atIndex:0];
[self.navigationController.view setNeedsLayout];

The last line will cause scrollview insets to be re-calculated for the new view controller's contents.

Solution 3

FYI in case anyone is having a similar problem: this issue can occur even without embedded view controllers. It appears that automaticallyAdjustsScrollViewInsets is only applied if your scrollview (or tableview/collectionview/webview) is the first view in their view controller's hierarchy.

I often add a UIImageView first in my hierarchy in order to have a background image. If you do this, you have to manually set the edge insets of the scrollview in viewDidLayoutSubviews:

- (void) viewDidLayoutSubviews {
    CGFloat top = self.topLayoutGuide.length;
    CGFloat bottom = self.bottomLayoutGuide.length;
    UIEdgeInsets newInsets = UIEdgeInsetsMake(top, 0, bottom, 0);
    self.collectionView.contentInset = newInsets;

}

Solution 4

I found a better solution,use the undocumented method of UINavigationController.

#import <UIKit/UIKit.h>

@interface UINavigationController (ContentInset)


- (void) computeAndApplyScrollContentInsetDeltaForViewController:(UIViewController*) controller;

@end

#import "UINavigationController+ContentInset.h"

@interface UINavigationController()


- (void)_computeAndApplyScrollContentInsetDeltaForViewController:(id)arg1; 

@end


@implementation UINavigationController (ContentInset)

- (void) computeAndApplyScrollContentInsetDeltaForViewController:(UIViewController*) controller
{
    if ([UINavigationController instancesRespondToSelector:@selector(_computeAndApplyScrollContentInsetDeltaForViewController:)])
        [self _computeAndApplyScrollContentInsetDeltaForViewController:controller];
}

@end

then,do like this

- (void) cycleFromViewController: (UIViewController*) oldC
                toViewController: (UIViewController*) newC
{
    [oldC willMoveToParentViewController:nil];                        
    [self addChildViewController:newC];
  
    [self transitionFromViewController: oldC toViewController: newC   
                              duration: 0.25 options:0
                            animations:^{
                                newC.view.frame = oldC.view.frame;                                    
                                [self.navigationController computeAndApplyScrollContentInsetDeltaForViewController:newC];
                            }
                            completion:^(BOOL finished) {
                                [oldC removeFromParentViewController];                  
                                [newC didMoveToParentViewController:self];
                            }];
}

Share:
12,784
djibouti33
Author by

djibouti33

Updated on June 06, 2022

Comments

  • djibouti33
    djibouti33 almost 2 years

    UPDATE

    Based on Tim's answer, I implemented the following in each view controller that had a scrollview (or subclass) that was part of my custom container:

    - (void)didMoveToParentViewController:(UIViewController *)parent
    {
        if (parent) {
            CGFloat top = parent.topLayoutGuide.length;
            CGFloat bottom = parent.bottomLayoutGuide.length;
    
            // this is the most important part here, because the first view controller added 
            // never had the layout issue, it was always the second. if we applied these
            // edge insets to the first view controller, then it would lay out incorrectly.
            // first detect if it's laid out correctly with the following condition, and if
            // not, manually make the adjustments since it seems like UIKit is failing to do so
            if (self.collectionView.contentInset.top != top) {
                UIEdgeInsets newInsets = UIEdgeInsetsMake(top, 0, bottom, 0);
                self.collectionView.contentInset = newInsets;
                self.collectionView.scrollIndicatorInsets = newInsets;
            }
        }
    
        [super didMoveToParentViewController:parent];
    }
    

    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    I have custom container view controller called SegmentedPageViewController. I set this as a UINavigationController's rootViewController.

    The purpose of SegmentedPageViewController is to allow a UISegmentedControl, set as the NavController's titleView, to switch between different child view controllers.

    enter image description here

    These child view controllers all contain either a scrollview, tableview, or collection view.

    We're finding that the first view controller loads fine, correctly positioned underneath the navigation bar. But when we switch to a new view controller, the navbar isn't respected and the view is set underneath the nav bar.

    enter image description here

    We're using auto layout and interface builder. We've tried everything we can think of, but can't find a consistent solution.

    Here's the main code block responsible for setting the first view controller and switching to another one when a user taps on the segmented control:

    - (void)switchFromViewController:(UIViewController *)oldVC toViewController:(UIViewController *)newVC
    {
        if (newVC == oldVC) return;
    
        // Check the newVC is non-nil otherwise expect a crash: NSInvalidArgumentException
        if (newVC) {
    
            // Set the new view controller frame (in this case to be the size of the available screen bounds)
            // Calulate any other frame animations here (e.g. for the oldVC)
            newVC.view.frame = self.view.bounds;
    
            // Check the oldVC is non-nil otherwise expect a crash: NSInvalidArgumentException
            if (oldVC) {
                // **** THIS RUNS WHEN A NEW VC IS SET ****
                // DIFFERENT FROM FIRST VC IN THAT WE TRANSITION INSTEAD OF JUST SETTING
    
    
                // Start both the view controller transitions
                [oldVC willMoveToParentViewController:nil];
                [self addChildViewController:newVC];
    
                // Swap the view controllers
                // No frame animations in this code but these would go in the animations block
                [self transitionFromViewController:oldVC
                                  toViewController:newVC
                                          duration:0.25
                                           options:UIViewAnimationOptionLayoutSubviews
                                        animations:^{}
                                        completion:^(BOOL finished) {
                                            // Finish both the view controller transitions
                                            [oldVC removeFromParentViewController];
                                            [newVC didMoveToParentViewController:self];
                                            // Store a reference to the current controller
                                            self.currentViewController = newVC;
                                        }];
            } else {
    
                // **** THIS RUNS WHEN THE FIRST VC IS SET ****
                // JUST STANDARD VIEW CONTROLLER CONTAINMENT
    
                // Otherwise we are adding a view controller for the first time
                // Start the view controller transition
                [self addChildViewController:newVC];
    
                // Add the new view controller view to the view hierarchy
                [self.view addSubview:newVC.view];
    
                // End the view controller transition
                [newVC didMoveToParentViewController:self];
    
                // Store a reference to the current controller
                self.currentViewController = newVC;
            }
        }
    
    }