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

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

当我添加一个子视图控制器并将其视图放入层次结构中时,我希望在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指定了“Under Top Bars”和“Under Bottom Bars”。 其中一个视图特别限制为顶层布局指南,无论如何,它在设备上似乎距离容器导航栏底部20px ...

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


    据我几小时的调试后可以看出,布局指南是只读的,并且来自用于基于约束的布局的私有类。 覆盖访问器什么都不做(即使它们被调用),并且这只是令人讨厌的烦人。


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

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

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

    我不确定你会绑定什么布局属性,无论是NSLayoutAttributeHeight还是NSLayoutAttributeBottom

    链接地址: http://www.djcxy.com/p/68289.html

    上一篇: How to set topLayoutGuide position for child view controller

    下一篇: RootViewController animation transition, initial orientation is wrong