web-dev-qa-db-fra.com

Le guide de configuration supérieur du contrôleur de navigation n'est pas honoré par la transition personnalisée

Version courte:

J'ai un problème avec le guide de mise en page de mise en page automatique lorsqu'il est utilisé en conjonction avec une transition personnalisée et UINavigationController dans iOS7. Plus précisément, la contrainte entre le guide de mise en page supérieur et la vue texte n'est pas respectée. Quelqu'un a-t-il rencontré ce problème?


Version longue:

J'ai une scène qui a défini sans ambiguïté des contraintes (c'est-à-dire en haut, en bas, à gauche et à droite) qui rendent une vue comme suit:

right

Mais lorsque j'utilise cela avec une transition personnalisée sur le contrôleur de navigation, la contrainte supérieure du guide de mise en page supérieur semble désactivée et s'affiche comme suit, comme si le guide de mise en page supérieur était en haut de l'écran plutôt qu'en bas. du contrôleur de navigation:

wrong

Il semblerait que le "guide de mise en page supérieur" avec le contrôleur de navigation soit confus lors de l'utilisation de la transition personnalisée. Les autres contraintes sont appliquées correctement. Et si je fais pivoter l'appareil et le fais pivoter à nouveau, tout est soudainement rendu correctement, il ne semble donc pas que les contraintes ne soient pas définies correctement. De même, lorsque je désactive ma transition personnalisée, les vues s'affichent correctement.

Ayant dit cela, _autolayoutTrace signale que les objets UILayoutGuide souffrent de AMBIGUOUS LAYOUT, quand je cours:

(lldb) po [[UIWindow keyWindow] _autolayoutTrace]

Mais ces guides de disposition sont toujours signalés comme ambigus chaque fois que je les regarde, même si je me suis assuré qu'il n'y a pas de contraintes manquantes (j'ai fait la sélection habituelle du contrôleur de vue et en choisissant "Ajouter des contraintes manquantes pour le contrôleur de vue" ou en sélectionnant tout des commandes et faire de même pour eux).

En termes de précision avec laquelle je fais la transition, j'ai spécifié un objet conforme à UIViewControllerAnimatedTransitioning dans la méthode animationControllerForOperation:

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

    return nil;
}

Et

@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

J'ai également fait un rendu de ce qui précède, en définissant le frame de la vue plutôt que le transform, avec le même résultat.

J'ai également essayé manuellement de m'assurer que les contraintes sont réappliquées en appelant layoutIfNeeded. J'ai également essayé setNeedsUpdateConstraints, setNeedsLayout, etc.

En fin de compte, quelqu'un a-t-il réussi à marier la transition personnalisée du contrôleur de navigation avec des contraintes qui utilisent le guide de mise en page supérieur?

61
Rob

J'ai résolu ce problème en corrigeant la contrainte de hauteur du topLayoutGuide. Ajuster edgesForExtendedLayout n'était pas une option pour moi, car j'avais besoin de la vue de destination pour sous-chevaucher la barre de navigation, mais aussi pour pouvoir mettre en page les sous-vues à l'aide de topLayoutGuide.

L'inspection directe des contraintes en jeu montre qu'iOS ajoute une contrainte de hauteur à topLayoutGuide avec une valeur égale à la hauteur de la barre de navigation du contrôleur de navigation. Sauf que dans iOS 7, l'utilisation d'une transition d'animation personnalisée laisse la contrainte avec une hauteur de 0. Ils ont corrigé cela dans iOS 8.

C'est la solution que j'ai trouvée pour corriger la contrainte (c'est en Swift mais l'équivalent devrait fonctionner en Obj-C). J'ai testé qu'il fonctionne sur iOS 7 et 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

J'ai réussi à résoudre mon problème en ajoutant cette ligne:

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

}

À partir de la documentation d'Apple pour [finalFrameForViewController]: https://developer.Apple.com/library/ios/documentation/UIKit/Reference/UIViewControllerContextTransitioning_protocol/#//Apple_ref/occ/intfm/UIViewControllingController finalFrameForViewController :

27
Hamdan Javed

J'ai lutté avec exactement le même problème. Mettre cela dans le viewDidLoad de mon toViewController m'a vraiment aidé:

self.edgesForExtendedLayout = UIRectEdgeNone;

Cela n'a pas résolu tous mes problèmes et je cherche toujours une meilleure approche, mais cela a certainement facilité les choses.

10
Jasper Kuperus

Mettez simplement le code suivant dans viewDidLoad

self.extendedLayoutIncludesOpaqueBars = YES;
5
Cocody

Pour info, j'ai fini par utiliser une variation de réponse d'Alex , en changeant par programme la constante de contrainte de hauteur du guide de mise en page supérieur dans la méthode animateTransition. Je ne poste que cela pour partager le rendu Objective-C (et éliminer le 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;
    }
}

Merci, Alex.

4
Rob

Comme @Rob l'a mentionné, topLayoutGuide n'est pas fiable lors de l'utilisation de transitions personnalisées dans UINavigationController. J'ai travaillé autour de cela en utilisant mon propre guide de mise en page. Vous pouvez voir le code en action dans ce projet de démonstration . Points forts:

Une catégorie pour les guides de mise en page personnalisés:

@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 est la classe qui servira de guide de mise en page. Il doit s'agir d'une sous-classe UIView pour éviter les plantages (je me demande pourquoi cela ne fait pas partie de l'interface 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

Le UINavigationControllerDelegate est celui responsable de la "correction" du guide de mise en page avant la 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;
}

Enfin, UIViewController utilise hp_topLayoutGuide au lieu de topLayoutGuide dans les contraintes, et l'indique en remplaçant 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;
}

J'espère que ça aide.

3
hpique

j'ai trouvé le chemin. Décochez d'abord la propriété "Extend Edges" du contrôleur. après que la barre de navigation soit de couleur foncée. Ajoutez une vue au contrôleur et définissez LayoutConstraint haut et bas -100. Ensuite, définissez la propriété clipsubview de la vue sur no (pour un effet transculent sur la barre de navigation). Mon mauvais anglais pour ça. :)

2
Yusuf terzi

J'ai eu le même problème, j'ai fini par implémenter ma propre vue de guide topLayout et à lui imposer des contraintes plutôt qu'à topLayoutGuide. Pas idéal. Le poster uniquement ici au cas où quelqu'un serait coincé et chercher une solution de piratage rapide http://www.github.com/stringcode86/SCTopLayoutGuide

1
stringCode

Voici la solution simple que j'utilise et qui fonctionne très bien pour moi: pendant la phase de configuration de - (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext, définissez manuellement vos "from" et "to" viewController.view.frame. Origin.y = navigationController.navigationBar.frame.size.height. Vos vues de mise en page automatique se positionneront verticalement comme prévu.

Moins le pseudo-code (par exemple, vous avez probablement votre propre façon de déterminer si un appareil exécute iOS7), voici à quoi ressemble ma méthode:

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

Quelques bonus sur cet exemple:

  • Pour les poussées de contrôleur de vue, cette transition personnalisée fait glisser le fichier push vers ViewController.view au-dessus du déplacement de ViewController.view. Pour les pops, fromViewController.view glisse vers la droite et révèle un immobile toViewController.view en dessous. Dans l'ensemble, ce n'est qu'une subtile torsion sur la transition du contrôleur de vue iOS7 + stock.
  • Le [UIView animateWithDuration:...] le bloc d'achèvement indique la manière correcte de gérer les transitions personnalisées terminées et annulées. Cette petite friandise était un moment classique de gifle; j'espère que cela aide quelqu'un d'autre là-bas.

Enfin, je voudrais souligner que, pour autant que je sache, il s'agit d'un problème uniquement sur iOS7 qui a été corrigé dans iOS8: ma transition de contrôleur de vue personnalisé qui est rompue dans iOS7 fonctionne très bien dans iOS8 sans modification. Cela étant dit, vous devez également vérifier que c'est ce que vous voyez, et si c'est le cas, exécutez le correctif uniquement sur les appareils exécutant iOS7.x. Comme vous pouvez le voir dans l'exemple de code ci-dessus, la valeur d'ajustement y est 0,0f, sauf si l'appareil exécute iOS7.x.

1
John Jacecko

essayez:

self.edgesforextendedlayout=UIRectEdgeNone

Ou définissez simplement la barre de navigation opaque et définissez l'image d'arrière-plan ou la couleur d'arrière-plan sur la barre de navigation

0
vivek

J'ai rencontré ce même problème mais sans utiliser un UINavigationController et juste positionner une vue hors du topLayoutGuide. La mise en page serait correcte lors de son premier affichage, une transition aurait lieu vers une autre vue, puis en quittant et en revenant à la première vue, la mise en page serait rompue car topLayoutGuide ne serait plus là.

J'ai résolu ce problème en capturant les encarts de zone de sécurité avant la transition, puis en les réimplémentant, non pas en ajustant mes contraintes, mais en les définissant sur additionalSafeAreaInsets de viewController.

J'ai trouvé que cette solution fonctionnait bien car je n'ai pas besoin d'ajuster mon code de mise en page et de rechercher parmi les contraintes et je peux simplement réimplémenter l'espace qui s'y trouvait auparavant. Cela pourrait être plus difficile si vous utilisez réellement la propriété additionalSafeAreaInsets.

Exemple

J'ai ajouté une variable à mon transitionManager pour capturer les encarts sûrs qui existent lorsque le transitionManager est créé.

class MyTransitionManager: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {

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

    ...

Ensuite, lors de la transition entrante, je sauvegarde ces encarts.

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

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

Dans le cas de l'iPhone X, cela ressemble à UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)

Maintenant, à la sortie, les encarts de la même vue à partir de laquelle nous avons fait la transition dans l'entrée seront .zero, Nous ajoutons donc nos encarts capturés au additionalSafeAreaInsets sur le viewController, qui les placera sur notre vue pour nous ainsi que de mettre à jour la mise en page. Une fois notre animation terminée, nous réinitialisons le additionalSafeAreaInsets sur .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