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
(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
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 than pass the values to the children, you can have a custom class as in my example, a protocol, or you can maybe access the scroll view from the child's hierarchy
I think they mean you should constrain the layout guides using autolayout, i.e. 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.
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.