Uso del diseño automático en UITableView para diseños de celdas dinámicos y alturas de fila variables

1501

¿Cómo se usa el diseño automático dentro de UITableViewCells en una vista de tabla para permitir que el contenido y las subvistas de cada celda determinen la altura de la fila (en sí / automáticamente), mientras se mantiene un rendimiento de desplazamiento suave?

smileyborg
fuente
Aquí está el código de muestra en swift 2.3 github.com/dpakthakur/DynamicCellHeight
Deepak Thakur
Para comprender la mayoría de las respuestas con respecto a tener un UILabel multilínea, necesita una comprensión completa de cómo contentSizey cómo preferredMaxLayoutWidthfunciona. Ver aquí . Dicho esto, si configura sus restricciones correctamente, entonces no debería necesitar preferredMaxLayoutWidthy, de hecho, puede crear resultados inesperados.
Miel

Respuestas:

2389

TL; DR: ¿No te gusta leer? Vaya directamente a los proyectos de muestra en GitHub:

Descripción conceptual

Los primeros 2 pasos a continuación son aplicables independientemente de las versiones de iOS que esté desarrollando.

1. Configurar y agregar restricciones

En su UITableViewCellsubclase, agregue restricciones para que las subvistas de la celda tengan sus bordes fijados en los bordes de contentView de la celda (lo más importante en los bordes superior e inferior). NOTA: no ancle subvistas a la celda misma; solo a la celda contentView! Deje que el tamaño de contenido intrínseco de estas subvistas controle la altura de la vista de contenido de la celda de la vista de tabla asegurándose de que la resistencia de compresión de contenido y las restricciones de abrazo de contenido en la dimensión vertical para cada subvista no se anulen por las restricciones de mayor prioridad que ha agregado. ( ¿Eh? Haz clic aquí. )

Recuerde, la idea es tener las subvistas de la celda conectadas verticalmente a la vista de contenido de la celda para que puedan "ejercer presión" y hacer que la vista de contenido se expanda para adaptarse a ellas. Usando una celda de ejemplo con algunas subvistas, aquí hay una ilustración visual de cómo deberían verse algunas (¡no todas!) De sus restricciones:

Ejemplo de ilustración de restricciones en una celda de vista de tabla.

Puede imaginar que a medida que se agregue más texto a la etiqueta del cuerpo de varias líneas en la celda de ejemplo anterior, tendrá que crecer verticalmente para ajustarse al texto, lo que forzará efectivamente que la celda crezca en altura. (¡Por supuesto, debe obtener las restricciones correctas para que esto funcione correctamente!)

Definir correctamente sus restricciones es definitivamente la parte más difícil y más importante para obtener alturas de celda dinámicas trabajando con Auto Layout. Si comete un error aquí, podría evitar que todo lo demás funcione, ¡así que tómese su tiempo! Recomiendo configurar sus restricciones en el código porque sabe exactamente qué restricciones se agregan y dónde, y es mucho más fácil depurar cuando las cosas salen mal. Agregar restricciones en el código puede ser tan fácil y significativamente más poderoso que Interface Builder que utiliza anclas de diseño o una de las fantásticas API de código abierto disponibles en GitHub.

  • Si agrega restricciones en el código, debe hacerlo una vez desde el updateConstraintsmétodo de su subclase UITableViewCell. Tenga en cuenta que updateConstraintsse puede llamar más de una vez, por lo que para evitar agregar las mismas restricciones más de una vez, asegúrese de ajustar el código de adición de restricciones dentro updateConstraintsde un cheque para una propiedad booleana como didSetupConstraints(que establece en SÍ después de ejecutar su restricción -adición de código una vez). Por otro lado, si tiene un código que actualiza las restricciones existentes (como ajustar la constantpropiedad en algunas restricciones), colóquelo dentro updateConstraintspero fuera de la verificación para didSetupConstraintsque pueda ejecutarse cada vez que se llame al método.

2. Determine los identificadores únicos de reutilización de celdas de vista de tabla

Para cada conjunto único de restricciones en la celda, use un identificador de reutilización de celda único. En otras palabras, si sus celdas tienen más de un diseño único, cada diseño único debe recibir su propio identificador de reutilización. (Un buen indicio de que necesita usar un nuevo identificador de reutilización es cuando la variante de celda tiene un número diferente de subvistas, o las subvistas se organizan de manera distinta).

Por ejemplo, si estaba mostrando un mensaje de correo electrónico en cada celda, podría tener 4 diseños únicos: mensajes con solo un asunto, mensajes con un asunto y un cuerpo, mensajes con un asunto y un archivo adjunto de foto, y mensajes con un asunto, cuerpo y adjunto de foto. Cada diseño tiene restricciones completamente diferentes necesarias para lograrlo, por lo que una vez que se inicializa la celda y se agregan las restricciones para uno de estos tipos de celda, la celda debe obtener un identificador de reutilización único específico para ese tipo de celda. Esto significa que cuando retira una celda para su reutilización, las restricciones ya se han agregado y están listas para ese tipo de celda.

Tenga en cuenta que debido a las diferencias en el tamaño del contenido intrínseco, ¡las celdas con las mismas restricciones (tipo) aún pueden tener alturas variables! No confunda diseños fundamentalmente diferentes (diferentes restricciones) con diferentes marcos de vista calculados (resueltos a partir de restricciones idénticas) debido a diferentes tamaños de contenido.

  • No agregue celdas con conjuntos de restricciones completamente diferentes al mismo grupo de reutilización (es decir, use el mismo identificador de reutilización) y luego intente eliminar las restricciones antiguas y establecer nuevas restricciones desde cero después de cada cola. El motor de diseño automático interno no está diseñado para manejar cambios a gran escala en las restricciones, y verá problemas de rendimiento masivos.

Para iOS 8: celdas de tamaño automático

3. Habilite la estimación de altura de fila

Para habilitar las celdas de vista de tabla de tamaño automático, debe establecer la propiedad rowHeight de la vista de tabla en UITableViewAutomaticDimension. También debe asignar un valor a la propiedad estimadaRowHeight. Tan pronto como se establecen estas dos propiedades, el sistema usa el diseño automático para calcular la altura real de la fila

Apple: trabajar con celdas de vista de tabla de tamaño automático

Con iOS 8, Apple ha internalizado gran parte del trabajo que antes tenía que implementar antes de iOS 8. Para permitir que funcione el mecanismo de celda de tamaño automático, primero debe establecer la rowHeightpropiedad en la vista de tabla a la constante UITableViewAutomaticDimension. Luego, simplemente necesita habilitar la estimación de altura de fila configurando la estimatedRowHeightpropiedad de la vista de tabla en un valor distinto de cero, por ejemplo:

self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 44.0; // set to whatever your "average" cell height is

Lo que esto hace es proporcionar a la vista de tabla una estimación temporal / marcador de posición para las alturas de fila de las celdas que aún no están en pantalla. Luego, cuando estas celdas estén a punto de desplazarse en la pantalla, se calculará la altura real de la fila. Para determinar la altura real de cada fila, la vista de tabla pregunta automáticamente a cada celda qué altura contentViewdebe basarse en el ancho fijo conocido de la vista de contenido (que se basa en el ancho de la vista de tabla, menos cualquier elemento adicional como un índice de sección o vista de accesorios) y las restricciones de diseño automático que ha agregado a la vista de contenido y subvistas de la celda. Una vez que se ha determinado esta altura real de la celda, la altura estimada anterior para la fila se actualiza con la nueva altura real (y cualquier ajuste al contenido de la vista de tabla contentSize / contentOffset se realiza según sea necesario).

En términos generales, la estimación que proporciona no tiene que ser muy precisa: solo se usa para dimensionar correctamente el indicador de desplazamiento en la vista de tabla, y la vista de tabla hace un buen trabajo al ajustar el indicador de desplazamiento para estimaciones incorrectas como usted Desplazar celdas en pantalla. Debe establecer la estimatedRowHeightpropiedad en la vista de tabla (en viewDidLoado similar) a un valor constante que sea el alto de fila "promedio". Solo si las alturas de sus filas tienen una variabilidad extrema (por ejemplo, difieren en un orden de magnitud) y nota que el indicador de desplazamiento "salta" a medida que se desplaza, debe molestarse en implementar tableView:estimatedHeightForRowAtIndexPath:para hacer el cálculo mínimo requerido para devolver una estimación más precisa para cada fila.

Para soporte de iOS 7 (implementando el dimensionamiento automático de la celda usted mismo)

3. Haga un pase de diseño y obtenga la altura de la celda

Primero, cree una instancia fuera de pantalla de una celda de vista de tabla, una instancia para cada identificador de reutilización , que se usa estrictamente para los cálculos de altura. (Fuera de la tableView:cellForRowAtIndexPath:pantalla, lo que significa que la referencia de celda se almacena en una propiedad / ivar en el controlador de vista y nunca se devuelve para que la vista de tabla se muestre en la pantalla). A continuación, la celda se debe configurar con el contenido exacto (por ejemplo, texto, imágenes, etc.) que se mantendría si se mostrara en la vista de tabla.

Luego, fuerce a la celda a diseñar inmediatamente sus subvistas, y luego use el systemLayoutSizeFittingSize:método en la UITableViewCell's contentViewpara averiguar cuál es la altura requerida de la celda. Úselo UILayoutFittingCompressedSizepara obtener el tamaño más pequeño requerido para adaptarse a todos los contenidos de la celda. La altura puede ser devuelta desde el tableView:heightForRowAtIndexPath:método delegado.

4. Use las alturas de fila estimadas

Si la vista de la tabla tiene más de un par de docenas de filas, descubrirá que al hacer la resolución de restricciones de Diseño automático puede atascarse rápidamente el subproceso principal cuando se carga por primera vez la vista de la tabla, como tableView:heightForRowAtIndexPath:se llama en cada fila en la primera carga ( para calcular el tamaño del indicador de desplazamiento).

A partir de iOS 7, puede (y absolutamente debe) usar la estimatedRowHeightpropiedad en la vista de tabla. Lo que esto hace es proporcionar a la vista de tabla una estimación temporal / marcador de posición para las alturas de fila de las celdas que aún no están en pantalla. Luego, cuando estas celdas están a punto de desplazarse en la pantalla, se calculará la altura real de la fila (llamando tableView:heightForRowAtIndexPath:) y la altura estimada se actualizará con la real.

En términos generales, la estimación que proporciona no tiene que ser muy precisa: solo se usa para dimensionar correctamente el indicador de desplazamiento en la vista de tabla, y la vista de tabla hace un buen trabajo al ajustar el indicador de desplazamiento para estimaciones incorrectas como usted Desplazar celdas en pantalla. Debe establecer la estimatedRowHeightpropiedad en la vista de tabla (en viewDidLoado similar) a un valor constante que sea el alto de fila "promedio". Solo si las alturas de sus filas tienen una variabilidad extrema (por ejemplo, difieren en un orden de magnitud) y nota que el indicador de desplazamiento "salta" a medida que se desplaza, debe molestarse en implementar tableView:estimatedHeightForRowAtIndexPath:para hacer el cálculo mínimo requerido para devolver una estimación más precisa para cada fila.

5. (Si es necesario) Agregue el almacenamiento en caché de altura de fila

Si ha hecho todo lo anterior y todavía encuentra que el rendimiento es inaceptablemente lento cuando se resuelve la restricción tableView:heightForRowAtIndexPath:, desafortunadamente deberá implementar un almacenamiento en caché para las alturas de las celdas. (Este es el enfoque sugerido por los ingenieros de Apple.) La idea general es dejar que el motor de Autolayout resuelva las restricciones la primera vez, luego almacene en caché la altura calculada para esa celda y use el valor en caché para todas las solicitudes futuras para la altura de esa celda. El truco, por supuesto, es asegurarse de borrar la altura en caché de una celda cuando ocurra algo que pueda hacer que cambie la altura de la celda; principalmente, esto será cuando cambie el contenido de esa celda o cuando ocurran otros eventos importantes (como el ajuste del usuario el control deslizante de tamaño de texto de tipo dinámico).

Código de muestra genérico de iOS 7 (con muchos comentarios jugosos)

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Determine which reuse identifier should be used for the cell at this 
    // index path, depending on the particular layout required (you may have
    // just one, or may have many).
    NSString *reuseIdentifier = ...;

    // Dequeue a cell for the reuse identifier.
    // Note that this method will init and return a new cell if there isn't
    // one available in the reuse pool, so either way after this line of 
    // code you will have a cell with the correct constraints ready to go.
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier];

    // Configure the cell with content for the given indexPath, for example:
    // cell.textLabel.text = someTextForThisCell;
    // ...

    // Make sure the constraints have been set up for this cell, since it 
    // may have just been created from scratch. Use the following lines, 
    // assuming you are setting up constraints from within the cell's 
    // updateConstraints method:
    [cell setNeedsUpdateConstraints];
    [cell updateConstraintsIfNeeded];

    // If you are using multi-line UILabels, don't forget that the 
    // preferredMaxLayoutWidth needs to be set correctly. Do it at this 
    // point if you are NOT doing it within the UITableViewCell subclass 
    // -[layoutSubviews] method. For example: 
    // cell.multiLineLabel.preferredMaxLayoutWidth = CGRectGetWidth(tableView.bounds);

    return cell;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Determine which reuse identifier should be used for the cell at this 
    // index path.
    NSString *reuseIdentifier = ...;

    // Use a dictionary of offscreen cells to get a cell for the reuse 
    // identifier, creating a cell and storing it in the dictionary if one 
    // hasn't already been added for the reuse identifier. WARNING: Don't 
    // call the table view's dequeueReusableCellWithIdentifier: method here 
    // because this will result in a memory leak as the cell is created but 
    // never returned from the tableView:cellForRowAtIndexPath: method!
    UITableViewCell *cell = [self.offscreenCells objectForKey:reuseIdentifier];
    if (!cell) {
        cell = [[YourTableViewCellClass alloc] init];
        [self.offscreenCells setObject:cell forKey:reuseIdentifier];
    }

    // Configure the cell with content for the given indexPath, for example:
    // cell.textLabel.text = someTextForThisCell;
    // ...

    // Make sure the constraints have been set up for this cell, since it 
    // may have just been created from scratch. Use the following lines, 
    // assuming you are setting up constraints from within the cell's 
    // updateConstraints method:
    [cell setNeedsUpdateConstraints];
    [cell updateConstraintsIfNeeded];

    // Set the width of the cell to match the width of the table view. This
    // is important so that we'll get the correct cell height for different
    // table view widths if the cell's height depends on its width (due to 
    // multi-line UILabels word wrapping, etc). We don't need to do this 
    // above in -[tableView:cellForRowAtIndexPath] because it happens 
    // automatically when the cell is used in the table view. Also note, 
    // the final width of the cell may not be the width of the table view in
    // some cases, for example when a section index is displayed along 
    // the right side of the table view. You must account for the reduced 
    // cell width.
    cell.bounds = CGRectMake(0.0, 0.0, CGRectGetWidth(tableView.bounds), CGRectGetHeight(cell.bounds));

    // Do the layout pass on the cell, which will calculate the frames for 
    // all the views based on the constraints. (Note that you must set the 
    // preferredMaxLayoutWidth on multiline UILabels inside the 
    // -[layoutSubviews] method of the UITableViewCell subclass, or do it 
    // manually at this point before the below 2 lines!)
    [cell setNeedsLayout];
    [cell layoutIfNeeded];

    // Get the actual height required for the cell's contentView
    CGFloat height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;

    // Add an extra point to the height to account for the cell separator, 
    // which is added between the bottom of the cell's contentView and the 
    // bottom of the table view cell.
    height += 1.0;

    return height;
}

// NOTE: Set the table view's estimatedRowHeight property instead of 
// implementing the below method, UNLESS you have extreme variability in 
// your row heights and you notice the scroll indicator "jumping" 
// as you scroll.
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Do the minimal calculations required to be able to return an 
    // estimated row height that's within an order of magnitude of the 
    // actual height. For example:
    if ([self isTallCellAtIndexPath:indexPath]) {
        return 350.0;
    } else {
        return 40.0;
    }
}

Proyectos de muestra

Estos proyectos son ejemplos totalmente funcionales de vistas de tabla con alturas de fila variables debido a las celdas de vista de tabla que contienen contenido dinámico en UILabels.

Xamarin (C # / .NET)

Si está utilizando Xamarin, consulte este proyecto de muestra elaborado por @KentBoogaart .

smileyborg
fuente
1
Si bien esto funciona bien, considero que se estima un poco los tamaños requeridos (posiblemente debido a varios problemas de redondeo) y tengo que agregar un par de puntos a la altura final para que todo mi texto encaje dentro de las etiquetas
DBD
55
@ Alex311 Muy interesante, gracias por proporcionar este ejemplo. Hice una pequeña prueba de mi parte y escribí algunos comentarios aquí: github.com/Alex311/TableCellWithAutoLayout/commit/…
smileyborg
3
Recomiendo encarecidamente el almacenamiento en caché de la celda para cada tipo de identificador de reutilización de celda. Cada vez que baja la altura, está sacando una celda de la cola que no se está agregando. El almacenamiento en caché puede reducir significativamente la cantidad de veces que se llama al inicializador para las celdas de la tabla. Por alguna razón, cuando no estaba almacenando en caché, la cantidad de memoria utilizada seguía creciendo a medida que me desplazaba.
Alex311
1
Storyboards, en realidad. No creo que funcione para las células prototipo (al menos sin volver a crear una instancia de todo el VC). Podría ser posible evitar la fuga por completo al quitar heightForRowAtIndexPath, mantener la celda y devolverla la próxima vez que cellForRowAtIndexPathse llame.
nschum
2
¿ iOS8Todavía se recomienda la implementación iOS9y iOS10, o hay nuevos enfoques desde que se publicó esta respuesta?
koen
175

Para iOS 8 anterior es realmente simple:

override func viewDidLoad() {  
    super.viewDidLoad()

    self.tableView.estimatedRowHeight = 80
    self.tableView.rowHeight = UITableView.automaticDimension
}

o

func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    return UITableView.automaticDimension
}

Pero para iOS 7, la clave es calcular la altura después de la distribución automática:

func calculateHeightForConfiguredSizingCell(cell: GSTableViewCell) -> CGFloat {
    cell.setNeedsLayout()
    cell.layoutIfNeeded()
    let height = cell.contentView.systemLayoutSizeFittingSize(UILayoutFittingExpandedSize).height + 1.0
    return height
}

Importante

  • Si hay etiquetas de varias líneas, no olvides configurarlo numberOfLinesen 0.

  • No olvides label.preferredMaxLayoutWidth = CGRectGetWidth(tableView.bounds)

El código de ejemplo completo está aquí .

William Hu
fuente
77
Creo que no necesitamos implementar la función heightForRowAtIndexPath en el caso de
OS8
Como @eddwinpaz señala en otra respuesta, es importante que las restricciones utilizadas para bloquear la altura de la fila NO incluyan margen.
Richard
1
+1 para "Si las etiquetas de varias líneas, no olvides establecer el número de líneas en 0", esto hacía que mis celdas no tuvieran un tamaño dinámico.
Ian-Fogelman
Llámame fanático del control, pero incluso en los días de iOS 11 todavía no me gusta usar UITableViewAutomaticDimension. Tuve algunas malas experiencias en el pasado. Como tal, normalmente uso las soluciones de iOS7 enumeradas aquí. La nota de William de no olvidar label.preferredMaxLayoutWidth aquí me salvó.
Brian Sachetta
Cuando nos desplazamos, la etiqueta comienza a aparecer en varias líneas y la altura de la fila tampoco aumenta
Karanveer Singh
94

Ejemplo rápido de una altura variable UITableViewCell

Actualizado para Swift 3

La respuesta rápida de William Hu es buena, pero me ayuda a tener algunos pasos simples pero detallados al aprender a hacer algo por primera vez. El siguiente ejemplo es mi proyecto de prueba mientras aprendía a hacer una UITableViewaltura de celda variable. Lo basé en este ejemplo básico UITableView para Swift .

El proyecto terminado debería verse así:

ingrese la descripción de la imagen aquí

Crea un nuevo proyecto

Puede ser solo una aplicación de vista única.

Agrega el código

Agregue un nuevo archivo Swift a su proyecto. Llámalo MyCustomCell. Esta clase mantendrá los puntos de venta de las vistas que agregue a su celda en el guión gráfico. En este ejemplo básico solo tendremos una etiqueta en cada celda.

import UIKit
class MyCustomCell: UITableViewCell {
    @IBOutlet weak var myCellLabel: UILabel!
}

Conectaremos esta salida más tarde.

Abra ViewController.swift y asegúrese de tener el siguiente contenido:

import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    // These strings will be the data for the table view cells
    let animals: [String] = [
        "Ten horses:  horse horse horse horse horse horse horse horse horse horse ",
        "Three cows:  cow, cow, cow",
        "One camel:  camel",
        "Ninety-nine sheep:  sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep baaaa sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep",
        "Thirty goats:  goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat "]

    // Don't forget to enter this in IB also
    let cellReuseIdentifier = "cell"

    @IBOutlet var tableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // delegate and data source
        tableView.delegate = self
        tableView.dataSource = self

        // Along with auto layout, these are the keys for enabling variable cell height
        tableView.estimatedRowHeight = 44.0
        tableView.rowHeight = UITableViewAutomaticDimension
    }

    // number of rows in table view
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.animals.count
    }

    // create a cell for each table view row
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let cell:MyCustomCell = self.tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier) as! MyCustomCell
        cell.myCellLabel.text = self.animals[indexPath.row]
        return cell
    }

    // method to run when table view cell is tapped
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print("You tapped cell number \(indexPath.row).")
    }
}

Nota IMPORTANTE:

  • Son las siguientes dos líneas de código (junto con el diseño automático) las que hacen posible la altura de celda variable:

    tableView.estimatedRowHeight = 44.0
    tableView.rowHeight = UITableViewAutomaticDimension
    

Configurar el guión gráfico

Agregue una vista de tabla a su controlador de vista y use el diseño automático para fijarlo a los cuatro lados. Luego arrastre una celda de vista de tabla a la vista de tabla. Y en la celda Prototipo, arrastre una Etiqueta. Use el diseño automático para anclar la etiqueta a los cuatro bordes de la vista de contenido de la Celda de vista de tabla.

ingrese la descripción de la imagen aquí

Nota IMPORTANTE:

  • El diseño automático funciona junto con las dos importantes líneas de código que mencioné anteriormente. Si no usa el diseño automático, no funcionará.

Otras configuraciones de IB

Nombre de clase personalizado e identificador

Seleccione la celda de vista de tabla y configure la clase personalizada como MyCustomCell(el nombre de la clase en el archivo Swift que agregamos). También establezca el Identificador para que sea cell(la misma cadena que usamos para el cellReuseIdentifieren el código anterior.

ingrese la descripción de la imagen aquí

Líneas cero para etiqueta

Establezca el número de líneas 0en su etiqueta. Esto significa varias líneas y permite que la etiqueta se redimensione en función de su contenido.

ingrese la descripción de la imagen aquí

Conecta los enchufes

  • Controle el arrastre desde la Vista de tabla en el guión gráfico a la tableViewvariable en el ViewControllercódigo.
  • Haga lo mismo para la Etiqueta en su celda Prototipo con la myCellLabelvariable en la MyCustomCellclase.

Terminado

Debería poder ejecutar su proyecto ahora y obtener celdas con alturas variables.

Notas

  • Este ejemplo solo funciona para iOS 8 y posteriores. Si todavía necesita admitir iOS 7, esto no funcionará para usted.
  • Sus propias celdas personalizadas en sus proyectos futuros probablemente tendrán más de una etiqueta. Asegúrate de tener todo bien clavado para que el diseño automático pueda determinar la altura correcta para usar. También es posible que deba usar resistencia a la compresión vertical y abrazos. Consulte este artículo para obtener más información al respecto.
  • Si no está fijando los bordes iniciales y finales (izquierdo y derecho), también puede necesitar configurar las etiquetas preferredMaxLayoutWidthpara que sepa cuándo ajustar la línea. Por ejemplo, si ha agregado una restricción Centro horizontalmente a la etiqueta en el proyecto anterior en lugar de fijar los bordes inicial y final, entonces necesitaría agregar esta línea al tableView:cellForRowAtIndexPathmétodo:

     cell.myCellLabel.preferredMaxLayoutWidth = tableView.bounds.width

Ver también

Suragch
fuente
¿No es realmente mejor establecer la preferredMaxLayoutWidthcontra la celda contentSize? De esa manera, si tiene un accesorioView o se deslizó para editar, ¿aún se lo tendrá en cuenta?
Miel
@Honey, es muy posible que tengas razón. No me he mantenido en iOS desde hace aproximadamente un año y estoy demasiado oxidado para responderle bien en este momento.
Suragch
65

Envolví la solución iOS7 de @ smileyborg en una categoría

Decidí incluir esta solución inteligente de @smileyborg en una UICollectionViewCell+AutoLayoutDynamicHeightCalculationcategoría.

La categoría también rectifica los problemas descritos en la respuesta de @ wildmonkey (cargar una celda desde una punta y systemLayoutSizeFittingSize:regresar CGRectZero)

No tiene en cuenta ningún almacenamiento en caché, pero se adapta a mis necesidades en este momento. Siéntase libre de copiar, pegar y hackearlo.

UICollectionViewCell + AutoLayoutDynamicHeightCalculation.h

#import <UIKit/UIKit.h>

typedef void (^UICollectionViewCellAutoLayoutRenderBlock)(void);

/**
 *  A category on UICollectionViewCell to aid calculating dynamic heights based on AutoLayout contraints.
 *
 *  Many thanks to @smileyborg and @wildmonkey
 *
 *  @see stackoverflow.com/questions/18746929/using-auto-layout-in-uitableview-for-dynamic-cell-layouts-variable-row-heights
 */
@interface UICollectionViewCell (AutoLayoutDynamicHeightCalculation)

/**
 *  Grab an instance of the receiving type to use in order to calculate AutoLayout contraint driven dynamic height. The method pulls the cell from a nib file and moves any Interface Builder defined contrainsts to the content view.
 *
 *  @param name Name of the nib file.
 *
 *  @return collection view cell for using to calculate content based height
 */
+ (instancetype)heightCalculationCellFromNibWithName:(NSString *)name;

/**
 *  Returns the height of the receiver after rendering with your model data and applying an AutoLayout pass
 *
 *  @param block Render the model data to your UI elements in this block
 *
 *  @return Calculated constraint derived height
 */
- (CGFloat)heightAfterAutoLayoutPassAndRenderingWithBlock:(UICollectionViewCellAutoLayoutRenderBlock)block collectionViewWidth:(CGFloat)width;

/**
 *  Directly calls `heightAfterAutoLayoutPassAndRenderingWithBlock:collectionViewWidth` assuming a collection view width spanning the [UIScreen mainScreen] bounds
 */
- (CGFloat)heightAfterAutoLayoutPassAndRenderingWithBlock:(UICollectionViewCellAutoLayoutRenderBlock)block;

@end

UICollectionViewCell + AutoLayoutDynamicHeightCalculation.m

#import "UICollectionViewCell+AutoLayout.h"

@implementation UICollectionViewCell (AutoLayout)

#pragma mark Dummy Cell Generator

+ (instancetype)heightCalculationCellFromNibWithName:(NSString *)name
{
    UICollectionViewCell *heightCalculationCell = [[[NSBundle mainBundle] loadNibNamed:name owner:self options:nil] lastObject];
    [heightCalculationCell moveInterfaceBuilderLayoutConstraintsToContentView];
    return heightCalculationCell;
}

#pragma mark Moving Constraints

- (void)moveInterfaceBuilderLayoutConstraintsToContentView
{
    [self.constraints enumerateObjectsUsingBlock:^(NSLayoutConstraint *constraint, NSUInteger idx, BOOL *stop) {
        [self removeConstraint:constraint];
        id firstItem = constraint.firstItem == self ? self.contentView : constraint.firstItem;
        id secondItem = constraint.secondItem == self ? self.contentView : constraint.secondItem;
        [self.contentView addConstraint:[NSLayoutConstraint constraintWithItem:firstItem
                                                                     attribute:constraint.firstAttribute
                                                                     relatedBy:constraint.relation
                                                                        toItem:secondItem
                                                                     attribute:constraint.secondAttribute
                                                                    multiplier:constraint.multiplier
                                                                      constant:constraint.constant]];
    }];
}

#pragma mark Height

- (CGFloat)heightAfterAutoLayoutPassAndRenderingWithBlock:(UICollectionViewCellAutoLayoutRenderBlock)block
{
    return [self heightAfterAutoLayoutPassAndRenderingWithBlock:block
                                            collectionViewWidth:CGRectGetWidth([[UIScreen mainScreen] bounds])];
}

- (CGFloat)heightAfterAutoLayoutPassAndRenderingWithBlock:(UICollectionViewCellAutoLayoutRenderBlock)block collectionViewWidth:(CGFloat)width
{
    NSParameterAssert(block);

    block();

    [self setNeedsUpdateConstraints];
    [self updateConstraintsIfNeeded];

    self.bounds = CGRectMake(0.0f, 0.0f, width, CGRectGetHeight(self.bounds));

    [self setNeedsLayout];
    [self layoutIfNeeded];

    CGSize calculatedSize = [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];

    return calculatedSize.height;

}

@end

Ejemplo de uso:

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
    MYSweetCell *cell = [MYSweetCell heightCalculationCellFromNibWithName:NSStringFromClass([MYSweetCell class])];
    CGFloat height = [cell heightAfterAutoLayoutPassAndRenderingWithBlock:^{
        [(id<MYSweetCellRenderProtocol>)cell renderWithModel:someModel];
    }];
    return CGSizeMake(CGRectGetWidth(self.collectionView.bounds), height);
}

Afortunadamente no tendremos que hacer este jazz en iOS8, ¡pero ahí está por ahora!

Adam Waite
fuente
2
Simplemente deberías poder simplemente usar a: [YourCell new]y usar eso como el muñeco. Siempre que el código de construcción del código de restricción se active en su instancia, y active un pase de diseño mediante programación, debería estar listo.
Adam Waite
1
¡Gracias! Esto funciona. Tu categoría es genial. Es lo que me hizo darme cuenta de que esta técnica también funciona UICollectionViews.
Ricardo Sanchez-Saez
2
¿Cómo haría esto usando una celda prototipo definida en un guión gráfico?
David Potter
57

Aquí está mi solución:

Usted necesita decirle a la TableViewdel estimatedHeightantes de cargar la vista. De lo contrario, no podrá comportarse como se esperaba.

C objetivo

- (void)viewWillAppear:(BOOL)animated {
    _messageField.delegate = self;
    _tableView.estimatedRowHeight = 65.0;
    _tableView.rowHeight = UITableViewAutomaticDimension;
}

Actualización a Swift 4.2

override func viewWillAppear(_ animated: Bool) {
    tableView.rowHeight = UITableView.automaticDimension
    tableView.estimatedRowHeight = 65.0
}
Eddwin Paz
fuente
2
Configurar el autolayout correctamente, junto con este código que se agregó en viewDidLoad hizo el truco.
Bruce
1
pero ¿y si estimatedRowHeightvaría fila por fila? ¿Debo sobre o subestimar? uso mínimo o máximo de la altura que uso tableView?
János
1
@ János este es el punto de rowHeight. Para que esto se comporte como se espera, debe usar restricciones sin márgenes y alinear con TableViewCell los objetos. y supongo que está utilizando un UITextView, por lo que aún necesita eliminar autoscroll = false; de ​​lo contrario, mantendrá la altura y la altura relativa no actuará como se esperaba.
Eddwin Paz
1
Esa es, con mucho, la solución más sólida. No se preocupe estimatedRowHeight, afecta principalmente el tamaño de la barra de desplazamiento, nunca la altura de las celdas reales. Sea audaz en la altura que elija: afectará la animación de inserción / eliminación.
SwiftArchitect
Aquí está el código de muestra en swift 2.3 github.com/dpakthakur/DynamicCellHeight
Deepak Thakur
47

La solución propuesta por @smileyborg es casi perfecta. Si tiene una celda personalizada y desea una o más UILabelcon alturas dinámicas, entonces el método systemLayoutSizeFittingSize combinado con AutoLayout habilitado devuelve a CGSizeZeromenos que mueva todas las restricciones de la celda de la celda a su contentView (como lo sugiere @TomSwift aquí Cómo cambiar el tamaño de la supervista a ¿se ajusta a todas las subvistas con autolayout? ).

Para hacerlo, debe insertar el siguiente código en su implementación personalizada UITableViewCell (gracias a @Adrian).

- (void)awakeFromNib{
    [super awakeFromNib];
    for (NSLayoutConstraint *cellConstraint in self.constraints) {
        [self removeConstraint:cellConstraint];
        id firstItem = cellConstraint.firstItem == self ? self.contentView : cellConstraint.firstItem;
        id seccondItem = cellConstraint.secondItem == self ? self.contentView : cellConstraint.secondItem;
        NSLayoutConstraint *contentViewConstraint =
        [NSLayoutConstraint constraintWithItem:firstItem
                                 attribute:cellConstraint.firstAttribute
                                 relatedBy:cellConstraint.relation
                                    toItem:seccondItem
                                 attribute:cellConstraint.secondAttribute
                                multiplier:cellConstraint.multiplier
                                  constant:cellConstraint.constant];
        [self.contentView addConstraint:contentViewConstraint];
    }
}

Mezclar la respuesta de @smileyborg con esto debería funcionar.

wildmonkey
fuente
systemLayoutSizeFittingSizenecesita ser llamado en contentView, no celular
onmyway133
25

Un problema bastante importante que acabo de encontrar para publicar como respuesta.

La respuesta de @ smileyborg es mayormente correcta. Sin embargo, si tiene algún código en el layoutSubviewsmétodo de su clase de celda personalizada, por ejemplo, configurando preferredMaxLayoutWidth, entonces no se ejecutará con este código:

[cell.contentView setNeedsLayout];
[cell.contentView layoutIfNeeded];

Me confundió por un tiempo. Entonces me di cuenta de que es porque esos solo están activando las Subvistas de diseño en contentViewla celda, no en la misma.

Mi código de trabajo se ve así:

TCAnswerDetailAppSummaryCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"TCAnswerDetailAppSummaryCell"];
[cell configureWithThirdPartyObject:self.app];
[cell layoutIfNeeded];
CGFloat height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
return height;

Tenga en cuenta que si está creando una nueva celda, estoy bastante seguro de que no necesita llamar setNeedsLayoutya que ya debería estar configurada. En los casos en que guarde una referencia a una celda, probablemente debería llamarla. De cualquier manera, no debería doler nada.

Otro consejo si está utilizando subclases de celda donde está configurando cosas como preferredMaxLayoutWidth. Como @smileyborg menciona, "la celda de la vista de la tabla aún no tiene su ancho fijado al ancho de la vista de la tabla". Esto es cierto, y es un problema si está haciendo su trabajo en su subclase y no en el controlador de vista. Sin embargo, simplemente puede establecer el marco de la celda en este punto utilizando el ancho de la tabla:

Por ejemplo en el cálculo de altura:

self.summaryCell = [self.tableView dequeueReusableCellWithIdentifier:@"TCAnswerDetailDefaultSummaryCell"];
CGRect oldFrame = self.summaryCell.frame;
self.summaryCell.frame = CGRectMake(oldFrame.origin.x, oldFrame.origin.y, self.tableView.frame.size.width, oldFrame.size.height);

(Resulta que guardo en caché esta celda particular para su reutilización, pero eso es irrelevante).

Bob Spryn
fuente
19

Siempre que su diseño en su celda sea bueno.

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [self tableView:tableView cellForRowAtIndexPath:indexPath];

    return [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
}

Actualización: debe usar el cambio de tamaño dinámico introducido en iOS 8.

Chris Van Buskirk
fuente
Esto funciona para mí en iOS7, es ahora bien llamar tableView:cellForRowAtIndexPath:en el tableView:heightForRowAtIndexPath:momento?
Hormigas
Ok, entonces esto no funciona, pero cuando llamo y guardo systemLayoutSizeFittingSize:en tableView:cellForRowAtIndexPath:caché el resultado y luego lo uso tableView:heightForRowAtIndexPath:, funciona bien siempre y cuando las restricciones estén configuradas correctamente, por supuesto.
Hormigas
Esto solo funciona si usa dequeueReusableCellWithIdentifier: en lugar de dequeueReusableCellWithIdentifier: forIndexPath:
Antoine
1
Realmente no creo que llamar a tableView: cellForRowAtIndexPath: directamente sea una buena manera.
Itachi
19

(para Xcode 8.x / Xcode 9.x leído en la parte inferior)

Tenga cuidado con el siguiente problema en Xcode 7.x, que puede ser una fuente de confusión:

Interface Builder no maneja la configuración de celda de dimensionamiento automático correctamente. Incluso si sus restricciones son absolutamente válidas, IB seguirá quejándose y le dará sugerencias y errores confusos. La razón es que IB no está dispuesto a cambiar la altura de la fila según lo dicten sus restricciones (de modo que la celda se ajuste a su contenido). En cambio, mantiene fija la altura de la fila y comienza a sugerirle que cambie sus restricciones, lo que debe ignorar .

Por ejemplo, imagine que ha configurado todo bien, sin advertencias, sin errores, todo funciona.

ingrese la descripción de la imagen aquí

Ahora, si cambia el tamaño de fuente (en este ejemplo, estoy cambiando el tamaño de fuente de la etiqueta de descripción de 17.0 a 18.0).

ingrese la descripción de la imagen aquí

Debido a que el tamaño de la fuente aumentó, la etiqueta ahora quiere ocupar 3 filas (antes de eso ocupaba 2 filas).

Si Interface Builder funcionara como se esperaba, cambiaría el tamaño de la altura de la celda para acomodar la nueva altura de la etiqueta. Sin embargo, lo que realmente sucede es que IB muestra el ícono rojo de error de diseño automático y sugiere que modifique las prioridades de abrazo / compresión.

ingrese la descripción de la imagen aquí

Debe ignorar estas advertencias. En cambio, lo que puede * hacer es cambiar manualmente la altura de la fila (seleccione Celda> Inspector de tamaño> Altura de fila).

ingrese la descripción de la imagen aquí

¡Estaba cambiando esta altura un clic a la vez (usando el paso hacia arriba / abajo) hasta que desaparecen los errores de la flecha roja! (en realidad, recibirá advertencias amarillas, en cuyo punto simplemente continúe y haga 'marcos de actualización', todo debería funcionar).

* Tenga en cuenta que en realidad no tiene que resolver estos errores rojos o advertencias amarillas en Interface Builder: en tiempo de ejecución, todo funcionará correctamente (incluso si IB muestra errores / advertencias). Solo asegúrese de que en el tiempo de ejecución en el registro de la consola no obtenga ningún error de AutoLayout.

De hecho, tratar de actualizar siempre la altura de la fila en IB es súper molesto y, a veces, casi imposible (debido a los valores fraccionarios).

Para evitar las molestas advertencias / errores de IB, puede seleccionar las vistas involucradas y elegir Size Inspectorla propiedadAmbiguityVerify Position Only

ingrese la descripción de la imagen aquí


Xcode 8.x / Xcode 9.x parece (a veces) estar haciendo las cosas de manera diferente a Xcode 7.x, pero aún de manera incorrecta. Por ejemplo, incluso cuando compression resistance priority/ hugging priorityse establece en requerido (1000), Interface Builder podría estirar o recortar una etiqueta para que se ajuste a la celda (en lugar de cambiar el tamaño de la altura de la celda para que se ajuste a la etiqueta). Y en tal caso, es posible que ni siquiera muestre advertencias o errores de AutoLayout. O a veces hace exactamente lo que hizo Xcode 7.x, descrito anteriormente.

Nikolay Suvandzhiev
fuente
Hola, ¿es posible dar altura dinámica para la celda, que tiene vista de tabla con celda con contenido dinámico de celda?
Jignesh B
18

Para establecer la dimensión automática para la altura de la fila y la altura estimada de la fila, asegúrese de seguir los siguientes pasos para que la dimensión automática sea efectiva para el diseño de altura de celda / fila.

  • Asignar e implementar datos de vista de tabla Fuente y delegar
  • Asignar UITableViewAutomaticDimensiona rowHeight & estimadoRowHeight
  • Implementar métodos delegate / dataSource (es decir, heightForRowAty devolverle un valor UITableViewAutomaticDimension)

-

C objetivo:

// in ViewController.h
#import <UIKit/UIKit.h>

@interface ViewController : UIViewController <UITableViewDelegate, UITableViewDataSource>

  @property IBOutlet UITableView * table;

@end

// in ViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];
    self.table.dataSource = self;
    self.table.delegate = self;

    self.table.rowHeight = UITableViewAutomaticDimension;
    self.table.estimatedRowHeight = UITableViewAutomaticDimension;
}

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {

    return UITableViewAutomaticDimension;
}

Rápido:

@IBOutlet weak var table: UITableView!

override func viewDidLoad() {
    super.viewDidLoad()

    // Don't forget to set dataSource and delegate for table
    table.dataSource = self
    table.delegate = self

    // Set automatic dimensions for row height
    // Swift 4.2 onwards
    table.rowHeight = UITableView.automaticDimension
    table.estimatedRowHeight = UITableView.automaticDimension


    // Swift 4.1 and below
    table.rowHeight = UITableViewAutomaticDimension
    table.estimatedRowHeight = UITableViewAutomaticDimension

}



// UITableViewAutomaticDimension calculates height of label contents/text
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    // Swift 4.2 onwards
    return UITableView.automaticDimension

    // Swift 4.1 and below
    return UITableViewAutomaticDimension
}

Para instancia de etiqueta en UITableviewCell

  • Establecer número de líneas = 0 (& modo de salto de línea = truncar cola)
  • Establezca todas las restricciones (arriba, abajo, derecha izquierda) con respecto a su contenedor de supervista / celda.
  • Opcional : establezca la altura mínima para la etiqueta, si desea un área vertical mínima cubierta por la etiqueta, incluso si no hay datos.

ingrese la descripción de la imagen aquí

Nota : Si tiene más de una etiqueta (UIElements) con longitud dinámica, que debe ajustarse de acuerdo con su tamaño de contenido: Ajuste 'Prioridad de contenido y resistencia a la compresión' para las etiquetas que desea expandir / comprimir con mayor prioridad.

Krunal
fuente
1
Gracias parece una solución clara y simple, pero tengo un problema, no estoy usando etiquetas, sino vistas de texto, así que necesito que la altura de la fila aumente a medida que se agregan datos. Mi problema es pasar información a heightForRowAt. Puedo medir la altura de mis vistas de texto cambiantes pero ahora necesito cambiar la altura de la fila. Agradecería un poco de ayuda por favor
Jeremy Andrews
@JeremyAndrews Seguro te ayudará. Plantea tu pregunta con el código fuente que has probado y detalla el problema.
Krunal
Me está funcionando sin implementar la tableView: heightForRowfuente de datos.
Hemang
@Hemang: desde iOS 11+ funciona sin él tableView: heightForRow. (para iOS 10- tableView: heightForRowse requiere)
Krunal
1
@Hemang: la solución depende de la definición exacta de la consulta. Aquí tengo una solución común general para su consulta. Ponga la condición dentro tableView: heightForRow..if (indexPath.row == 0) { return 100} else { return UITableView.automaticDimension }
Krunal
16

Al igual que @ Bob-Spryn, me encontré con un problema bastante importante que estoy publicando como respuesta.

Luché con @ smileyborg de respuesta por un tiempo. El Gotcha que me encontré es que si usted ha definido su celular prototipo en IB con elementos adicionales ( UILabels, UIButtons, etc.) en IB cuando se instancia la celda con [ [YourTableViewCellClass alloc] init]no va a crear una instancia de todos los otros elementos dentro de esa célula a menos que haya código escrito para hacer eso. (Tuve una experiencia similar con initWithStyle).

Para que el guión gráfico cree una instancia de todos los elementos adicionales con los que obtiene su celda [tableView dequeueReusableCellWithIdentifier:@"DoseNeeded"](No, [tableView dequeueReusableCellWithIdentifier:forIndexPath:]ya que esto causará problemas interesantes). Cuando haga esto, se instanciarán todos los elementos que definió en IB.

nickb
fuente
15

Vista de tabla dinámica Altura de celda y diseño automático

Una buena manera de resolver el problema con el diseño automático del guión gráfico:

- (CGFloat)heightForImageCellAtIndexPath:(NSIndexPath *)indexPath {
  static RWImageCell *sizingCell = nil;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    sizingCell = [self.tableView dequeueReusableCellWithIdentifier:RWImageCellIdentifier];
  });

  [sizingCell setNeedsLayout];
  [sizingCell layoutIfNeeded];

  CGSize size = [sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
  return size.height;
}
Mohnasm
fuente
1
Esto ya se ha cubierto ampliamente en la respuesta aceptada a esta pregunta.
smileyborg
2
Sí, lo sé ... pero no quería usar PureLayout y el 'truco' de dispatch_once me ayudó mucho, resolverlo solo usando el Storyboard.
Mohnasm
13
tableView.estimatedRowHeight = 343.0
tableView.rowHeight = UITableViewAutomaticDimension

ingrese la descripción de la imagen aquí

Abo3atef
fuente
13

Otra "solución": omita toda esta frustración y use un UIScrollView en su lugar para obtener un resultado que se vea y se sienta idéntico a UITableView.

Esa fue la "solución" dolorosa para mí, después de haber pasado literalmente más de 20 horas muy frustrantes en total tratando de construir algo como lo que smileyborg sugirió y fallando durante muchos meses y tres versiones de lanzamientos de App Store.

Mi opinión es que si realmente necesita soporte para iOS 7 (para nosotros, es esencial), entonces la tecnología es demasiado frágil y lo intentará. Y ese UITableView es completamente excesivo en general, a menos que esté utilizando algunas de las funciones avanzadas de edición de filas y / o realmente necesite admitir más de 1000 "filas" (en nuestra aplicación, en realidad nunca es más de 20 filas).

La ventaja adicional es que el código se vuelve increíblemente simple en comparación con toda la basura delegada y viceversa que viene con UITableView. Es solo un bucle de código en viewOnLoad que se ve elegante y es fácil de administrar.

Aquí hay algunos consejos sobre cómo hacerlo:

  1. Usando Storyboard o un archivo plumín, cree un ViewController y una vista raíz asociada.

  2. Arrastre sobre UIScrollView a su vista raíz.

  3. Agregue restricciones arriba, abajo, izquierda y derecha a la vista de nivel superior para que UIScrollView llene toda la vista raíz.

  4. Agregue un UIView dentro del UIScrollView y llámelo "contenedor". Agregue restricciones superiores, inferiores, izquierda y derecha a UIScrollView (su elemento primario). TRUCO CLAVE: Agregue también restricciones de "Anchos iguales" para vincular UIScrollView y UIView.

    NOTA: Recibirá un error "la vista de desplazamiento tiene una altura de contenido desplazable ambigua" y que su UIView de contenedor debe tener una altura de 0 píxeles. Ninguno de los errores parece importar cuando la aplicación se está ejecutando.

  5. Cree archivos de plumín y controladores para cada una de sus "celdas". Use UIView no UITableViewCell.

  6. En su ViewController raíz, esencialmente agrega todas las "filas" al contenedor UIView y agrega restricciones mediante programación que vinculan sus bordes izquierdo y derecho a la vista del contenedor, sus bordes superiores a la vista superior del contenedor (para el primer elemento) o al anterior célula. Luego, vincule la celda final con el fondo del contenedor.

Para nosotros, cada "fila" está en un archivo plumín. Entonces el código se ve así:

class YourRootViewController {

    @IBOutlet var container: UIView! //container mentioned in step 4

    override func viewDidLoad() {

        super.viewDidLoad()

        var lastView: UIView?
        for data in yourDataSource {

            var cell = YourCellController(nibName: "YourCellNibName", bundle: nil)
            UITools.addViewToTop(container, child: cell.view, sibling: lastView)
            lastView = cell.view
            //Insert code here to populate your cell
        }

        if(lastView != nil) {
            container.addConstraint(NSLayoutConstraint(
                item: lastView!,
                attribute: NSLayoutAttribute.Bottom,
                relatedBy: NSLayoutRelation.Equal,
                toItem: container,
                attribute: NSLayoutAttribute.Bottom,
                multiplier: 1,
                constant: 0))
        }

        ///Add a refresh control, if you want - it seems to work fine in our app:
        var refreshControl = UIRefreshControl()
        container.addSubview(refreshControl!)
    }
}

Y aquí está el código para UITools.addViewToTop:

class UITools {
    ///Add child to container, full width of the container and directly under sibling (or container if sibling nil):
    class func addViewToTop(container: UIView, child: UIView, sibling: UIView? = nil)
    {
        child.setTranslatesAutoresizingMaskIntoConstraints(false)
        container.addSubview(child)

        //Set left and right constraints so fills full horz width:

        container.addConstraint(NSLayoutConstraint(
            item: child,
            attribute: NSLayoutAttribute.Leading,
            relatedBy: NSLayoutRelation.Equal,
            toItem: container,
            attribute: NSLayoutAttribute.Left,
            multiplier: 1,
            constant: 0))

        container.addConstraint(NSLayoutConstraint(
            item: child,
            attribute: NSLayoutAttribute.Trailing,
            relatedBy: NSLayoutRelation.Equal,
            toItem: container,
            attribute: NSLayoutAttribute.Right,
            multiplier: 1,
            constant: 0))

        //Set vertical position from last item (or for first, from the superview):
        container.addConstraint(NSLayoutConstraint(
            item: child,
            attribute: NSLayoutAttribute.Top,
            relatedBy: NSLayoutRelation.Equal,
            toItem: sibling == nil ? container : sibling,
            attribute: sibling == nil ? NSLayoutAttribute.Top : NSLayoutAttribute.Bottom,
            multiplier: 1,
            constant: 0))
    }
}

El único "problema" que he encontrado con este enfoque hasta ahora es que UITableView tiene una buena característica de encabezados de sección "flotantes" en la parte superior de la vista a medida que se desplaza. La solución anterior no hará eso a menos que agregue más programación, pero para nuestro caso particular, esta característica no era 100% esencial y nadie se dio cuenta cuando desapareció.

Si desea divisores entre sus celdas, simplemente agregue una vista UIV de 1 píxel de alto en la parte inferior de su "celda" personalizada que se parece a un divisor.

Asegúrese de activar "rebotes" y "rebotar verticalmente" para que funcione el control de actualización y que parezca más una vista de tabla.

TableView muestra algunas filas y divisores vacíos debajo de su contenido, si no llena la pantalla completa donde esta solución no lo hace. Pero personalmente, prefiero que las filas vacías no estuvieran allí de todos modos, con una altura de celda variable siempre me pareció "defectuoso" tener las filas vacías allí.

Espero que algún otro programador lea mi publicación ANTES de perder más de 20 horas tratando de resolverlo con Table View en su propia aplicación. :)

alpsystems.com
fuente
11

Tuve que usar vistas dinámicas (vistas de configuración y restricciones por código) y cuando quise establecer el ancho de la etiqueta PreferredMaxLayoutWidth fue 0. Así que tengo una altura de celda incorrecta.

Entonces agregué

[cell layoutSubviews];

antes de ejecutar

[cell setNeedsUpdateConstraints];
[cell updateConstraintsIfNeeded];

Después de que el ancho de la etiqueta era el esperado y la altura dinámica se calculaba correctamente.

Mansurov Ruslan
fuente
9

Supongamos que tiene una celda con una subvista y desea que la altura de la celda sea lo suficientemente alta como para abarcar la subvista + relleno.

1) Establezca la restricción inferior de la subvista igual a cell.contentView menos el relleno que desea. No establezca restricciones en la celda o cell.contentView en sí.

2) Establezca la rowHeightpropiedad tableView o tableView:heightForRowAtIndexPath:en UITableViewAutomaticDimension.

3) Establezca la estimatedRowHeightpropiedad de tableView o tableView:estimatedHeightForRowAtIndexPath:adivine mejor la altura.

Eso es.

Vadoff
fuente
7

Si realiza el diseño mediante programación, esto es lo que debe tener en cuenta para iOS 10 utilizando anclajes en Swift.

Hay tres reglas / pasos

NÚMERO 1: establezca estas dos propiedades de tableview en viewDidLoad, la primera le dice a la tableview que debe esperar tamaños dinámicos en sus celdas, la segunda es solo para permitir que la aplicación calcule el tamaño del indicador de la barra de desplazamiento, por lo que ayuda para actuación.

    tableView.rowHeight = UITableViewAutomaticDimension
    tableView.estimatedRowHeight = 100

NÚMERO 2: es importante que necesite agregar las subvistas al contentView de la celda, no a la vista, y también usar su guía de diseño de diseños para anclar las subvistas en la parte superior e inferior, este es un ejemplo práctico de cómo hacerlo.

override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)
    setUpViews()
}

private func setUpViews() {

    contentView.addSubview(movieImageView)
    contentView.addSubview(descriptionLabel)
    let marginGuide = contentView.layoutMarginsGuide

    NSLayoutConstraint.activate([
        movieImageView.heightAnchor.constraint(equalToConstant: 80),
        movieImageView.widthAnchor.constraint(equalToConstant: 80),
        movieImageView.leftAnchor.constraint(equalTo: marginGuide.leftAnchor),
        movieImageView.topAnchor.constraint(equalTo: marginGuide.topAnchor, constant: 20),

        descriptionLabel.leftAnchor.constraint(equalTo: movieImageView.rightAnchor, constant: 15),
        descriptionLabel.rightAnchor.constraint(equalTo: marginGuide.rightAnchor),
        descriptionLabel.bottomAnchor.constraint(equalTo: marginGuide.bottomAnchor, constant: -15),
        descriptionLabel.topAnchor.constraint(equalTo: movieImageView.topAnchor)

        ])
}

Cree un método que agregue las subvistas y realice el diseño, llámelo en el método init.

NÚMERO 3: NO LLAME AL MÉTODO:

  override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    }

Si lo hace, anulará su implementación.

Siga estas 3 reglas para celdas dinámicas en vistas de tabla.

Aquí hay una implementación de trabajo https://github.com/jamesrochabrun/MinimalViewController

James Rochabrun
fuente
en Swift 5 UITableViewAutomaticDimension renombrado a UITableView.automaticDimension
James Rochabrun
4

Si tienes una cuerda larga . por ejemplo, uno que no tiene un salto de línea. Entonces usted podría encontrarse con algunos problemas.

La solución "supuesta" se menciona en la respuesta aceptada y en algunas otras respuestas. Solo necesitas agregar

cell.myCellLabel.preferredMaxLayoutWidth = tableView.bounds.width

Encuentro la respuesta de Suragh la más completa y concisa , por lo tanto, no es confusa.

Aunque no explican por qué son necesarios estos cambios. Vamos a hacer eso.

Suelta el siguiente código en un proyecto.

import UIKit

class ViewController: UIViewController {

    lazy var label : UILabel = {
        let lbl = UILabel()
        lbl.translatesAutoresizingMaskIntoConstraints = false
        lbl.backgroundColor = .red
        lbl.textColor = .black
        return lbl
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        // step0: (0.0, 0.0)
        print("empty Text intrinsicContentSize: \(label.intrinsicContentSize)")
        // ----------
        // step1: (29.0, 20.5)
        label.text = "hiiiii"
        print("hiiiii intrinsicContentSize: \(label.intrinsicContentSize)")
        // ----------
        // step2: (328.0, 20.5)
        label.text = "translatesAutoresizingMaskIntoConstraints"
        print("1 translate intrinsicContentSize: \(label.intrinsicContentSize)")
        // ----------
        // step3: (992.0, 20.5)
        label.text = "translatesAutoresizingMaskIntoConstraints translatesAutoresizingMaskIntoConstraints translatesAutoresizingMaskIntoConstraints"
        print("3 translate intrinsicContentSize: \(label.intrinsicContentSize)")
        // ----------
        // step4: (328.0, 20.5)
        label.text = "translatesAutoresizingMaskIntoConstraints\ntranslatesAutoresizingMaskIntoConstraints\ntranslatesAutoresizingMaskIntoConstraints"
        print("3 translate w/ line breaks (but the line breaks get ignored, because numberOfLines is defaulted to `1` and it will force it all to fit into one line! intrinsicContentSize: \(label.intrinsicContentSize)")
        // ----------
        // step5: (328.0, 61.0)
        label.numberOfLines = 0
        print("3 translate w/ line breaks and '0' numberOfLines intrinsicContentSize: \(label.intrinsicContentSize)")
        // ----------
        // step6: (98.5, 243.5)
        label.preferredMaxLayoutWidth = 100
        print("3 translate w/ line breaks | '0' numberOfLines | preferredMaxLayoutWidth: 100 intrinsicContentSize: \(label.intrinsicContentSize)")

        setupLayout()
    }
    func setupLayout(){
        view.addSubview(label)
        label.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        label.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
    }
}

Tenga en cuenta que no he agregado ninguna restricción de tamaño . Solo he agregado restricciones centerX, centerY. Pero aún así la etiqueta tendrá el tamaño correcto ¿Por qué?

Debido a contentSize.

Para procesar esto mejor, primero mantenga el paso 0, luego comente los pasos 1-6. Dejate setupLayout()quedar. Observa el comportamiento.

Luego, descomente el paso 1 y observe.

Luego, descomente el paso 2 y observe.

Haga esto hasta que haya descomentado los 6 pasos y haya observado sus comportamientos.

¿Qué puede concluir de todo esto? ¿Qué factores pueden cambiar el contenSize?

  1. Longitud del texto: si tiene un texto más largo, el ancho de su tamaño de contenido intrínseco aumentará
  2. Saltos de línea: si agrega, \nentonces el ancho de intrinsicContentSize será el ancho máximo de todas las líneas. Si una línea tiene 25 caracteres, otra tiene 2 caracteres y otra tiene 21 caracteres, su ancho se calculará en función de los 25 caracteres
  3. Número de líneas permitidas: debe establecer el valor numberOfLinespara 0que no tenga varias líneas. Su numberOfLinesajustará de su intrinsicContentSize altura
  4. Cómo hacer ajustes: imagine que en función de su texto, el ancho 200y el alto de su contenido intrínseco eran altos 100, pero desea limitar el ancho al contenedor de la etiqueta, ¿qué va a hacer? La solución es establecerlo en el ancho deseado. Para ello, establezca preferredMaxLayoutWidthque 130su nuevo intrinsicContentSize tendrá un ancho aproximado 130. La altura obviamente sería más que 100porque necesitarías más líneas. Dicho esto, si sus restricciones están configuradas correctamente, ¡no necesitará usar esto en absoluto! Para más información al respecto, vea esta respuesta y sus comentarios. Solo necesita usarlo preferredMaxLayoutWidthsi no tiene restricciones que restrinjan el ancho / alto, como en uno podría decir "no ajuste el texto a menos que exceda elpreferredMaxLayoutWidth". Sin embargo, con 100% de certeza si se establece el líder / detrás y numberOfLinesa 0continuación, usted es bueno! Larga historia corta, que la mayoría de las respuestas aquí recomendamos que se use se equivocan! Usted no lo necesita. Necesitarlo es una señal de que sus limitaciones no están configurados correctamente o simplemente no tienes restricciones

  5. Tamaño de fuente: también tenga en cuenta que si aumenta su tamaño de fuente, la altura del tamaño de contenido intrínseco aumentará. No lo mostré en mi código. Puedes probar eso por tu cuenta.

Entonces, de vuelta a su ejemplo de tableViewCell:

Todo lo que necesitas hacer es:

  • establecer el numberOfLinesa0
  • restringir la etiqueta correctamente a los márgenes / bordes
  • No hay necesidad de configurar preferredMaxLayoutWidth.
Miel
fuente
1

En mi caso, tengo que crear una celda personalizada con una imagen que proviene del servidor y puede ser de cualquier ancho y alto. Y dos UILabels con tamaño dinámico (ancho y alto)

He logrado lo mismo aquí en mi respuesta con autolayout y programáticamente:

Básicamente por encima de la respuesta @smileyBorg ayudó, pero systemLayoutSizeFittingSize nunca funcionó para mí, en mi enfoque:

1. No se utiliza la propiedad de cálculo automático de altura de fila. 2. Sin uso de altura estimada 3. Sin necesidad de actualizaciones innecesarias Restricciones. 4.No se utiliza el ancho de diseño máximo preferido automático. 5. No uso systemLayoutSizeFittingSize (debería tener uso pero no funciona para mí, no sé lo que está haciendo internamente), sino que mi método - (flotante) getViewHeight funciona y sé lo que está haciendo internamente.

¿Es posible tener diferentes alturas en una celda UITableView cuando uso varias formas diferentes de mostrar la celda?

Alok
fuente
1

En mi caso, el relleno fue debido a las alturas sectionHeader y sectionFooter, donde el guión gráfico me permitió cambiarlo al mínimo 1. Entonces, en el método viewDidLoad:

tableView.sectionHeaderHeight = 0
tableView.sectionFooterHeight = 0
Rashid
fuente
1

Acabo de hacer algunos tratan de tonto y error con los valores de 2 rowHeighty estimatedRowHeightya sólo pensé que podría proporcionar alguna información de depuración:

Si los configura a ambos O solo configura el estimatedRowHeight, obtendrá el comportamiento deseado:

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 1.00001 // MUST be greater than 1

Se sugiere que haga todo lo posible para obtener la estimación correcta, pero el resultado final no es diferente. Solo afectará su rendimiento.

ingrese la descripción de la imagen aquí


Si solo establece la altura de fila, es decir, solo haga:

tableView.rowHeight = UITableViewAutomaticDimension

su resultado final no sería el deseado:

ingrese la descripción de la imagen aquí


Si establece el valor estimatedRowHeighten 1 o menor, se bloqueará independientemente de rowHeight.

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 1 

Choqué con el siguiente mensaje de error:

Terminating app due to uncaught exception
'NSInternalInconsistencyException', reason: 'table view row height
must not be negative - provided height for index path (<NSIndexPath:
0xc000000000000016> {length = 2, path = 0 - 0}) is -1.000000'
    ...some other lines...

libc++abi.dylib: terminating with uncaught exception of type
NSException
Miel
fuente
1

Con respecto a la respuesta aceptada por @smileyborg, he encontrado

[cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]

ser poco confiable en algunos casos donde las restricciones son ambiguas. Es mejor forzar el motor de diseño para calcular la altura en una dirección, utilizando la categoría de ayuda en UIView a continuación:

-(CGFloat)systemLayoutHeightForWidth:(CGFloat)w{
    [self setNeedsLayout];
    [self layoutIfNeeded];
    CGSize size = [self systemLayoutSizeFittingSize:CGSizeMake(w, 1) withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel];
    CGFloat h = size.height;
    return h;
}

Donde w: es el ancho de la vista de tabla

desfile ferroviario
fuente
0

Simplemente agregue estas dos funciones en su controlador de vista para resolver su problema. Aquí, list es una matriz de cadenas que contiene la cadena de cada fila.

 func tableView(_ tableView: UITableView, 
   estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
        tableView.rowHeight = self.calculateHeight(inString: list[indexPath.row])

    return (tableView.rowHeight) 
}

func calculateHeight(inString:String) -> CGFloat
{
    let messageString = input.text
    let attributes : [NSAttributedStringKey : Any] = [NSAttributedStringKey(rawValue: NSAttributedStringKey.font.rawValue) : UIFont.systemFont(ofSize: 15.0)]

    let attributedString : NSAttributedString = NSAttributedString(string: messageString!, attributes: attributes)

    let rect : CGRect = attributedString.boundingRect(with: CGSize(width: 222.0, height: CGFloat.greatestFiniteMagnitude), options: .usesLineFragmentOrigin, context: nil)

    let requredSize:CGRect = rect
    return requredSize.height
}
Parth Barot
fuente
-1
swift 4

    @IBOutlet weak var tableViewHeightConstraint: NSLayoutConstraint!
    @IBOutlet weak var tableView: UITableView!
    private var context = 1
 override func viewDidLoad() {
        super.viewDidLoad()

        self.tableView.addObserver(self, forKeyPath: "contentSize", options: [.new,.prior], context: &context)
    }
  // Added observer to adjust tableview height based on the content

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if context == &self.context{
            if let size = change?[NSKeyValueChangeKey.newKey] as? CGSize{
                print("-----")
                print(size.height)
                tableViewHeightConstraint.constant = size.height + 50
            }
        }
    }

//Remove observer
 deinit {

        NotificationCenter.default.removeObserver(self)

    }
Manee ios
fuente
-1

Si la altura de la celda es dinámica por el contenido, debe contarla con precisión y luego devolver el valor de altura antes de que se renderice la celda. Una manera fácil es definir el método de conteo en el código de celda de la vista de tabla para que el controlador llame al método de delegado de altura de celda de tabla. No olvide contar el ancho real del marco de la celda (el valor predeterminado es 320) si la altura depende del ancho de la tabla o pantalla. Es decir, en el método de delegado de altura de celda de la tabla, use cell.frame para corregir el ancho de celda primero, luego llame al método de altura de conteo definido en la celda para obtener el valor adecuado y devolverlo .

PD. El código para generar el objeto de celda podría definirse en otro método para que llame al método de delegado de celda de vista de tabla diferente.

Shrdi
fuente
-3

UITableView.automaticDimension se puede configurar a través de Interface Builder:

Xcode> Storyboard> Size Inspector

Celda de vista de tabla> Altura de fila> Automático

Inspector de tallas

pkamb
fuente
-4

otra solución iOs7 + iOs8 en Swift

var cell2height:CGFloat=44

override func viewDidLoad() {
    super.viewDidLoad()
    theTable.rowHeight = UITableViewAutomaticDimension
    theTable.estimatedRowHeight = 44.0;
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell =  tableView.dequeueReusableCellWithIdentifier("myTableViewCell", forIndexPath: indexPath) as! myTableViewCell
    cell2height=cell.contentView.height
    return cell
}

func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    if #available(iOS 8.0, *) {
        return UITableViewAutomaticDimension
    } else {
        return cell2height
    }
}
djdance
fuente
nota: systemLayoutSizeFittingSize no funciona en mi caso
djdance
la altura de la celda no es correcta cellForRowAtIndexPath, la celda aún no se presenta en este punto.
Andrii Chernenko
en iOs7 es un valor fijo, es decir, funciona. Puede configurar fuera de cellForRowAtIndexPath si lo desea
djdance