How to set topLayoutGuide to a child view controller

I implement a custom container that is very similar to the UINavigationController, except that it does not contain the entire controller stack. It has a UINavigationBar, which is limited to the topLayoutGuide container controller, which turned out to be 20px on top, which is good.

When I add a child view controller and put its view in the hierarchy, I want its topLayoutGuide to be seen in IB and used to highlight the child elements of the child view controller's view that appear at the bottom of my navigation bar. It should be noted what should be done in the relevant documentation:

The value of this property is, in particular, the length value of the property of the object returned when this property is requested. This value is limited either by the view controller or by closing the container controller (for example, the navigation bar or the controller tabs):

  • A view controller that is not in the container view controller restricts this property to indicate the bottom of the status bar, if one is visible,
    or specify the top edge of the view controller view.
  • The view controller in the container view controller does not set this property value. Instead, the container view controller limits the value to:
    • Bottom of the navigation bar if the navigation bar is displayed
    • The bottom of the status bar, if only the status bar is visible
    • The top edge of the view controller view if neither the status bar nor the navigation bar is displayed.

But I don’t quite understand how to “limit its value”, since both the topLayoutGuide properties and its lengths are readonly.

I tried this code to add 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 IB, I define "Under Top Bars" and "Under Bottom Bars" for the game PhaseController. One of the views is specially tied to the top guide of the layout, one way or another on the device it looks 20px from the bottom of the container’s navigation bar ...

What is the correct way to implement a custom container controller with this behavior?

+28
ios objective-c autolayout uiviewcontroller ios7
Oct 25 '13 at 11:09
source share
4 answers

As far as I was able to tell after a few hours of debugging, the layout guides are read-only and are derived from private classes used for layout with restrictions. Redefining accessories does nothing (although they are called), and all this is just insanely annoying.

+25
Jan 19 '14 at 21:26
source share

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

The working solution is to remove the apple layout restrictions and add your own restrictions. I made a small category for this.

Here is the code, but I suggest cocoapod. He received unit tests and is likely to be up to date with the latest developments.

 // // 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 `UILayoutSupportConstraint` objects for topLayoutGuide @property (nonatomic, strong) NSArray *tt_recordedTopLayoutSupportConstraints; // recorded apple `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 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 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 
+5
Sep 17 '14 at 0:21
source share

I think they mean that you should limit the layout guide using autolayout, i.e. an NSLayoutConstraint object, instead of manually setting the length property. The length property is available for classes that prefer not to use autostart, but it seems that custom container controllers do not have this choice.

I suggest that it is best practice to restrict the priority in the container view controller, which "sets" the value of the length property to UILayoutPriorityRequired .

I'm not sure which layout attribute you will associate, perhaps NSLayoutAttributeHeight or NSLayoutAttributeBottom .

+1
Apr 17 '14 at 1:54 on
source share

In the parent view controller

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

and then pass the values ​​to the children, you can have your own class, as in my example, the protocol, or you can access the scroll view from the child hierarchy

0
May 26 '17 at 1:49 pm
source share



All Articles