iOS AutoLayout multi-line UILabel


La siguiente pregunta es una especie de continuación de esta:

IOS: Etiqueta 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 el diseño automático pueda saber cómo mostrarlo correctamente. UILabel es solo un ejemplo de una situación en la que una vista no puede por sí misma saber qué tamaño necesita para mostrarse. Depende de qué anchura se proporciona.

Como mwhuss señaló, setPreferredMaxLayoutWidth hizo el truco de hacer que la etiqueta abarcara varias líneas. Pero esa no es la cuestión principal aquí. La pregunta es dónde y cuándo obtengo este valor width que envío como argumento a setPreferredMaxLayoutWidth .

Me las arreglé para hacer algo que se ve de fiar, así que corrígeme si me equivoco de alguna manera y dime por favor si sabes una mejor manera.

En el Uiview's

-(CGSize) intrinsicContentSize

I setPreferredMaxLayoutWidth para mis UILabels según self.marco.ancho.

UIViewController

-(void) viewDidLayoutSubviews

Es el primer método de devolución de llamada que sé donde subviews de la vista principal se nombran con sus marcos exactos que habitan en la pantalla. Desde el interior de ese método, entonces, opero en mis subviews, invalidando sus tamaños intrínsecos para que los UILabels se rompan en múltiples líneas basadas en el ancho que se les asignó.

Author: Community, 2013-07-05

9 answers

Parece molesto que una etiqueta UILabel no tenga por defecto su ancho para el ancho máximo de diseño preferido, si tiene restricciones que definen inequívocamente ese ancho para usted.

En casi todos los casos que he usado etiquetas bajo Diseño automático, 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.

Por lo tanto, para que esto suceda automáticamente, he utilizado una subclase UILabel, que anula setBounds:. Toma, llama a la super implementación, entonces, si no es el caso ya, establezca el ancho de diseño máximo preferido para que sea el ancho de tamaño de los límites.

El énfasis es importante: establecer el diseño máximo preferido causa que se realice otro pase de diseño, por lo que puede terminar con un bucle infinito.

 42
Author: jrturton,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-07-05 16:18:09

Hay una respuesta a esta pregunta en objc.io en la sección "Tamaño de Contenido Intrínseco de Texto Multilínea" de Advanced Auto Layout Toolbox. Aquí está la información relevante:

El tamaño de contenido intrínseco de UILabel y NSTextField es ambiguo para texto multilínea. 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 máximo de línea para calcular el tamaño de contenido intrínseco.

Dado que generalmente no conocemos este valor de antemano, necesitamos tomar un enfoque de dos pasos para hacerlo bien. Primero dejamos que el Diseño automático haga su trabajo, y luego usamos el marco resultante en el pase de diseño para actualizar el ancho máximo preferido y volver a activar el diseño.

El código que dan para su uso dentro de un controlador de vista:

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];
    myLabel.preferredMaxLayoutWidth = myLabel.frame.size.width;
    [self.view layoutIfNeeded];
}

Echa un vistazo a su post, hay más información sobre por qué es necesario hacer el diseño dos veces.

 56
Author: nevan king,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-01-27 08:55:52

Update

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 errores en iOS 7 e iOS 8. https://github.com/nicksnyder/ios-cell-layout

Respuesta original

Esta es una solución completa que funciona para mí en iOS 7 y iOS 8

Objetivo C

@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

Swift

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()
    }
}
 31
Author: Nick Snyder,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-17 15:27:29

Tuvimos una situación en la que un UILabel auto-layouted dentro de un UIScrollView se presentó bien en retrato, pero cuando se giró al paisaje, la altura del UILabel no se recalculó.

Encontramos que la respuesta de @jrturton arregló esto, presumiblemente porque ahora el preferredMaxLayoutWidth está establecido correctamente.

Aquí está el código que usamos. Simplemente configure la clase personalizada de Interface builder para que sea CVFixedWidthMultiLineLabel.

CVFixedWidthMultiLineLabel.h

@interface CVFixedWidthMultiLineLabel : UILabel

@end 

CVFixedWidthMultiLineLabel.m

@implementation CVFixedWidthMultiLineLabel

// Fix for layout failure for multi-line text from
// http://stackoverflow.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
 9
Author: Ben Clayton,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-05-16 13:21:59

Usando boundingRectWithSize

Resolví mi lucha con dos etiquetas multilínea en un legado UITableViewCell que estaba usando "\n " como un salto de línea midiendo 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 era tan simple como:

CGFloat labelPreferredWidth = [self preferredMaxLayoutWidthForLabel:textLabel];
if (labelPreferredWidth > 0.0f) {
    textLabel.preferredMaxLayoutWidth = labelPreferredWidth;
}
[textLabel layoutIfNeeded];
 2
Author: Cameron Lowell Palmer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-06-17 17:58:05

Como no se me permite 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 el preferredMaxLayoutWidth siempre estaba 0 en updateViewConstraints. Y, sin embargo, siempre tenía el valor deseado cuando se comprueba en setBounds:. No logré saber CUÁNDO se estableció el preferredMaxLayoutWidth correcto. Anulé setPreferredMaxLayoutWidth: en la subclase UILabel, pero nunca fue llamada.

Resumido, I:

  • ...UILabel sublcassed
  • ...y anular setBounds: a, si no está ya establecido , establezca preferredMaxLayoutWidth a CGRectGetWidth(bounds)
  • ...llame [super updateViewConstraints] antes de lo siguiente
  • ...llame a layoutIfNeeded antes de usar preferredMaxLayoutWidth en el cálculo del tamaño de la etiqueta

EDIT: Esta solución solo parece funcionar, o ser necesaria, a veces. Acabo de tener un problema (iOS 7/8) donde la altura de la etiqueta no se calculó correctamente, ya que preferredMaxLayoutWidth devolvió 0 después del diseño el proceso se había ejecutado una vez. Así que después de un poco de ensayo y error (y después de haber encontrado esta entrada de Blog ) cambié a usar UILabel nuevamente y solo configuré las restricciones de diseño automático superior, inferior, izquierda y derecha. Y por cualquier razón, la altura de la etiqueta se estableció correctamente después de actualizar el texto.

 2
Author: dergab,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-04-13 10:14:15

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 era visible en la interfaz de usuario y causó un "parpadeo visible", es decir, primero la etiqueta se renderizó con la altura de dos líneas, luego se volvió a renderizar con la altura de una sola línea.

Esto no era 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".

 1
Author: jbandi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-03-08 23:30:46

Una solución limpia es establecer rowcount = 0 y usar una propiedad para la restricción de altura de su etiqueta. Luego, después de establecer el contenido, 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.

 0
Author: netshark1000,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-11-26 23:20:24

En iOS 8, puede solucionar problemas de diseño de etiquetas multilínea en una celda llamando a cell.layoutIfNeeded() después de desquejar 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.

 0
Author: phatmann,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-11-05 17:01:42