iOS AutoLayout multi-line UILabel
(9)
La siguiente pregunta es una especie de continuación de esta:
iOS: UILabel multilínea en diseño automático
La idea principal es que cada vista debe indicar su tamaño "preferido" (intrínseco) para que AutoLayout pueda saber cómo mostrarlo correctamente. UILabel es solo un ejemplo de una situación en la que una vista no puede saber por sí misma qué tamaño necesita para visualizar. Depende de qué ancho se proporciona.
Como señaló mwhuss , setPreferredMaxLayoutWidth hizo el truco de hacer que la etiqueta abarque varias líneas. Pero esa no es la pregunta principal aquí. La pregunta es dónde y cuándo obtengo este valor de ancho que envío como argumento a setPreferredMaxLayoutWidth .
Logré hacer algo que parece legítimo, así que corrígeme si me equivoco de alguna manera y dime si sabes una mejor manera.
En el UIView''s
-(CGSize) intrinsicContentSize
Establecí PreferredMaxLayoutWidth para mis UILabels de acuerdo con self.frame.width.
UIViewController''s
-(void) viewDidLayoutSubviews
es el primer método de devolución de llamada que sé donde se asignan subvistas de la vista principal con sus marcos exactos que habitan en la pantalla. Desde dentro de ese método, yo, entonces, opero en mis subvistas, invalidando sus tamaños intrínsecos para que los UILabels se dividan en líneas múltiples en función del ancho que se les asignó.
Usar boundingRectWithSize
Resolví mi lucha con dos etiquetas multilínea en una UITableViewCell
heredada que estaba usando "/ n" como un salto de línea al medir el ancho deseado de esta manera:
- (CGFloat)preferredMaxLayoutWidthForLabel:(UILabel *)label
{
CGFloat preferredMaxLayoutWidth = 0.0f;
NSString *text = label.text;
UIFont *font = label.font;
if (font != nil) {
NSMutableParagraphStyle *mutableParagraphStyle = [[NSMutableParagraphStyle alloc] init];
mutableParagraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
NSDictionary *attributes = @{NSFontAttributeName: font,
NSParagraphStyleAttributeName: [mutableParagraphStyle copy]};
CGRect boundingRect = [text boundingRectWithSize:CGSizeZero options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:nil];
preferredMaxLayoutWidth = ceilf(boundingRect.size.width);
NSLog(@"Preferred max layout width for %@ is %0.0f", text, preferredMaxLayoutWidth);
}
return preferredMaxLayoutWidth;
}
Entonces, llamar al método fue tan simple como:
CGFloat labelPreferredWidth = [self preferredMaxLayoutWidthForLabel:textLabel];
if (labelPreferredWidth > 0.0f) {
textLabel.preferredMaxLayoutWidth = labelPreferredWidth;
}
[textLabel layoutIfNeeded];
Parece molesto que un UILabel no adopte de manera predeterminada su ancho para el ancho de diseño máximo preferido, si tiene restricciones que definan inequívocamente ese ancho para usted.
En casi todos los casos que utilicé etiquetas en Autolayout, el ancho máximo de diseño preferido ha sido el ancho real de la etiqueta, una vez que se ha realizado el resto de mi diseño.
Entonces, para que esto suceda automáticamente, he usado una subclase UILabel, que anula setBounds:
Aquí, llame a la implementación súper, entonces, si ya no es el caso , configure el ancho de diseño máximo preferido para que sea el ancho del tamaño de los límites.
El énfasis es importante: configurar el diseño máximo preferido hace que se realice otra pasada de diseño, por lo que puede terminar con un ciclo infinito.
Como lo sugirió otra respuesta, traté de anular viewDidLayoutSubviews
:
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
_subtitleLabel.preferredMaxLayoutWidth = self.view.bounds.size.width - 40;
[self.view layoutIfNeeded];
}
Esto funcionó, pero fue visible en la interfaz de usuario y causó un "parpadeo visible", es decir, primero la etiqueta se procesó con la altura de dos líneas, luego se volvió a renderizar con la altura de una sola línea.
Esto no fue aceptable para mí.
Encontré entonces una mejor solución anulando updateViewConstraints
:
-(void)updateViewConstraints {
[super updateViewConstraints];
// Multiline-Labels and Autolayout do not work well together:
// In landscape mode the width is still "portrait" when the label determines the count of lines
// Therefore the preferredMaxLayoutWidth must be set
_subtitleLabel.preferredMaxLayoutWidth = self.view.bounds.size.width - 40;
}
Esta fue la mejor solución para mí, porque no causó el "parpadeo visual".
Tuvimos una situación en la que UILabel con un trazado automático dentro de UIScrollView se diseñó bien en vertical, pero cuando se rotó a horizontal, la altura del UILabel no se volvió a calcular.
Descubrimos que la respuesta de @jrturton solucionó esto, presumiblemente porque ahora el preferredMaxLayoutWidth está configurado correctamente.
Aquí está el código que usamos. Simplemente configure la clase personalizada del constructor Interfaz para que sea CVFixedWidthMultiLineLabel .
CVFixedWidthMultiLineLabel.h
@interface CVFixedWidthMultiLineLabel : UILabel
@end
CVFixedWidthMultiLineLabel.m
@implementation CVFixedWidthMultiLineLabel
// Fix for layout failure for multi-line text from
// http://.com/questions/17491376/ios-autolayout-multi-line-uilabel
- (void) setBounds:(CGRect)bounds {
[super setBounds:bounds];
if (bounds.size.width != self.preferredMaxLayoutWidth) {
self.preferredMaxLayoutWidth = self.bounds.size.width;
}
}
@end
Una solución limpia es establecer rowcount = 0
y usar una propiedad para la restricción de altura de su etiqueta. Luego, una vez que el contenido está configurado, llame
CGSize sizeThatFitsLabel = [_subtitleLabel sizeThatFits:CGSizeMake(_subtitleLabel.frame.size.width, MAXFLOAT)];
_subtitleLabelHeightConstraint.constant = ceilf(sizeThatFitsLabel.height);
-(void) updateViewConstraints
tiene un problema desde iOS 7.1.
Hay una respuesta a esta pregunta en objc.io en la sección "Tamaño de contenido intrínseco de texto de varias líneas" de Advanced Auto Layout Toolbox . Aquí está la información relevante:
El tamaño del contenido intrínseco de UILabel y NSTextField es ambiguo para el texto de varias líneas. La altura del texto depende del ancho de las líneas, que aún no se ha determinado al resolver las restricciones. Para resolver este problema, ambas clases tienen una nueva propiedad llamada preferredMaxLayoutWidth, que especifica el ancho de línea máximo para calcular el tamaño de contenido intrínseco.
Dado que generalmente no conocemos este valor por adelantado, tenemos que tomar un enfoque de dos pasos para hacerlo bien. Primero permitimos que Auto Layout haga su trabajo, y luego usamos el marco resultante en el pase de diseño para actualizar el ancho máximo preferido y disparar el diseño nuevamente.
El código que dan para usar dentro de un controlador de vista:
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
myLabel.preferredMaxLayoutWidth = myLabel.frame.size.width;
[self.view layoutIfNeeded];
}
Eche un vistazo a su publicación, hay más información sobre por qué es necesario hacer el diseño dos veces.
Como no puedo agregar un comentario, estoy obligado a agregarlo como respuesta. La versión de jrturton solo funcionó para mí si llamo layoutIfNeeded
en updateViewConstraints
antes de obtener el preferredMaxLayoutWidth
de la etiqueta en cuestión. Sin la llamada a layoutIfNeeded
preferredMaxLayoutWidth
siempre era 0
en updateViewConstraints
. Y, sin embargo, siempre tenía el valor deseado cuando estaba marcado en setBounds:
No pude conocer CUÁNDO se configuró el preferredMaxLayoutWidth
. setPreferredMaxLayoutWidth:
en la subclase UILabel
, pero nunca se llamó.
Resumido, yo:
- ... sublubicado UILabel
- ... y anula
setBounds:
a, si no está ya establecido , establezcapreferredMaxLayoutWidth
enCGRectGetWidth(bounds)
- ... llame a
[super updateViewConstraints]
antes de lo siguiente - ...
layoutIfNeeded
antes de obtenerpreferredMaxLayoutWidth
para usar en el cálculo del tamaño de la etiqueta
EDITAR: Esta solución parece funcionar, o ser necesaria, a veces. Acabo de tener un problema (iOS 7/8) en el que la altura de la etiqueta no se calculó correctamente, ya que preferredMaxLayoutWidth
devolvió 0
después de que el proceso de diseño se hubiera ejecutado una vez. Así que después de un poco de prueba y error (y de haber encontrado esta entrada en el blog ) UILabel
a usar UILabel
nuevamente y simplemente establecí restricciones de diseño automático superior, inferior, izquierda y derecha. Y por cualquier motivo, la altura de la etiqueta se estableció correctamente después de actualizar el texto.
En iOS 8, puede corregir problemas de diseño de etiquetas de varias líneas en una celda al llamar a cell.layoutIfNeeded()
después de eliminar y configurar la celda. La llamada es inofensiva en iOS 9.
Ver la respuesta de Nick Snyder. Esta solución fue tomada de su código en https://github.com/nicksnyder/ios-cell-layout/blob/master/CellLayout/TableViewController.swift .
Actualizar
Mi respuesta original parece ser útil, así que la he dejado intacta a continuación, sin embargo, en mis propios proyectos he encontrado una solución más confiable que funciona alrededor de los errores en iOS 7 e iOS 8. https://github.com/nicksnyder/ios -diseño de la celda
Respuesta original
Esta es una solución completa que funciona para mí en iOS 7 e iOS 8
C objetivo
@implementation AutoLabel
- (void)setBounds:(CGRect)bounds {
if (bounds.size.width != self.bounds.size.width) {
[self setNeedsUpdateConstraints];
}
[super setBounds:bounds];
}
- (void)updateConstraints {
if (self.preferredMaxLayoutWidth != self.bounds.size.width) {
self.preferredMaxLayoutWidth = self.bounds.size.width;
}
[super updateConstraints];
}
@end
Rápido
import Foundation
class EPKAutoLabel: UILabel {
override var bounds: CGRect {
didSet {
if (bounds.size.width != oldValue.size.width) {
self.setNeedsUpdateConstraints();
}
}
}
override func updateConstraints() {
if(self.preferredMaxLayoutWidth != self.bounds.size.width) {
self.preferredMaxLayoutWidth = self.bounds.size.width
}
super.updateConstraints()
}
}