uinavigationcontroller ios7 autolayout custom-transition

uinavigationcontroller - La guía de diseño superior del controlador de navegación no se respetó con la transición personalizada



ios7 autolayout (12)

Version corta:

Tengo un problema con la guía de diseño superior de diseño automático cuando se utiliza junto con la transición personalizada y UINavigationController en iOS7. Específicamente, la restricción entre la guía de diseño superior y la vista de texto no se respeta. ¿Alguien ha encontrado este problema?

Versión larga:

Tengo una escena que define inequívocamente las restricciones (es decir, arriba, abajo, izquierda y derecha) que hacen que una vista sea así:

Pero cuando uso esto con una transición personalizada en el controlador de navegación, la restricción superior a la guía de diseño superior parece desactivada y se representa de la siguiente manera, como si la guía de diseño superior estuviera en la parte superior de la pantalla, en lugar de en la parte inferior del controlador de navegación:

Parece que la "guía de diseño superior" con el controlador de navegación se confunde cuando se utiliza la transición personalizada. El resto de las restricciones se están aplicando correctamente. Y si giro el dispositivo y lo vuelvo a girar, todo se procesa de forma incorrecta, por lo que no parece que las limitaciones no estén definidas correctamente. Del mismo modo, cuando desactivo mi transición personalizada, las vistas se procesan correctamente.

Habiendo dicho eso, _autolayoutTrace informa que los objetos UILayoutGuide sufren de AMBIGUOUS LAYOUT , cuando ejecuto:

(lldb) po [[UIWindow keyWindow] _autolayoutTrace]

Pero esas guías de diseño siempre se informan como ambiguas cada vez que las miro, aunque me he asegurado de que no faltan restricciones (hice la selección habitual del controlador de vista y elegí "Agregar restricciones faltantes para el controlador de vista" o seleccioné todas de los controles y haciendo lo mismo por ellos).

En términos de la precisión con la que estoy haciendo la transición, he especificado un objeto que cumple con UIViewControllerAnimatedTransitioning en el método animationControllerForOperation :

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

Y

@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

También hice una interpretación de lo anterior, estableciendo el frame de la vista en lugar de la transform , con el mismo resultado.

También intenté manualmente asegurarme de que las restricciones se volvieran a aplicar llamando a layoutIfNeeded . También probé setNeedsUpdateConstraints , setNeedsLayout , etc.

En pocas palabras, ¿alguien se casó exitosamente con la transición personalizada del controlador de navegación con restricciones que utilizan la guía de diseño superior?


Aquí está la solución simple que estoy usando que funciona muy bien para mí: durante la fase de configuración de - (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext , configure manualmente su "de" y "a" viewController.view.frame.origin .y = navigationController.navigationBar.frame.size.height. Hará que tus vistas de diseño automático se posicionen verticalmente como esperas.

Menos el pseudo-código (ej. Usted probablemente tiene su propia manera de determinar si un dispositivo está ejecutando iOS7), así es como se ve mi método:

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

Un par de cosas extra sobre este ejemplo:

  • Para las pulsaciones del controlador de vista, esta transición personalizada desliza el empujado a ViewController.view en la parte superior de la vista inmóvil fromViewController.view. En el caso de pops, fromViewController.view se desliza hacia la derecha y revela una vista inmóvil de ViewController.view debajo de él. Con todo, es solo un giro sutil en la transición del controlador de vista iOS7 + stock.
  • El bloque de finalización [UIView animateWithDuration:...] muestra la forma correcta de manejar las transiciones personalizadas completadas y canceladas. Este pequeño bocado fue un clásico momento de golpe de cabeza; Espero que ayude a alguien más allá.

Por último, me gustaría señalar que, por lo que puedo decir, este es un problema exclusivo de iOS7 que se ha solucionado en iOS8: mi transición de controlador de vista personalizada que está rota en iOS7 funciona muy bien en iOS8 sin modificaciones. Una vez dicho esto, debe verificar que esto es lo que está viendo también, y si es así, solo ejecute la corrección en los dispositivos con iOS7.x. Como puede ver en el ejemplo de código anterior, el valor de ajuste de y es 0.0f a menos que el dispositivo ejecute iOS7.x.


Como se mencionó en @Rob, topLayoutGuide no es confiable cuando se usan transiciones personalizadas en UINavigationController . Trabajé alrededor de esto usando mi propia guía de diseño. Puede ver el código en acción en este proyecto de demostración . Reflejos:

Una categoría para guías de diseño personalizadas:

@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 es la clase que actuará como una guía de diseño. Tiene que ser una subclase UIView para evitar bloqueos (me pregunto por qué esto no forma parte de la interfaz 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 es el responsable de "corregir" la guía de diseño antes de la transición:

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

Finalmente, UIViewController utiliza hp_topLayoutGuide lugar de topLayoutGuide en las restricciones, y lo indica anulando 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; }

Espero eso ayude.


En el guión gráfico, agregue otra restricción vertical a la parte superior de la vista principal. También tengo el mismo problema, pero agregar esa restricción me ayuda a evitar restricciones manuales. Ver captura de pantalla aquí link

Otra solución es calcular el marco de VC ... algo como esto:

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

Avíseme si ha encontrado una mejor solución. También he estado luchando con este problema y presenté ideas anteriores.


FYI, terminé empleando una variación de la respuesta de Alex , cambiando programáticamente la constante de constricción de altura de la guía de diseño superior en el método animateTransition . Solo estoy publicando esto para compartir la representación de Objective-C (y eliminar la prueba 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; } }

Gracias, Alex.


Lo topLayoutGuide arreglando la restricción de altura de topLayoutGuide . edgesForExtendedLayout fue una opción para mí ajustar edgesForExtendedLayout , ya que necesitaba la vista de destino para colocar debajo de la barra de navegación, pero también para poder crear subvistas usando topLayoutGuide .

La inspección directa de las restricciones en juego muestra que iOS agrega una restricción de altura a topLayoutGuide con un valor igual al alto de la barra de navegación del controlador de navegación. Excepto que en iOS 7, el uso de una transición de animación personalizada deja la restricción con una altura de 0. Lo arreglaron en iOS 8.

Esta es la solución que se me ocurrió para corregir la restricción (está en Swift, pero el equivalente debería funcionar en Obj-C). He probado que funciona en iOS 7 y 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 ... }


Logré solucionar mi problema agregando esta línea:

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

A:

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

De la documentación de Apple para [finalFrameForViewController]: https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewControllerContextTransitioning_protocol/#//apple_ref/occ/intfm/UIViewControllerContextTransitioning/finalFrameForViewController :


Luché con exactamente el mismo problema. Poner esto en viewDidLoad de mi toViewController realmente me ayudó:

self.edgesForExtendedLayout = UIRectEdgeNone;

Esto no resolvió todos mis problemas y todavía estoy buscando un mejor enfoque, pero esto ciertamente lo hizo un poco más fácil.


Me encontré con este mismo problema, pero sin utilizar un UINavigationController y simplemente posicionar una vista fuera de topLayoutGuide . El diseño sería correcto cuando se muestre por primera vez, se realizaría una transición a otra vista y luego, al salir y regresar a la primera vista, el diseño se rompería ya que topLayoutGuide ya no estaría allí.

Resolví este problema capturando las inserciones de área segura antes de la transición y luego reimplementándolas, no ajustando mis restricciones, sino estableciéndolas en los join de ruta additionalSafeAreaInsets de viewController.

Encontré que esta solución funciona bien, ya que no tengo que ajustar ninguno de mis códigos de diseño y buscar restricciones y simplemente puedo volver a implementar el espacio que estaba allí anteriormente. Esto podría ser más difícil si realmente está utilizando la propiedad additionalSafeAreaInsets .

Ejemplo

Agregué una variable a mi transitionManager para capturar las inserciones seguras que existen cuando se crea transitionManager.

class MyTransitionManager: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate { private var presenting = true private var container:UIView? private var safeInsets:UIEdgeInsets? ...

Luego, durante la transición de entrada, guardo esas inserciones.

let toView = viewControllers.to.view let fromView = viewControllers.from.view if #available(iOS 11.0, *) { safeInsets = toView.safeAreaInsets }

En el caso del iPhone X esto parece algo así como UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)

Ahora, al salir, las inserciones de la misma vista desde la que realizamos la transición serán .zero por lo que agregaremos nuestras inserciones capturadas a los additionalSafeAreaInsets en viewController, lo que los establecerá en nuestra vista para nosotros y también actualizará el diseño. Una vez que .zero nuestra animación, reiniciaremos additionalSafeAreaInsets nuevamente a .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)


Simplemente coloque el siguiente código para viewDidLoad

self.extendedLayoutIncludesOpaqueBars = YES;


Tuve el mismo problema, terminé implementando mi propia vista de guía de topLayout y aplicando restricciones en lugar de topLayoutGuide. No es ideal. Solo publíquelo aquí en caso de que alguien se quede atascado y busque una solución rápida para hack http://www.github.com/stringcode86/SCTopLayoutGuide


encontré la manera. Primero desmarque la propiedad "Extender bordes" del controlador. después de que la barra de navegación se vuelva de color oscuro. Agregue una vista al controlador y configure la parte superior e inferior de LayoutConstraint -100. A continuación, haga que la propiedad clipubview de view no (para efecto transculent navigaionbar). Mi inglés es una mala historia para eso. :)


tratar :

self.edgesforextendedlayout=UIRectEdgeNone

O simplemente configura la barra de navegación como opaca y configura la imagen de fondo o el color de fondo en la barra de navegación