topLayoutGuide in child view controller

27,976

Solution 1

While this answer might be correct, I still found myself having to travel the containment tree up to find the right parent view controller and get what you describe as the "real topLayoutGuide". This way I can manually implement automaticallyAdjustsScrollViewInsets.

This is how I'm doing it:

In my table view controller (a subclass of UIViewController actually), I have this:

- (void)viewWillLayoutSubviews {
    [super viewWillLayoutSubviews];

    _tableView.frame = self.view.bounds;

    const UIEdgeInsets insets = (self.automaticallyAdjustsScrollViewInsets) ? UIEdgeInsetsMake(self.ms_navigationBarTopLayoutGuide.length,
                                                                                               0.0,
                                                                                               self.ms_navigationBarBottomLayoutGuide.length,
                                                                                               0.0) : UIEdgeInsetsZero;
    _tableView.contentInset = _tableView.scrollIndicatorInsets = insets;
}

Notice the category methods in UIViewController, this is how I implemented them:

@implementation UIViewController (MSLayoutSupport)

- (id<UILayoutSupport>)ms_navigationBarTopLayoutGuide {
    if (self.parentViewController &&
        ![self.parentViewController isKindOfClass:UINavigationController.class]) {
        return self.parentViewController.ms_navigationBarTopLayoutGuide;
    } else {
        return self.topLayoutGuide;
    }
}

- (id<UILayoutSupport>)ms_navigationBarBottomLayoutGuide {
    if (self.parentViewController &&
        ![self.parentViewController isKindOfClass:UINavigationController.class]) {
        return self.parentViewController.ms_navigationBarBottomLayoutGuide;
    } else {
        return self.bottomLayoutGuide;
    }
}

@end

Hope this helps :)

Solution 2

you can add a constraint in the storyboard and change it in viewWillLayoutSubviews

something like this:

- (void)viewWillLayoutSubviews
{
    [super viewWillLayoutSubviews];
    self.topGuideConstraint.constant = [self.parentViewController.topLayoutGuide length];
}

Solution 3

I might be wrong, but in my opinion the behaviour is correct. The topLayout value can be used by the container view controller to layout its view's subviews.

The reference says:

To use a top layout guide without using constraints, obtain the guide’s position relative to the top bound of the containing view.

In the parent, relative to the containing view, the value will be 64.

In the child, relative to the containing view (the parent), the value will be 0.

In the container View Controller you could use the property this way:

- (void) viewWillLayoutSubviews {

    CGRect viewBounds = self.view.bounds;
    CGFloat topBarOffset = self.topLayoutGuide.length;

    for (UIView *view in [self.view subviews]){
        view.frame = CGRectMake(viewBounds.origin.x, viewBounds.origin.y+topBarOffset, viewBounds.size.width, viewBounds.size.height-topBarOffset);
    }
}

The Child view controller does not need to know that there are a Navigation and a Status bar : its parent will have already laid out its subviews taking that into account.

If I create a new page based project, embed it in a navigation controller, and add this code to the parent view controllers it seems to be working fine:

enter image description here

Solution 4

The documentation says to use topLayoutGuide in viewDidLayoutSubviews if you are using a UIViewController subclass, or layoutSubviews if you are using a UIView subclass.

If you use it in those methods you should get an appropriate non-zero value.

Documentation link: https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewController_Class/Reference/Reference.html#//apple_ref/occ/instp/UIViewController/topLayoutGuide

Solution 5

In case if you have UIPageViewController like OP does and you have for example collection view controllers as children. Turns out the fix for content inset is simple and it works on iOS 8:

- (void)viewWillLayoutSubviews {
    [super viewWillLayoutSubviews];

    UIEdgeInsets insets = self.collectionView.contentInset;
    insets.top = self.parentViewController.topLayoutGuide.length;
    self.collectionView.contentInset = insets;
    self.collectionView.scrollIndicatorInsets = insets;
}
Share:
27,976
hpique
Author by

hpique

iOS, Android &amp; Mac developer. Founder of Robot Media. @hpique

Updated on July 05, 2022

Comments

  • hpique
    hpique almost 2 years

    I have a UIPageViewController with translucent status bar and navigation bar. Its topLayoutGuide is 64 pixels, as expected.

    However, the child view controllers of the UIPageViewController report a topLayoutGuide of 0 pixels, even if they're shown under the status bar and navigation bar.

    Is this the expected behavior? If so, what's the best way to position a view of a child view controller under the real topLayoutGuide?

    (short of using parentViewController.topLayoutGuide, which I'd consider a hack)

  • kmikael
    kmikael over 10 years
    But if the child view controllers are UIScrollView subclasses, you don't get the effect of the content showing up under the translucent navigation bar, if you set the frame like this, which is the whole purpose of having a translucent navigation bar in the first place.
  • Mazyod
    Mazyod almost 10 years
    I hope this answer is voted to inifitum... I went and removed like 10 places of wrong hard coded values all over the app written by lazy devs. Bug solved, and no more hard-code-ness.
  • Stian Høiland
    Stian Høiland over 9 years
    Care to share some code for this? I'm thick in the skull when it comes to constraint-based layout :)
  • Nat
    Nat over 9 years
    Great answer, thanks. Only to make things pure: UIEdgeInsets values are of CGFloat not double kind. I'd be better to set them as 0.f or 0.0f instead of 0.0 :).
  • pronebird
    pronebird over 9 years
    It does not make sense if you want to extend edges under navigation bar.
  • Archaeopterasa
    Archaeopterasa over 9 years
    Actually this works in iOS 7 too as long as you first remove the existing constraint(s) on the layout guide before adding your own.
  • Petar
    Petar over 9 years
    @Archaeopterasa can you elaborate on this please ? What existing constraints on the layout guide are you referring to ?
  • Petar
    Petar over 9 years
    @Jesse Rusak what exactly has been changed ? How can we apply the same change to apps running iOS 7 to achieve the same behaviour. I have made a specific question for this : stackoverflow.com/questions/28024425/…
  • Archaeopterasa
    Archaeopterasa over 9 years
    @pe60t0 The auto-added constraints can be removed (the ones that show up in debug output as _UILayoutSupportConstraint instead of NSLayoutConstraint). In my code I wanted to constrain a child view controller's layout guide to match the parent view controller's, so I first went through the constraints list and removed the one that set its height, and then was able to constrain it like a normal view. (The catch, since there always is one in autolayout: orientation changes are potentially messy, and require careful order-of-operations.)
  • pronebird
    pronebird over 9 years
    You don't call super, this is very neat LOL
  • Loadex
    Loadex about 9 years
    This works perfectly in 99% cases. But not if you have a UINavigationController as a childViewController.
  • Conor
    Conor about 9 years
    Thank you. I had to add a [_tableView setContentOffset:CGPointMake(0, 0 - _tableView.contentInset.top) animated:NO]; to have it scroll to the top correctly
  • Tim Johnsen
    Tim Johnsen over 8 years
    This just saved me a huge headache :)
  • Majid Bashir
    Majid Bashir over 7 years
    This solution worked for me in case for page controller , if someone has a page controller and added constraints to the view for child
  • LOP_Luke
    LOP_Luke over 7 years
    This answer fixed the top layout guide issues in my child view controllers with no navigation bar. Swift 3 version: override func viewWillLayoutSubviews() {self.topGuideConstraint.constant = (self.parent ?? self).topLayoutGuide.length}
  • Gligor
    Gligor almost 7 years
    What a genious! Thank you very much for this answer, saved me from pulling my hair out.
  • Jonathan.
    Jonathan. about 6 years
    Can you expand on this answer? I can't work out what constraint to add. Everyone that I add conflicts with an existing constraint on iOS 10.
  • koen
    koen over 5 years
    Unfortunately this no longer works in iOS 11 and higher: 'topLayoutGuide' is deprecated: first deprecated in iOS 11.0 - Use view.safeAreaLayoutGuide.topAnchor instead of topLayoutGuide.bottomAnchor