子视图控制器中的topLayoutGuide

70

我有一个带有透明状态栏和导航栏的UIPageViewController。 它的topLayoutGuide为64像素,符合预期。

然而,UIPageViewController的子视图控制器报告topLayoutGuide为0像素,即使它们在状态栏和导航栏下方也是如此。

这是预期行为吗? 如果是,最佳方法是将子视图控制器的视图定位在真正的topLayoutGuide下方是什么?

(除了使用parentViewController.topLayoutGuide,我认为这是一种hack方法)


3
我想知道他们是否已经彻底实现了嵌套ViewController容器的topLayoutGuide。这引出了一个问题,如果要实现自定义容器,该如何处理它... - Mike Pollard
1
你确定你在 viewWillLayoutSubviews 或者 viewDidLayoutSubviews 方法中测试了 topLayoutGuide 属性吗?如果我记得正确的话,在 viewWillAppear / viewDidAppear 方法中 topLayoutGuide 属性不能保证返回正确的值。 - Carlos P
2
我正在viewWillLayoutSubviews中进行测试,是的。 - hpique
@smileyborg 这就是我正在做的事情,结果与长度返回的值一致。 - hpique
3
我认为肯定有一些奇怪的事情正在发生。我有一个包含UITabbarController的UINavigationController。初始选择的选项卡始终会获得正确的间距,以出现在导航栏下方。但是当切换到其他选项卡时,顶部单元格会出现在顶部栏下面。 - Stephen Darlington
显示剩余5条评论
11个回答

49

虽然这个答案可能是正确的,但我仍然发现自己不得不遍历包含树来找到正确的父视图控制器并获取您所描述的“真正的topLayoutGuide”。这样我就可以手动实现automaticallyAdjustsScrollViewInsets

这是我正在做的:

在我的表视图控制器(实际上是UIViewController的子类)中,我有这个:

- (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;
}

注意UIViewController中的类别方法,这是我实现它们的方式:

@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

希望这可以帮到你 :)


1
我希望这个答案能够被无限投票...我去除了应用程序中懒惰开发人员编写的错误硬编码值的10个位置。问题解决了,不再有硬编码的问题。 - Mazyod
非常好的答案,谢谢。只是为了让事情更纯粹:UIEdgeInsets的值是CGFloat类型而不是double类型。最好将它们设置为0.f或0.0f,而不是0.0 :)。 - Nat
这在99%的情况下完美运行。但是如果您有一个UINavigationController作为childViewController,则不起作用。 - Loadex
2
谢谢。我不得不添加一个 [_tableView setContentOffset:CGPointMake(0, 0 - _tableView.contentInset.top) animated:NO]; 来使它正确地滚动到顶部。 - Conor
这真的为我节省了很大的麻烦:) - Tim Johnsen
显示剩余2条评论

11

我可能错了,但在我看来这种行为是正确的。 topLayout 值可用于容器视图控制器来布置其视图的子视图。

参考文献说:

要使用顶部布局指南而不使用约束,请获取指南相对于包含视图的顶部边界的位置。

在父级中,相对于包含视图,该值将为 64。

在子级中,相对于包含视图(父级),该值将为 0。

在容器视图控制器中,您可以使用该属性的方式如下:

- (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);
    }
}

Child View Controller不需要知道Navigation和Status Bar的存在,因为其父控制器已经考虑到这一点并布局了其子视图。

如果我创建一个基于页面的新项目,在导航控制器中嵌入它,并将此代码添加到父视图控制器中,似乎一切正常:

输入图像描述


20
但是,如果子视图控制器是 UIScrollView 的子类,则如果您设置了如此的框架,您将无法获得内容显示在半透明导航栏下面的效果,而这正是使用半透明导航栏的初衷。请注意不要改变原来的意思。 - kmikael
你没有调用super,这非常整洁,哈哈 - pronebird
这个解决方案对于页面控制器对我起作用了,如果有人有一个页面控制器并为其子视图添加了约束。 - Majid Bashir

11

您可以在故事板中添加约束,并在viewWillLayoutSubviews中更改它。

像这样:

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

1
如果您想在导航栏下延伸边缘,这是没有意义的。 - pronebird
这个答案解决了我的子视图控制器中顶部布局指南的问题,而且没有导航栏。Swift 3 版本:override func viewWillLayoutSubviews() {self.topGuideConstraint.constant = (self.parent ?? self).topLayoutGuide.length} - LOP_Luke

8

3

如果你有一个像OP一样的UIPageViewController,并且你有例如集合视图控制器作为子控制器。事实证明,在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;
}

2

这个问题在iOS 8中已经得到解决。

如何设置子视图控制器的topLayoutGuide位置

基本上,容器视图控制器应该像处理其他视图一样约束子视图控制器的(top|bottom|left|right)LayoutGuide。(在iOS 7中,它已经被完全约束在一个必需的优先级下,所以这种方法不起作用。)


能否分享一些代码?当涉及到基于约束的布局时,我有点脑子不太灵光 :) - Stian Høiland
实际上,在 iOS 7 中也可以这样做,只要在添加自己的约束之前先删除布局指南上的现有约束即可。 - Archaeopterasa
@Archaeopterasa,您能详细说明一下吗?您所提到的布局指南上的现有限制是什么? - Petar
@Jesse Rusak,具体做了哪些更改?我们如何将相同的更改应用于运行iOS 7的应用程序以实现相同的行为。我已经针对此问题提出了一个具体的问题:http://stackoverflow.com/questions/28024425/ios-toplayoutguide-using-a-child-view-controller-difference-between-ios-7-and-io - Petar
@pe60t0 自动添加的约束条件可以被删除(在调试输出中显示为_UILayoutSupportConstraint而不是NSLayoutConstraint的那些)。在我的代码中,我想将子视图控制器的布局指南限制为与父视图控制器的匹配,因此我首先遍历了约束列表并删除了设置其高度的约束条件,然后就能像普通视图一样对其进行约束。(由于自动布局总会有一个陷阱:方向更改可能会很混乱,并且需要仔细的操作顺序。) - Archaeopterasa
你能详细说明一下这个答案吗?我无法确定要添加什么限制条件。我添加的每一个都与iOS 10上现有的限制条件冲突。 - Jonathan.

1
我认为这些指南确实是为嵌套的子控制器设置的。例如,假设您有:
- 一个100x50的屏幕,顶部带有20像素的状态栏。 - 一个覆盖整个窗口的顶级视图控制器。其 topLayoutGuide 为20。 - 一个嵌套在顶层视图内的视图控制器,覆盖底部95个像素,例如距屏幕顶部下方5个像素。该视图应具有topLayoutGuide为15,因为其顶部15个像素被状态栏覆盖。
这是有意义的:它意味着嵌套的视图控制器可以设置约束以防止不必要的重叠,就像顶级视图一样。它不必关心它是嵌套的,或者父级在屏幕上的位置在哪里显示它,并且父视图控制器不需要知道子级想要如何与状态栏交互。
这似乎也是文档 - 或至少是某些文档 - 所说的:
“顶部布局指南指示视图控制器的视图顶部与覆盖视图的最下面的栏之间的距离(以点为单位)。”

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

这并没有说明只适用于顶层视图控制器。

但是,我不知道实际发生了什么。我确实看到过具有非零topLayoutGuides的子视图控制器,但我仍在摸索其中的奇怪之处。(在我的情况下,顶部指南应该为零,因为视图不在屏幕顶部,这正是我目前正在苦苦思考的问题...)


0

对@NachoSoto答案的Swifty实现:

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
    }
}

0

这是一个不幸的行为,似乎在iOS 11中通过安全区域API重构得到了纠正。也就是说,您始终会从根视图控制器获取正确的值。例如,如果您想要在iOS 11之前获取上部安全区域高度:

Swift 4

let root = UIApplication.shared.keyWindow!.rootViewController!
let topLayoutGuideLength = root.topLayoutGuide.length

0

不确定是否还有人遇到这个问题,因为我几分钟前还遇到了。
我的问题类似于这样(源自https://knuspermagier.de/2014-fixing-uipageviewcontrollers-top-layout-guide-problems.html的gif)。
简而言之,我的pageViewController有3个子视图控制器。第一个视图控制器没问题,但是当我滑动到下一个视图控制器时,整个视图会错误地向上偏移(大约20像素,我猜),但在手指离开屏幕后会恢复正常。
我彻夜寻找解决方案,但仍然没有找到合适的方法。 然后突然我想到了这个疯狂的想法:

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

}];

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

}];

我的listViewControllers有3个子视图控制器。索引为0的那个有问题,所以我首先将它设置为pageviewcontroller的根视图控制器,然后立即将其设置回第一个视图控制器(正如我所预期的那样)。 哇,它起作用了!
希望这能帮到你!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接