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

我正在实现一个自定义的容器,它非常类似于UINavigationController,除了它不包含整个控制器堆栈。 它有一个UINavigationBar,它被限制在容器控制器的topLayoutGuide中,它恰好是顶部20px,这是OK的。

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

此属性的值特别是查询此属性时返回的对象的length属性的值。 该值受到视图控制器或其封闭容器视图控制器(如导航或选项卡栏控制器)的约束,如下所示:

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

但是我不太了解如何“限制它的价值”,因为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指定了“顶部酒吧”和“底部酒吧”。 其中一个意见是特别限制在顶部的布局指南,无论如何,在设备上它似乎是从容器的导航栏的底部20px …

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

据我几小时的debugging后,可以告诉布局指南是只读的,并从用于基于约束的布局私有类派生。 覆盖访问器什么都不做(即使他们被称为),这一切都只是令人讨厌的烦人。

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

一个工作的解决scheme是删除苹果的布局约束,并添加自己的约束。 我为此做了一个小类。

这是代码 – 但我build议椰子树。 它有unit testing,更可能是最新的。

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

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

我认为最好的做法是在容器视图控制器中设置length属性的值为UILayoutPriorityRequired的约束的优先级。

我不知道你会绑定什么布局属性,无论是NSLayoutAttributeHeightNSLayoutAttributeBottom可能。

在父视图控制器中

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

而不是将值传递给孩子,你可以有一个自定义的类,如我的例子,一个协议,或者你可以从孩子的层次结构访问滚动视图