¿Cómo cambiar el tamaño de la supervista para que se ajuste a todas las subvistas con autolayout?

142

Entiendo que el autolayout es que toma el tamaño de la supervista y se basa en restricciones y tamaños intrínsecos, calcula las posiciones de las subvistas.

¿Hay alguna manera de revertir este proceso? Quiero cambiar el tamaño de la supervista sobre la base de restricciones y tamaños intrínsecos. ¿Cuál es la forma más sencilla de lograr esto?

Tengo una vista diseñada en Xcode que utilizo como encabezado UITableView. Esta vista incluye una etiqueta y un botón. El tamaño de la etiqueta difiere según los datos. Dependiendo de las restricciones, la etiqueta presiona con éxito el botón hacia abajo o si hay una restricción entre el botón y la parte inferior de la supervista, la etiqueta se comprime.

He encontrado algunas preguntas similares, pero no tienen respuestas buenas y fáciles.

DAK
fuente
28
Debe seleccionar una de las respuestas a continuación, ya que seguramente Tom Swifts respondió su pregunta. Todos los carteles pasaron una gran cantidad de tiempo para ayudarlo, ahora debe hacer su parte y seleccionar la respuesta que más le guste.
David H

Respuestas:

149

La API correcta para usar es UIView systemLayoutSizeFittingSize:, pasando cualquiera UILayoutFittingCompressedSizeo UILayoutFittingExpandedSize.

Para una UIViewdistribución automática normal, esto debería funcionar siempre que sus restricciones sean correctas. Si desea usarlo en un UITableViewCell(para determinar la altura de la fila, por ejemplo), debe llamarlo contra su celda contentViewy tomar la altura.

Existen otras consideraciones si tiene uno o más UILabel en su punto de vista que son multilínea. Para estos es imperitivo que la preferredMaxLayoutWidthpropiedad se establezca correctamente de modo que la etiqueta proporcione una correcta intrinsicContentSize, que se utilizará en el systemLayoutSizeFittingSize'scálculo.

EDITAR: por solicitud, agregando un ejemplo de cálculo de altura para una celda de vista de tabla

El uso de la distribución automática para el cálculo de la altura de la celda de la tabla no es súper eficiente, pero sin duda es conveniente, especialmente si tiene una celda que tiene un diseño complejo.

Como dije anteriormente, si está utilizando una línea múltiple UILabel, es imprescindible sincronizarlo preferredMaxLayoutWidthcon el ancho de la etiqueta. Yo uso una UILabelsubclase personalizada para hacer esto:

@implementation TSLabel

- (void) layoutSubviews
{
    [super layoutSubviews];

    if ( self.numberOfLines == 0 )
    {
        if ( self.preferredMaxLayoutWidth != self.frame.size.width )
        {
            self.preferredMaxLayoutWidth = self.frame.size.width;
            [self setNeedsUpdateConstraints];
        }
    }
}

- (CGSize) intrinsicContentSize
{
    CGSize s = [super intrinsicContentSize];

    if ( self.numberOfLines == 0 )
    {
        // found out that sometimes intrinsicContentSize is 1pt too short!
        s.height += 1;
    }

    return s;
}

@end

Aquí hay una subclase inventada de UITableViewController que demuestra heightForRowAtIndexPath:

#import "TSTableViewController.h"
#import "TSTableViewCell.h"

@implementation TSTableViewController

- (NSString*) cellText
{
    return @"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
}

#pragma mark - Table view data source

- (NSInteger) numberOfSectionsInTableView: (UITableView *) tableView
{
    return 1;
}

- (NSInteger) tableView: (UITableView *)tableView numberOfRowsInSection: (NSInteger) section
{
    return 1;
}

- (CGFloat) tableView: (UITableView *) tableView heightForRowAtIndexPath: (NSIndexPath *) indexPath
{
    static TSTableViewCell *sizingCell;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        sizingCell = (TSTableViewCell*)[tableView dequeueReusableCellWithIdentifier: @"TSTableViewCell"];
    });

    // configure the cell
    sizingCell.text = self.cellText;

    // force layout
    [sizingCell setNeedsLayout];
    [sizingCell layoutIfNeeded];

    // get the fitting size
    CGSize s = [sizingCell.contentView systemLayoutSizeFittingSize: UILayoutFittingCompressedSize];
    NSLog( @"fittingSize: %@", NSStringFromCGSize( s ));

    return s.height;
}

- (UITableViewCell *) tableView: (UITableView *) tableView cellForRowAtIndexPath: (NSIndexPath *) indexPath
{
    TSTableViewCell *cell = (TSTableViewCell*)[tableView dequeueReusableCellWithIdentifier: @"TSTableViewCell" ];

    cell.text = self.cellText;

    return cell;
}

@end

Una celda personalizada simple:

#import "TSTableViewCell.h"
#import "TSLabel.h"

@implementation TSTableViewCell
{
    IBOutlet TSLabel* _label;
}

- (void) setText: (NSString *) text
{
    _label.text = text;
}

@end

Y, aquí hay una imagen de las restricciones definidas en el Storyboard. Tenga en cuenta que no hay restricciones de alto / ancho en la etiqueta, que se infieren de las etiquetas intrinsicContentSize:

ingrese la descripción de la imagen aquí

TomSwift
fuente
1
¿Puede dar un ejemplo de cómo sería una implementación de heightForRowAtIndexPath: usar este método con una celda que contiene una etiqueta de varias líneas? Lo he jugado un poco y no he conseguido que funcione. ¿Cómo se obtiene una celda (especialmente si la celda está configurada en un guión gráfico)? ¿Qué limitaciones necesitas para que funcione?
rdelmar
@rdelmar - claro. He agregado un ejemplo a mi respuesta.
TomSwift
77
Esto no funcionó para mí hasta que agregué una restricción vertical final desde la parte inferior de la celda hasta la parte inferior de la subvista más baja de la celda. Parece que las restricciones verticales deben incluir el espacio vertical superior e inferior entre la celda y su contenido para que el cálculo de la altura de la celda tenga éxito.
Eric Baker
1
Solo necesitaba un truco más para que funcionara. Y el consejo de @EricBaker finalmente lo logró. Gracias por compartir ese hombre. Tengo un tamaño distinto de cero ahora, ya sea contra el sizingCello su contentView.
MkVal
1
Para aquellos que tienen problemas para no obtener la altura correcta, asegúrese de que su sizingCellancho coincida con su tableViewancho.
MkVal
30

El comentario de Eric Baker me indicó la idea central de que para que una vista tenga su tamaño determinado por el contenido colocado dentro de ella, entonces el contenido colocado dentro de ella debe tener una relación explícita con la vista que lo contiene para impulsar su altura (o ancho) dinámicamente . "Agregar subvista" no crea esta relación como podría suponer. Debe elegir qué subvista va a controlar la altura y / o el ancho del contenedor ... más comúnmente cualquier elemento de la interfaz de usuario que haya colocado en la esquina inferior derecha de su interfaz de usuario general. Aquí hay algunos códigos y comentarios en línea para ilustrar el punto.

Tenga en cuenta que esto puede ser de valor particular para aquellos que trabajan con vistas de desplazamiento, ya que es común diseñar alrededor de una sola vista de contenido que determina su tamaño (y lo comunica a la vista de desplazamiento) dinámicamente en función de lo que haya agregado. Buena suerte, espero que esto ayude a alguien por ahí.

//
//  ViewController.m
//  AutoLayoutDynamicVerticalContainerHeight
//

#import "ViewController.h"

@interface ViewController ()
@property (strong, nonatomic) UIView *contentView;
@property (strong, nonatomic) UILabel *myLabel;
@property (strong, nonatomic) UILabel *myOtherLabel;
@end

@implementation ViewController

- (void)viewDidLoad
{
    // INVOKE SUPER
    [super viewDidLoad];

    // INIT ALL REQUIRED UI ELEMENTS
    self.contentView = [[UIView alloc] init];
    self.myLabel = [[UILabel alloc] init];
    self.myOtherLabel = [[UILabel alloc] init];
    NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(_contentView, _myLabel, _myOtherLabel);

    // TURN AUTO LAYOUT ON FOR EACH ONE OF THEM
    self.contentView.translatesAutoresizingMaskIntoConstraints = NO;
    self.myLabel.translatesAutoresizingMaskIntoConstraints = NO;
    self.myOtherLabel.translatesAutoresizingMaskIntoConstraints = NO;

    // ESTABLISH VIEW HIERARCHY
    [self.view addSubview:self.contentView]; // View adds content view
    [self.contentView addSubview:self.myLabel]; // Content view adds my label (and all other UI... what's added here drives the container height (and width))
    [self.contentView addSubview:self.myOtherLabel];

    // LAYOUT

    // Layout CONTENT VIEW (Pinned to left, top. Note, it expects to get its vertical height (and horizontal width) dynamically based on whatever is placed within).
    // Note, if you don't want horizontal width to be driven by content, just pin left AND right to superview.
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[_contentView]" options:0 metrics:0 views:viewsDictionary]]; // Only pinned to left, no horizontal width yet
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_contentView]" options:0 metrics:0 views:viewsDictionary]]; // Only pinned to top, no vertical height yet

    /* WHATEVER WE ADD NEXT NEEDS TO EXPLICITLY "PUSH OUT ON" THE CONTAINING CONTENT VIEW SO THAT OUR CONTENT DYNAMICALLY DETERMINES THE SIZE OF THE CONTAINING VIEW */
    // ^To me this is what's weird... but okay once you understand...

    // Layout MY LABEL (Anchor to upper left with default margin, width and height are dynamic based on text, font, etc (i.e. UILabel has an intrinsicContentSize))
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_myLabel]" options:0 metrics:0 views:viewsDictionary]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[_myLabel]" options:0 metrics:0 views:viewsDictionary]];

    // Layout MY OTHER LABEL (Anchored by vertical space to the sibling label that comes before it)
    // Note, this is the view that we are choosing to use to drive the height (and width) of our container...

    // The LAST "|" character is KEY, it's what drives the WIDTH of contentView (red color)
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_myOtherLabel]-|" options:0 metrics:0 views:viewsDictionary]];

    // Again, the LAST "|" character is KEY, it's what drives the HEIGHT of contentView (red color)
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_myLabel]-[_myOtherLabel]-|" options:0 metrics:0 views:viewsDictionary]];

    // COLOR VIEWS
    self.view.backgroundColor = [UIColor purpleColor];
    self.contentView.backgroundColor = [UIColor redColor];
    self.myLabel.backgroundColor = [UIColor orangeColor];
    self.myOtherLabel.backgroundColor = [UIColor greenColor];

    // CONFIGURE VIEWS

    // Configure MY LABEL
    self.myLabel.text = @"HELLO WORLD\nLine 2\nLine 3, yo";
    self.myLabel.numberOfLines = 0; // Let it flow

    // Configure MY OTHER LABEL
    self.myOtherLabel.text = @"My OTHER label... This\nis the UI element I'm\narbitrarily choosing\nto drive the width and height\nof the container (the red view)";
    self.myOtherLabel.numberOfLines = 0;
    self.myOtherLabel.font = [UIFont systemFontOfSize:21];
}

@end

Cómo cambiar el tamaño de la supervista para que se ajuste a todas las subvistas con autolayout.png

John Erck
fuente
3
Este es un gran truco y no es muy conocido. Para repetir: si las vistas internas tienen una altura intrínseca y están fijadas en la parte superior e inferior, la vista externa no necesita especificar su altura y de hecho abarcará su contenido. Es posible que deba ajustar la compresión de contenido y los abrazos para que las vistas internas obtengan los resultados deseados.
phatmann
Esto es dinero! Uno de los mejores ejemplos concisos de VFL que he visto.
Evan R
Esto es lo que estaba buscando (Gracias @John), así que publiqué una versión rápida de esto aquí
James
1
"Debe elegir qué subvista va a controlar la altura y / o el ancho del contenedor ... más comúnmente cualquier elemento de la interfaz de usuario que haya colocado en la esquina inferior derecha de su interfaz de usuario general". Estoy usando PureLayout, Y esta fue la clave para mí. Para una vista principal, con una vista secundaria, era la vista secundaria para fijar en la parte inferior derecha lo que de repente le dio un tamaño a la vista principal. ¡Gracias!
Steven Elliott
1
Elegir una vista para manejar el ancho es exactamente lo que no puedo hacer. A veces una subvista es más ancha, a veces otra subvista es más ancha. ¿Alguna idea para ese caso?
Henning
3

Puede hacer esto creando una restricción y conectándola a través del generador de interfaces

Ver explicación: Auto_Layout_Constraints_in_Interface_Builder

raywenderlich begin-auto-layout

AutolayoutPG Artículos restricción Fundamentos

@interface ViewController : UIViewController {
    IBOutlet NSLayoutConstraint *leadingSpaceConstraint;
    IBOutlet NSLayoutConstraint *topSpaceConstraint;
}
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *leadingSpaceConstraint;

conecte esta salida de Restricción con su Restricción de subvista o conecte Super Restricción también y configúrela de acuerdo con sus requisitos de esta manera

 self.leadingSpaceConstraint.constant = 10.0;//whatever you want to assign

Espero que esto lo aclare.

chandan
fuente
Por lo tanto, es posible configurar restricciones creadas en XCode a partir del código fuente. Pero la pregunta es cómo configurarlos para cambiar el tamaño de la supervista.
DAK
SÍ @DAK. por favor asegúrese de diseño de supervista. Ponga la restricción en la supervista y haga también la restricción de altura, de modo que cada vez que su subvista aumente, aumente automáticamente la altura de la restricción de la supervista.
chandan
Esto también funcionó para mí. Ayuda que tenga celdas de tamaño fijo (altura de 60 px). Entonces, cuando se carga la vista, configuro mi restricción de altura IBOutlet'd en 60 * x donde x es el número de celdas. En última instancia, es probable que desee esto en una vista de desplazamiento para que pueda ver todo.
Sandy Chapman
-1

Esto se puede hacer para una normal subviewdentro de una más grande UIView, pero no funciona automáticamente headerViews. La altura de a headerViewestá determinada por lo que devuelve, por tableView:heightForHeaderInSection:lo que debe calcularla en heightfunción heightdel UILabelespacio más para el UIButtony cualquier paddingcosa que necesite. Necesitas hacer algo como esto:

-(CGFloat)tableView:(UITableView *)tableView 
          heightForHeaderInSection:(NSInteger)section {
    NSString *s = self.headeString[indexPath.section];
    CGSize size = [s sizeWithFont:[UIFont systemFontOfSize:17] 
                constrainedToSize:CGSizeMake(281, CGFLOAT_MAX)
                    lineBreakMode:NSLineBreakByWordWrapping];
    return size.height + 60;
}

Aquí headerStringestá la cadena que desea llenar UILabel, y el número 281 es el widthde UILabel(como se configuró en Interface Builder)

rdelmar
fuente
Lamentablemente esto no funciona. El "Tamaño para ajustar el contenido" en la supervista elimina la restricción que conecta la parte inferior del botón a la supervista como lo había predicho. En el tiempo de ejecución, la etiqueta cambia de tamaño y presiona el botón, pero la supervista no cambia automáticamente de tamaño.
DAK
@DAK, lo siento, el problema es que su vista es el encabezado de una vista de tabla. No entendí tu situación. Estaba pensando que la vista que encierra el botón y la etiqueta estaba dentro de otra vista, pero parece que la está usando como encabezado (en lugar de ser una vista dentro del encabezado). Entonces, mi respuesta original no funcionará. He cambiado mi respuesta a lo que creo que debería funcionar.
rdelmar
Esto es similar a mi implementación actual, pero esperaba que haya una mejor manera. Preferiría evitar actualizar este código cada vez que cambie mi encabezado.
DAK
@Dak, lo siento, no creo que haya una mejor manera, esa es la forma en que funcionan las vistas de tabla.
rdelmar el
Por favor mira mi respuesta. La "mejor manera" es usar el sistema UIViewLayoutSizeFittingSize :. Dicho esto, realizar una distribución automática completa en una vista o celda solo para obtener la altura requerida en una vista de tabla es bastante costoso. Lo haría para células complejas, pero tal vez recurra a una solución como la suya para casos más simples.
TomSwift