reloadData () de UITableView com altura de célula dinâmica causa rolagem irregular


142

Eu sinto que isso pode ser um problema comum e queria saber se havia alguma solução comum para isso.

Basicamente, meu UITableView possui alturas dinâmicas de células para cada célula. Se eu não estiver na parte superior do UITableView e eu tableView.reloadData(), a rolagem para cima se tornará irregular.

Acredito que isso se deve ao fato de que, ao recarregar os dados, enquanto estou rolando a tela para cima, o UITableView está recalculando a altura de cada célula que fica visível. Como mitigar isso ou como recarregar os dados apenas de um determinado IndexPath até o final do UITableView?

Além disso, quando consigo rolar até o topo, posso rolar para baixo e para cima, sem problemas, sem pular. Provavelmente, porque as alturas do UITableViewCell já foram calculadas.


Algumas coisas ... (1) Sim, você pode definitivamente recarregar determinadas linhas usando reloadRowsAtIndexPaths. Mas (2) o que você quer dizer com "nervoso" e (3) você definiu uma altura estimada de linha? (Apenas tentando descobrir se há uma solução melhor que permitiria que você atualizar a tabela dinâmica.)
Lyndsey Scott

@LyndseyScott, sim, eu configurei uma altura de linha estimada. Por nervoso, quero dizer que, enquanto eu rolar para cima, as linhas estão mudando para cima. Acredito que isso ocorra porque defino uma altura estimada de linha de 128 e, à medida que rolo para cima, todas as minhas postagens acima no UITableView são menores, diminuindo a altura, fazendo com que minha tabela salte. Estou pensando em fazer reloadRowsAtIndexPaths da linha xaté a última linha no meu TableView ... mas, como estou inserindo novas linhas, não funcionará, não sei como será o final da minha tableview antes de recarregar os dados.
David

2
@LyndseyScott ainda não consigo resolver o problema, existe alguma boa solução?
rad

1
Você já encontrou uma solução para esse problema? Estou enfrentando exatamente o mesmo problema do seu vídeo.
user3344977

1
Nenhuma das respostas abaixo funcionou para mim.
Srujan Simha

Respostas:


221

Para evitar saltar, salve as alturas das células quando elas carregam e forneça o valor exato em tableView:estimatedHeightForRowAtIndexPath:

Rápido:

var cellHeights = [IndexPath: CGFloat]()

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    cellHeights[indexPath] = cell.frame.size.height
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    return cellHeights[indexPath] ?? UITableView.automaticDimension
}

Objetivo C:

// declare cellHeightsDictionary
NSMutableDictionary *cellHeightsDictionary = @{}.mutableCopy;

// declare table dynamic row height and create correct constraints in cells
tableView.rowHeight = UITableViewAutomaticDimension;

// save height
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    [cellHeightsDictionary setObject:@(cell.frame.size.height) forKey:indexPath];
}

// give exact height value
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSNumber *height = [cellHeightsDictionary objectForKey:indexPath];
    if (height) return height.doubleValue;
    return UITableViewAutomaticDimension;
}

1
Obrigado, vc realmente salvou meu dia :) Trabalha em objc também #
Artem Z.

3
Não se esqueça de inicializar cellHeightsDictionary: cellHeightsDictionary = [NSMutableDictionary dictionary];
Gerharbo 5/11

1
estimatedHeightForRowAtIndexPath:retorna um valor duplo pode causar um *** Assertion failure in -[UISectionRowData refreshWithSection:tableView:tableViewRowData:]erro. Para consertar, em return floorf(height.floatValue);vez disso.
Liushuaikobe

Oi @ lgor, Estou tendo o mesmo problema e tentando implementar sua solução. O problema que estou recebendo é estimado como HightForRowAtIndexPath é chamado antes de willDisplayCell, portanto, a altura da célula não é calculada quando é chamado H estimadoForRowAtIndexPath. Qualquer ajuda?
Madhuri

1
As alturas efetivas do @ Madhuri devem ser calculadas em "heightForRowAtIndexPath", que é chamado para todas as células na tela imediatamente antes do willDisplayCell, que definirá a altura no dicionário para uso posterior em estimadoRowHeight (na atualização da tabela).
31417 Donnit

109

Versão rápida 3 da resposta aceita.

var cellHeights: [IndexPath : CGFloat] = [:]


func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    cellHeights[indexPath] = cell.frame.size.height
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    return cellHeights[indexPath] ?? 70.0 
}

Graças isso funcionou muito bem! na verdade, eu consegui remover minha implementação func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {, isso lida com todo o cálculo de altura que eu preciso.
Natalia

Depois de muitas horas lutando com saltos persistentes, descobri que esqueci de adicionar UITableViewDelegateà minha aula. A conformidade com esse protocolo é necessária porque contém a willDisplayfunção mostrada acima . Espero poder salvar alguém da mesma luta.
MJQZ1347

Obrigado pela resposta Swift. No meu caso, eu estava tendo um comportamento SUPER estranho de células saindo de ordem ao recarregar quando a exibição da tabela foi rolada para / perto da parte inferior. De agora em diante, usarei isso sempre que tiver células de tamanho próprio.
precisa saber é o seguinte

Funciona perfeitamente em Swift 4.2
Adam S.

Um salva-vidas. Tão útil ao tentar adicionar mais itens na fonte de dados. Impede o salto de células recém-adicionadas para o centro da tela.
Philip Borbon

38

O salto é devido a uma altura estimada ruim. Quanto mais o estimado RowHeight diferir da altura real, mais a tabela poderá pular quando for recarregada, especialmente quanto mais abaixo ela for rolada. Isso ocorre porque o tamanho estimado da tabela difere radicalmente do tamanho real, forçando a tabela a ajustar seu tamanho e deslocamento de conteúdo. Portanto, a altura estimada não deve ser um valor aleatório, mas perto do que você pensa que a altura será. Eu também experimentei quando defino UITableViewAutomaticDimension se suas células são do mesmo tipo então

func viewDidLoad() {
     super.viewDidLoad()
     tableView.estimatedRowHeight = 100//close to your cell height
}

se você tem variedade de células em seções diferentes, acho que o melhor lugar é

func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
     //return different sizes for different cells if you need to
     return 100
}

2
obrigado, é exatamente por isso que o meu tableView estava tão nervoso.
Louis de Decker

1
Uma resposta antiga, mas ainda é atual a partir de 2018. Ao contrário de todas as outras respostas, essa sugere a configuração de EstimadaRowHeigh uma vez em viewDidLoad, que ajuda quando as células têm a mesma ou a mesma altura. Thanx. Em alternativa, esimatedRowHeight pode ser definido através do Interface Builder em Inspetor de tamanhos> Exibição de tabela> Estimativa.
Vitalii

desde que uma altura estimada mais precisa me ajudasse. Eu também tive um multi-seção agrupados estilo exibição de tabela, e teve que implementartableView(_:estimatedHeightForHeaderInSection:)
nteissler

25

Resposta @Igor está funcionando bem neste caso, oSwift-4código dela.

// declaration & initialization  
var cellHeightsDictionary: [IndexPath: CGFloat] = [:]  

nos seguintes métodos de UITableViewDelegate

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
  // print("Cell height: \(cell.frame.size.height)")
  self.cellHeightsDictionary[indexPath] = cell.frame.size.height
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
  if let height =  self.cellHeightsDictionary[indexPath] {
    return height
  }
  return UITableView.automaticDimension
}

6
Como lidar com a inserção / exclusão de linhas usando esta solução? O TableView salta, pois os dados do dicionário não são reais.
Alexey Chekanov

1
funciona bem! especialmente na última célula ao recarregar a linha.
Ning

19

Eu tentei todas as soluções alternativas acima, mas nada funcionou.

Depois de passar horas e passar por todas as frustrações possíveis, descobri uma maneira de consertar isso. Esta solução é um salvador de vida! Trabalhou como um encanto!

Swift 4

let lastContentOffset = tableView.contentOffset
tableView.beginUpdates()
tableView.endUpdates()
tableView.layer.removeAllAnimations()
tableView.setContentOffset(lastContentOffset, animated: false)

Eu o adicionei como uma extensão, para tornar o código mais limpo e evitar escrever todas essas linhas toda vez que eu quiser recarregar.

extension UITableView {

    func reloadWithoutAnimation() {
        let lastScrollOffset = contentOffset
        beginUpdates()
        endUpdates()
        layer.removeAllAnimations()
        setContentOffset(lastScrollOffset, animated: false)
    }
}

finalmente ..

tableView.reloadWithoutAnimation()

OU você pode realmente adicionar essas linhas no seu UITableViewCell awakeFromNib()método

layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale

e faz normal reloadData()


1
Como isso recarrega? Você chama , reloadWithoutAnimationmas onde está a reloadparte?
Matt

@matt você pode ligar tableView.reloadData()primeiro e depois tableView.reloadWithoutAnimation()ainda funciona.
Srujan Simha 12/11/19

Ótimo! Nenhuma das opções acima também não funcionou para mim. Mesmo todas as alturas e alturas estimadas são totalmente iguais. Interessante.
TY Kucuk

1
Não trabalhe para mim. Está travado em tableView.endUpdates (). Alguém pode me ajudar!
Kakashi

12

Eu uso mais maneiras de corrigi-lo:

Para o controlador de exibição:

var cellHeights: [IndexPath : CGFloat] = [:]


func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    cellHeights[indexPath] = cell.frame.size.height
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    return cellHeights[indexPath] ?? 70.0 
}

como a extensão para UITableView

extension UITableView {
  func reloadSectionWithouAnimation(section: Int) {
      UIView.performWithoutAnimation {
          let offset = self.contentOffset
          self.reloadSections(IndexSet(integer: section), with: .none)
          self.contentOffset = offset
      }
  }
}

O resultado é

tableView.reloadSectionWithouAnimation(section: indexPath.section)

1
A chave para mim foi implementar sua extensão UITableView aqui. Muito esperto. Obrigado rastislv
BennyTheNerd

Funciona perfeitamente, mas possui apenas uma desvantagem: você perde a animação ao inserir cabeçalho, rodapé ou linha.
Soufian Hossam

Onde reloadSectionWithouAnimation seria chamado? Por exemplo, os usuários podem postar uma imagem no meu aplicativo (como o Instagram); Posso redimensionar as imagens, mas na maioria dos casos tenho que rolar a célula da tabela para que isso aconteça. Quero que a célula tenha o tamanho correto assim que a tabela passar por reloadData.
Luke Irvin

11

Encontrei isso hoje e observei:

  1. É apenas o iOS 8, de fato.
  2. Substituir cellForRowAtIndexPathnão ajuda.

A correção foi realmente bem simples:

Substitua estimatedHeightForRowAtIndexPathe verifique se ele retorna os valores corretos.

Com isso, todos os estranhos tremores e pulos no UITableViews pararam.

NOTA: Eu realmente sei o tamanho das minhas células. Existem apenas dois valores possíveis. Se as células são realmente de tamanho variável, então você pode querer armazenar em cache a cell.bounds.size.heightpartir detableView:willDisplayCell:forRowAtIndexPath:


2
Foi corrigido o problema substituindo o método estimadoHeightForRowAtIndexPath por um valor alto, por exemplo, 300f
Flappy

1
@ Flappy, é interessante como a solução fornecida por você funciona e é mais curta que outras técnicas sugeridas. Considere publicá-lo como uma resposta.
Rohan Sanap 20/03/19

9

Na verdade, você pode recarregar apenas determinadas linhas usando reloadRowsAtIndexPaths, ex:

tableView.reloadRowsAtIndexPaths(indexPathArray, withRowAnimation: UITableViewRowAnimation.None)

Mas, em geral, você também pode animar as alterações na altura da célula da tabela da seguinte maneira:

tableView.beginUpdates()
tableView.endUpdates()

Eu tentei o método beginUpdates / endUpdates, mas isso afeta apenas as linhas visíveis da minha tabela. Ainda tenho o problema ao rolar para cima.
David

@ David Provavelmente porque você está usando alturas de linha estimadas.
precisa

Devo me livrar do meu EstimatedRowHeights e substituí-lo por beginUpdates e endUpdates?
David

@ David Você não estaria "substituindo" nada, mas isso realmente depende do comportamento desejado ... Se você quiser usar a altura estimada das linhas e apenas recarregar os índices abaixo da parte visível atual da tabela, faça o seguinte: Eu disse usando reloadRowsAtIndexPaths
Lyndsey Scott

Um dos meus problemas ao tentar o método reladRowsAtIndexPaths é que estou implementando rolagem infinita; portanto, quando reloadingData, é porque acabei de adicionar mais 15 linhas ao dataSource. Isso significa que os indexPaths para essas linhas ainda não existem no UITableView
David

3

Aqui está uma versão um pouco mais curta:

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    return self.cellHeightsDictionary[indexPath] ?? UITableViewAutomaticDimension
}

3

Substituindo o método estimadoHeightForRowAtIndexPath por um valor alto, por exemplo, 300f

Isto deve resolver o problema :)


2

Acredito que foi introduzido um erro no iOS11.

É quando você faz um reloadtableView contentOffSetinesperadamente alterado. De fato contentOffset, não deve mudar após uma recarga. Isso costuma ocorrer devido a erros de cálculo deUITableViewAutomaticDimension

Você precisa salvar o seu contentOffSete redefini-lo com o valor salvo após o término da recarga.

func reloadTableOnMain(with offset: CGPoint = CGPoint.zero){

    DispatchQueue.main.async { [weak self] () in

        self?.tableView.reloadData()
        self?.tableView.layoutIfNeeded()
        self?.tableView.contentOffset = offset
    }
}

Como você usa isso?

someFunctionThatMakesChangesToYourDatasource()
let offset = tableview.contentOffset
reloadTableOnMain(with: offset)

Esta resposta foi derivada daqui


2

Este funcionou para mim no Swift4:

extension UITableView {

    func reloadWithoutAnimation() {
        let lastScrollOffset = contentOffset
        reloadData()
        layoutIfNeeded()
        setContentOffset(lastScrollOffset, animated: false)
    }
}

1

Nenhuma dessas soluções funcionou para mim. Aqui está o que eu fiz com o Swift 4 & Xcode 10.1 ...

Em viewDidLoad (), declare a altura da linha dinâmica da tabela e crie restrições corretas nas células ...

tableView.rowHeight = UITableView.automaticDimension

Também em viewDidLoad (), registre todas as pontas de suas células tableView na visualização de tabela como esta:

tableView.register(UINib(nibName: "YourTableViewCell", bundle: nil), forCellReuseIdentifier: "YourTableViewCell")
tableView.register(UINib(nibName: "YourSecondTableViewCell", bundle: nil), forCellReuseIdentifier: "YourSecondTableViewCell")
tableView.register(UINib(nibName: "YourThirdTableViewCell", bundle: nil), forCellReuseIdentifier: "YourThirdTableViewCell")

Em tableView heightForRowAt, retorne a altura igual à altura de cada célula em indexPath.row ...

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

    if indexPath.row == 0 {
        let cell = Bundle.main.loadNibNamed("YourTableViewCell", owner: self, options: nil)?.first as! YourTableViewCell
        return cell.layer.frame.height
    } else if indexPath.row == 1 {
        let cell = Bundle.main.loadNibNamed("YourSecondTableViewCell", owner: self, options: nil)?.first as! YourSecondTableViewCell
        return cell.layer.frame.height
    } else {
        let cell = Bundle.main.loadNibNamed("YourThirdTableViewCell", owner: self, options: nil)?.first as! YourThirdTableViewCell
        return cell.layer.frame.height
    } 

}

Agora forneça uma altura de linha estimada para cada célula em tableView estimadoHeightForRowAt. Seja o mais preciso possível ...

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {

    if indexPath.row == 0 {
        return 400 // or whatever YourTableViewCell's height is
    } else if indexPath.row == 1 {
        return 231 // or whatever YourSecondTableViewCell's height is
    } else {
        return 216 // or whatever YourThirdTableViewCell's height is
    } 

}

Isso deve funcionar ...

Não precisei salvar e definir contentOffset ao chamar tableView.reloadData ()


1

Eu tenho 2 alturas de células diferentes.

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        let cellHeight = CGFloat(checkIsCleanResultSection(index: indexPath.row) ? 130 : 160)
        return Helper.makeDeviceSpecificCommonSize(cellHeight)
    }

Depois que adicionei o estimadoHeightForRowAt , não houve mais pulos.

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    let cellHeight = CGFloat(checkIsCleanResultSection(index: indexPath.row) ? 130 : 160)
    return Helper.makeDeviceSpecificCommonSize(cellHeight)
}

0

Tente ligar cell.layoutSubviews()antes de retornar o celular func cellForRowAtIndexPath(_ indexPath: NSIndexPath) -> UITableViewCell?. É conhecido bug no iOS8.


0

Você pode usar o seguinte em ViewDidLoad()

tableView.estimatedRowHeight = 0     // if have just tableViewCells <br/>

// use this if you have tableview Header/footer <br/>
tableView.estimatedSectionFooterHeight = 0 <br/>
tableView.estimatedSectionHeaderHeight = 0

0

Eu tive esse comportamento de salto e inicialmente consegui atenuá-lo definindo a altura exata estimada do cabeçalho (porque eu tinha apenas uma possível visualização do cabeçalho), no entanto, os saltos começaram a ocorrer dentro dos cabeçalhos especificamente, não afetando mais a tabela inteira.

Seguindo as respostas aqui, eu tinha a pista de que estava relacionada a animações, então descobri que a exibição da tabela estava dentro de uma pilha e, às vezes, chamava-se stackView.layoutIfNeeded()dentro de um bloco de animação. Minha solução final foi garantir que essa chamada não acontecesse, a menos que "realmente" fosse necessário, porque o layout "se necessário" apresentava comportamentos visuais nesse contexto, mesmo quando "não necessário".


0

Eu tive o mesmo problema. Eu paginei e recarreguei os dados sem animação, mas não ajudou o pergaminho a evitar pular. Eu tenho tamanhos diferentes de IPhones, o pergaminho não estava agitado no iphone8, mas estava agitado no iphone7 +

Apliquei as seguintes alterações na função viewDidLoad :

    self.myTableView.estimatedRowHeight = 0.0
    self.myTableView.estimatedSectionFooterHeight = 0
    self.myTableView.estimatedSectionHeaderHeight = 0

e meu problema resolvido. Espero que ajude você também.


0

Uma das abordagens para resolver esse problema que encontrei é

CATransaction.begin()
UIView.setAnimationsEnabled(false)
CATransaction.setCompletionBlock {
   UIView.setAnimationsEnabled(true)
}
tableView.reloadSections([indexPath.section], with: .none)
CATransaction.commit()

-2

Na verdade, eu descobri se você usa reloadRowscausando um problema de salto. Então você deve tentar usar reloadSectionsassim:

UIView.performWithoutAnimation {
    tableView.reloadSections(NSIndexSet(index: indexPath.section) as IndexSet, with: .none)
}
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.