导航控制器顶部布局指南不符合自定义过渡

时间:2013-12-01 13:44:01

标签: uinavigationcontroller ios7 autolayout custom-transition

简短版本:

与自定义转换和iOS7中的UINavigationController结合使用时,我遇到了自动布局顶部布局指南的问题。具体而言,顶级布局指南和文本视图之间的约束不受尊重。有没有人遇到过这个问题?


长版:

我有一个明确定义约束(即顶部,底部,左侧和右侧)的场景,它可以呈现如下视图:

right

但是当我在导航控制器上使用自定义转换时,顶部布局指南的顶部约束似乎关闭,它呈现如下,好像顶部布局指南位于屏幕顶部,而不是在导航控制器的底部:

wrong

使用自定义转换时,导航控制器的“顶部布局指南”似乎变得混乱。其余约束正确应用。如果我旋转设备并再次旋转它,一切都会突然正确呈现,所以看起来并不是没有正确定义约束。同样,当我关闭自定义转换时,视图会正确呈现。

话虽如此,_autolayoutTrace报告UILayoutGuide个对象在我运行时遇到AMBIGUOUS LAYOUT

(lldb) po [[UIWindow keyWindow] _autolayoutTrace]

但是每当我看到它们时,这些布局指南总是被报告为含糊不清,即使我已经确保没有遗漏约束(我已经完成了对视图控制器的习惯选择并选择“为视图控制器添加缺少的约束”或者选择所有控件并为它们做同样的事情。)

就我进行过渡的精确程度而言,我在UIViewControllerAnimatedTransitioning方法中指定了一个符合animationControllerForOperation的对象:

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

    return nil;
}

@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

我还完成了上述内容的设置,设置视图的frame而不是transform,结果相同。

我还尝试通过调用layoutIfNeeded手动确保重新应用约束。我还尝试了setNeedsUpdateConstraintssetNeedsLayout

最重要的是,有没有人成功结合导航控制器的自定义过渡和使用顶部布局指南的约束?

12 个答案:

答案 0 :(得分:27)

通过添加以下行来管理我的问题:

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

要:

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

}

来自Apple的[finalFrameForViewController]文档:https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewControllerContextTransitioning_protocol/#//apple_ref/occ/intfm/UIViewControllerContextTransitioning/finalFrameForViewController

答案 1 :(得分:15)

我通过修复topLayoutGuide的高度约束来解决这个问题。调整edgesForExtendedLayout对我来说不是一个选项,因为我需要目标视图来覆盖导航栏,但也能够使用topLayoutGuide布局子视图。

直接检查游戏中的约束表明iOS向topLayoutGuide添加了一个高度约束,其值等于导航控制器导航栏的高度。除了在iOS 7中,使用自定义动画过渡使约束的高度为0.他们在iOS 8中修复了此问题。

这是我提出的用于纠正约束的解决方案(它在Swift中,但等效应该在Obj-C中起作用)。我已经测试过它可以在iOS 7和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 ...
}

答案 2 :(得分:10)

我遇到了同样的问题。把它放在我的toViewController的viewDidLoad中真的帮助了我:

self.edgesForExtendedLayout = UIRectEdgeNone;

这并没有解决我的所有问题,我仍然在寻找更好的方法,但这确实让它变得更容易。

答案 3 :(得分:4)

仅供参考,我最终使用Alex's answer的变体,以编程方式更改animateTransition方法中的顶部布局指南的高度约束常量。我只发布此内容以分享Objective-C再现(并取消constant == 0测试)。

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

谢谢,Alex。

答案 4 :(得分:4)

只需将以下代码添加到viewDidLoad

即可
self.extendedLayoutIncludesOpaqueBars = YES;

答案 5 :(得分:3)

正如@Rob所提到的,topLayoutGuideUINavigationController中使用自定义转换时不可靠。我使用自己的布局指南解决了这个问题。您可以在此demo project中查看代码。亮点:

自定义布局指南的类别:

@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是将作为布局指南的类。它必须是UIView子类才能避免崩溃(我想知道为什么这不是UILayoutSupport接口的一部分)。

@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

UINavigationControllerDelegate是负责在转换前“修复”布局指南的人:

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

最后,UIViewController在约束中使用hp_topLayoutGuide而不是topLayoutGuide,并通过覆盖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;
}

希望它有所帮助。

答案 6 :(得分:2)

我找到了方法。首先取消选中控制器的“Extend Edges”属性。之后导航栏变暗了。将视图添加到控制器并设置顶部和底部LayoutConstraint -100。然后使视图的clipsubview属性为no(对于navigaionbar transculent effect)。我的英文故事很糟糕。 :)

答案 7 :(得分:1)

我遇到了同样的问题,最终实现了我自己的topLayout指南视图并对其进行了约束,而不是topLayoutGuide。不理想。只有在有人卡住并寻找快速解决问题的解决方案http://www.github.com/stringcode86/SCTopLayoutGuide

时才发布此处

答案 8 :(得分:1)

这是简单解决方案,我使用它非常适合我:在- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext的设置阶段,手动设置您的&# 34;从&#34;和&#34;到&#34; viewController.view.frame.origin.y = navigationController.navigationBar.frame.size.height。它会使您的自动布局视图按预期垂直定位。

减去伪代码(例如你可能有自己的方法来确定设备是否运行iOS7),这就是我的方法:

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

关于这个例子的一些额外的事情:

  • 对于视图控制器推送,此自定义转换会在未移动的fromViewController.view之上滑动推送的toViewController.view。对于pop,fromViewController.view向右滑动并在其下显示一个不动的toViewController.view。总而言之,它只是iOS7 +视图控制器转换的微妙转折。
  • [UIView animateWithDuration:...]完成块显示处理已完成&amp;的正确方法。取消自定义转换。这个微小的花絮是一个经典的头脑瞬间;希望它可以帮助其他人。

最后,我想指出的是,就我所知,这是一个仅在iOS7中修复的iOS7问题:我在iOS7中打破的自定义视图控制器转换工作得很好iOS8无需修改。话虽这么说,你应该验证这也是你所看到的,如果是这样,只在运行iOS7.x的设备上运行修复程序。正如您在上面的代码示例中所看到的,除非设备运行的是iOS7.x,否则y调整值为0.0f。

答案 9 :(得分:0)

尝试:

self.edgesforextendedlayout=UIRectEdgeNone

或者只是将导航栏设置为不透明并将背景图像或背景颜色设置为导航栏

答案 10 :(得分:0)

我遇到了同样的问题,但未使用UINavigationController,只是将视图定位在topLayoutGuide之外。首次显示时,布局将是正确的,转换将发生在另一个视图中,然后在退出并返回到第一个视图时,布局将被打破,因为topLayoutGuide将不再存在。

我通过在转换之前捕获安全区域insets然后重新实现它们来解决这个问题,而不是通过调整我的约束,而是通过在viewController additionalSafeAreaInsets上设置它们。

我发现这个解决方案运行良好,因为我不必调整任何布局代码并搜索约束,我可以重新实现之前的空间。如果您实际使用additionalSafeAreaInsets属性,则可能会更加困难。

实施例

我在transitionManager中添加了一个变量来捕获创建transitionManager时存在的安全插件。

class MyTransitionManager: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {

    private var presenting = true
    private var container:UIView?
    private var safeInsets:UIEdgeInsets?

    ...

然后在进入转换期间,我保存了这些插入内容。

    let toView = viewControllers.to.view
    let fromView = viewControllers.from.view

    if #available(iOS 11.0, *) {
        safeInsets = toView.safeAreaInsets
    }

对于iPhone X,它看起来像UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)

现在退出时,我们在入口处转换的相同视图的插图将是.zero所以我们将捕获的插图添加到viewController上的additionalSafeAreaInsets,这将在我们的视图中设置它们为我们以及更新布局。动画制作完成后,我们会将additionalSafeAreaInsets重置为.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)

答案 11 :(得分:-1)

在storyboard中为主视图的顶部添加另一个垂直约束。我也有同样的问题,但添加该约束可以帮助我避免手动约束。请参见此处的屏幕截图link

其他解决方案是计算toVC框架......如下所示:

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);

如果您找到了更好的解决方案,请告诉我。我也一直在努力解决这个问题,我想出了以前的想法。