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
This solution targets iOS 8 and above in Swift.
My app's root view controller is a navigation controller. Initially this navigation controller has one UIViewController
on its stack which I will call the parent view controller.
When the user taps a nav bar button, the parent view controller toggles between two child view controllers on its view using the containment API (toggling between mapped results and listed results). The parent view controller holds references to the two child view controllers. The second child view controller is not created until the first time the user taps the toggle button. The second child view controller is a table view controller and exhibited the issue where it underlaps the navigation bar regardless of how its automaticallyAdjustsScrollViewInsets
property was set.
To fix this I call adjustChild:tableView:insetTop
(shown below) after the child table view controller is created and in the parent view controller's viewDidLayoutSubviews
method.
The child view controller's table view and the parent view controller's topLayoutGuide.length
are passed to the adjustChild:tableView:insetTop
method like this...
// called right after childViewController is created
adjustChild(childViewController.tableView, insetTop: topLayoutGuide.length)
The adjustChild method...
private func adjustChild(tableView: UITableView, insetTop: CGFloat) {
tableView.contentInset.top = insetTop
tableView.scrollIndicatorInsets.top = insetTop
// make sure the tableview is scrolled at least as far as insetTop
if (tableView.contentOffset.y > -insetTop) {
tableView.contentOffset.y = -insetTop
}
}
The statement tableView.scrollIndicatorInsets.top = insetTop
adjusts the scroll indicator on the right side of the table view so it begins just below the navigation bar. This is subtle and is easily overlooked until you become aware of it.
The following is what viewDidLayoutSubviews
looks like...
override func viewDidLayoutSubviews() {
if let childViewController = childViewController {
adjustChild(childViewController.tableView, insetTop: topLayoutGuide.length)
}
}
Note that all the code above appears in the parent view controller.
I figured this out with help from Christopher Pickslay's answer to the question "iOS 7 Table view fail to auto adjust content inset".
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;
}
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
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];
}];
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.
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.