【问题标题】:How to set topLayoutGuide position for child view controller如何为子视图控制器设置 topLayoutGuide 位置
【发布时间】:2013-11-04 11:21:20
【问题描述】:

我正在实现一个与 UINavigationController 非常相似的自定义容器,只是它不包含整个控制器堆栈。它有一个 UINavigationBar,它被限制在容器控制器的 topLayoutGuide 上,恰好离顶部 20px,这没关系。

当我添加一个子视图控制器并将其视图放入层次结构时,我希望它的 topLayoutGuide 在 IB 中看到并用于布置子视图控制器的视图的子视图,以显示在导航栏的底部。相关文档中有说明要做什么:

这个属性的值,具体来说就是长度的值 查询该属性时返回的对象的属性。这 value 受视图控制器或其封闭的约束 容器视图控制器(例如导航或标签栏 控制器),如下:

  • 不在容器视图控制器内的视图控制器约束此属性以指示状态栏的底部(如果可见)
    否则指示视图控制器视图的顶部边缘。
  • 容器视图控制器中的视图控制器不设置此属性的值。相反,容器视图控制器 约束值以指示:
    • 导航栏的底部,如果导航栏可见
    • 状态栏的底部,如果只有一个状态栏可见
    • 视图控制器视图的顶部边缘,如果状态栏和导航栏都不可见

但我不太明白如何“限制它的值”,因为 topLayoutGuide 和它的长度属性都是只读的。

我已尝试使用此代码添加子视图控制器:

[self addChildViewController:gamePhaseController];
UIView *gamePhaseControllerView = gamePhaseController.view;
gamePhaseControllerView.translatesAutoresizingMaskIntoConstraints = NO;
[self.contentContainer addSubview:gamePhaseControllerView];

NSArray *horizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"|-0-[gamePhaseControllerView]-0-|"
                                                                         options:0
                                                                         metrics:nil
                                                                           views:NSDictionaryOfVariableBindings(gamePhaseControllerView)];

NSLayoutConstraint *topLayoutGuideConstraint = [NSLayoutConstraint constraintWithItem:gamePhaseController.topLayoutGuide
                                                                            attribute:NSLayoutAttributeTop
                                                                            relatedBy:NSLayoutRelationEqual
                                                                               toItem:self.navigationBar
                                                                            attribute:NSLayoutAttributeBottom
                                                                           multiplier:1 constant:0];
NSLayoutConstraint *bottomLayoutGuideConstraint = [NSLayoutConstraint constraintWithItem:gamePhaseController.bottomLayoutGuide
                                                                               attribute:NSLayoutAttributeBottom
                                                                               relatedBy:NSLayoutRelationEqual
                                                                                  toItem:self.bottomLayoutGuide
                                                                               attribute:NSLayoutAttributeTop
                                                                              multiplier:1 constant:0];
[self.view addConstraint:topLayoutGuideConstraint];
[self.view addConstraint:bottomLayoutGuideConstraint];
[self.contentContainer addConstraints:horizontalConstraints];
[gamePhaseController didMoveToParentViewController:self];

_contentController = gamePhaseController;

在 IB 中,我为 gamePhaseController 指定了“Under Top Bars”和“Under Bottom Bars”。其中一个视图特别受限于顶部布局指南,无论如何在设备上它似乎距离容器导航栏底部 20px...

实现具有这种行为的自定义容器控制器的正确方法是什么?

【问题讨论】:

  • 据我了解您引用的文档,处理这种情况的正确方法是在容器视图控制器中覆盖 topLayoutGuide 并返回适当的 y 值。不幸的是,正如@Stefan Fisk 指出的那样,这似乎是不可能的。我认为这是苹果方面的一个错误(或者更确切地说是一个缺失的功能)。
  • 这在 iOS 8 上运行良好。知道到底发生了什么变化吗?
  • 仍然对我不起作用。系统倾向于为子视图控制器的顶部布局指南设置零高度和零顶部偏移约束。现在可以使用 Xcode 6 中的视图层次调试轻松确认。添加更多约束将创建“无法满足约束”的情况。
  • 覆盖 topLayoutGuide 并提供此答案中显示的类stackoverflow.com/a/33215299/259521

标签: ios objective-c uiviewcontroller ios7 autolayout


【解决方案1】:

在父视图控制器中

- (void)viewDidLayoutSubviews {

    [super viewDidLayoutSubviews];

    for (UIViewController * childViewController in self.childViewControllers) {

        // Pass the layouts to the child
        if ([childViewController isKindOfClass:[MyCustomViewController class]]) {
            [(MyCustomViewController *)childViewController parentTopLayoutGuideLength:self.topLayoutGuide.length parentBottomLayoutGuideLength:self.bottomLayoutGuide.length];
        }

    }

}

除了将值传递给孩子之外,您还可以像我的示例中那样拥有一个自定义类,一个协议,或者您可以从孩子的层次结构中访问滚动视图

【讨论】:

    【解决方案2】:

    (更新:现在可作为 cocoapod 使用,请参阅 https://github.com/stefreak/TTLayoutSupport

    一个可行的解决方案是删除苹果的布局约束并添加您自己的约束。我为此做了一个小分类。

    这是代码 - 但我建议使用 cocoapod。它有单元测试,并且更有可能是最新的。

    //
    //  UIViewController+TTLayoutSupport.h
    //
    //  Created by Steffen on 17.09.14.
    //
    
    #import <UIKit/UIKit.h>
    
    @interface UIViewController (TTLayoutSupport)
    
    @property (assign, nonatomic) CGFloat tt_bottomLayoutGuideLength;
    
    @property (assign, nonatomic) CGFloat tt_topLayoutGuideLength;
    
    @end
    

    -

    #import "UIViewController+TTLayoutSupport.h"
    #import "TTLayoutSupportConstraint.h"
    #import <objc/runtime.h>
    
    @interface UIViewController (TTLayoutSupportPrivate)
    
    // recorded apple's `UILayoutSupportConstraint` objects for topLayoutGuide
    @property (nonatomic, strong) NSArray *tt_recordedTopLayoutSupportConstraints;
    
    // recorded apple's `UILayoutSupportConstraint` objects for bottomLayoutGuide
    @property (nonatomic, strong) NSArray *tt_recordedBottomLayoutSupportConstraints;
    
    // custom layout constraint that has been added to control the topLayoutGuide
    @property (nonatomic, strong) TTLayoutSupportConstraint *tt_topConstraint;
    
    // custom layout constraint that has been added to control the bottomLayoutGuide
    @property (nonatomic, strong) TTLayoutSupportConstraint *tt_bottomConstraint;
    
    // this is for NSNotificationCenter unsubscription (we can't override dealloc in a category)
    @property (nonatomic, strong) id tt_observer;
    
    @end
    
    @implementation UIViewController (TTLayoutSupport)
    
    - (CGFloat)tt_topLayoutGuideLength
    {
        return self.tt_topConstraint ? self.tt_topConstraint.constant : self.topLayoutGuide.length;
    }
    
    - (void)setTt_topLayoutGuideLength:(CGFloat)length
    {
        [self tt_ensureCustomTopConstraint];
    
        self.tt_topConstraint.constant = length;
    
        [self tt_updateInsets:YES];
    }
    
    - (CGFloat)tt_bottomLayoutGuideLength
    {
        return self.tt_bottomConstraint ? self.tt_bottomConstraint.constant : self.bottomLayoutGuide.length;
    }
    
    - (void)setTt_bottomLayoutGuideLength:(CGFloat)length
    {
        [self tt_ensureCustomBottomConstraint];
    
        self.tt_bottomConstraint.constant = length;
    
        [self tt_updateInsets:NO];
    }
    
    - (void)tt_ensureCustomTopConstraint
    {
        if (self.tt_topConstraint) {
            // already created
            return;
        }
    
        // recording does not work if view has never been accessed
        __unused UIView *view = self.view;
        // if topLayoutGuide has never been accessed it may not exist yet
        __unused id<UILayoutSupport> topLayoutGuide = self.topLayoutGuide;
    
        self.tt_recordedTopLayoutSupportConstraints = [self findLayoutSupportConstraintsFor:self.topLayoutGuide];
        NSAssert(self.tt_recordedTopLayoutSupportConstraints.count, @"Failed to record topLayoutGuide constraints. Is the controller's view added to the view hierarchy?");
        [self.view removeConstraints:self.tt_recordedTopLayoutSupportConstraints];
    
        NSArray *constraints =
            [TTLayoutSupportConstraint layoutSupportConstraintsWithView:self.view
                                                         topLayoutGuide:self.topLayoutGuide];
    
        // todo: less hacky?
        self.tt_topConstraint = [constraints firstObject];
    
        [self.view addConstraints:constraints];
    
        // this fixes a problem with iOS7.1 (GH issue #2), where the contentInset
        // of a scrollView is overridden by the system after interface rotation
        // this should be safe to do on iOS8 too, even if the problem does not exist there.
        __weak typeof(self) weakSelf = self;
        self.tt_observer = [[NSNotificationCenter defaultCenter] addObserverForName:UIDeviceOrientationDidChangeNotification
                                                                             object:nil
                                                                              queue:[NSOperationQueue mainQueue]
                                                                         usingBlock:^(NSNotification *note) {
            __strong typeof(self) self = weakSelf;
            [self tt_updateInsets:NO];
        }];
    }
    
    - (void)tt_ensureCustomBottomConstraint
    {
        if (self.tt_bottomConstraint) {
            // already created
            return;
        }
    
        // recording does not work if view has never been accessed
        __unused UIView *view = self.view;
        // if bottomLayoutGuide has never been accessed it may not exist yet
        __unused id<UILayoutSupport> bottomLayoutGuide = self.bottomLayoutGuide;
    
        self.tt_recordedBottomLayoutSupportConstraints = [self findLayoutSupportConstraintsFor:self.bottomLayoutGuide];
        NSAssert(self.tt_recordedBottomLayoutSupportConstraints.count, @"Failed to record bottomLayoutGuide constraints. Is the controller's view added to the view hierarchy?");
        [self.view removeConstraints:self.tt_recordedBottomLayoutSupportConstraints];
    
        NSArray *constraints =
        [TTLayoutSupportConstraint layoutSupportConstraintsWithView:self.view
                                                  bottomLayoutGuide:self.bottomLayoutGuide];
    
        // todo: less hacky?
        self.tt_bottomConstraint = [constraints firstObject];
    
        [self.view addConstraints:constraints];
    }
    
    - (NSArray *)findLayoutSupportConstraintsFor:(id<UILayoutSupport>)layoutGuide
    {
        NSMutableArray *recordedLayoutConstraints = [[NSMutableArray alloc] init];
    
        for (NSLayoutConstraint *constraint in self.view.constraints) {
            // I think an equality check is the fastest check we can make here
            // member check is to distinguish accidentally created constraints from _UILayoutSupportConstraints
            if (constraint.firstItem == layoutGuide && ![constraint isMemberOfClass:[NSLayoutConstraint class]]) {
                [recordedLayoutConstraints addObject:constraint];
            }
        }
    
        return recordedLayoutConstraints;
    }
    
    - (void)tt_updateInsets:(BOOL)adjustsScrollPosition
    {
        // don't update scroll view insets if developer didn't want it
        if (!self.automaticallyAdjustsScrollViewInsets) {
            return;
        }
    
        UIScrollView *scrollView;
    
        if ([self respondsToSelector:@selector(tableView)]) {
            scrollView = ((UITableViewController *)self).tableView;
        } else if ([self respondsToSelector:@selector(collectionView)]) {
            scrollView = ((UICollectionViewController *)self).collectionView;
        } else {
            scrollView = (UIScrollView *)self.view;
        }
    
        if ([scrollView isKindOfClass:[UIScrollView class]]) {
            CGPoint previousContentOffset = CGPointMake(scrollView.contentOffset.x, scrollView.contentOffset.y + scrollView.contentInset.top);
    
            UIEdgeInsets insets = UIEdgeInsetsMake(self.tt_topLayoutGuideLength, 0, self.tt_bottomLayoutGuideLength, 0);
            scrollView.contentInset = insets;
            scrollView.scrollIndicatorInsets = insets;
    
            if (adjustsScrollPosition && previousContentOffset.y == 0) {
                scrollView.contentOffset = CGPointMake(previousContentOffset.x, -scrollView.contentInset.top);
            }
        }
    }
    
    @end
    
    @implementation UIViewController (TTLayoutSupportPrivate)
    
    - (NSLayoutConstraint *)tt_topConstraint
    {
        return objc_getAssociatedObject(self, @selector(tt_topConstraint));
    }
    
    - (void)setTt_topConstraint:(NSLayoutConstraint *)constraint
    {
        objc_setAssociatedObject(self, @selector(tt_topConstraint), constraint, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    - (NSLayoutConstraint *)tt_bottomConstraint
    {
        return objc_getAssociatedObject(self, @selector(tt_bottomConstraint));
    }
    
    - (void)setTt_bottomConstraint:(NSLayoutConstraint *)constraint
    {
        objc_setAssociatedObject(self, @selector(tt_bottomConstraint), constraint, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    - (NSArray *)tt_recordedTopLayoutSupportConstraints
    {
        return objc_getAssociatedObject(self, @selector(tt_recordedTopLayoutSupportConstraints));
    }
    
    - (void)setTt_recordedTopLayoutSupportConstraints:(NSArray *)constraints
    {
        objc_setAssociatedObject(self, @selector(tt_recordedTopLayoutSupportConstraints), constraints, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    - (NSArray *)tt_recordedBottomLayoutSupportConstraints
    {
        return objc_getAssociatedObject(self, @selector(tt_recordedBottomLayoutSupportConstraints));
    }
    
    - (void)setTt_recordedBottomLayoutSupportConstraints:(NSArray *)constraints
    {
        objc_setAssociatedObject(self, @selector(tt_recordedBottomLayoutSupportConstraints), constraints, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    - (void)setTt_observer:(id)tt_observer
    {
        objc_setAssociatedObject(self, @selector(tt_observer), tt_observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    - (id)tt_observer
    {
        return objc_getAssociatedObject(self, @selector(tt_observer));
    }
    

    -

    //
    //  TTLayoutSupportConstraint.h
    //
    //  Created by Steffen on 17.09.14.
    //
    
    #import <UIKit/UIKit.h>
    
    @interface TTLayoutSupportConstraint : NSLayoutConstraint
    
    + (NSArray *)layoutSupportConstraintsWithView:(UIView *)view topLayoutGuide:(id<UILayoutSupport>)topLayoutGuide;
    
    + (NSArray *)layoutSupportConstraintsWithView:(UIView *)view bottomLayoutGuide:(id<UILayoutSupport>)bottomLayoutGuide;
    
    @end
    

    -

    //
    //  TTLayoutSupportConstraint.m
    // 
    //  Created by Steffen on 17.09.14.
    //
    
    #import "TTLayoutSupportConstraint.h"
    
    @implementation TTLayoutSupportConstraint
    
    + (NSArray *)layoutSupportConstraintsWithView:(UIView *)view topLayoutGuide:(id<UILayoutSupport>)topLayoutGuide
    {
        return @[
                 [TTLayoutSupportConstraint constraintWithItem:topLayoutGuide
                                                     attribute:NSLayoutAttributeHeight
                                                     relatedBy:NSLayoutRelationEqual
                                                        toItem:nil
                                                     attribute:NSLayoutAttributeNotAnAttribute
                                                    multiplier:1.0
                                                      constant:0.0],
                 [TTLayoutSupportConstraint constraintWithItem:topLayoutGuide
                                                     attribute:NSLayoutAttributeTop
                                                     relatedBy:NSLayoutRelationEqual
                                                        toItem:view
                                                     attribute:NSLayoutAttributeTop
                                                    multiplier:1.0
                                                      constant:0.0],
                 ];
    }
    
    + (NSArray *)layoutSupportConstraintsWithView:(UIView *)view bottomLayoutGuide:(id<UILayoutSupport>)bottomLayoutGuide
    {
        return @[
                 [TTLayoutSupportConstraint constraintWithItem:bottomLayoutGuide
                                                     attribute:NSLayoutAttributeHeight
                                                     relatedBy:NSLayoutRelationEqual
                                                        toItem:nil
                                                     attribute:NSLayoutAttributeNotAnAttribute
                                                    multiplier:1.0
                                                      constant:0.0],
                 [TTLayoutSupportConstraint constraintWithItem:bottomLayoutGuide
                                                     attribute:NSLayoutAttributeBottom
                                                     relatedBy:NSLayoutRelationEqual
                                                        toItem:view
                                                     attribute:NSLayoutAttributeBottom
                                                    multiplier:1.0
                                                      constant:0.0],
                 ];
    }
    
    @end
    

    【讨论】:

    • 感谢您的解决方案。它启发了我做一个小实验。事实证明,可以轻松识别布局指南约束:constraint.firstItem == childControllerTopLayoutGuide &amp;&amp; constraint.secondItem == nil 并更改其常量似乎可以完成这项工作(更新原始 id&lt;LayoutSupport&gt; 对象)。您评估过这种方法吗?
    • 这是一个非常酷的想法,应该大大简化该解决方案。我会尝试一下,也许会将其纳入我的答案中。谢谢:)
    • 这实际上是在使用私有API吗? _UILayoutSupportConstraint 类是私有的,是不是禁止向它发送消息?
    • 我觉得没问题。它实际上比修改系统控件的子视图(并且不会被拒绝的应用程序)的侵入性更小。毕竟这些只是 our 视图中的约束。话又说回来,iOS9 可能会改变这些布局指南的实现方式,这将打破 :]
    • 这不再需要了,从 topLayoutGuide 和 bottomLayoutGuide 返回的有效布局指南类已经被找出并在 iOS 9 上工作:stackoverflow.com/a/33215299/259521
    【解决方案3】:

    我认为他们的意思是你应该使用自动布局来约束布局指南,即一个 NSLayoutConstraint 对象,而不是手动设置长度属性。 length 属性可用于选择不使用自动布局的类,但对于自定义容器视图控制器,您似乎没有此选择。

    我认为最佳实践是在容器视图控制器中设置约束的优先级,将长度属性的值“设置”为UILayoutPriorityRequired

    我不确定你会绑定什么布局属性,可能是NSLayoutAttributeHeightNSLayoutAttributeBottom

    【讨论】:

    • 这似乎完全合理;不幸的是,它不起作用,因为布局指南已经完全受到限制。
    • @JesseRusak 你是如何设置你的约束以便它在 iOS 8 beta 上工作的?我所有的实验都产生了关于模糊约束的警告。
    • @Sven 我刚刚创建了一个约束,它将 child.topLayoutConstraint 的 NSLayoutAttributeBottom 指定为我的容器视图中的某些内容。我没有做任何花哨的事情;如果它不起作用,也许做一个最小的例子并发布一个问题? (如果你这样做,请戳我。)
    • @JesseRusak 我也无法让它工作。我假设如果你添加这样一个约束,你实际上是在将你的“我的容器视图中的东西”约束到未更改的 topLayoutGuide。反之亦然。
    • @OrtwinGentz 约束总是双向的,所以我不确定你的意思。正如我之前建议的那样,如果您遇到这种方法不起作用的情况,我鼓励您发布一个新问题并将其链接到此处。
    【解决方案4】:

    据我经过数小时的调试后得知,布局指南是只读的,并且派生自用于基于约束的布局的私有类。覆盖访问器什么都不做(即使它们被调用),而且这一切都非常烦人。

    【讨论】:

    • 请注意,我的回答是基于我在 12 月份所做的工作,在以后的版本中可能会有所改变。
    • 不幸的是,这是正确的答案。我已经记录了一个雷达(“功能请求:覆盖 layoutGuides”),请随意欺骗它:openradar.appspot.com/21123507
    • 该属性是只读的,但您可以简单地将其传递给他的孩子,因此它可以进行自定义调整,请参阅下面的答案
    猜你喜欢
    • 2013-10-09
    • 1970-01-01
    • 2015-02-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-02-09
    • 2015-01-02
    • 2011-05-06
    相关资源
    最近更新 更多