70

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)

hpique
  • 119,096
  • 131
  • 338
  • 476
  • 3
    I wonder if they haven't implemented `topLayoutGuide` thoroughly enough for nested ViewController containment. Begs the question of how one might deal with it if implementing a custom container... – Mike Pollard Oct 02 '13 at 16:23
  • 1
    Are you sure you're testing the topLayoutGuide property in either viewWillLayoutSubviews or viewDidLayoutSubviews ? If I recall correctly, its not guaranteed to return a correct value in the viewWillAppear / viewDidAppear methods... – Carlos P Oct 02 '13 at 21:05
  • 2
    I'm testing it in `viewWillLayoutSubviews`, yes. – hpique Oct 03 '13 at 06:37
  • Yeah, I haven't found a solution for this either. There are many bugs with child view controllers and extended edges. iOS7 is just not ready. – Léo Natan Oct 04 '13 at 15:47
  • Not sure if this would produce a useful/different result, but you could try using Auto Layout & VFL to actually pin the top edge of each child view controller's view to the `topLayoutGuide` object (as opposed to setting its frame based on the value returned by `length`). – smileyborg Oct 05 '13 at 06:37
  • @smileyborg That's what I'm doing, and the result is consistent with the value returned by length. – hpique Oct 07 '13 at 06:56
  • If I understand correctly, this is the expected behavior: [link]:(https://developer.apple.com/library/ios/documentation/uikit/reference/UIViewController_Class/Reference/Reference.html#//apple_ref/occ/instp/UIViewController/topLayoutGuide). The Container View Controller lays out the view of the child controller, and not the opposite. So the child doesn't need to know where is the topLayoutGuide, because it's view is layed out and given the correct position by it's parent controller – LombaX Oct 10 '13 at 17:03
  • @LombaX It doesn't work like than with UINavigationController, for example. The child view controller has to use the topLayoutGuide. – hpique Oct 10 '13 at 17:09
  • @LombaX The point is to allow the child view controller to move *its* subviews according to the guide, not to move itself. – Glenn Maynard Nov 13 '13 at 01:09
  • 3
    I think there's definately something a bit funky going on. I have a UINavigationController with a UITabbarController inside. The tab that's initially selected always gets the correct spacing to appear below the navigation bar. But when switching to the other tabs the top cells appear beneath the top bar. – Stephen Darlington Nov 29 '13 at 09:05

11 Answers11

49

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 :)

Community
  • 1
  • 1
NachoSoto
  • 1,743
  • 13
  • 17
  • 1
    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. – Mazyod Jun 02 '14 at 13:20
  • 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 :). – Nat Nov 19 '14 at 12:07
  • This works perfectly in 99% cases. But not if you have a UINavigationController as a childViewController. – Loadex Mar 17 '15 at 16:51
  • 2
    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 – Conor Apr 30 '15 at 16:10
  • This just saved me a huge headache :) – Tim Johnsen Jan 27 '16 at 06:04
  • What a genious! Thank you very much for this answer, saved me from pulling my hair out. – Gligor Aug 08 '17 at 21:06
  • 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` – koen Sep 15 '18 at 17:53
11

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

vinaut
  • 2,416
  • 15
  • 13
  • 20
    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. – kmikael Oct 15 '13 at 18:36
  • You don't call super, this is very neat LOL – pronebird Feb 09 '15 at 15:14
  • This solution worked for me in case for page controller , if someone has a page controller and added constraints to the view for child – Majid Bashir Aug 26 '16 at 13:21
11

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];
}
chliul
  • 343
  • 3
  • 9
  • 1
    It does not make sense if you want to extend edges under navigation bar. – pronebird Dec 15 '14 at 16:49
  • 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}` – LOP_Luke Nov 22 '16 at 20:36
8

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

CornPuff
  • 1,924
  • 20
  • 24
3

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;
}
pronebird
  • 12,068
  • 5
  • 54
  • 82
2

This has been addressed in iOS 8.

How to set topLayoutGuide position for child view controller

Essentially, the container view controller should constrain the child view controller's (top|bottom|left|right)LayoutGuide as it would any other view. (In iOS 7, it was already fully constrained at a required priority, so this didn't work.)

Community
  • 1
  • 1
Jesse Rusak
  • 56,530
  • 12
  • 101
  • 102
  • Care to share some code for this? I'm thick in the skull when it comes to constraint-based layout :) – Stian Høiland Sep 05 '14 at 00:12
  • 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. – Archaeopterasa Jan 12 '15 at 04:04
  • @Archaeopterasa can you elaborate on this please ? What existing constraints on the layout guide are you referring to ? – Petar Jan 19 '15 at 12:53
  • @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 : http://stackoverflow.com/questions/28024425/ios-toplayoutguide-using-a-child-view-controller-difference-between-ios-7-and-io – Petar Jan 19 '15 at 12:56
  • @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.) – Archaeopterasa Jan 21 '15 at 01:51
  • 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. – Jonathan. May 17 '18 at 07:02
1

I think the guides are definitely meant to be set for nested child controllers. For example, suppose you have:

  • A 100x50 screen, with a 20 pixel status bar at the top.
  • A top-level view controller, covering the whole window. Its topLayoutGuide is 20.
  • A nested view controller inside the top view covering the bottom 95 pixels, eg. 5 pixels down from the top of the screen. This view should have a topLayoutGuide of 15, since its top 15 pixels are covered by the status bar.

That would make sense: it means that the nested view controller can set constraints to prevent unwanted overlap, just like a top-level one. It doesn't have to care that it's nested, or where on the screen its parent is displaying it, and the parent view controller doesn't need to know how the child wants to interact with the status bar.

That also seems to be what the documentation--or some of the documentation, at least--says:

The top layout guide indicates the distance, in points, between the top of a view controller’s view and the bottom of the bottommost bar that overlays the view

(https://developer.apple.com/library/ios/documentation/UIKit/Reference/UILayoutSupport_Protocol/Reference/Reference.html)

That doesn't say anything about only working for top-level view controllers.

But, I don't know if this is what actually happens. I've definitely seen child view controllers with nonzero topLayoutGuides, but I'm still figuring out the quirks. (In my case the top guide should be zero, since the view isn't at the top of the screen, which is what I'm banging my head against at the moment...)

Glenn Maynard
  • 55,829
  • 10
  • 121
  • 131
0

This is the approach for the known guide length. Create constrains not to guides, but to view's top with fixed constants assuming guide distance will be.

kirander
  • 2,202
  • 20
  • 18
0

Swifty implementation of @NachoSoto answer:

extension UIViewController {

    func navigationBarTopLayoutGuide() -> UILayoutSupport {
        if let parentViewController = self.parentViewController {
            if !parentViewController.isKindOfClass(UINavigationController) {
                return parentViewController.navigationBarTopLayoutGuide()
            }
        }

        return self.topLayoutGuide
    }

    func navigationBarBottomLayoutGuide() -> UILayoutSupport {
        if let parentViewController = self.parentViewController {
            if !parentViewController.isKindOfClass(UINavigationController) {
                return parentViewController.navigationBarBottomLayoutGuide()
            }
        }

        return self.bottomLayoutGuide
    }
}
Nat
  • 12,032
  • 9
  • 56
  • 103
0

Not sure if anyone still got problem with this, as I still did a few minutes ago.
My problem is like this (source gif from https://knuspermagier.de/2014-fixing-uipageviewcontrollers-top-layout-guide-problems.html).
For short, my pageViewController has 3 child viewcontrollers. First viewcontroller is fine, but when I slide to the next one, the whole view is incorrectly offset to the top (~20 pixel, I guess), but will return to normal after my finger is off the screen.
I stayed up all night looking for solution for this but still no luck finding one. Then suddenly I came up with this crazy idea:

[pageViewController setViewControllers:@[listViewControllers[1]] direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:^(BOOL finished) {

}];

[pageViewController setViewControllers:@[listViewControllers[0]] direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:^(BOOL finished) {

}];

My listViewControllers has 3 child viewcontrollers. The one at index 0 has problem, so I firstly set it as root of pageviewcontroller, and right after that set it back to the first view controller (as I expected). Voila, it worked!
Hope it helps!

0

This is an unfortunate behavior that appears to have been rectified in iOS 11 with the safe-area API revamp. That said, you will always get the correct value off the root view controller. For example, if you want the upper safe area height pre-iOS 11:

Swift 4

let root = UIApplication.shared.keyWindow!.rootViewController!
let topLayoutGuideLength = root.topLayoutGuide.length
trndjc
  • 11,654
  • 3
  • 38
  • 51