Usando o Layout automático no UITableView para layouts de células dinâmicos e alturas de linhas variáveis


1501

Como você usa o Layout automático dentro de UITableViewCells em uma exibição de tabela para permitir que o conteúdo e as subvisões de cada célula determinem a altura da linha (ela própria / automaticamente), mantendo o desempenho de rolagem suave?


Aqui está o código de exemplo no swift 2.3 github.com/dpakthakur/DynamicCellHeight
Deepak Thakur

Para entender a maioria das respostas em relação a ter um UILabel de várias linhas, você precisa de um entendimento completo de como contentSizee preferredMaxLayoutWidthtrabalhar. Veja aqui . Dito isto, se você configurar suas restrições corretamente, não será necessário preferredMaxLayoutWidthe poderá criar resultados inesperados.
Mel

Respostas:


2388

TL; DR: Não gosta de ler? Vá direto para os projetos de amostra no GitHub:

Descrição conceitual

As duas primeiras etapas abaixo são aplicáveis, independentemente de quais versões do iOS você está desenvolvendo.

1. Configurar e adicionar restrições

Na sua UITableViewCellsubclasse, adicione restrições para que as subvisões da célula tenham suas bordas fixadas nas bordas do contentView da célula (o mais importante nas bordas superior E inferior). NOTA: não fixe subvisões à própria célula; apenas para o celular contentView! Permita que o tamanho do conteúdo intrínseco dessas subvisões conduza a altura da visualização do conteúdo da célula, garantindo que a resistência à compactação e as restrições de restrição de conteúdo na dimensão vertical de cada subvisão não sejam substituídas pelas restrições de prioridade mais alta que você adicionou. ( Hein? Clique aqui. )

Lembre-se de que a ideia é ter as subvisões da célula conectadas verticalmente à visualização de conteúdo da célula, para que possam "exercer pressão" e fazer com que a visualização de conteúdo se expanda para ajustá-las. Usando uma célula de exemplo com algumas subvisões, aqui está uma ilustração visual de como algumas (não todas!) De suas restrições teriam a aparência:

Ilustração de exemplo de restrições em uma célula de exibição de tabela.

Você pode imaginar que, à medida que mais texto for adicionado ao rótulo do corpo com várias linhas na célula de exemplo acima, ele precisará crescer verticalmente para caber no texto, o que forçará efetivamente a célula a crescer em altura. (Obviamente, você precisa acertar as restrições para que funcione corretamente!)

Ajustar corretamente suas restrições é definitivamente a parte mais difícil e importante de obter alturas dinâmicas de células trabalhando com o Layout Automático. Se você cometer um erro aqui, isso poderá impedir que tudo funcione - por isso não se apresse! Eu recomendo configurar suas restrições no código porque você sabe exatamente quais restrições estão sendo adicionadas e onde é muito mais fácil depurar quando as coisas dão errado. Adicionar restrições no código pode ser tão fácil quanto e significativamente mais poderoso que o Interface Builder usando âncoras de layout ou uma das fantásticas APIs de código aberto disponíveis no GitHub.

  • Se você estiver adicionando restrições no código, faça isso uma vez no updateConstraintsmétodo da sua subclasse UITableViewCell. Observe que updateConstraintspode ser chamado mais de uma vez; portanto, para evitar adicionar as mesmas restrições mais de uma vez, envolva seu código de adição de restrição updateConstraintsem uma verificação de uma propriedade booleana como didSetupConstraints(que você definiu como YES depois de executar sua restrição código de adição uma vez). Por outro lado, se você tiver um código que atualize as restrições existentes (como ajustar a constantpropriedade em algumas restrições), coloque-o dentro, updateConstraintsmas fora da verificação, para didSetupConstraintsque ele possa ser executado sempre que o método for chamado.

2. Determinar identificadores exclusivos de reutilização de célula no modo de exibição de tabela

Para cada conjunto exclusivo de restrições na célula, use um identificador de reutilização de célula exclusivo. Em outras palavras, se suas células tiverem mais de um layout exclusivo, cada layout exclusivo receberá seu próprio identificador de reutilização. (Uma boa dica de que você precisa usar um novo identificador de reutilização é quando a variante de célula tem um número diferente de sub-visualizações ou as sub-visualizações são organizadas de maneira distinta.)

Por exemplo, se você estivesse exibindo uma mensagem de email em cada célula, poderá ter quatro layouts exclusivos: mensagens com apenas um assunto, mensagens com um assunto e um corpo, mensagens com um assunto e um anexo de foto e mensagens com um assunto, corpo e anexo de foto. Cada layout possui restrições completamente diferentes necessárias para alcançá-la. Assim, quando a célula é inicializada e as restrições são adicionadas a um desses tipos de células, a célula deve obter um identificador de reutilização exclusivo específico para esse tipo de célula. Isso significa que, quando você desenfileirar uma célula para reutilização, as restrições já foram adicionadas e estão prontas para esse tipo de célula.

Observe que, devido a diferenças no tamanho do conteúdo intrínseco, as células com as mesmas restrições (tipo) ainda podem ter alturas variadas! Não confunda layouts fundamentalmente diferentes (restrições diferentes) com diferentes frames de vista calculados (resolvidos com restrições idênticas) devido a tamanhos diferentes de conteúdo.

  • Não adicione células com conjuntos de restrições completamente diferentes ao mesmo pool de reutilização (ou seja, use o mesmo identificador de reutilização) e tente remover as restrições antigas e configurar novas restrições do zero após cada desenfileiramento. O mecanismo interno de Auto Layout não foi projetado para lidar com mudanças em larga escala nas restrições, e você verá grandes problemas de desempenho.

Para iOS 8 - Células de auto-dimensionamento

3. Ative a estimativa de altura da linha

Para habilitar células de exibição de tabela de dimensionamento automático, você deve definir a propriedade rowHeight da exibição de tabela como UITableViewAutomaticDimension. Você também deve atribuir um valor à propriedade ratedRowHeight. Assim que essas duas propriedades são definidas, o sistema usa o Layout automático para calcular a altura real da linha

Apple: Trabalhando com células de exibição de tabela de dimensionamento automático

Com o iOS 8, a Apple internalizou grande parte do trabalho que anteriormente deveria ser implementado por você antes do iOS 8. Para permitir que o mecanismo de célula de auto-dimensionamento funcione, você deve primeiro definir a rowHeightpropriedade na exibição de tabela como constante UITableViewAutomaticDimension. Em seguida, basta ativar a estimativa da altura da linha configurando a estimatedRowHeightpropriedade da visualização da tabela para um valor diferente de zero, por exemplo:

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

O que isso faz é fornecer à visualização da tabela uma estimativa / espaço reservado temporário para as alturas de linha das células que ainda não estão na tela. Então, quando essas células estiverem prestes a rolar na tela, a altura real da linha será calculada. Para determinar a altura real de cada linha, a visualização da tabela pergunta automaticamente a cada célula qual a altura contentViewnecessária para se basear na largura fixa conhecida da visualização de conteúdo (que é baseada na largura da visualização da tabela, menos itens adicionais, como um índice de seção ou visualização acessória) e as restrições de layout automáticas adicionadas à visualização e subvisões de conteúdo da célula. Depois que essa altura real da célula é determinada, a altura estimada antiga da linha é atualizada com a nova altura real (e todos os ajustes no contentSize / contentOffset da visualização da tabela são feitos conforme necessário).

De um modo geral, a estimativa que você fornece não precisa ser muito precisa - é usada apenas para dimensionar corretamente o indicador de rolagem na exibição de tabela, e a exibição de tabela faz um bom trabalho ao ajustar o indicador de rolagem para estimativas incorretas, conforme você rolar as células na tela. Você deve definir a estimatedRowHeightpropriedade na visualização da tabela (em viewDidLoadou similar) para um valor constante que seja a altura da linha "média". Somente se a altura da sua linha tiver extrema variabilidade (por exemplo, diferir por uma ordem de magnitude) e você notar o indicador de rolagem "pulando" à medida que você rola, você deve se preocupar em implementar tableView:estimatedHeightForRowAtIndexPath:o cálculo mínimo necessário para retornar uma estimativa mais precisa para cada linha.

Para suporte ao iOS 7 (implementando o dimensionamento automático de células)

3. Faça um passe de layout e obtenha a altura da célula

Primeiro, instancie uma instância fora da tela de uma célula de exibição de tabela, uma instância para cada identificador de reutilização , usada estritamente para cálculos de altura. (Fora da tableView:cellForRowAtIndexPath:tela, o que significa que a referência da célula é armazenada em uma propriedade / ivar no controlador de exibição e nunca retornada da exibição de tabela para renderizar na tela.) Em seguida, a célula deve ser configurada com o conteúdo exato (por exemplo, texto, imagens etc.) que seria válido se fosse exibido na visualização da tabela.

Então, forçar a célula para de imediato o layout de seus subviews, e depois usar o systemLayoutSizeFittingSize:método no UITableViewCell's contentViewpara descobrir o que a altura necessária da célula é. Use UILayoutFittingCompressedSizepara obter o menor tamanho necessário para caber em todo o conteúdo da célula. A altura pode então ser retornada do tableView:heightForRowAtIndexPath:método delegado.

4. Use as alturas estimadas das linhas

Se a visualização da tabela contiver mais de duas dúzias de linhas, você descobrirá que a solução de restrições de Layout Automático pode atolar rapidamente o encadeamento principal ao carregar a visualização da tabela pela primeira vez, como tableView:heightForRowAtIndexPath:é chamado em todas as linhas após o primeiro carregamento ( para calcular o tamanho do indicador de rolagem).

No iOS 7, você pode (e absolutamente deveria) usar a estimatedRowHeightpropriedade na exibição de tabela. O que isso faz é fornecer à visualização da tabela uma estimativa / espaço reservado temporário para as alturas de linha das células que ainda não estão na tela. Então, quando essas células estiverem prestes a rolar na tela, a altura real da linha será calculada (chamando tableView:heightForRowAtIndexPath:) e a altura estimada atualizada com a atual.

De um modo geral, a estimativa que você fornece não precisa ser muito precisa - é usada apenas para dimensionar corretamente o indicador de rolagem na exibição de tabela, e a exibição de tabela faz um bom trabalho ao ajustar o indicador de rolagem para estimativas incorretas, conforme você rolar as células na tela. Você deve definir a estimatedRowHeightpropriedade na visualização da tabela (em viewDidLoadou similar) para um valor constante que seja a altura da linha "média". Somente se a altura da sua linha tiver extrema variabilidade (por exemplo, diferir por uma ordem de magnitude) e você notar o indicador de rolagem "pulando" à medida que você rola, você deve se preocupar em implementar tableView:estimatedHeightForRowAtIndexPath:o cálculo mínimo necessário para retornar uma estimativa mais precisa para cada linha.

5. (Se necessário) Adicionar cache de altura da linha

Se você fez tudo o que foi dito acima e ainda está percebendo que o desempenho é inaceitavelmente lento ao resolver as restrições tableView:heightForRowAtIndexPath:, infelizmente você precisará implementar algum cache para a altura das células. (Essa é a abordagem sugerida pelos engenheiros da Apple.) A idéia geral é permitir que o mecanismo de Autolayout resolva as restrições pela primeira vez, depois armazene em cache a altura calculada para essa célula e use o valor em cache para todas as solicitações futuras da altura dessa célula. O truque, é claro, é garantir que você limpe a altura em cache de uma célula quando acontecer algo que possa causar a alteração da altura da célula - principalmente, quando o conteúdo dessa célula for alterado ou quando ocorrerem outros eventos importantes (como o ajuste do usuário o controle deslizante de tamanho de texto Tipo dinâmico).

Código de exemplo genérico do iOS 7 (com muitos comentários interessantes)

- (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;
    }
}

Projetos de amostra

Esses projetos são exemplos completos de visualizações de tabela com alturas de linha variáveis ​​devido às células de visualização de tabela que contêm conteúdo dinâmico nos UILabels.

Xamarin (C # /. NET)

Se você estiver usando o Xamarin, confira este projeto de amostra elaborado por @KentBoogaart .


1
Enquanto isso está funcionando bem, acho que subestima ligeiramente os tamanhos necessários (possivelmente devido a vários problemas de arredondamento) e preciso acrescentar alguns pontos à altura final para que todo o meu texto caiba dentro dos rótulos
DBD

5
@ Alex311 Muito interessante, obrigado por fornecer este exemplo. Eu fiz um pequeno teste no meu fim e escreveu-se alguns comentários aqui: github.com/Alex311/TableCellWithAutoLayout/commit/...
smileyborg

3
Eu recomendo em cache a célula para cada tipo de identificador de reutilização de célula. Cada vez que você desenfileirar a altura, você está retirando uma célula da fila que não está sendo adicionada novamente. O armazenamento em cache pode reduzir significativamente a quantidade de vezes que o inicializador para as células da tabela é chamado. Por alguma razão, quando eu não estava armazenando em cache, a quantidade de memória usada continuava crescendo como eu iria rolar.
Alex311

1
Storyboards, na verdade. Não acho que isso funcione para células protótipo (pelo menos sem re-instanciar todo o VC). Pode ser possível evitar completamente o vazamento desenfileirando no heightForRowAtIndexPath, mantendo a célula e retornando-a na próxima vez que cellForRowAtIndexPathfor chamado.
Nschum 6/05

2
A iOS8implementação ainda é recomendada iOS9e iOS10, ou existem novas abordagens desde que esta resposta foi publicada?
319 koen

175

Para o iOS 8 acima, é realmente simples:

override func viewDidLoad() {  
    super.viewDidLoad()

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

ou

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

Mas para o iOS 7, a chave é calcular a altura após o pagamento automático:

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

Importante

  • Se várias etiquetas de linhas, não se esqueça de definir numberOfLinescomo 0.

  • Não esqueça label.preferredMaxLayoutWidth = CGRectGetWidth(tableView.bounds)

O código de exemplo completo está aqui .


7
Eu acho que não precisamos para implementar a função heightForRowAtIndexPath no caso OS8
jpulikkottil

Como o @eddwinpaz observa em outra resposta, é importante que as restrições usadas para bloquear a altura da linha NÃO incluam margem.
Richard

1
+1 para "Se vários rótulos de linhas, não se esqueça de definir o numberOfLines como 0", isso fazia com que minhas células não tivessem tamanho dinâmico.
Ian-Fogelman

Me chame de maníaca por controle, mas mesmo nos dias do iOS 11 eu ainda não gosto de usar UITableViewAutomaticDimension. Teve algumas experiências ruins com ele no passado. Como tal, normalmente uso as soluções iOS7 listadas aqui. A nota de William para não esquecer label.preferredMaxLayoutWidth aqui me salvou.
precisa

Quando rolamos, o rótulo começa a aparecer em várias linhas e a altura da linha também não aumenta
Karanveer Singh 18/06/19

94

Exemplo rápido de uma altura variável UITableViewCell

Atualizado para o Swift 3

A resposta rápida de William Hu é boa, mas me ajuda a seguir alguns passos simples, porém detalhados, ao aprender a fazer algo pela primeira vez. O exemplo abaixo é o meu projeto de teste enquanto aprendo a fazer UITableViewalturas de células variáveis. Baseei-lo em este exemplo UITableView básica para Swift .

O projeto final deve ficar assim:

insira a descrição da imagem aqui

Crie um novo projeto

Pode ser apenas um aplicativo de exibição única.

Adicione o código

Adicione um novo arquivo Swift ao seu projeto. Nomeie-o MyCustomCell. Esta classe manterá as saídas para as visualizações adicionadas ao seu celular no storyboard. Neste exemplo básico, teremos apenas um rótulo em cada célula.

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

Mais tarde conectaremos esta tomada.

Abra ViewController.swift e verifique se você tem o seguinte conteúdo:

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:

  • São as duas linhas de código a seguir (junto com o layout automático) que tornam possível a altura variável da célula:

    tableView.estimatedRowHeight = 44.0
    tableView.rowHeight = UITableViewAutomaticDimension

Configurar o storyboard

Adicione uma visualização de tabela ao seu controlador de visualização e use o layout automático para fixá-la nos quatro lados. Em seguida, arraste uma célula de exibição de tabela para a exibição de tabela. E na célula Prototype, arraste um Label. Use o layout automático para fixar o rótulo nas quatro arestas da exibição de conteúdo da célula de exibição de tabela.

insira a descrição da imagem aqui

Nota importante:

  • O layout automático funciona em conjunto com as duas importantes linhas de código que eu mencionei acima. Se você não usar o layout automático, não funcionará.

Outras configurações de IB

Nome da classe personalizada e Identificador

Selecione a célula de exibição de tabela e defina a classe personalizada como MyCustomCell(o nome da classe no arquivo Swift que adicionamos). Defina também o identificador como cell(a mesma string que usamos cellReuseIdentifierno código acima.

insira a descrição da imagem aqui

Zero linhas para etiqueta

Defina o número de linhas 0na sua etiqueta. Isso significa várias linhas e permite que o rótulo se redimensione com base em seu conteúdo.

insira a descrição da imagem aqui

Ligar as tomadas

  • Arraste a tecla Control da exibição de tabela no storyboard para a tableViewvariável no ViewControllercódigo.
  • Faça o mesmo para o Label na sua célula Prototype para a myCellLabelvariável na MyCustomCellclasse.

Acabado

Agora você deve poder executar seu projeto e obter células com alturas variáveis.

Notas

  • Este exemplo funciona apenas para iOS 8 e posteriores. Se você ainda precisa oferecer suporte ao iOS 7, isso não funcionará para você.
  • Suas próprias células personalizadas em seus projetos futuros provavelmente terão mais de um rótulo. Certifique-se de colocar tudo corretamente, para que o layout automático possa determinar a altura correta a ser usada. Você também pode ter que usar resistência à compressão vertical e abraços. Veja este artigo para saber mais sobre isso.
  • Se você não estiver fixando as arestas à esquerda e à direita (esquerda e direita), também poderá ser necessário definir as etiquetas preferredMaxLayoutWidthpara que ele saiba quando alinhar a linha. Por exemplo, se você tivesse adicionado uma restrição de Horizontalmente ao Centro no rótulo do projeto acima, em vez de fixar as arestas à esquerda e à direita, seria necessário adicionar esta linha ao tableView:cellForRowAtIndexPathmétodo:

     cell.myCellLabel.preferredMaxLayoutWidth = tableView.bounds.width

Veja também


Não é realmente melhor colocar preferredMaxLayoutWidtho celular contra o celular contentSize? Dessa forma, se você tiver um acessórioView ou ele foi deslocado para edição, ele ainda será levado em consideração?
Mel

@ Mel, você pode muito bem estar certo. Não acompanho o iOS há um ano e estou muito enferrujado para responder bem agora.
Suragch

65

Eu envolvi a solução iOS7 da smileyborg em uma categoria

Decidi agrupar essa solução inteligente por @smileyborg em uma UICollectionViewCell+AutoLayoutDynamicHeightCalculationcategoria.

A categoria também corrige os problemas descritos na resposta do @ wildmonkey (carregar uma célula de uma ponta e systemLayoutSizeFittingSize:retornar CGRectZero)

Ele não leva em consideração nenhum cache, mas atende às minhas necessidades no momento. Sinta-se livre para copiar, colar e invadir.

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

Exemplo 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);
}

Felizmente, não teremos que fazer esse jazz no iOS8, mas já está por enquanto!


2
Você deve ser capaz de simplesmente usar um: [YourCell new]e usar isso como manequim. Contanto que o código de construção do código de restrição seja acionado em sua instância, e você acione um passe de layout de forma programática, você deve estar pronto.
Adam Waite

1
Obrigado! Isso funciona. Sua categoria é ótima. Foi o que me fez perceber que essa técnica também funciona UICollectionViews.
Ricardo Sanchez-Saez

2
Como você faria isso usando uma célula protótipo definida em um storyboard?
David Potter

57

Aqui está a minha solução:

Você precisa informar TableViewo estimatedHeightantes de carregar a exibição. Caso contrário, não será capaz de se comportar como o esperado.

Objetivo-C

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

Atualização para o Swift 4.2

override func viewWillAppear(_ animated: Bool) {
    tableView.rowHeight = UITableView.automaticDimension
    tableView.estimatedRowHeight = 65.0
}

2
A configuração correta do autolayout, juntamente com esse código foi adicionado no viewDidLoad.
19715 Bruce

1
mas e se estimatedRowHeighthouver variação linha por linha? devo acima ou abaixo da estimativa? usar mínimo ou máximo da altura em que uso tableView?
János

1
@ János este é o ponto de rowHeight. Para fazer isso se comportar conforme o esperado, você precisa usar restrições sem margens e alinhar com os objetos TableViewCell. e eu suponho que você esteja usando um UITextView, então você ainda precisará remover autoscroll = false, caso contrário, manterá a altura e a altura relativa não funcionará conforme o esperado.
Eddwin Paz 23/10

1
Essa é de longe a solução mais robusta. Não se preocupe estimatedRowHeight, isso afeta principalmente o tamanho da barra de rolagem, nunca a altura das células reais. Seja ousado na altura que você escolher: isso afetará a animação de inserção / exclusão.
precisa saber é o seguinte

Aqui está o código de exemplo no swift 2.3 github.com/dpakthakur/DynamicCellHeight
Deepak Thakur

47

A solução proposta por @smileyborg é quase perfeita. Se você tem um celular personalizado e você quer uma ou mais UILabelcom alturas dinâmicas então o systemLayoutSizeFittingSize método combinado com AutoLayout habilitado retorna a CGSizeZeromenos que você mover todas as restrições a sua celulares da célula à sua contentView (como sugerido por @TomSwift aqui Como redimensionar superview para encaixar todas as subvisões com a execução automática? ).

Para fazer isso, você precisa inserir o seguinte código na sua implementação personalizada do UITableViewCell (graças 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];
    }
}

Misturar a resposta @smileyborg com isso deve funcionar.


systemLayoutSizeFittingSizeprecisa ser chamado em contentView, não celular
onmyway133

25

Uma pegadinha importante o suficiente que eu acabei de encontrar para postar como resposta.

A resposta de @ smileyborg está correta. No entanto, se você tiver algum código no layoutSubviewsmétodo da sua classe de célula personalizada, por exemplo, definindo o preferredMaxLayoutWidth, ele não será executado com este código:

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

Isso me confundiu por um tempo. Então eu percebi que é porque eles estão apenas acionando layoutSubviews no contentView, não na própria célula.

Meu código de trabalho é assim:

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

Observe que, se você estiver criando uma nova célula, tenho certeza de que não precisa ligar, setNeedsLayoutpois ela já deve estar definida. Nos casos em que você salva uma referência em uma célula, provavelmente deve chamá-la. De qualquer maneira, não deve doer nada.

Outra dica se você estiver usando subclasses de células em que está definindo coisas como preferredMaxLayoutWidth. Como menciona @smileyborg, "a célula da visualização da tabela ainda não teve sua largura fixa na largura da visualização da tabela". Isso é verdade, e problemas se você estiver fazendo seu trabalho em sua subclasse e não no controlador de exibição. No entanto, você pode simplesmente definir o quadro da célula neste momento usando a largura da tabela:

Por exemplo, no cálculo da 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);

(Por acaso, armazene em cache essa célula específica para reutilização, mas isso é irrelevante).



19

Contanto que seu layout no seu celular seja bom.

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

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

Atualização: você deve usar o redimensionamento dinâmico introduzido no iOS 8.


Isso está funcionando para mim em iOS7, é agora OK para chamar tableView:cellForRowAtIndexPath:de tableView:heightForRowAtIndexPath:agora?
Formigas

Ok, então isso não está funcionando, mas quando eu chamo e armazeno systemLayoutSizeFittingSize:em tableView:cellForRowAtIndexPath:cache o resultado, em seguida, usá- tableView:heightForRowAtIndexPath:lo funciona bem, desde que as restrições estejam configuradas corretamente, é claro!
Ants

Isso só funciona se você usar dequeueReusableCellWithIdentifier: em vez de dequeueReusableCellWithIdentifier: forIndexPath:
Antoine

1
Realmente não acho que chamar tableView: cellForRowAtIndexPath: diretamente seja uma boa maneira.
Itachi

19

(para o Xcode 8.x / Xcode 9.x lido na parte inferior)

Cuidado com o seguinte problema no Xcode 7.x, que pode ser uma fonte de confusão:

O Interface Builder não lida corretamente com a configuração de célula de dimensionamento automático. Mesmo que suas restrições sejam absolutamente válidas, o IB ainda reclamará e fornecerá sugestões e erros confusos. O motivo é que o IB não está disposto a alterar a altura da linha conforme suas restrições determinam (para que a célula se ajuste ao seu conteúdo). Em vez disso, mantém a altura da linha fixa e começa a sugerir que você altere suas restrições, as quais você deve ignorar .

Por exemplo, imagine que você configurou tudo bem, sem avisos, sem erros, tudo funciona.

insira a descrição da imagem aqui

Agora, se você alterar o tamanho da fonte (neste exemplo, estou alterando o tamanho da fonte do rótulo da descrição de 17.0 para 18.0).

insira a descrição da imagem aqui

Como o tamanho da fonte aumentou, o rótulo agora quer ocupar 3 linhas (antes disso estava ocupando 2 linhas).

Se o Interface Builder funcionasse conforme o esperado, ele redimensionaria a altura da célula para acomodar a nova altura da etiqueta. No entanto, o que realmente acontece é que o IB exibe o ícone vermelho de erro de layout automático e sugere que você modifique as prioridades de abraço / compactação.

insira a descrição da imagem aqui

Você deve ignorar esses avisos. O que você pode fazer * é alterar manualmente a altura da linha (selecione Célula> Inspetor de tamanho> Altura da linha).

insira a descrição da imagem aqui

Eu estava mudando essa altura, um clique de cada vez (usando o deslizante para cima / para baixo) até que os erros de seta vermelha desaparecessem! (você realmente receberá avisos amarelos; nesse ponto, vá em frente e faça 'atualizar quadros', tudo deverá funcionar).

* Observe que você não precisa realmente resolver esses erros vermelhos ou avisos amarelos no Interface Builder - em tempo de execução, tudo funcionará corretamente (mesmo que o IB mostre erros / avisos). Apenas verifique se, em tempo de execução no log do console, você não está recebendo nenhum erro de AutoLayout.

De fato, tentar sempre atualizar a altura da linha no IB é super irritante e às vezes quase impossível (devido a valores fracionários).

Para evitar os irritantes avisos IB / erros, você pode selecionar os pontos de vista envolvidos e Size Inspectorpara a propriedade AmbiguityescolherVerify Position Only

insira a descrição da imagem aqui


O Xcode 8.x / Xcode 9.x parece (às vezes) fazer coisas de maneira diferente do Xcode 7.x, mas ainda incorretamente. Por exemplo, mesmo quando compression resistance priority/ hugging priorityestá definido como necessário (1000), o Interface Builder pode esticar ou recortar um rótulo para caber na célula (em vez de redimensionar a altura da célula para caber no rótulo). E, nesse caso, ele pode nem mostrar avisos ou erros de AutoLayout. Ou, às vezes, faz exatamente o que o Xcode 7.x fez, descrito acima.


oi é possível dar altura dinâmica para célula, que tendo tableview com célula com conteúdo dinâmico de célula.
precisa

18

Para definir a dimensão automática para a altura da linha e a altura estimada da linha, verifique as etapas a seguir, efetue a cota automática para o layout da altura da célula / linha.

  • Atribuir e implementar dados de visualização de tabela
  • Atribuir UITableViewAutomaticDimensiona rowHeight e estimadoRowHeight
  • Implementar métodos delegate / dataSource (ou seja, heightForRowAte retornar um valor UITableViewAutomaticDimensionpara ele)

-

Objetivo C:

// 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 instância de rótulo em UITableviewCell

  • Definir número de linhas = 0 (& modo de quebra de linha = cauda truncada)
  • Defina todas as restrições (superior, inferior, direita esquerda) com relação ao seu superview / contêiner de célula.
  • Opcional : defina a altura mínima para a etiqueta, se desejar que a área vertical mínima seja coberta pela etiqueta, mesmo se não houver dados.

insira a descrição da imagem aqui

Nota : Se você tiver mais de um rótulo (UIElements) com comprimento dinâmico, o qual deverá ser ajustado de acordo com o tamanho do conteúdo: Ajuste 'Prioridade de resistência a compressão e compressão de conteúdo' para rótulos que você deseja expandir / compactar com prioridade mais alta.


1
Obrigado, parece uma solução clara e simples, mas tenho um problema: não estou usando rótulos, mas visualizações de texto, por isso preciso que a altura da linha aumente conforme os dados são adicionados. Meu problema é então passar informações para heightForRowAt. Posso medir a altura das minhas visualizações de texto alteradas, mas agora preciso alterar a altura da linha. Gostaria de receber alguma ajuda, por favor
Jeremy Andrews

@JeremyAndrews Sure irá ajudá-lo. Faça a sua pergunta com o código fonte que você tentou e detalhe sobre o problema.
Krunal

Está funcionando para mim sem implementar a tableView: heightForRowfonte de dados.
#

@ Hemang - Desde o iOS 11 ou superior, ele funciona sem tableView: heightForRow. (para iOS 10 tableView: heightForRowé necessário)
Krunal

1
@ Hemang - A solução depende da definição exata da consulta. Aqui eu tenho solução comum geral para sua consulta. Coloque condição dentro tableView: heightForRow..if (indexPath.row == 0) { return 100} else { return UITableView.automaticDimension }
Krunal

16

Como @ Bob-Spryn, me deparei com uma pegadinha importante o suficiente para postar isso como resposta.

Eu lutei com a resposta de @ smileyborg por um tempo. O problema que eu encontrei é se você definiu sua célula protótipo no IB com elementos adicionais ( UILabels, UIButtonsetc.) no IB quando você instancia a célula com [ [YourTableViewCellClass alloc] init]ela não instancia todos os outros elementos nessa célula, a menos que você código escrito para fazer isso. (Eu tive uma experiência semelhante com initWithStyle.)

Para que o storyboard instancie todos os elementos adicionais, obtenha sua célula [tableView dequeueReusableCellWithIdentifier:@"DoseNeeded"](Não, [tableView dequeueReusableCellWithIdentifier:forIndexPath:]pois isso causará problemas interessantes.) Quando você fizer isso, todos os elementos que você definiu no IB serão instanciados.


15

Altura da célula da exibição dinâmica da tabela e layout automático

Uma boa maneira de resolver o problema com o Layout automático do storyboard:

- (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;
}

1
Isso já foi amplamente abordado na resposta aceita a esta pergunta.
smileyborg

2
Sim, eu sei ... mas eu não queria usar o PureLayout e o 'truque' de dispatch_once me ajudou muito, para trabalhar com o Storyboard.
Mohnasm 22/08/14

13
tableView.estimatedRowHeight = 343.0
tableView.rowHeight = UITableViewAutomaticDimension

insira a descrição da imagem aqui


13

Outra "solução": pule toda essa frustração e use um UIScrollView para obter um resultado que pareça idêntico ao UITableView.

Essa foi a "solução" dolorosa para mim, depois de ter gasto literalmente mais de 20 horas muito frustrantes tentando criar algo parecido com o que o smileyborg sugeriu e falhou ao longo de muitos meses e três versões dos lançamentos da App Store.

Meu ponto de vista é que, se você realmente precisa do suporte ao iOS 7 (para nós, é essencial), a tecnologia é muito quebradiça e você arranca os cabelos. E que o UITableView é um exagero completo, a menos que você esteja usando alguns dos recursos avançados de edição de linha e / ou precise realmente suportar mais de 1000 "linhas" (em nosso aplicativo, na realidade, nunca é mais do que 20 linhas).

O bônus adicional é que o código fica incrivelmente simples em comparação com toda a porcaria de delegado e para trás e para a frente que acompanha o UITableView. É apenas um único loop de código no viewOnLoad que parece elegante e fácil de gerenciar.

Aqui estão algumas dicas sobre como fazê-lo:

  1. Usando o Storyboard ou um arquivo de ponta, crie um ViewController e uma visualização raiz associada.

  2. Arraste sobre um UIScrollView para a visualização raiz.

  3. Adicione restrições superiores, inferiores, esquerda e direita à exibição de nível superior, para que o UIScrollView preencha toda a exibição raiz.

  4. Adicione um UIView dentro do UIScrollView e chame-o de "contêiner". Adicione restrições superior, inferior, esquerda e direita ao UIScrollView (seu pai). TRUQUE DE CHAVE: Adicione também restrições de "Larguras iguais" para vincular o UIScrollView e o UIView.

    NOTA: Você receberá um erro "a exibição de rolagem possui altura de conteúdo rolável ambígua" e que o UIView do contêiner deve ter uma altura de 0 pixels. Nenhum dos erros parece importar quando o aplicativo está sendo executado.

  5. Crie arquivos e controladores de ponta para cada uma das suas "células". Use UIView não UITableViewCell.

  6. No seu ViewController raiz, você basicamente adiciona todas as "linhas" ao UIView do contêiner e adiciona programaticamente restrições que vinculam suas bordas esquerda e direita à exibição do contêiner, suas bordas superiores à parte superior da exibição do contêiner (para o primeiro item) ou à anterior célula. Em seguida, vincule a célula final ao fundo do contêiner.

Para nós, cada "linha" está em um arquivo de ponta. Portanto, o código se parece com isso:

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!)
    }
}

E aqui está o 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))
    }
}

A única "pegadinha" que encontrei com essa abordagem até agora é que o UITableView possui um bom recurso de cabeçalhos de seção "flutuantes" na parte superior da exibição enquanto você rola. A solução acima não fará isso, a menos que você adicione mais programação, mas, no nosso caso particular, esse recurso não era 100% essencial e ninguém notou quando foi embora.

Se você deseja divisórias entre as células, basta adicionar uma visualização UIV de 1 pixel na parte inferior da "célula" personalizada que se parece com um divisor.

Certifique-se de ativar "devoluções" e "devolver verticalmente" para que o controle de atualização funcione e, portanto, pareça mais uma exibição de tabela.

O TableView mostra algumas linhas e divisórias vazias no seu conteúdo, se ele não preencher a tela inteira, onde esta solução não. Mas, pessoalmente, prefiro que essas linhas vazias não estivessem lá de qualquer maneira - com altura variável da célula, sempre parecia "buggy" para mim ter as linhas vazias lá.

Esperamos que algum outro programador leia meu post ANTES de perder mais de 20 horas tentando descobrir isso com o Table View em seu próprio aplicativo. :)


11

Eu tive que usar visualizações dinâmicas (visualizações de configuração e restrições por código) e quando desejei definir a largura do rótulo preferencialMaxLayoutWidth era 0. Portanto, eu tenho uma altura de célula errada.

Então eu adicionei

[cell layoutSubviews];

antes de executar

[cell setNeedsUpdateConstraints];
[cell updateConstraintsIfNeeded];

Depois que a largura da etiqueta estava como o esperado e a altura dinâmica foi calculada corretamente.


9

Digamos que você tenha uma célula com uma subvisão e deseje que a altura da célula seja alta o suficiente para abranger a subvisão + preenchimento.

1) Defina a restrição inferior da subvisão igual à cell.contentView menos o preenchimento desejado. Não defina restrições na célula ou cell.contentView em si.

2) Defina a rowHeightpropriedade do tableView ou tableView:heightForRowAtIndexPath:como UITableViewAutomaticDimension.

3) Defina a estimatedRowHeightpropriedade tableView ou o tableView:estimatedHeightForRowAtIndexPath:melhor palpite da altura.

É isso aí.


7

Se você faz o layout programaticamente, eis o que considerar no iOS 10 usando âncoras no Swift.

Existem três regras / etapas

NÚMERO 1: defina essas duas propriedades da visualização da tabela em viewDidLoad, a primeira informa à visualização da tabela que deve esperar tamanhos dinâmicos em suas células; a segunda é apenas para permitir que o aplicativo calcule o tamanho do indicador da barra de rolagem, ajudando a desempenho.

    tableView.rowHeight = UITableViewAutomaticDimension
    tableView.estimatedRowHeight = 100

NÚMERO 2: É importante que você inclua as subvisões no contentView da célula, e não na visualização, e também use seu layoutsmarginguide para ancorar as subviews nas partes superior e inferior; este é um exemplo prático de como fazê-lo.

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)

        ])
}

Crie um método que adicione as subvisões e execute o layout, chame-o no método init.

NÚMERO 3: NÃO CHAMA O MÉTODO:

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

Se você fizer isso, substituirá sua implementação.

Siga estas 3 regras para células dinâmicas nas visualizações de tablaturas.

aqui está uma implementação funcional https://github.com/jamesrochabrun/MinimalViewController


em Swift 5 UITableViewAutomaticDimension renomeado para UITableView.automaticDimension
James Rochabrun

4

Se você tem uma corda longa . por exemplo, um que não tem uma quebra de linha. Então você pode ter alguns problemas.

A correção "alegada" é mencionada pela resposta aceita e poucas outras respostas. Você só precisa adicionar

cell.myCellLabel.preferredMaxLayoutWidth = tableView.bounds.width

Acho a resposta de Suragh a mais completa e concisa , portanto não confusa.

Embora não explique por que essas mudanças são necessárias. Vamos fazer isso.

Solte o seguinte código em um projeto.

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
    }
}

Observe que eu não adicionei nenhuma restrição de tamanho . Eu apenas adicionei restrições centerX, centerY. Mas ainda assim a etiqueta será dimensionada corretamente Por quê?

Por causa de contentSize.

Para melhor processar isso, primeiro mantenha a etapa 0 e depois comente as etapas 1 a 6. Vamos setupLayout()ficar. Observe o comportamento.

Descomente a etapa 1 e observe.

Em seguida, remova o comentário do passo 2 e observe.

Faça isso até que você descomente todas as 6 etapas e observe o comportamento delas.

O que pode concluir disso tudo? Quais fatores podem mudar o contenSize?

  1. Comprimento do texto: se você tiver um texto mais longo, a largura do seu intrinsicContentSize aumentará
  2. Quebras de linha: se você adicionar \n, a largura do intrinsicContentSize será a largura máxima de todas as linhas. Se uma linha possui 25 caracteres, outra possui 2 caracteres e outra possui 21 caracteres, sua largura será calculada com base nos 25 caracteres
  3. Número de linhas permitidas: você deve definir numberOfLinescomo 0diferente, caso contrário não terá várias linhas. Você numberOfLinesajustará a altura do seu conteúdo intrínseco
  4. Fazendo ajustes: imagine que, com base no seu texto, a largura 200e a altura do seu intrinsicContentSize eram 100, mas você queria limitar a largura ao contêiner do rótulo, o que você fará? A solução é configurá-lo para a largura desejada. Você faz isso configurando preferredMaxLayoutWidthpara 130que seu novo intrinsicContentSize tenha uma largura aproximada 130. Obviamente, a altura seria maior do que 100porque você precisaria de mais linhas. Dito isto, se suas restrições estiverem definidas corretamente, você não precisará usá-las! Para mais informações, consulte esta resposta e seus comentários. Você só precisa usar preferredMaxLayoutWidthse não houver restrições que restrinjam a largura / altura, como pode ser dito "não envolva o texto a menos que exceda apreferredMaxLayoutWidth". Mas, com 100% de certeza se você definir o líder / fuga e numberOfLinespara 0, em seguida, você é bom! Longa história curta maioria respostas aqui que recomendo usá-lo está errado! Você não precisa dele. Precisando é um sinal de que suas limitações não estão definidos corretamente ou você simplesmente não tem restrições

  5. Tamanho da fonte: Observe também que, se você aumentar o fontSize, a altura do intrinsicContentSize aumentará. Eu não mostrei isso no meu código. Você pode tentar isso sozinho.

Então, de volta ao seu exemplo tableViewCell:

Tudo que você precisa fazer é:

  • defina numberOfLinescomo0
  • restringir o rótulo corretamente às margens / bordas
  • Não há necessidade de definir preferredMaxLayoutWidth.

1

No meu caso, eu tenho que criar uma célula personalizada com uma imagem que vem do servidor e pode ter qualquer largura e altura. E dois UILabels com tamanho dinâmico (largura e altura)

Eu consegui o mesmo aqui na minha resposta com autolayout e programaticamente:

Basicamente acima, a resposta @smileyBorg ajudou, mas o systemLayoutSizeFittingSize nunca funcionou para mim. Na minha abordagem:

1. Não há uso da propriedade de cálculo automático de altura da linha. 2. Sem uso de altura estimada 3. Não há necessidade de atualizações desnecessárias. Uso 4.No da largura máxima preferida automática do layout. 5. Nenhum uso de systemLayoutSizeFittingSize (deve ter uso, mas não está funcionando para mim, não sei o que está fazendo internamente), mas o meu método - (float) getViewHeight está funcionando e sei o que está fazendo internamente.

É possível ter alturas diferentes em uma célula UITableView quando eu uso várias maneiras diferentes de exibir a célula?


1

No meu caso, o preenchimento foi por causa das alturas sectionHeader e sectionFooter, em que o storyboard me permitiu alterá-lo para o mínimo 1. Portanto, no método viewDidLoad:

tableView.sectionHeaderHeight = 0
tableView.sectionFooterHeight = 0

1

Eu apenas fiz algumas tentativas e erros estúpidos com os 2 valores de rowHeighte estimatedRowHeightpensei que poderia fornecer algumas dicas de depuração:

Se você definir os dois OU apenas definir estimatedRowHeight, obterá o comportamento desejado:

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

É recomendável que você faça o melhor para obter a estimativa correta, mas o resultado final não é diferente. Isso afetará apenas o seu desempenho.

insira a descrição da imagem aqui


Se você definir apenas o rowHeight, ou seja, faça apenas:

tableView.rowHeight = UITableViewAutomaticDimension

seu resultado final não seria o desejado:

insira a descrição da imagem aqui


Se você definir estimatedRowHeightcomo 1 ou menor, ocorrerá falha independentemente do rowHeight.

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 1 

Fui com a seguinte mensagem de erro:

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

1

Em relação à resposta aceita por @smileyborg, eu encontrei

[cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]

não ser confiável em alguns casos em que as restrições são ambíguas. Melhor forçar o mecanismo de layout a calcular a altura em uma direção, usando a categoria auxiliar no UIView abaixo:

-(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;
}

Onde w: é a largura da tableview


0

Basta adicionar essas duas funções no seu controlador de exibição, pois isso resolverá o seu problema. Aqui, list é uma matriz de strings que contém sua string de cada linha.

 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
}

-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)

    }

-1

Se a altura da célula for dinâmica pelo conteúdo, você deve contá-la com precisão e retornar o valor da altura antes que a célula seja renderizada. Uma maneira fácil é definir o método de contagem no código de célula da visualização de tabela para o controlador chamar no método delegado de altura da célula da tabela. Não se esqueça de contar a largura real do quadro da célula (o padrão é 320) se a altura depender da largura da tabela ou tela. Ou seja, no método delegado de altura da célula da tabela, use cell.frame para corrigir primeiro a largura da célula e, em seguida, chame o método de altura de contagem definido na célula para obter o valor adequado e retorná-lo .

PS. O código para gerar o objeto da célula pode ser definido em outro método para chamar o método delegado da célula da visualização de tabela diferente.


-3

UITableView.automaticDimension pode ser definido através do Interface Builder:

Xcode> Storyboard> Inspetor de tamanho

Célula de exibição de tabela> Altura da linha> Automático

Inspetor de tamanho


-4

mais uma solução iOs7 + iOs8 no 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
    }
}

note: systemLayoutSizeFittingSize não funciona no meu caso
djdance

a altura da célula não está correta cellForRowAtIndexPath, a célula ainda não está definida neste momento.
Andrii Chernenko

no iOs7 é um valor fixo, ou seja, funciona. Você pode definir em cellForRowAtIndexPath fora se você quiser
djdance
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.