As alturas das células dinâmicas UITableView corrigem apenas após alguma rolagem


117

Eu tenho um UITableViewcom um personalizado UITableViewCelldefinido em um storyboard usando layout automático. A célula possui várias linhas UILabels.

O UITableViewparece calcular corretamente as alturas das células, mas para as primeiras células essa altura não é dividida corretamente entre os rótulos. Depois de rolar um pouco, tudo funciona conforme o esperado (mesmo as células que estavam inicialmente incorretas).

- (void)viewDidLoad {
    [super viewDidLoad]
    // ...
    self.tableView.rowHeight = UITableViewAutomaticDimension;
}


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    TableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"TestCell"];
    // ...
    // Set label.text for variable length string.
    return cell;
}

Existe alguma coisa que eu possa estar perdendo, que está fazendo com que o layout automático não consiga fazer seu trabalho nas primeiras vezes?

Criei um projeto de amostra que demonstra esse comportamento.

Projeto de amostra: Vista superior da tabela do projeto de amostra no primeiro carregamento. Projeto de amostra: mesmas células após rolar para baixo e voltar para cima.


1
Eu também estou enfrentando esse problema, mas a resposta abaixo não está funcionando para mim, Qualquer ajuda sobre isso
Shangari C

1
enfrentando o mesmo problema de adicionar layoutIfNeeded para celular não funciona bem? mais alguma sugestão
Máx.

1
Ótimo local. Isso ainda é um problema em 2019: /
Fattie,

Eu descobri o que me ajudou no seguinte link - é uma discussão bastante abrangente sobre células de visualização de tabela de altura variável: stackoverflow.com/a/18746930/826946
Andy Weinstein

Respostas:


139

Não sei se isso está claramente documentado ou não, mas adicionar [cell layoutIfNeeded]antes de devolver o celular resolve seu problema.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    TableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"TestCell"];
    NSUInteger n1 = firstLabelWordCount[indexPath.row];
    NSUInteger n2 = secondLabelWordCount[indexPath.row];
    [cell setNumberOfWordsForFirstLabel:n1 secondLabel:n2];

    [cell layoutIfNeeded]; // <- added

    return cell;
}

3
Eu me pergunto, por que é necessário apenas na primeira vez? Funciona tão bem se eu ligar para -layoutIfNeededno do celular -awakeFromNib. Prefiro ligar apenas para layoutIfNeededonde sei por que é necessário.
blackp

2
Funcionou para mim! E eu adoraria saber por que é necessário!
Mustafa

Não funcionou se você renderizar a classe de tamanho, que é diferente de qualquer X qualquer
Andrei Konstantinov

@AndreyKonstantinov Sim, isso não funciona com classe de tamanho, como faço para que funcione com classe de tamanho?
z22

Funciona sem classe de tamanho, alguma solução com classe de tamanho?
Yuvrajsinh 02 de

38

Isso funcionou para mim quando outras soluções semelhantes não:

override func didMoveToSuperview() {
    super.didMoveToSuperview()
    layoutIfNeeded()
}

Este parece ser um bug real, já que estou muito familiarizado com o AutoLayout e como usar UITableViewAutomaticDimension, no entanto, ainda ocasionalmente me deparo com esse problema. Estou feliz por finalmente encontrar algo que funciona como uma solução alternativa.


1
Melhor do que a resposta aceita, pois só é chamada quando a célula está sendo carregada do xib em vez de cada exibição de célula. Muito menos linhas de código também.
Robert Wagstaff,

3
Não se esqueça de ligarsuper.didMoveToSuperview()
cicerocamargo

@cicerocamargo Apple diz sobre didMoveToSuperview: "A implementação padrão deste método não faz nada."
Ivan Smetanin,

4
@IvanSmetanin não importa se a implementação padrão não faz nada, você ainda deve chamar o método super, pois ele pode realmente fazer algo no futuro.
TNguyen

(A propósito, você DEFINITIVAMENTE deve chamar super. Sobre isso. Nunca vi um projeto em que a equipe em geral não subclasse mais de uma célula, então, é claro que você deve ter certeza de pegar todas elas. E como um problema separado, exatamente o que TNguyen disse.)
Fattie,

22

Adicionando [cell layoutIfNeeded]em cellForRowAtIndexPathnão funciona para células que são inicialmente enrolado out-of-view.

Nem prefaciar com [cell setNeedsLayout].

Você ainda tem que rolar certas células para fora e de volta à vista para que sejam redimensionadas corretamente.

Isso é muito frustrante, já que a maioria dos desenvolvedores possui Dynamic Type, AutoLayout e Self-Sizing Cells funcionando corretamente - exceto neste caso irritante. Este bug afeta todos os meus controladores de visualização de tabela "mais altos".


O mesmo para mim. quando eu rolar pela primeira vez, o tamanho da célula é 44px. se eu rolar para tirar a célula de vista e voltar, ela será dimensionada corretamente. Você achou alguma solução?
Máximo de

Obrigado @ ashy_32bit isso resolveu para mim também.
ImpurestClub de

parece ser um bug normal, @Scenario certo? coisas horríveis.
Fattie

3
Usar em [cell layoutSubviews]vez de layoutIfNeeded pode ser uma solução possível. Consulte stackoverflow.com/a/33515872/1474113
ypresto

14

Tive a mesma experiência em um de meus projetos.

Por que isso acontece?

Célula projetada no Storyboard com alguma largura para algum dispositivo. Por exemplo, 400px. Por exemplo, sua etiqueta tem a mesma largura. Quando carrega do storyboard, tem largura de 400 px.

Aqui está um problema:

tableView:heightForRowAtIndexPath: chamado antes do layout da célula é subvisualizações.

Portanto, ele calculou a altura do rótulo e da célula com largura de 400 px. Mas você roda em aparelho com tela, por exemplo, 320px. E esta altura calculada automaticamente está incorreta. Só porque o celular layoutSubviewssó acontece depois. tableView:heightForRowAtIndexPath: Mesmo que você configure preferredMaxLayoutWidthseu rótulo manualmente, layoutSubviewsisso não ajuda.

Minha solução:

1) Subclasse UITableViewe substituição dequeueReusableCellWithIdentifier:forIndexPath:. Defina a largura da célula igual à largura da tabela e o layout da célula de força.

- (UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [super dequeueReusableCellWithIdentifier:identifier forIndexPath:indexPath];
    CGRect cellFrame = cell.frame;
    cellFrame.size.width = self.frame.size.width;
    cell.frame = cellFrame;
    [cell layoutIfNeeded];
    return cell;
}

2) Subclasse UITableViewCell. Defina preferredMaxLayoutWidthmanualmente para seus rótulos em layoutSubviews. Além disso, você precisa do layout manualmente contentView, porque ele não faz o layout automaticamente após a mudança do quadro da célula (não sei por que, mas é)

- (void)layoutSubviews {
    [super layoutSubviews];
    [self.contentView layoutIfNeeded];
    self.yourLongTextLabel.preferredMaxLayoutWidth = self.yourLongTextLabel.width;
}

1
Quando me deparei com esse problema, também precisava garantir que o quadro do tableview estava correto, então chamei [self.tableview layoutIfNeeded] antes que o tableview fosse preenchido (em viewDidLoad).
GK100

Nunca parei para pensar que tinha a ver com uma largura incorreta. em cell.frame.size.width = tableview.frame.widthseguida cell.layoutIfNeeded(), a cellForRowAtfunção funcionou para mim
Cam Connor

10

Eu tenho um problema semelhante, no primeiro carregamento, a altura da linha não foi calculada, mas depois de alguma rolagem ou vá para outra tela e volto a esta tela as linhas são calculadas. No primeiro carregamento, meus itens são carregados da internet e no segundo carregamento meus itens são carregados primeiro do Core Data e recarregados da internet e eu percebi que a altura das linhas são calculadas no recarregamento da internet. Portanto, percebi que quando tableView.reloadData () é chamado durante a animação segue (mesmo problema com push e segue segue), a altura da linha não foi calculada. Então, escondi o tableview na inicialização do modo de exibição e coloquei um carregador de atividade para evitar um efeito desagradável para o usuário e chamei tableView.reloadData após 300 ms e agora o problema está resolvido. Acho que é um bug do UIKit, mas essa solução alternativa resolve o problema.

Eu coloquei essas linhas (Swift 3.0) no meu gerenciador de conclusão de carga de item

DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300), execute: {
        self.tableView.isHidden = false
        self.loader.stopAnimating()
        self.tableView.reloadData()
    })

Isso explica por que, para algumas pessoas, colocar reloadData em layoutSubviews resolve o problema


Este é o único WA que funcionou para mim também. Exceto que eu não uso isHidden, mas configurei o alfa da tabela para 0 ao adicionar a fonte de dados e, em seguida, para 1 após recarregar.
PJ_Finnegan

6

nenhuma das soluções acima funcionou para mim, o que funcionou é esta receita de mágica: chame-os nesta ordem:

tableView.reloadData()
tableView.layoutIfNeeded() tableView.beginUpdates() tableView.endUpdates()

meus dados do tableView são preenchidos a partir de um serviço da web, no retorno de chamada da conexão, escrevo as linhas acima.


1
Para o swift 5 isso funcionou até mesmo na visualização de coleção, Deus te abençoe, mano.
Govani Dhruv Vijaykumar

Para mim, isso retornou a altura e o layout corretos da célula, mas não tinha dados
Darrow Hartman

Tente setNeedsDisplay () após essas linhas
JAHelia

5

No meu caso, a última linha do UILabel foi truncada quando a célula foi exibida pela primeira vez. Aconteceu de forma bastante aleatória e a única maneira de dimensionar corretamente era rolar a célula para fora da visualização e trazê-la de volta. Tentei todas as soluções possíveis exibidas até agora (layoutIfNeeded..reloadData), mas nada funcionou para mim. O truque era definir "Autoshrink" para Minimuum Font Scale (0,5 para mim). De uma chance


isso funcionou para mim, sem aplicar nenhuma das outras soluções listadas acima. Ótimo achado.
mckeejm,

2
Mas isso autoshrinks o texto ... por que alguém iria querer ter tamanhos diferentes de texto em uma exibição de tabela? Parece terrível.
Lucius Degeer

5

Adicione uma restrição para todo o conteúdo dentro de uma célula personalizada de visualização de tabela, em seguida, estime a altura da linha de visualização de tabela e defina a altura da linha para dimensão automática com uma carga viewdid:

    override func viewDidLoad() {
    super.viewDidLoad()

    tableView.estimatedRowHeight = 70
    tableView.rowHeight = UITableViewAutomaticDimension
}

Para corrigir esse problema de carregamento inicial, aplique o método layoutIfNeeded em uma célula de visualização de tabela personalizada:

class CustomTableViewCell: UITableViewCell {

override func awakeFromNib() {
    super.awakeFromNib()
    self.layoutIfNeeded()
    // Initialization code
}
}

É importante definir estimatedRowHeightum valor> 0 e não UITableViewAutomaticDimension(que é -1), caso contrário, a altura da linha automática não funcionará.
PJ_Finnegan

5

Tentei a maioria das respostas a esta pergunta e não consegui fazer nenhuma delas funcionar. A única solução funcional que encontrei foi adicionar o seguinte à minha UITableViewControllersubclasse:

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    UIView.performWithoutAnimation {
        tableView.beginUpdates()
        tableView.endUpdates()
    }
}

A UIView.performWithoutAnimationchamada é necessária, caso contrário, você verá a animação normal da visualização da tabela conforme o controlador da visualização carrega.


funcionando e definitivamente melhor do que invocar reloadData duas vezes
Jimmy George Thomas

2
Magia negra. Fazer em viewWillAppearnão funcionou para mim, mas fazê-lo em viewDidAppearsim.
Martin,

Esta solução 'funcionou' para mim quando implementada no viewDidAppear. Não é, de forma alguma, ideal para a experiência do usuário, pois o conteúdo da tabela muda conforme ocorre o dimensionamento correto.
richardpiazza

incrível, eu tentei muitos exemplos, mas falhou, você é um demônio
Shakeel Ahmed

O mesmo que @martin
AJ Hernandez


3

ligar para cell.layoutIfNeeded()dentro cellForRowAtfuncionou para mim no iOS 10 e no iOS 11, mas não no iOS 9.

para conseguir esse trabalho no ios 9 também, ligo cell.layoutSubviews()e funcionou.


2

Capturando a captura de tela para sua referênciaPara mim, nenhuma dessas abordagens funcionou, mas descobri que o rótulo tinha um Preferred Widthconjunto explícito no Interface Builder. Removendo isso (desmarcando "Explícito") e usando UITableViewAutomaticDimensionfuncionou como esperado.


2

Tentei todas as soluções nesta página, mas desmarcar usar classes de tamanho e, em seguida, verificar novamente resolveu meu problema.

Edit: Desmarcar as classes de tamanho causa muitos problemas no storyboard, então tentei outra solução. Preenchi minha table view em meu view controller viewDidLoade viewWillAppearmétodos. Isso resolveu meu problema.


1

Tenho o problema de redimensionar o rótulo, então preciso apenas fazer
chatTextLabel.text = chatMessage.message chatTextLabel? .UpdateConstraints () após configurar o texto

// código completo

func setContent() {
    chatTextLabel.text = chatMessage.message
    chatTextLabel?.updateConstraints()

    let labelTextWidth = (chatTextLabel?.intrinsicContentSize().width) ?? 0
    let labelTextHeight = chatTextLabel?.intrinsicContentSize().height

    guard labelTextWidth < originWidth && labelTextHeight <= singleLineRowheight else {
      trailingConstraint?.constant = trailingConstant
      return
    }
    trailingConstraint?.constant = trailingConstant + (originWidth - labelTextWidth)

  }

1

No meu caso, fui atualizando em outro ciclo. Portanto, a altura de tableViewCell foi atualizada após a configuração de labelText. Excluí o bloco assíncrono.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
     let cell = tableView.dequeueReusableCell(withIdentifier:Identifier, for: indexPath) 
     // Check your cycle if update cycle is same or not
     // DispatchQueue.main.async {
        cell.label.text = nil
     // }
}

Eu também tenho um bloco assíncrono, mas é necessário? Não há alternativa?
Nazar Medeiros

1

Apenas certifique-se de não definir o texto do rótulo no método delegado 'willdisplaycell' de visualização de tabela. Defina o texto do rótulo no método delegado 'cellForRowAtindexPath' para cálculo de altura dinâmica.

De nada :)


1

No meu caso, uma exibição de pilha na célula estava causando o problema. Aparentemente, é um bug. Depois de removê-lo, o problema foi resolvido.


1
Tentei todas as outras soluções, apenas a sua solução funcionou, obrigado por compartilhá-la
tamtoum1987

1

O problema é que as células iniciais são carregadas antes de termos uma altura de linha válida. A solução alternativa é forçar o recarregamento da tabela quando a exibição for exibida.

- (void)viewDidAppear:(BOOL)animated
{
  [super viewDidAppear:animated];
  [self.tableView reloadData];
}

esta foi a solução muito melhor que me ajudou. Não corrigiu 100% da célula, mas até 80% estavam tomando seu tamanho real. Muito obrigado
TheTravloper

1

Apenas para iOS 12+, 2019 em diante ...

Um exemplo contínuo da incompetência bizarra ocasional da Apple, onde os problemas duram literalmente anos.

Parece ser o caso de

        cell.layoutIfNeeded()
        return cell

vai consertar isso. (Você está perdendo algum desempenho, é claro.)

Assim é a vida com a Apple.


0

No meu caso, o problema com a altura da célula ocorre depois que a visualização da tabela inicial é carregada e ocorre uma ação do usuário (tocar em um botão em uma célula que tem o efeito de alterar a altura da célula). Não consegui fazer a célula mudar sua altura, a menos que eu:

[self.tableView reloadData];

Eu tentei

[cell layoutIfNeeded];

mas isso não funcionou.


0

No Swift 3. Tive que chamar self.layoutIfNeeded () cada vez que atualizo o texto da célula reutilizável.

import UIKit
import SnapKit

class CommentTableViewCell: UITableViewCell {

    static let reuseIdentifier = "CommentTableViewCell"

    var comment: Comment! {
        didSet {
            textLbl.attributedText = comment.attributedTextToDisplay()
            self.layoutIfNeeded() //This is a fix to make propper automatic dimentions (height).
        }
    }

    internal var textLbl = UILabel()

    override func layoutSubviews() {
        super.layoutSubviews()

        if textLbl.superview == nil {
            textLbl.numberOfLines = 0
            textLbl.lineBreakMode = .byWordWrapping
            self.contentView.addSubview(textLbl)
            textLbl.snp.makeConstraints({ (make) in
                make.left.equalTo(contentView.snp.left).inset(10)
                make.right.equalTo(contentView.snp.right).inset(10)
                make.top.equalTo(contentView.snp.top).inset(10)
                make.bottom.equalTo(contentView.snp.bottom).inset(10)
            })
        }
    }
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let comment = comments[indexPath.row]
        let cell = tableView.dequeueReusableCell(withIdentifier: CommentTableViewCell.reuseIdentifier, for: indexPath) as! CommentTableViewCell
        cell.selectionStyle = .none
        cell.comment = comment
        return cell
    }

commentsTableView.rowHeight = UITableViewAutomaticDimension
    commentsTableView.estimatedRowHeight = 140

0

Nenhuma das soluções acima funcionou, mas a seguinte combinação de sugestões funcionou.

Tive que adicionar o seguinte em viewDidLoad ().

DispatchQueue.main.async {

        self.tableView.reloadData()

        self.tableView.setNeedsLayout()
        self.tableView.layoutIfNeeded()

        self.tableView.reloadData()

    }

A combinação acima de reloadData, setNeedsLayout e layoutIfNeeded funcionou, mas nenhuma outra. No entanto, pode ser específico para as células do projeto. E sim, tive que invocar reloadData duas vezes para fazê-lo funcionar.

Defina também o seguinte em viewDidLoad

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = MyEstimatedHeight

Em tableView (_ tableView: UITableView, cellForRowAt indexPath: IndexPath)

cell.setNeedsLayout()
cell.layoutIfNeeded() 

Meu semelhante reloadData/beginUpdates/endUpdates/reloadDatatambém está funcionando; reloadData deve ser chamado uma segunda vez. Não precisa embrulhar async.
raio de

0

Corri para esse problema e corrigi-lo movendo meu código de inicialização view / label DE tableView(willDisplay cell:)PARA tableView(cellForRowAt:).


que NÃO é a maneira recomendada de inicializar uma célula. willDisplayterá um desempenho melhor do que cellForRowAt. Use o último apenas para instanciar a célula certa.
Martin,

2 anos depois, estou lendo aquele antigo comentário que escrevi. Este foi um conselho ruim. Mesmo que willDisplaytenha melhor desempenho, é recomendável inicializar sua UI de célula cellForRowAtquando o layout for automático. Na verdade, o layout é calculado por UIKit após cellForRowAt et antes de willDisplay. Portanto, se a altura de sua célula depende de seu conteúdo, inicialize o conteúdo do rótulo (ou qualquer outro) em cellForRowAt.
Martin

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


//  call the method dynamiclabelHeightForText
}

use o método acima que retorna a altura da linha dinamicamente. E atribua a mesma altura dinâmica à etiqueta que você está usando.

-(int)dynamiclabelHeightForText:(NSString *)text :(int)width :(UIFont *)font
{

    CGSize maximumLabelSize = CGSizeMake(width,2500);

    CGSize expectedLabelSize = [text sizeWithFont:font
                                constrainedToSize:maximumLabelSize
                                    lineBreakMode:NSLineBreakByWordWrapping];


    return expectedLabelSize.height;


}

Este código ajuda a encontrar a altura dinâmica do texto exibido no rótulo.


Na verdade, Ramesh, isso não deve ser necessário, desde que as propriedades tableView.estimatedRowHeight e tableView.rowHeight = UITableViewAutomaticDimension estejam definidas. Além disso, certifique-se de que a célula customizada tenha restrições apropriadas nos vários widgets e no contentView.
BonanzaDriver de
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.