Navigation controller top layout guide not honored with custom transition

UinavigationcontrollerIos7AutolayoutCustom Transition

Uinavigationcontroller Problem Overview


Short version:

I am having a problem with auto layout top layout guide when used in conjunction with custom transition and UINavigationController in iOS7. Specifically, the constraint between the top layout guide and the text view is not being honored. Has anyone encountered this issue?


Long version:

I have a scene which has unambiguously define constraints (i.e. top, bottom, left and right) that renders a view like so:

right

But when I use this with a custom transition on the navigation controller, the top constraint to the top layout guide seems off and it renders is as follows, as if the top layout guide was at the top of the screen, rather than at the bottom of the navigation controller:

wrong

It would appear that the "top layout guide" with the navigation controller is getting confused when employing the custom transition. The rest of the constraints are being applied correctly. And if I rotate the device and rotate it again, everything is suddenly rendered correctly, so it does not appear to be not a matter that the constraints are not defined properly. Likewise, when I turn off my custom transition, the views render correctly.

Having said that, _autolayoutTrace is reporting that the UILayoutGuide objects suffer from AMBIGUOUS LAYOUT, when I run:

(lldb) po [[UIWindow keyWindow] _autolayoutTrace]

But those layout guides are always reported as ambiguous whenever I look at them even though I've ensured that there are no missing constraints (I've done the customary selecting of view controller and choosing "Add missing constraints for view controller" or selecting all of the controls and doing the same for them).

In terms of how precisely I'm doing the transition, I've specified an object that conforms to UIViewControllerAnimatedTransitioning in the animationControllerForOperation method:

- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                  animationControllerForOperation:(UINavigationControllerOperation)operation
                                               fromViewController:(UIViewController*)fromVC
                                                 toViewController:(UIViewController*)toVC
{
    if (operation == UINavigationControllerOperationPush)
        return [[PushAnimator alloc] init];

    return nil;
}

And

@implementation PushAnimator

- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext
{
    return 0.5;
}

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    UIViewController* toViewController   = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIViewController* fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];

    [[transitionContext containerView] addSubview:toViewController.view];
    CGFloat width = fromViewController.view.frame.size.width;

    toViewController.view.transform = CGAffineTransformMakeTranslation(width, 0);

    [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
        fromViewController.view.transform = CGAffineTransformMakeTranslation(-width / 2.0, 0);
        toViewController.view.transform = CGAffineTransformIdentity;
    } completion:^(BOOL finished) {
        fromViewController.view.transform = CGAffineTransformIdentity;
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
    }];
}

@end

I've also done a rendition of the above, setting the frame of the view rather than the transform, with the same result.

I've also tried manually make sure that the constraints are re-applied by calling layoutIfNeeded. I've also tried setNeedsUpdateConstraints, setNeedsLayout, etc.

Bottom line, has anyone successfully married custom transition of navigation controller with constraints that use top layout guide?

Uinavigationcontroller Solutions


Solution 1 - Uinavigationcontroller

Managed to fix my issue by adding this line:

toViewController.view.frame = [transitionContext finalFrameForViewController:toViewController];

To:

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext fromVC:(UIViewController *)fromVC toVC:(UIViewController *)toVC fromView:(UIView *)fromView toView:(UIView *)toView {
    
    // Add the toView to the container
    UIView* containerView = [transitionContext containerView];
    [containerView addSubview:toView];
    [containerView sendSubviewToBack:toView];
    
    
    // animate
    toVC.view.frame = [transitionContext finalFrameForViewController:toVC];
    NSTimeInterval duration = [self transitionDuration:transitionContext];
    [UIView animateWithDuration:duration animations:^{
        fromView.alpha = 0.0;
    } completion:^(BOOL finished) {
        if ([transitionContext transitionWasCancelled]) {
            fromView.alpha = 1.0;
        } else {
            // reset from- view to its original state
            [fromView removeFromSuperview];
            fromView.alpha = 1.0;
        }
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
    }];
    
}

From Apple's Documentation for [finalFrameForViewController] : https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewControllerContextTransitioning_protocol/#//apple_ref/occ/intfm/UIViewControllerContextTransitioning/finalFrameForViewController:

Solution 2 - Uinavigationcontroller

I solved this by fixing the height constraint of the topLayoutGuide. Adjusting edgesForExtendedLayout wasn't an option for me, as I needed the destination view to underlap the navigation bar, but also to be able to layout subviews using topLayoutGuide.

Directly inspecting the constraints in play shows that iOS adds a height constraint to the topLayoutGuide with value equal to the height of the navigation bar of the navigation controller. Except, in iOS 7, using a custom animation transition leaves the constraint with a height of 0. They fixed this in iOS 8.

This is the solution I came up with to correct the constraint (it's in Swift but the equivalent should work in Obj-C). I've tested that it works on iOS 7 and 8.

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
    let fromView = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!.view
    let destinationVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
    destinationVC.view.frame = transitionContext.finalFrameForViewController(destinationVC)
    let container = transitionContext.containerView()
    container.addSubview(destinationVC.view)

    // Custom transitions break topLayoutGuide in iOS 7, fix its constraint
    if let navController = destinationVC.navigationController {
        for constraint in destinationVC.view.constraints() as [NSLayoutConstraint] {
            if constraint.firstItem === destinationVC.topLayoutGuide
                && constraint.firstAttribute == .Height
                && constraint.secondItem == nil
                && constraint.constant == 0 {
                constraint.constant = navController.navigationBar.frame.height
            }
        }
    }

    // Perform your transition animation here ...
}

Solution 3 - Uinavigationcontroller

I struggled with the exact same problem. Putting this in the viewDidLoad of my toViewController really helped me out:

self.edgesForExtendedLayout = UIRectEdgeNone;

This did not solve all my issues and I'm still looking for a better approach, but this certainly made it a bit easier.

Solution 4 - Uinavigationcontroller

Just put the following code toviewDidLoad

self.extendedLayoutIncludesOpaqueBars = YES;

Solution 5 - Uinavigationcontroller

FYI, I ended up employing a variation of Alex's answer, programmatically changing the top layout guide's height constraint constant in the animateTransition method. I'm only posting this to share the Objective-C rendition (and eliminate the constant == 0 test).

CGFloat navigationBarHeight = toViewController.navigationController.navigationBar.frame.size.height;

for (NSLayoutConstraint *constraint in toViewController.view.constraints) {
    if (constraint.firstItem == toViewController.topLayoutGuide
        && constraint.firstAttribute == NSLayoutAttributeHeight
        && constraint.secondItem == nil
        && constraint.constant < navigationBarHeight) {
        constraint.constant += navigationBarHeight;
    }
}

Thanks, Alex.

Solution 6 - Uinavigationcontroller

As @Rob mentioned, topLayoutGuide is not reliable when using custom transitions in UINavigationController. I worked around this by using my own layout guide. You can see the code in action in this demo project. Highlights:

A category for custom layout guides:

@implementation UIViewController (hp_layoutGuideFix)

- (BOOL)hp_usesTopLayoutGuideInConstraints
{
    return NO;
}

- (id<UILayoutSupport>)hp_topLayoutGuide
{
    id<UILayoutSupport> object = objc_getAssociatedObject(self, @selector(hp_topLayoutGuide));
    return object ? : self.topLayoutGuide;
}

- (void)setHp_topLayoutGuide:(id<UILayoutSupport>)hp_topLayoutGuide
{
    HPLayoutSupport *object = objc_getAssociatedObject(self, @selector(hp_topLayoutGuide));
    if (object != nil && self.hp_usesTopLayoutGuideInConstraints)
    {
        [object removeFromSuperview];
    }
    HPLayoutSupport *layoutGuide = [[HPLayoutSupport alloc] initWithLength:hp_topLayoutGuide.length];
    if (self.hp_usesTopLayoutGuideInConstraints)
    {
        [self.view addSubview:layoutGuide];
    }
    objc_setAssociatedObject(self, @selector(hp_topLayoutGuide), layoutGuide, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

HPLayoutSupport is the class that will act as a layout guide. It has to be a UIView subclass to avoid crashes (I wonder why this isn't part of the UILayoutSupport interface).

@implementation HPLayoutSupport {
    CGFloat _length;
}

- (id)initWithLength:(CGFloat)length
{
    self = [super init];
    if (self)
    {
        self.translatesAutoresizingMaskIntoConstraints = NO;
        self.userInteractionEnabled = NO;
        _length = length;
    }
    return self;
}

- (CGSize)intrinsicContentSize
{
    return CGSizeMake(1, _length);
}

- (CGFloat)length
{
    return _length;
}

@end

The UINavigationControllerDelegate is the one responsible for "fixing" the layout guide before the transition:

- (id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                   animationControllerForOperation:(UINavigationControllerOperation)operation
                                                fromViewController:(UIViewController *)fromVC
                                                  toViewController:(UIViewController *)toVC
{
    toVC.hp_topLayoutGuide = fromVC.hp_topLayoutGuide;
    id <UIViewControllerAnimatedTransitioning> animator;
    // Initialise animator
    return animator;
}

Finally, the UIViewController uses hp_topLayoutGuide instead of topLayoutGuide in the constraints, and indicates this by overriding hp_usesTopLayoutGuideInConstraints:

- (void)updateViewConstraints
{
    [super updateViewConstraints];
    id<UILayoutSupport> topLayoutGuide = self.hp_topLayoutGuide;
    // Example constraint
    NSDictionary *views = NSDictionaryOfVariableBindings(_imageView, _dateLabel, topLayoutGuide);
    NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[topLayoutGuide][_imageView(240)]-8-[_dateLabel]" options:NSLayoutFormatAlignAllCenterX metrics:nil views:views];
    [self.view addConstraints:constraints];
}

- (BOOL)hp_usesTopLayoutGuideInConstraints
{
    return YES;
}

Hope it helps.

Solution 7 - Uinavigationcontroller

i found way. First uncheck "Extend Edges" property of controller. after that navigation bar getting dark color. Add a view to controller and set top and bottom LayoutConstraint -100. Then make view's clipsubview property no (for navigaionbar transculent effect). My english bad sory for that. :)

Solution 8 - Uinavigationcontroller

I had the same problem, ended up implementing my own topLayout guide view and making constraints to it rather then to topLayoutGuide. Not ideal. Only posting it here in case someone is stuck and looking for quick hacky solution http://www.github.com/stringcode86/SCTopLayoutGuide

Solution 9 - Uinavigationcontroller

Here's the simple solution I'm using that's working great for me: during the setup phase of - (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext, manually set your "from" and "to" viewController.view.frame.origin.y = navigationController.navigationBar.frame.size.height. It'll make your auto layout views position themselves vertically as you expect.

Minus the pseudo-code (e.g. you probably have your own way of determining if a device is running iOS7), this is what my method looks like:

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
	UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
	UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
	UIView *container = [transitionContext containerView];
	
	CGAffineTransform destinationTransform;
	UIViewController *targetVC;
	CGFloat adjustmentForIOS7AutoLayoutBug = 0.0f;
	
	// We're doing a view controller POP
	if(self.isViewControllerPop)
	{
		targetVC = fromViewController;
		
		[container insertSubview:toViewController.view belowSubview:fromViewController.view];
		
		// Only need this auto layout hack in iOS7; it's fixed in iOS8
		if(_device_is_running_iOS7_)
		{
			adjustmentForIOS7AutoLayoutBug = toViewController.navigationController.navigationBar.frame.size.height;
			[toViewController.view setFrameOriginY:adjustmentForIOS7AutoLayoutBug];
		}
		
		destinationTransform = CGAffineTransformMakeTranslation(fromViewController.view.bounds.size.width,adjustmentForIOS7AutoLayoutBug);
	}
	// We're doing a view controller PUSH
	else
	{
		targetVC = toViewController;
		
		[container addSubview:toViewController.view];
		
		// Only need this auto layout hack in iOS7; it's fixed in iOS8
		if(_device_is_running_iOS7_)
		{
			adjustmentForIOS7AutoLayoutBug = toViewController.navigationController.navigationBar.frame.size.height;
		}
		
		toViewController.view.transform = CGAffineTransformMakeTranslation(toViewController.view.bounds.size.width,adjustmentForIOS7AutoLayoutBug);
		destinationTransform = CGAffineTransformMakeTranslation(0.0f,adjustmentForIOS7AutoLayoutBug);
	}
	
	[UIView animateWithDuration:_animation_duration_
						  delay:_animation_delay_if_you_need_one_
						options:([transitionContext isInteractive] ? UIViewAnimationOptionCurveLinear : UIViewAnimationOptionCurveEaseOut)
					 animations:^(void)
	 {
		 targetVC.view.transform = destinationTransform;
	 }
					 completion:^(BOOL finished)
	 {
		 [transitionContext completeTransition:([transitionContext transitionWasCancelled] ? NO : YES)];
	 }];
}

A couple of bonus things about this example:

  • For view controller pushes, this custom transition slides the pushed toViewController.view on top of the unmoving fromViewController.view. For pops, fromViewController.view slides off to the right and reveals an unmoving toViewController.view under it. All in all, it's just a subtle twist on the stock iOS7+ view controller transition.
  • The [UIView animateWithDuration:...] completion block shows the correct way to handle completed & cancelled custom transitions. This tiny tidbit was a classic head-slap moment; hope it helps somebody else out there.

Lastly, I'd like to point out that as far as I can tell, this is an iOS7-only issue that has been fixed in iOS8: my custom view controller transition that is broken in iOS7 works just fine in iOS8 without modification. That being said, you should verify that this is what you're seeing too, and if so, only run the fix on devices running iOS7.x. As you can see in the code example above, the y-adjustment value is 0.0f unless the device is running iOS7.x.

Solution 10 - Uinavigationcontroller

I ran into this same issue but without using a UINavigationController and just positioning a view off of the topLayoutGuide. The layout would be correct when first displayed, a transition would take place to another view, and then upon exiting and returning to the first view, the layout would be broken as that topLayoutGuide would no longer be there.

I solved this problem by capturing the safe area insets prior to the transition and then reimplementing them, not by adjusting my constraints, but by setting them on the viewController's additionalSafeAreaInsets.

I found this solution to work well as I don't have to adjust any of my layout code and search through constraints and I can just reimplementing the space that was there previously. This could be more difficult if you are actually using the additionalSafeAreaInsets property.

Example

I added a variable to my transitionManager to capture the safe insets that exist when the transitionManager is created.

class MyTransitionManager: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {
    
    private var presenting = true
    private var container:UIView?
    private var safeInsets:UIEdgeInsets?

    ...

Then during the entering transition I save those insets.

    let toView = viewControllers.to.view
    let fromView = viewControllers.from.view
    
    if #available(iOS 11.0, *) {
        safeInsets = toView.safeAreaInsets
    }

In the case of the iPhone X this looks something like UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)

Now when exiting, the insets on that same view we transitioned from in the entrance will be .zero so we add our captured insets to the additionalSafeAreaInsets on the viewController, which will set them on our view for us as well as update the layout. Once our animation is done, we reset the additionalSafeAreaInsets back to .zero.

    if #available(iOS 11.0, *) {
        if safeInsets != nil {
            viewControllers.to.additionalSafeAreaInsets = safeInsets!
        }
    }

    ...then in the animation completion block

    if #available(iOS 11.0, *) {
        if self.safeInsets != nil {
            viewControllers.to.additionalSafeAreaInsets = .zero
        }
    }
            
    transitionContext.completeTransition(true)

Solution 11 - Uinavigationcontroller

try :

self.edgesforextendedlayout=UIRectEdgeNone

Or just set navigationbar opaque and set background image or backgroundcolor to navigationbar

Solution 12 - Uinavigationcontroller

In storyboard add another vertical constraint to main view's top. I have the same problem too but adding that constraint help me to avoid manual constraints. See screenshot here link

Other solution is to calculate toVC frame... something like this:

float y = toVC.navigationController.navigationBar.frame.origin.y + toVC.navigationController.navigationBar.frame.size.height;
toVC.view.frame = CGRectMake(0, y, toVC.view.frame.size.width, toVC.view.frame.size.height - y);

Let me know if you have found a better solution. I have been struggling with this issue as well and I came up with previous ideas.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionRobView Question on Stackoverflow
Solution 1 - UinavigationcontrollerHamdan JavedView Answer on Stackoverflow
Solution 2 - UinavigationcontrollerAlex PretzlavView Answer on Stackoverflow
Solution 3 - UinavigationcontrollerJasper KuperusView Answer on Stackoverflow
Solution 4 - UinavigationcontrollerCocodyView Answer on Stackoverflow
Solution 5 - UinavigationcontrollerRobView Answer on Stackoverflow
Solution 6 - UinavigationcontrollerhpiqueView Answer on Stackoverflow
Solution 7 - UinavigationcontrollerYusuf terziView Answer on Stackoverflow
Solution 8 - UinavigationcontrollerstringCodeView Answer on Stackoverflow
Solution 9 - UinavigationcontrollerJohn JaceckoView Answer on Stackoverflow
Solution 10 - Uinavigationcontrollerskabob11View Answer on Stackoverflow
Solution 11 - Uinavigationcontrollervivek bhoraniyaView Answer on Stackoverflow
Solution 12 - UinavigationcontrollerAнгелView Answer on Stackoverflow