如何为子视图控制器设置顶部布局指南位置

31

我正在实现一个自定义容器,它与UINavigationController非常相似,除了它不持有整个控制器堆栈。它有一个UINavigationBar,该导航栏受限于容器控制器的topLayoutGuide,该topLayoutGuide与顶部相距20像素,这是可以接受的。

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

  

此属性的值特别是查询此属性时返回的对象的长度属性的值。此值受以下两种方式之一约束:视图控制器或其封闭容器视图控制器(如导航栏或标签栏控制器):

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

但是我不太明白如何“约束其值”,因为topLayoutGuide和其length属性都是只读的。

我尝试了以下代码添加子视图控制器:

[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"。其中一个视图被特别限制在顶部布局指南中,但在设备上,它似乎比容器的导航栏底部偏移20像素...
如何正确实现具有此行为的自定义容器控制器?

据我理解您引用的文档,处理这种情况的正确方法是在容器视图控制器中覆盖 topLayoutGuide 并返回适当的 y 值。不幸的是,正如 @Stefan Fisk 指出的那样,这似乎是不可能的。我认为这是苹果方面的一个 bug(或者说是缺失的功能)。 - de.
这在 iOS 8 上运行良好。有任何确切的变化想法吗? - Petar
对我来说仍然不起作用。系统倾向于为子视图控制器的顶部布局指南设置零高度和零顶部偏移约束。可以使用Xcode 6中的视图层次结构调试轻松确认。添加更多约束将创建“不可满足的约束”情况。 - Danchoys
重写topLayoutGuide并提供此答案中显示的类 https://dev59.com/K2Eh5IYBdhLWcg3w321G#33215299 - malhal
4个回答

30

在我经过数小时的调试后,据我所知布局指南是只读的,并且派生自用于约束基础布局的私有类。覆盖访问器没有任何效果(即使它们被调用了),这一切都令人非常恼火。


请注意,我的回答是基于我在去年12月所做的工作,后续版本可能会有所改变。 - Stefan Fisk
2
很遗憾,这是正确的答案。我已经记录了一个雷达(“功能请求:覆盖layoutGuides”),请随意复制它:http://openradar.appspot.com/21123507 - Ortwin Gentz
该属性是只读的,但您可以将其传递给子级,以便它可以进行自定义调整,请参见下面的答案。 - Peter Lapisu

5

(更新:现在作为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

更好的方法是在指南上设置顶部和底部约束,而不是它们的高度。将这些新创建的顶部和底部约束的优先级设置为@900,将允许容器控制器通过添加所需的约束来实际“约束”指南。例如,自定义容器控制器可能会将子控制器的顶部指南的顶部设置为其导航栏的底部。如果容器控制器决定隐藏该栏,它也将很好地工作。使用当前的实现,它将不得不更新长度属性。 - Danchoys
我测试过了,它可以工作,但说实话,我已经完全忘记了非自动布局方面的事情。 - Danchoys
我记得最后我使用了观察者模式(KVO)来观察布局指南,并在每次它改变为不期望的值时调整它的值。虽然有些取巧,但效果符合预期 :) - Błażej
@Danchoys,您能通过约束来更改布局指南的值/位置吗?如果可以的话,那对Ortwin Gentz也适用。那将是一个完美的解决方案。我得试一试!感谢您的启发 :) - Błażej
1
这已经不再需要了,在iOS 9上已经找到了从topLayoutGuide和bottomLayoutGuide返回的有效布局指南类:https://dev59.com/K2Eh5IYBdhLWcg3w321G#33215299 - malhal
显示剩余10条评论

1
我认为他们的意思是你应该使用自动布局来约束布局指南,即使用NSLayoutConstraint对象,而不是手动设置长度属性。长度属性是为那些选择不使用自动布局的类提供的,但似乎对于自定义容器视图控制器来说,你没有这个选择。
我假设最佳实践是将容器视图控制器中“设置”长度属性值的约束优先级设置为UILayoutPriorityRequired。
我不确定要绑定哪个布局属性,可能是NSLayoutAttributeHeight或NSLayoutAttributeBottom。

这看起来非常合理,但不幸的是它不起作用,因为布局指南已经完全受限制了。 - Jesse Rusak
2
@JesseRusak 你是如何设置你的约束条件,以便在iOS 8 beta上运行?我所有的实验都产生了关于模糊约束的警告。 - Sven
1
@Sven 我刚刚创建了一个单一的约束,将child.topLayoutConstraint的NSLayoutAttributeBottom指定为容器视图中的某个位置。我没有做任何花哨的事情;如果不起作用,也许可以制作一个最小示例并发布问题?(如果你这样做,请提醒我。) - Jesse Rusak
@JesseRusak 我也没能让这个工作起来。我猜如果你添加这样的限制,你实际上是将“我的容器视图中的某些内容”约束到未更改的topLayoutGuide上。而不是相反的情况。 - Ortwin Gentz
@OrtwinGentz 约束始终是双向的,所以我不确定你的意思。就像我之前建议的那样,如果你有一个无法解决的问题,请发布一个新的问题并在这里链接它。 - Jesse Rusak
@JesseRusak,你建议通过添加自定义约束来更改布局指南。但事实并非如此。我放弃尝试更改布局指南了。它似乎与框架相互抵触。 - Ortwin Gentz

0

在父视图控制器中

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

    }

}

然后将值传递给子视图,您可以像我的示例中一样使用自定义类、协议,或者可能从子视图的层次结构中访问滚动视图。


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