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