Navigation controller top layout guide not honored with custom transition

前端 未结 12 1503
暖寄归人
暖寄归人 2020-12-04 11:35

Short version:

I am having a problem with auto layout top layout guide when used in conjunction with custom transition and UINavigationController in iO

相关标签:
12条回答
  • 2020-12-04 11:47

    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.

    0 讨论(0)
  • 2020-12-04 11:50

    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:

    0 讨论(0)
  • 2020-12-04 11:50

    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.

    0 讨论(0)
  • 2020-12-04 11:50

    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.

    0 讨论(0)
  • 2020-12-04 11:53

    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.

    0 讨论(0)
  • 2020-12-04 11:53

    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. :)

    0 讨论(0)
提交回复
热议问题