reloadData () de UITableView avec des hauteurs de cellule dynamiques provoque un défilement instable


142

J'ai l'impression que cela pourrait être un problème courant et je me demandais s'il y avait une solution commune à ce problème.

Fondamentalement, mon UITableView a des hauteurs de cellule dynamiques pour chaque cellule. Si je ne suis pas en haut de UITableView et moi tableView.reloadData(), le défilement vers le haut devient instable.

Je pense que cela est dû au fait que, parce que j'ai rechargé des données, au fur et à mesure que je défile vers le haut, l'UITableView recalcule la hauteur de chaque cellule qui devient visible. Comment puis-je atténuer cela ou comment recharger uniquement les données d'un certain IndexPath à la fin de UITableView?

De plus, lorsque je parviens à faire défiler tout le chemin vers le haut, je peux faire défiler vers le bas puis vers le haut, sans problème sans sauter. Cela est probablement dû au fait que les hauteurs UITableViewCell ont déjà été calculées.


Quelques choses ... (1) Oui, vous pouvez certainement recharger certaines lignes en utilisant reloadRowsAtIndexPaths. Mais (2) qu'entendez-vous par «nerveux» et (3) avez-vous défini une hauteur de ligne estimée? (J'essaie simplement de savoir s'il existe une meilleure solution qui vous permettrait de mettre à jour le tableau de manière dynamique.)
Lyndsey Scott

@LyndseyScott, oui, j'ai défini une hauteur de ligne estimée. Par nerveux, je veux dire que lorsque je fais défiler vers le haut, les lignes se déplacent vers le haut. Je pense que c'est parce que j'ai défini une hauteur de ligne estimée à 128, puis lorsque je fais défiler vers le haut, tous mes messages ci-dessus dans UITableView sont plus petits, ce qui réduit la hauteur, ce qui fait sauter ma table. Je pense faire reloadRowsAtIndexPaths de la ligne xà la dernière ligne de mon TableView ... mais parce que j'insère de nouvelles lignes, cela ne fonctionnera pas, je ne peux pas savoir quelle sera la fin de ma tableview avant de recharger les données.
David

2
@LyndseyScott je ne peux toujours pas résoudre le problème, y a-t-il une bonne solution?
rad

1
Avez-vous déjà trouvé une solution à ce problème? Je rencontre exactement le même problème que celui observé dans votre vidéo.
user3344977

1
Aucune des réponses ci-dessous n'a fonctionné pour moi.
Srujan Simha

Réponses:


221

Pour éviter de sauter, vous devez enregistrer les hauteurs des cellules lors du chargement et donner une valeur exacte dans tableView:estimatedHeightForRowAtIndexPath:

Rapide:

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
}

Objectif 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
Merci, vous sauvez vraiment ma journée :) Fonctionne aussi dans objc
Artem Z.

3
N'oubliez pas d'initialiser cellHeightsDictionary: cellHeightsDictionary = [NSMutableDictionary dictionary];
Gerharbo

1
estimatedHeightForRowAtIndexPath:renvoie une valeur double peut provoquer une *** Assertion failure in -[UISectionRowData refreshWithSection:tableView:tableViewRowData:]erreur. Pour le réparer, à la return floorf(height.floatValue);place.
liushuaikobe

Salut @lgor, j'ai le même problème et j'essaie de mettre en œuvre votre solution. Le problème que j'obtiens est estimeHeightForRowAtIndexPath est appelé avant willDisplayCell, de sorte que la hauteur de la cellule n'est pas calculée lors de l'appel estiméHeightForRowAtIndexPath. De l'aide?
Madhuri

1
Les hauteurs effectives @Madhuri doivent être calculées dans "heightForRowAtIndexPath", qui est appelée pour chaque cellule à l'écran juste avant willDisplayCell, qui définira la hauteur dans le dictionnaire pour une utilisation ultérieure dans EstimedRowHeight (lors du rechargement de la table).
Donnit le

109

Version Swift 3 de la réponse acceptée.

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 
}

Merci, cela a très bien fonctionné! en fait, j'ai pu supprimer mon implémentation de func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {, cela gère tout le calcul de hauteur dont j'ai besoin.
Natalia

Après avoir lutté pendant de nombreuses heures avec des sauts persistants, j'ai compris que j'avais oublié d'ajouter UITableViewDelegateà ma classe. Se conformer à ce protocole est nécessaire car il contient la willDisplayfonction indiquée ci-dessus . J'espère que je pourrai sauver quelqu'un de la même lutte.
MJQZ1347

Merci pour la réponse Swift. Dans mon cas, j'avais un comportement SUPER étrange de cellules qui ne fonctionnaient pas lors du rechargement lorsque la vue du tableau faisait défiler vers / près du bas. Je vais l'utiliser à partir de maintenant chaque fois que j'ai des cellules auto-dimensionnantes.
Trev14

Fonctionne parfaitement dans Swift 4.2
Adam S.

Un sauveur de vie. Tellement utile lorsque vous essayez d'ajouter plus d'éléments dans la source de données. Empêche le saut des cellules nouvellement ajoutées au centre de l'écran.
Philip Borbon

38

Le saut est dû à une mauvaise hauteur estimée. Plus le estiméRowHeight diffère de la hauteur réelle, plus la table peut sauter lorsqu'elle est rechargée, en particulier plus elle a été défilée vers le bas. En effet, la taille estimée du tableau diffère radicalement de sa taille réelle, ce qui oblige le tableau à ajuster la taille et le décalage de son contenu. Ainsi, la hauteur estimée ne doit pas être une valeur aléatoire mais proche de ce que vous pensez que la hauteur va être. J'ai également expérimenté quand je définis UITableViewAutomaticDimension si vos cellules sont du même type alors

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

si vous avez une variété de cellules dans différentes sections, je pense que le meilleur endroit est

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

2
merci, c'est exactement pourquoi mon tableView était si nerveux.
Louis de Decker

1
Une vieille réponse, mais elle est toujours d'actualité à partir de 2018. Contrairement à toutes les autres réponses, celle-ci suggère de définir estiméRowHeigh une fois dans viewDidLoad, ce qui aide lorsque les cellules sont de hauteur identique ou très similaire. Merci. BTW, ou esimatedRowHeight peut être défini via Interface Builder dans Inspecteur de taille> Vue de tableau> Estimation.
Vitalii

fourni une hauteur estimée plus précise m'a aidé. J'ai également eu un style de vue de tableau groupé à plusieurs sections et tableView(_:estimatedHeightForHeaderInSection:)
j'ai

25

La réponse @Igor fonctionne bien dans ce cas, leSwift-4code de celui-ci.

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

dans les méthodes suivantes 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
Comment gérer l'insertion / la suppression de lignes à l'aide de cette solution? TableView saute, car les données du dictionnaire ne sont pas réelles.
Alexey Chekanov

1
fonctionne très bien! en particulier sur la dernière cellule lors du rechargement de la ligne.
Ning

19

J'ai essayé toutes les solutions de contournement ci-dessus, mais rien n'a fonctionné.

Après avoir passé des heures et traversé toutes les frustrations possibles, j'ai trouvé un moyen de résoudre ce problème. Cette solution est un sauveur de vie! A travaillé comme un charme!

Swift 4

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

Je l'ai ajouté en tant qu'extension, pour rendre le code plus propre et éviter d'écrire toutes ces lignes à chaque fois que je veux recharger.

extension UITableView {

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

enfin ..

tableView.reloadWithoutAnimation()

OU vous pouvez ajouter ces lignes à votre UITableViewCell awakeFromNib()méthode

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

et fais normal reloadData()


1
Comment cela fait-il un rechargement? Vous appelez cela reloadWithoutAnimationmais où est la reloadpartie?
mat

@matt vous pouvez appeler en tableView.reloadData()premier et ensuite tableView.reloadWithoutAnimation(), cela fonctionne toujours.
Srujan Simha

Génial! Rien de ce qui précède n'a fonctionné pour moi non plus. Même toutes les hauteurs et les hauteurs estimées sont totalement identiques. Intéressant.
TY Kucuk

1
Ne travaille pas pour moi. C'est un crash à tableView.endUpdates (). Est-ce que quelqu'un peut m'aider!
Kakashi

12

J'utilise plus de moyens pour résoudre ce problème:

Pour le contrôleur de vue:

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 
}

comme extension pour UITableView

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

Le résultat est

tableView.reloadSectionWithouAnimation(section: indexPath.section)

1
La clé pour moi était d'implémenter son extension UITableView ici. Très intelligent. Merci rastislv
BennyTheNerd

Fonctionne parfaitement mais il n'a qu'un seul inconvénient, vous perdez l'animation lors de l'insertion d'en-tête, de pied de page ou de ligne.
Soufian Hossam

Où reloadSectionWithouAnimation serait-il appelé? Ainsi, par exemple, les utilisateurs peuvent publier une image dans mon application (comme Instagram); Je peux faire redimensionner les images, mais dans la plupart des cas, je dois faire défiler la cellule du tableau pour que cela se produise. Je veux que la cellule soit de la bonne taille une fois que le tableau passe par reloadData.
Luke Irvin

11

Je suis tombé sur ceci aujourd'hui et j'ai observé:

  1. C'est seulement iOS 8, en effet.
  2. Overridding cellForRowAtIndexPathn'aide pas.

Le correctif était en fait assez simple:

Remplacez estimatedHeightForRowAtIndexPathet assurez-vous qu'il renvoie les valeurs correctes.

Avec cela, tous les tremblements et sauts bizarres dans mes UITableViews se sont arrêtés.

REMARQUE: je connais en fait la taille de mes cellules. Il n'y a que deux valeurs possibles. Si vos cellules sont vraiment de taille variable, alors vous voudrez peut - être mettre en cache la cell.bounds.size.heightdetableView:willDisplayCell:forRowAtIndexPath:


2
Correction du problème lors du remplacement de la méthode EstimatedHeightForRowAtIndexPath avec une valeur élevée, par exemple 300f
Flappy

1
@Flappy il est intéressant de savoir comment la solution fournie par vous fonctionne et est plus courte que les autres techniques suggérées. Pensez à l'afficher comme réponse.
Rohan Sanap

9

Vous ne pouvez en fait recharger que certaines lignes en utilisant reloadRowsAtIndexPaths, par exemple:

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

Mais, en général, vous pouvez également animer les changements de hauteur de cellule du tableau comme ceci:

tableView.beginUpdates()
tableView.endUpdates()

J'ai essayé la méthode beginUpdates / endUpdates, mais cela n'affecte que les lignes visibles de ma table. J'ai toujours le problème lorsque je fais défiler vers le haut.
David

@David Probablement parce que vous utilisez des hauteurs de ligne estimées.
Lyndsey Scott

Dois-je me débarrasser de mon EstimatedRowHeights et le remplacer à la place par les paramètres beginUpdates et endUpdates?
David

@David Vous ne "remplaceriez" rien, mais cela dépend vraiment du comportement souhaité ... Si vous voulez utiliser la hauteur estimée des lignes et recharger simplement les index sous la partie visible actuelle du tableau, vous pouvez le faire comme J'ai dit en utilisant reloadRowsAtIndexPaths
Lyndsey Scott

L'un de mes problèmes avec l'essai de la méthode reladRowsAtIndexPaths est que j'implémente le défilement infini, donc lorsque je reloadingData, c'est parce que je viens d'ajouter 15 lignes supplémentaires à la source de données. Cela signifie que les indexPaths de ces lignes n'existent pas encore dans UITableView
David

3

Voici une version un peu plus courte:

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

3

Remplacement de la méthode EstimatedHeightForRowAtIndexPath avec une valeur élevée, par exemple 300f

Cela devrait résoudre le problème :)


2

Il y a un bogue qui, je crois, a été introduit dans iOS11.

C'est à ce moment-là que vous faites une reloadtableView contentOffSetest modifiée de manière inattendue. En fait contentOffset, ne devrait pas changer après un rechargement. Cela a tendance à se produire en raison d'erreurs de calculUITableViewAutomaticDimension

Vous devez enregistrer votre contentOffSetet le remettre à votre valeur enregistrée une fois votre rechargement terminé.

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

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

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

Comment l'utilisez vous?

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

Cette réponse est tirée d' ici


2

Celui-ci a fonctionné pour moi dans Swift4:

extension UITableView {

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

1

Aucune de ces solutions n'a fonctionné pour moi. Voici ce que j'ai fait avec Swift 4 et Xcode 10.1 ...

Dans viewDidLoad (), déclarez la hauteur de ligne dynamique du tableau et créez des contraintes correctes dans les cellules ...

tableView.rowHeight = UITableView.automaticDimension

Aussi dans viewDidLoad (), enregistrez toutes vos nibs de cellule tableView dans tableview comme ceci:

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

Dans tableView heightForRowAt, renvoie une hauteur égale à la hauteur de chaque cellule à 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
    } 

}

Donnez maintenant une hauteur de ligne estimée pour chaque cellule dans tableView EstimatedHeightForRowAt. Soyez aussi précis que possible ...

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
    } 

}

Cela devrait fonctionner...

Je n'avais pas besoin d'enregistrer et de définir contentOffset lors de l'appel de tableView.reloadData ()


1

J'ai 2 hauteurs de cellule différentes.

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

Après avoir ajouté estiméHeightForRowAt , il n'y avait plus de saut.

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

0

Essayez d'appeler cell.layoutSubviews()avant de renvoyer la cellule func cellForRowAtIndexPath(_ indexPath: NSIndexPath) -> UITableViewCell?. C'est un bogue connu dans iOS8.


0

Vous pouvez utiliser les éléments suivants dans 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

J'avais ce comportement de saut et j'ai d'abord pu l'atténuer en définissant la hauteur exacte de l'en-tête estimée (car je n'avais qu'une vue d'en-tête possible), mais les sauts ont alors commencé à se produire spécifiquement à l' intérieur des en-têtes, n'affectant plus l'ensemble du tableau.

Suite aux réponses ici, j'ai eu l'indice qu'il était lié aux animations, donc j'ai trouvé que la vue de table était à l'intérieur d'une vue de pile, et parfois nous appelions à l' stackView.layoutIfNeeded()intérieur d'un bloc d'animation. Ma solution finale était de m'assurer que cet appel ne se produise pas à moins que "vraiment" nécessaire, car la mise en page "si nécessaire" avait des comportements visuels dans ce contexte même quand "pas nécessaire".


0

J'ai eu le même problème. J'avais des données de pagination et de rechargement sans animation mais cela n'aidait pas le défilement à éviter de sauter. J'ai une taille différente d'IPhones, le scroll n'était pas nerveux sur iphone8 mais il était nerveux sur iphone7 +

J'ai appliqué les modifications suivantes sur la fonction viewDidLoad :

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

et mon problème résolu. J'espère que cela vous aide aussi.


0

L'une des approches pour résoudre ce problème que j'ai trouvé est

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

-2

En fait, je trouve que si vous utilisez reloadRowsun problème de saut. Ensuite, vous devriez essayer d'utiliser reloadSectionscomme ceci:

UIView.performWithoutAnimation {
    tableView.reloadSections(NSIndexSet(index: indexPath.section) as IndexSet, with: .none)
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.