iOS 7: Custom container view controller and content inset

14,984

Solution 1

Thanks to Johannes Fahrenkrug's hint, I figured out the following: automaticallyAdjustsScrollViewInsets indeed only seems to work as advertised when the child view controller's root view is a UIScrollView. For me, this means the following arrangement works:

Content view controller inside Navigation controller inside Custom container view controller.

While this doesn't:

Content view controller inside Custom container view controller inside Navigation controller.

The second option, however, seems more sensible from a logical point of view. The first option probably only works because the custom container view controller is used to affix a view to the bottom of the content. If I wanted to put the view between the navigation bar and the content, it wouldn't work that way.

Solution 2

I've had a similar problem. So on iOS 7, there is a new property on UIViewController called automaticallyAdjustsScrollViewInsets. By default, this is set to YES. When your view hierarchy is in any way more complicated than a scroll/table view inside a navigation or tab bar controller, this property didn't seem to work for me.

What I did was this: I created a base view controller class that all my other view controllers inherit from. That view controller then takes care of explicitly setting the insets:

Header:

#import <UIKit/UIKit.h>

@interface SPWKBaseCollectionViewController : UICollectionViewController

@end

Implementation:

#import "SPWKBaseCollectionViewController.h"

@implementation SPWKBaseCollectionViewController


- (void)viewDidLoad
{
    [super viewDidLoad];

    [self updateContentInsetsForInterfaceOrientation:self.interfaceOrientation];
}

- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
    [self updateContentInsetsForInterfaceOrientation:toInterfaceOrientation];

    [super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
}

- (void)updateContentInsetsForInterfaceOrientation:(UIInterfaceOrientation)orientation
{
    if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7) {
        UIEdgeInsets insets;

        if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
            insets = UIEdgeInsetsMake(64, 0, 56, 0);
        } else if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
            if (UIInterfaceOrientationIsPortrait(orientation)) {
                insets = UIEdgeInsetsMake(64, 0, 49, 0);
            } else {
                insets = UIEdgeInsetsMake(52, 0, 49, 0);
            }
        }

        self.collectionView.contentInset = insets;
        self.collectionView.scrollIndicatorInsets = insets;
    }
}

@end

This also works for web views:

self.webView.scrollView.contentInset = insets;
self.webView.scrollView.scrollIndicatorInsets = insets;

If there is a more elegant and yet reliable way to do this, please let me know! The hardcoded inset values smell pretty bad, but I don't really see another way when you want to keep iOS 5 compatibility.

Solution 3

FYI in case anyone is having a similar problem: 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

UINavigationController calls undocumented method _updateScrollViewFromViewController:toViewController in transition between controllers to update content insets. Here is my solution:

UINavigationController+ContentInset.h

#import <UIKit/UIKit.h>
@interface UINavigationController (ContentInset)
- (void) updateScrollViewFromViewController:(UIViewController*) from toViewController:(UIViewController*) to;
@end

UINavigationController+ContentInset.m

#import "UINavigationController+ContentInset.h"
@interface UINavigationController()
- (void) _updateScrollViewFromViewController:(UIViewController*) from toViewController:(UIViewController*) to;
@end

@implementation UINavigationController (ContentInset)
- (void) updateScrollViewFromViewController:(UIViewController*) from toViewController:(UIViewController*) to {
    if ([UINavigationController instancesRespondToSelector:@selector(_updateScrollViewFromViewController:toViewController:)])
        [self _updateScrollViewFromViewController:from toViewController:to];
}
@end

Call updateScrollViewFromViewController:toViewController somewhere in your custom container view controller

[self addChildViewController:newContentViewController];
[self transitionFromViewController:previewsContentViewController
                  toViewController:newContentViewController
                          duration:0.5f
                           options:0
                        animations:^{
                            [self.navigationController updateScrollViewFromViewController:self toViewController:newContentViewController];

                    }
                    completion:^(BOOL finished) {
                        [newContentViewController didMoveToParentViewController:self];
                    }];

Solution 5

You don't need to call any undocumented methods. All you need to do is call setNeedsLayout on your UINavigationController. See this other answer: https://stackoverflow.com/a/33344516/5488931

Share:
14,984
tajmahal
Author by

tajmahal

Updated on June 16, 2022

Comments

  • tajmahal
    tajmahal almost 2 years

    I have a table view controller wrapped in a navigation controller. The navigation controller seems to automatically apply the correct content inset to the table view controller when it is presented via presentViewController:animated:completion:. (Can anyone explain to me how this works exactly?)

    However, as soon as I wrap the combination in a custom container view controller and present that instead, the topmost part of the table view content is hidden behind the navigation bar. Is there anything I can do in order to preserve the automatic content inset behaviour in this configuration? Do I have to "pass through" something in the container view controller for this to work correctly?

    I'd like to avoid having to adjust the content inset manually or via Auto Layout as I want to continue supporting iOS 5.