Diseño automático con UIViews ocultos?

164

Siento que es un paradigma bastante común para mostrar / ocultar UIViews, con mayor frecuencia UILabels, dependiendo de la lógica empresarial. Mi pregunta es, ¿cuál es la mejor manera de usar AutoLayout para responder a vistas ocultas como si su marco fuera 0x0? Aquí hay un ejemplo de una lista dinámica de 1-3 características.

Lista de características dinámicas

En este momento tengo un espacio superior de 10 píxeles desde el botón hasta la última etiqueta, que obviamente no se deslizará hacia arriba cuando la etiqueta esté oculta. A partir de ahora, creé una salida para esta restricción y modifiqué la constante dependiendo de cuántas etiquetas estoy mostrando. Obviamente, esto es un poco extraño ya que estoy usando valores constantes negativos para presionar el botón hacia arriba sobre los marcos ocultos. También es malo porque no se limita a los elementos de diseño reales, solo cálculos estáticos furtivos basados ​​en alturas conocidas / relleno de otros elementos, y obviamente lucha contra lo que fue diseñado para AutoLayout.

Obviamente, podría crear nuevas restricciones dependiendo de mis etiquetas dinámicas, pero eso es una gran cantidad de microgestión y mucha verbosidad para tratar de colapsar algunos espacios en blanco. ¿Hay mejores enfoques? ¿Cambiar el tamaño de cuadro 0,0 y dejar que AutoLayout haga lo suyo sin manipular las restricciones? ¿Eliminar vistas por completo?

Honestamente, sin embargo, solo modificar la constante del contexto de la vista oculta requiere una sola línea de código con un cálculo simple. Recreando nuevas restricciones con constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant:parece tan pesado.

Editar febrero de 2018 : ver la respuesta de Ben con UIStackViews

Ryan Romanchuk
fuente
Gracias Ryan por esta pregunta. Me estaba volviendo loco qué hacer para los casos, como usted ha pedido. Cada vez que reviso el tutorial para autolayout, la mayoría de ellos dice que consulte el sitio de tutorial raywenderlich, que me resulta un poco difícil de entender.
Nassif

Respuestas:

82

UIStackViewEs probablemente el camino a seguir para iOS 9+. No solo maneja la vista oculta, también eliminará espacios y márgenes adicionales si se configura correctamente.

Ben Packard
fuente
8
Tenga cuidado al usar UIStackView en un UITableViewCell. Para mí, una vez que la vista se volvió muy complicada, el desplazamiento comenzó a volverse irregular, donde tartamudeaba cada vez que una celda se desplazaba hacia adentro / afuera. Debajo de las cubiertas, StackView está agregando / eliminando restricciones y esto no será necesariamente lo suficientemente eficiente para un desplazamiento suave.
Kento
77
Sería bueno dar un pequeño ejemplo de cómo la vista de pila maneja esto.
traducción
@Kento ¿Sigue siendo un problema en iOS 10?
Crashalot
3
Cinco años después ... ¿debo actualizar y configurar esto como la respuesta aceptada? Ha estado disponible durante tres ciclos de lanzamiento ahora.
Ryan Romanchuk
1
@RyanRomanchuk Deberías, esta es definitivamente la mejor respuesta ahora. Gracias ben!
Duncan Luk
234

Mi preferencia personal para mostrar / ocultar vistas es crear un IBOutlet con la restricción de ancho o altura adecuada.

Luego actualizo el constantvalor a0 ocultar, o lo que sea que el valor debería mostrar.

La gran ventaja de esta técnica es que se mantendrán las restricciones relativas. Por ejemplo, supongamos que tiene una vista A y una vista B con un espacio horizontal de x . Cuando el ancho de la vista A constantse establece en, 0.fentonces la vista B se moverá hacia la izquierda para llenar ese espacio.

No es necesario agregar o eliminar restricciones, lo cual es una operación pesada. Simplemente actualizar las restricciones constanthará el truco.

Max MacLeod
fuente
1
exactamente. Siempre y cuando, en este ejemplo, coloque la vista B a la derecha de la vista A utilizando un espacio horizontal. Uso esta técnica todo el tiempo y funciona muy bien
Max MacLeod
9
@MaxMacLeod Solo para asegurarnos: si el espacio entre las dos vistas es x, para que la vista B comience desde la posición de la vista A, también debemos cambiar la constante de restricción de espacio a 0, ¿verdad?
kernix
1
@kernix sí, si hubiera una restricción de espacio horizontal entre las dos vistas. constante 0, por supuesto, significa que no hay espacio. Entonces, ocultar la vista A al establecer su ancho en 0 movería la vista B hacia la izquierda para que quede al ras del contenedor. Por cierto, gracias por los votos!
Max MacLeod
2
@MaxMacLeod Gracias por su respuesta. Intenté hacer esto con una UIView que contiene otras vistas, pero las subvistas de esa vista no están ocultas cuando establezco su restricción de altura en 0. ¿Se supone que esta solución funciona con vistas que contienen subvistas? ¡Gracias!
shaunlim
66
También agradecería una solución que funcione para una UIView que contiene otras vistas ...
Mark Gibaud
96

La solución de usar una constante 0 cuando está oculta y otra constante si la muestra de nuevo es funcional, pero no es satisfactoria si su contenido tiene un tamaño flexible. Tendría que medir su contenido flexible y establecer una copia de seguridad constante. Esto se siente mal y tiene problemas si el contenido cambia de tamaño debido a eventos del servidor o de la IU.

Tengo una mejor solucion.

La idea es establecer una regla de altura 0 para que tenga una alta prioridad cuando ocultamos el elemento para que no ocupe espacio en la distribución automática.

Así es como lo haces:

1. configure un ancho (o alto) de 0 en el generador de interfaces con una prioridad baja.

establecer ancho 0 regla

Interface Builder no gritará sobre conflictos porque la prioridad es baja. Pruebe el comportamiento de altura estableciendo la prioridad en 999 temporalmente (1000 está prohibido mutar programáticamente, por lo que no lo usaremos). El generador de interfaces probablemente ahora gritará sobre restricciones conflictivas. Puede solucionarlos estableciendo prioridades en objetos relacionados a 900 más o menos.

2. Agregue una salida para que pueda modificar la prioridad de la restricción de ancho en el código:

conexión de salida

3. Ajuste la prioridad cuando oculte su elemento:

cell.alertTimingView.hidden    = place.closingSoon != true
cell.alertTimingWidth.priority = place.closingSoon == true ? 250 : 999
SimplGy
fuente
@SimplGy, ¿podría decirme qué es este lugar.closingSoon?
Niharika
No es parte de la solución general, @Niharika, es justo lo que era la regla de negocios en mi caso (muestre la vista si el restaurante está / no cerrará pronto).
SimplGy
11

En este caso, mapeo la altura de la etiqueta Autor a un IBOutlet apropiado:

@property (retain, nonatomic) IBOutlet NSLayoutConstraint* authorLabelHeight;

y cuando configuro la altura de la restricción en 0.0f, conservamos el "relleno", porque la altura del botón Reproducir lo permite.

cell.authorLabelHeight.constant = 0; //Hide 
cell.authorLabelHeight.constant = 44; //Show

ingrese la descripción de la imagen aquí

Mitul Marsoniya
fuente
9

Aquí hay muchas soluciones, pero mi enfoque habitual es diferente de nuevo :)

Establezca dos conjuntos de restricciones similares a la respuesta de Jorge Arimany y TMin:

ingrese la descripción de la imagen aquí

Las tres restricciones marcadas tienen el mismo valor para la constante. Las restricciones marcadas A1 y A2 tienen su prioridad establecida en 500, mientras que la restricción marcada B tiene su prioridad establecida en 250 (o UILayoutProperty.defaultLowen el código).

Conecte la restricción B a un IBOutlet. Luego, cuando oculta el elemento, solo necesita establecer la prioridad de restricción en alta (750):

constraintB.priority = .defaultHigh

Pero cuando el elemento es visible, vuelva a establecer la prioridad en baja (250):

constraintB.priority = .defaultLow

La ventaja (ciertamente menor) de este enfoque sobre el simple cambio isActivede la restricción B es que todavía tiene una restricción de trabajo si el elemento transitorio se elimina de la vista por otros medios.

SeanR
fuente
Esto tiene las ventajas de no tener valores duplicados en Storyboard y código como la altura / anchura del elemento. Muy elegante.
Robin Daugherty
¡¡Gracias!! Me ayudó
Parth Barot
Gran enfoque, gracias
Basil
5

Subclase la vista y anular func intrinsicContentSize() -> CGSize. Simplemente regrese CGSizeZerosi la vista está oculta.

Daniel
fuente
2
Esto es genial. Sin invalidateIntrinsicContentSizeembargo, algunos elementos de la interfaz de usuario necesitan un desencadenante . Puedes hacer eso de forma anulada setHidden.
DrMickeyLauer
5

Me acabo de enterar de que para que un UILabel no ocupe espacio, debes ocultarlo y configurar su texto en una cadena vacía. (iOS 9)

Conocer este hecho / error podría ayudar a algunas personas a simplificar sus diseños, posiblemente incluso el de la pregunta original, así que pensé que lo publicaría.

Matt Koala
fuente
1
No creo que necesites esconderlo. Establecer su texto en una cadena vacía debería ser suficiente.
Rob VS
4

Me sorprende que no haya un enfoque más elegante proporcionado por UIKit para este comportamiento deseado. Parece algo muy común querer poder hacer.

Dado que conectaba las restricciones IBOutletsy configuraba sus constantes para que se 0sintieran desagradables (y causaba NSLayoutConstraintadvertencias cuando su vista tenía subvistas), decidí crear una extensión que ofrezca un enfoque simple y detallado para ocultar / mostrar unUIView que tenga restricciones de Diseño automático

Simplemente oculta la vista y elimina las restricciones exteriores. Cuando vuelve a mostrar la vista, vuelve a agregar las restricciones. La única advertencia es que deberá especificar restricciones flexibles de conmutación por error para las vistas circundantes.

Editar Esta respuesta está dirigida a iOS 8.4 y a continuación. En iOS 9, solo usa el UIStackViewenfoque.

Albert Bori
fuente
1
Este es mi enfoque preferido también. Es una mejora significativa en otras respuestas aquí, ya que no solo reduce la vista a cero. El enfoque de squash tendrá problemas para cualquier cosa que no sean pagos bastante triviales. Un ejemplo de esto es la gran brecha donde está el elemento oculto en esta otra respuesta . Y puede terminar con violaciones de restricciones como se mencionó.
Benjohn
@DanielSchlaug Gracias por señalar eso. He actualizado la licencia a MIT. Sin embargo, requiero un máximo mental cinco cada vez que alguien implementa esta solución.
Albert Bori
4

La mejor práctica es que, una vez que todo tenga las restricciones de diseño correctas, agregue una altura o una restricción, según cómo desee que se muevan las vistas circundantes y conecte la restricción a una IBOutletpropiedad.

Asegúrese de que sus propiedades sean strong

en el código solo tiene que establecer la constante en 0 y activarla , para ocultar el contenido o desactivarlo para mostrar el contenido. Esto es mejor que perder el tiempo con el valor constante y restaurarlo. No olvides llamar layoutIfNeededdespués.

Si el contenido que se ocultará está agrupado, la mejor práctica es poner todo en una vista y agregar las restricciones a esa vista

@property (strong, nonatomic) IBOutlet UIView *myContainer;
@property (strong, nonatomic) IBOutlet NSLayoutConstraint *myContainerHeight; //should be strong!!

-(void) showContainer
{
    self.myContainerHeight.active = NO;
    self.myContainer.hidden = NO;
    [self.view layoutIfNeeded];
}
-(void) hideContainer
{
    self.myContainerHeight.active = YES;
    self.myContainerHeight.constant = 0.0f;
    self.myContainer.hidden = YES;
    [self.view layoutIfNeeded];
}

Una vez que tenga su configuración, puede probarla en IntefaceBuilder al establecer su restricción en 0 y luego volver al valor original. No olvide verificar otras prioridades de restricciones para que cuando esté oculto no haya ningún conflicto. Otra forma de probarlo es ponerlo a 0 y establecer la prioridad en 0, pero no debe olvidar restaurarlo a la prioridad más alta nuevamente.

Jorge Arimany
fuente
1
Esto se debe a que la restricción y la vista no siempre podrían ser retenidas por la jerarquía de vistas, por lo que si desactiva la restricción y oculta la vista, aún las conserva para mostrarlas nuevamente, independientemente de lo que decida retener su jerarquía de vistas o no.
Jorge Arimany
2

Mi método preferido es muy similar al sugerido por Jorge Arimany.

Prefiero crear múltiples restricciones. Primero cree sus restricciones para cuando la segunda etiqueta sea visible. Cree una salida para la restricción entre el botón y la segunda etiqueta (si está usando objc, asegúrese de que sea fuerte). Esta restricción determina la altura entre el botón y la segunda etiqueta cuando es visible.

Luego, cree otra restricción que especifique la altura entre el botón y la etiqueta superior cuando el segundo botón está oculto. Cree una salida para la segunda restricción y asegúrese de que esta salida tenga un puntero fuerte. Luego desmarque elinstalled casilla de verificación en el generador de interfaces y asegúrese de que la prioridad de la primera restricción sea inferior a la prioridad de esta segunda restricción.

Finalmente, cuando oculta la segunda etiqueta, cambie la .isActivepropiedad de estas restricciones y llamesetNeedsDisplay()

Y eso es todo, sin números mágicos, sin matemáticas, si tiene múltiples restricciones para activar y desactivar, incluso puede usar colecciones de salida para mantenerlas organizadas por estado. (AKA mantiene todas las restricciones ocultas en un OutletCollection y las no ocultas en otro y simplemente repite cada colección para alternar su estado .isActive).

Sé que Ryan Romanchuk dijo que no quería usar restricciones múltiples, pero siento que esto no es microgestión, y es más simple que crea vistas y restricciones dinámicamente mediante programación (que es lo que creo que quería evitar si yo Estoy leyendo la pregunta correcta).

He creado un ejemplo simple, espero que sea útil ...

import UIKit

class ViewController: UIViewController {

    @IBOutlet var ToBeHiddenLabel: UILabel!


    @IBOutlet var hiddenConstraint: NSLayoutConstraint!
    @IBOutlet var notHiddenConstraint: NSLayoutConstraint!


    @IBAction func HideMiddleButton(_ sender: Any) {

        ToBeHiddenLabel.isHidden = !ToBeHiddenLabel.isHidden
        notHiddenConstraint.isActive = !notHiddenConstraint.isActive
        hiddenConstraint.isActive = !hiddenConstraint.isActive

        self.view.setNeedsDisplay()
    }
}

ingrese la descripción de la imagen aquí

TMin
fuente
0

También proporcionaré mi solución, para ofrecer variedad.) Creo que crear una salida para el ancho / alto de cada elemento más el espacio es simplemente ridículo y explota el código, los posibles errores y la cantidad de complicaciones.

Mi método elimina todas las vistas (en mi caso, las instancias UIImageView), selecciona cuáles deben agregarse nuevamente y, en un bucle, agrega nuevamente cada una y crea nuevas restricciones. En realidad es muy simple, por favor siga adelante. Aquí está mi código rápido y sucio para hacerlo:

// remove all views
[self.twitterImageView removeFromSuperview];
[self.localQuestionImageView removeFromSuperview];

// self.recipients always has to be present
NSMutableArray *items;
items = [@[self.recipients] mutableCopy];

// optionally add the twitter image
if (self.question.sharedOnTwitter.boolValue) {
    [items addObject:self.twitterImageView];
}

// optionally add the location image
if (self.question.isLocal) {
    [items addObject:self.localQuestionImageView];
}

UIView *previousItem;
UIView *currentItem;

previousItem = items[0];
[self.contentView addSubview:previousItem];

// now loop through, add the items and the constraints
for (int i = 1; i < items.count; i++) {
    previousItem = items[i - 1];
    currentItem = items[i];

    [self.contentView addSubview:currentItem];

    [currentItem mas_remakeConstraints:^(MASConstraintMaker *make) {
        make.centerY.equalTo(previousItem.mas_centerY);
        make.right.equalTo(previousItem.mas_left).offset(-5);
    }];
}


// here I just connect the left-most UILabel to the last UIView in the list, whichever that was
previousItem = items.lastObject;

[self.userName mas_remakeConstraints:^(MASConstraintMaker *make) {
    make.right.equalTo(previousItem.mas_left);
    make.leading.equalTo(self.text.mas_leading);
    make.centerY.equalTo(self.attachmentIndicator.mas_centerY);;
}];

Obtengo un diseño y un espacio limpios y consistentes. Mi código usa Masonry, lo recomiendo: https://github.com/SnapKit/Masonry

Zoltán
fuente
0

Pruebe BoxView , hace que el diseño dinámico sea conciso y legible.
En tu caso es:

    boxView.optItems = [
        firstLabel.boxed.useIf(isFirstLabelShown),
        secondLabel.boxed.useIf(isSecondLabelShown),
        button.boxed
    ]
Vladimir
fuente