uinavigationcontrollerios7autolayoutcustom-transition

Navigation controller top layout guide not honored with custom transition


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?


Solution

  • 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 ...
    }