web-dev-qa-db-ja.com

カスタムトランジションでナビゲーションコントローラーの上部レイアウトガイドが適用されない

短縮版:

IOS7でカスタムトランジションおよびUINavigationControllerと組み合わせて使用​​すると、自動レイアウトトップレイアウトガイドで問題が発生します。具体的には、トップレイアウトガイドとテキストビューの間の制約が守られていません。誰もこの問題に遭遇しましたか?


ロングバージョン:

私は、次のようなビューをレンダリングする制約(つまり、上、下、左、右)を明確に定義したシーンを持っています:

right

ただし、Navigation Controllerのカスタムトランジションでこれを使用すると、上部レイアウトガイドの上部の制約がオフになり、上部レイアウトガイドが下部ではなく画面上部にあるかのようにレンダリングされますNavigation Controllerの:

wrong

カスタムトランジションを使用すると、Navigation Controllerの「トップレイアウトガイド」が混乱しているように見えます。残りの制約は正しく適用されています。そして、デバイスを回転させて再び回転させると、すべてが突然正しくレンダリングされるため、制約が適切に定義されていないことは問題ではないようです。同様に、カスタムトランジションをオフにすると、ビューが正しくレンダリングされます。

そうは言っても、 _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なども試しました。

結論として、トップレイアウトガイドを使用する制約を持つNavigation Controllerのカスタムトランジションと結婚した人はいますか?

61
Rob

topLayoutGuideの高さの制約を修正することでこれを解決しました。 edgesForExtendedLayoutを調整することはオプションではありませんでした。ナビゲーションバーの下に目的のビューが必要でしたが、topLayoutGuideを使用してサブビューをレイアウトできるようにしたからです。

プレイ中の制約を直接検査すると、iOSがtopLayoutGuideに、Navigation ControllerのNavigation Barの高さと等しい値を持つ高さ制約を追加することがわかります。ただし、iOS 7では、カスタムアニメーショントランジションを使用すると、制約の高さが0のままになります。iOS8ではこれを修正しました。

これは、制約を修正するために思いついた解決策です(Swiftにありますが、同等のものはObj-Cで動作するはずです)。iOS7および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 ...
}
15
Alex Pretzlav

次の行を追加して問題を解決しました:

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

27
Hamdan Javed

私はまったく同じ問題に苦労しました。これをtoViewControllerのviewDidLoadに入れると、本当に役立ちました。

self.edgesForExtendedLayout = UIRectEdgeNone;

これは私のすべての問題を解決しませんでしたし、私はまだより良いアプローチを探していますが、これにより確かに少し簡単になりました。

10
Jasper Kuperus

次のコードをto viewDidLoadに追加するだけです

self.extendedLayoutIncludesOpaqueBars = YES;
5
Cocody

参考までに、私は Alexの答え のバリエーションを採用し、animateTransitionメソッドでトップレイアウトガイドの高さ制約定数をプログラムで変更しました。 Objective-Cのレンディションを共有するためにのみ投稿しています(そして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;
    }
}

ありがとう、アレックス。

4
Rob

@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は制約でtopLayoutGuideの代わりにhp_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;
}

それが役に立てば幸い。

3
hpique

道を見つけた。まず、コントローラーの「エッジを拡張」プロパティのチェックを外します。ナビゲーションバーが暗い色になった後。ビューをコントローラーに追加し、上部と下部のLayoutConstraint -100を設定します。次に、ビューのclipsubviewプロパティをnoにします(navigaionbar transculentエフェクト用)。そのための私の英語の悪い悲しみ。 :)

2
Yusuf terzi

私も同じ問題を抱えていたため、自分のtopLayoutガイドビューを実装し、topLayoutGuideではなく、それに制約を加えました。理想的ではありません。誰かが立ち往生している場合にのみここに投稿し、迅速なハッキングソリューションを探しています http://www.github.com/stringcode86/SCTopLayoutGuide

1
stringCode

ここに私が使用しているsimpleソリューションがあります:- (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:...]完了ブロックは、完了およびキャンセルされたカスタム遷移を処理する正しい方法を示しています。この小さな一口は、古典的なヘッドスラップの瞬間でした。それが他の誰かに役立つことを願っています。

最後に、私が知る限り、これはiOS8で修正されたiOS7のみの問題であることを指摘したいと思います:iOS7で壊れている私のカスタムビューコントローラーの移行は、変更なしでiOS8でうまく機能します。そうは言っても、これが表示されていることを確認する必要があります。もしそうなら、iOS7.xを実行しているデバイスでのみ修正を実行してください。上記のコード例でわかるように、デバイスがiOS7.xを実行していない限り、y調整値は0.0fです。

1
John Jacecko

試してください:

self.edgesforextendedlayout=UIRectEdgeNone

または、Navigationbarを不透明に設定し、背景画像またはbackgroundcolorをnavigationbarに設定するだけです

0
vivek

私はこれと同じ問題にぶつかりましたが、UINavigationControllerを使用せず、ビューを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_になるので、キャプチャしたインセットを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)
_
0
skabob11