How to set topLayoutGuide position for child view controller

I'm implementing a custom container which is pretty similar to UINavigationController except for it does not hold the whole controller stack. It has a UINavigationBar which is constrained to the container controller's topLayoutGuide, which happens to be 20px off the top, which is OK.

When I add a child view controller and put its view into the hierarchy I want its topLayoutGuide seen in IB and used for laying out the child view controller's view's subviews to appear at the bottom of my navigation bar. There is a note of what is to be done in the relevant documentation:

The value of this property is, specifically, the value of the length property of the object returned when you query this property. This value is constrained by either the view controller or by its enclosing container view controller (such as a navigation or tab bar controller), as follows:

  • A view controller not within a container view controller constrains this property to indicate the bottom of the status bar, if visible,
    or else to indicate the top edge of the view controller's view.
  • A view controller within a container view controller does not set this property's value. Instead, the container view controller constrains the value to indicate:
  • The bottom of the navigation bar, if a navigation bar is visible
  • The bottom of the status bar, if only a status bar is visible
  • The top edge of the view controller's view, if neither a status bar nor navigation bar is visible
  • But I don't quite understand how to "constrain it's value" since both the topLayoutGuide and it's length properties are readonly.

    I've tried this code for adding a child view controller:

    [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;
    

    In the IB I specify "Under Top Bars" and "Under Bottom Bars" for the gamePhaseController. One of the views is specifically constrained to the top layout guide, anyway on the device it appears to be 20px off the the bottom of the container's navigation bar...

    What is the right way of implementing a custom container controller with this behavior?


    As far as I have been able to tell after hours of debugging, the layout guides are readonly, and derived from the private classes used for constraints based layout. Overriding the accessors does nothing (even though they are called), and it's all just craptastically annoying.


    (UPDATE: now available as cocoapod, see https://github.com/stefreak/TTLayoutSupport)

    A working solution is to remove apple's layout constraints and add your own constraints. I made a little category for this.

    Here is the code - but I suggest the cocoapod. It's got unit tests and is more likely to be up to date.

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

    I think they mean you should constrain the layout guides using autolayout, ie an NSLayoutConstraint object, instead of manually setting the length property. The length property is made available for classes that choose not to use autolayout, but it seems with custom container view controllers you do not have this choice.

    I assume the best practice is make the priority of the constraint in the container view controller that "sets" the value of the length property to UILayoutPriorityRequired .

    I'm not sure what layout attribute you would bind, either NSLayoutAttributeHeight or NSLayoutAttributeBottom probably.

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

    上一篇: 导航控制器顶部布局指南不符合自定义转换

    下一篇: 如何设置子视图控制器的topLayoutGuide位置