导航控制器在使用自定义转场时无法遵循顶部布局指南。

64

简短版:

iOS7中,当与自定义转换和UINavigationController一起使用时,我的自动布局顶部布局指南存在问题。具体地,顶部布局指南和文本视图之间的约束没有被遵守。有人遇到过这个问题吗?


详细版:

我有一个场景,其中已经明确定义了约束(即顶部、底部、左侧和右侧),呈现出以下视图:

right

但是当我将其与导航控制器上的自定义转换一起使用时,与顶部布局指南的顶部约束似乎不正确,并呈现为如下所示的情况,好像顶部布局指南位于屏幕顶部,而不是在导航控制器底部:

wrong

似乎在使用自定义转换时,“顶部布局指南”与导航控制器混淆了。其余约束都被正确应用。如果我旋转设备并再次旋转它,则一切都会突然正确地呈现,因此它似乎不是约束未正确定义的问题。同样,当我关闭自定义转换时,视图会正确呈现。

尽管如此,当我运行以下代码时,_autolayoutTrace报告UILayoutGuide对象存在AMBIGUOUS LAYOUT的问题:

(lldb) po [[UIWindow keyWindow] _autolayoutTrace]

但每当我查看这些布局指南时,它们总被报告为不明确,尽管我已确保没有缺少的约束(我已经惯常地选择视图控制器并选择“为视图控制器添加缺失的约束”或选择所有控件并为它们执行相同操作)。

至于我如何精确进行转换,我已在animationControllerForOperation方法中指定符合UIViewControllerAnimatedTransitioning的对象:

- (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 等等。

总之,有没有人成功地将导航控制器的自定义转换与使用顶部布局指南的约束条件结合起来?


嗨,@Rob。你找到解决方法了吗? - hpique
@hpique 不,除了手动指定视图顶部的偏移量而不是从顶部布局指南开始,或者完全关闭扩展布局之外,没有其他选择。 - Rob
12个回答

28

通过添加这行代码,我解决了我的问题:

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


4
这是唯一对我正常工作的东西![transitionContext finalFrameForViewController:toVC]已经有了正确的topLayoutGuide。你让我的一天变得美好!谢谢! - jdev
4
太棒了!这是唯一有效的回答!其他的都只是非常糟糕的把戏!谢谢! - Angel G. Olloqui
请注意,如果您的导航栏是半透明的,则仍然需要在toViewController的viewDidLoad()中设置'self.edgesForExtendedLayout = .None',以使其正常工作。 - ykay
我一直有一个持久的问题。原来是我的一个转换没有正确地执行这个操作,而其他的都做到了。谢谢! - Bob Wakefield
我在使用Xcode 9时遇到了这个问题,但只影响iOS 10及以下版本。这个解决方案对我很有效,而且似乎是最“正确”/最不破坏性的解决方案。谢谢! - JoGoFo

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

很高兴看到他们在iOS 8中修复了这个问题。我之前没有注意到。这一切都有道理。关于你上面的代码片段,有三件事情:1. 在iOS 7上测试时,我不得不删除constraint.constant条件,因为我的是20而不是0;2. 我认为你的意思是...frame.size.height而不是...frame.height。3. 我认为我们还需要拦截旋转事件并更改导航栏高度变化的constant。但手动调整顶部约束的想法是有道理的。 - Rob
@Rob:感谢您的反馈。有趣的是,您的约束常量为20,而我在测试的iOS 7设备和模拟器上都是0(更新:啊!我隐藏了状态栏。完全是这个原因)。frame.height是Swift提供的帮助程序 : )。我期望约束会在旋转时自动更新,但我的应用程序不支持旋转。您测试过吗? - Alex Pretzlav
啊,是的,它当然会改变旋转约束。实际上,即使它表现不佳,如果你旋转到横屏然后再旋转回来,顶部约束也会自动修复。问题只在于当你首次切换到新视图时调整初始约束,这样就可以解决它。 - Rob
这个答案对我有用!尝试编辑以尊重状态栏高度和navigationBarHidden属性。但是,嘿,人们为什么没有给出理由就拒绝它呢? - deej

10

我曾经也遇到过同样的问题。将以下代码放在我的toViewController的viewDidLoad中真的帮了我很大的忙:

self.edgesForExtendedLayout = UIRectEdgeNone;

这并没有解决我所有的问题,我仍在寻找更好的方法,但这肯定使它变得更容易了一些。


同意,如果你愿意放弃半透明的导航栏,这个方法非常好用。希望有人能找到解决方案或者苹果公司能够修复这个问题。 - Rob
2
嘿 Rob,你向苹果报告过这个问题了吗? - mskw

5
只需将以下代码放入viewDidLoad中即可。
self.extendedLayoutIncludesOpaqueBars = YES;

4

顺便提一下,我最终采用了Alex的答案的变体,在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。

3

如@Rob所提到的,当使用自定义转场时,topLayoutGuideUINavigationController中不可靠。我通过使用自己的布局指南来解决了这个问题。您可以在这个演示项目中查看代码实现。重点:

自定义布局指南的类别:

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

希望这有所帮助。

2
我找到了一种方法。首先取消控制器的“Extend Edges”属性,然后导航栏会变成深色。接着给控制器添加一个视图,并设置上下布局约束为-100。然后将该视图的clipsubview属性设置为no(以实现导航栏透明效果)。我的英语不好,请见谅。 :)

1
我遇到了相同的问题,但没有使用UINavigationController,而是将一个视图定位到topLayoutGuide。当首次显示布局时,会进行转换,然后在退出并返回到第一个视图时,布局会被破坏,因为那个topLayoutGuide不再存在。
我通过在过渡之前捕获安全区域插图来解决这个问题,然后重新实现它们,而不是通过调整我的约束来设置它们在viewController的additionalSafeAreaInsets上。
我发现这个解决方案很好,因为我不需要调整任何布局代码和搜索约束,我只需重新实现先前存在的空间即可。如果您实际上使用additionalSafeAreaInsets属性,则可能更加困难。
例子
我向我的过渡管理器添加了一个变量,以捕获创建过渡管理器时存在的安全插图。
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,所以我们将捕获的插图添加到视图控制器的 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)

1

我曾经遇到过同样的问题,最终实现了自己的顶部布局引导视图,并对其进行了约束,而不是对顶部布局引导进行约束。虽然并不理想,但在这里发布它,以防有人卡住了,正在寻找快速的hacky解决方案 http://www.github.com/stringcode86/SCTopLayoutGuide


1
这里是我正在使用的简单解决方案:在- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext的设置阶段,手动设置你的“from”和“to”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)];
     }];
}

这个例子还有几个额外的要点:

  • 对于视图控制器推送,这个自定义转场会将被推出的 toViewController.view 滑动到不动的 fromViewController.view 上方。对于弹出,fromViewController.view 会向右滑动并显示在它下面的 toViewController.view。总之,它只是 iOS7+ 原生视图控制器转场的微小变化。
  • [UIView animateWithDuration:...] 完成块展示了处理完成和取消的自定义转场的正确方式。这个小技巧是一个经典的想起来才拍脑门的时刻,希望能帮助其他人。

最后,我想指出,据我所知,这是一个仅在 iOS7 上出现的问题,在 iOS8 中已经修复:我的在 iOS7 上出现问题的自定义视图控制器转场在 iOS8 中没有修改就能正常工作。话虽如此,你应该验证一下你看到的情况,如果是这样,只在运行 iOS7.x 的设备上运行修复程序。如你在上面的代码示例中所见,y 调整值为 0.0f,除非设备运行 iOS7.x。


谢谢。看起来是将 y 调整下来并应用一个向下移动视图的 transform 的组合。这解决了导航栏的问题,但这样不会破坏底部的指南线吗? - Rob

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接