64

简洁版本:

UINavigationController当与自定义过渡和iOS7结合使用时,我遇到了自动布局顶部布局指南的问题。具体来说,顶部布局指南和文本视图之间的约束没有得到遵守。有没有人遇到过这个问题?


长版:

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

对

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

错误的

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

话虽如此,当我运行时_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等。

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

4

12 回答 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 :

于 2015-10-06T21:14:07.267 回答
15

我通过修复topLayoutGuide. 调整edgesForExtendedLayout对我来说不是一个选项,因为我需要目标视图来覆盖导航栏,而且还需要能够使用topLayoutGuide.

直接检查 play 中的约束表明 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 ...
}
于 2015-02-17T19:47:05.650 回答
10

我在完全相同的问题上苦苦挣扎。把它放在我的 toViewController 的 viewDidLoad 中真的帮助了我:

self.edgesForExtendedLayout = UIRectEdgeNone;

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

于 2013-12-03T20:04:03.997 回答
5

只需将以下代码放入viewDidLoad

self.extendedLayoutIncludesOpaqueBars = YES;
于 2015-09-21T09:43:24.873 回答
4

仅供参考,我最终采用了亚历克斯答案的变体,以编程方式更改方法中顶部布局指南的高度约束常数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;
    }
}

谢谢,亚历克斯。

于 2015-02-22T16:50:58.150 回答
3

正如@Rob 提到topLayoutGuide的,在UINavigationController. 我使用自己的布局指南解决了这个问题。您可以在这个演示项目中看到正在运行的代码。强调:

自定义布局指南的类别:

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

希望能帮助到你。

于 2014-02-26T00:44:12.103 回答
2

我找到了方法。首先取消选中控制器的“扩展边缘”属性。之后导航栏颜色变深。将视图添加到控制器并设置顶部和底部 LayoutConstraint -100。然后将视图的 clipsubview 属性设置为 no(用于 navigaionbar 半透明效果)。我的英语很抱歉。:)

于 2014-06-23T06:29:47.493 回答
1

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

于 2014-04-06T11:25:07.423 回答
1

这是我正在使用的对我来说非常有用的简单- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext解决方案:在设置阶段,手动设置“从”和“到”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 之上。对于 pops,fromViewController.view 向右滑动并在其下方显示一个不动的 toViewController.view。总而言之,这只是对现有的 iOS7+ 视图控制器转换的一个微妙改动。
  • [UIView animateWithDuration:...]完成块显示了处理已完成和取消的自定义转换的正确方法。这个小小的花絮是经典的拍头时刻。希望它可以帮助其他人。

最后,我想指出,据我所知,这是一个 iOS7 独有的问题,已在 iOS8 中修复:我在 iOS7 中损坏的自定义视图控制器转换在 iOS8 中运行良好,无需修改。话虽如此,您应该验证这也是您所看到的,如果是,请仅在运行 iOS7.x 的设备上运行修复程序。如您在上面的代码示例中所见,除非设备运行的是 iOS7.x,否则 y 调整值为 0.0f。

于 2014-10-06T23:48:29.807 回答
1

我遇到了同样的问题,但没有使用 aUINavigationController并且只是将视图定位在topLayoutGuide. 布局在第一次显示时是正确的,会发生到另一个视图的转换,然后在退出并返回到第一个视图时,布局将被破坏,因为它topLayoutGuide不再存在。

我通过在过渡之前捕获安全区域插图然后重新实现它们来解决这个问题,而不是通过调整我的约束,而是通过将它们设置在 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,因此我们将捕获的插图添加到additionalSafeAreaInsetsviewController 上,这将为我们将它们设置在我们的视图上并更新布局。一旦我们的动画完成,我们将additionalSafeAreaInsetsback 重置为.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)
于 2018-03-19T14:28:25.890 回答
0

尝试 :

self.edgesforextendedlayout=UIRectEdgeNone

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

于 2014-01-24T03:50:54.647 回答
-1

在情节提要中,将另一个垂直约束添加到主视图的顶部。我也有同样的问题,但添加该约束有助于我避免手动约束。在此处查看屏幕截图链接

其他解决方案是计算 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);

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

于 2014-07-15T02:32:31.307 回答