web-dev-qa-db-fra.com

reloadData () de UITableView avec les hauteurs de cellules dynamiques provoque un défilement rapide

Je pense que cela pourrait être un problème commun et je me demandais s'il existait une solution commune à ce problème.

Fondamentalement, mon UITableView a des hauteurs de cellule dynamiques pour chaque cellule. Si je ne suis pas au sommet de UITableView et que je tableView.reloadData(), le défilement devient aléatoire. 

Je pense que cela est dû au fait que, parce que j'ai rechargé des données et que je fais défiler l'écran vers le haut, UITableView recalcule la hauteur de chaque cellule entrant en visibilité. Comment puis-je atténuer cela, ou comment ne recharger des données qu'à partir d'un certain IndexPath jusqu'à la fin de UITableView? 

De plus, quand je parviens à faire défiler tout le chemin jusqu'au sommet, 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.

80
David

Pour éviter les sauts, vous devez enregistrer les hauteurs de cellules lors de leur chargement et donner la valeur exacte dans tableView:estimatedHeightForRowAtIndexPath:

// declare cellHeightsDictionary
NSMutableDictionary *cellHeightsDictionary;

// initialize in code (thanks to @Gerharbo)
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;
}
141
Igor

Swift 3 version de 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 
}
74
Casey Wagner

Le saut est dû à une mauvaise hauteur estimée. Plus le hauteur estimée de la propriété RowReight diffère de la hauteur réelle, plus la table risque de sauter lorsqu'elle est rechargée, en particulier si le défilement est terminé. En effet, la taille estimée de la table diffère radicalement de sa taille réelle, ce qui oblige la table à ajuster la taille et le décalage de son contenu . être. J'ai aussi vécu quand je mets UITableViewAutomaticDimension Si vos cellules sont du même type, puis 

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

si vous avez une variété de cellules dans différentes sections, alors 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
        }
25
Krishna Kishore

@Igoranswer fonctionne correctement dans ce cas, le code Swift-4.

// 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 UITableViewAutomaticDimension
}
21
Kiran Jasvanee

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

  1. C'est iOS 8 seulement, en effet.
  2. Remplacer cellForRowAtIndexPath n'aide pas.

Le correctif était en fait assez simple:

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

Avec cela, tous les trépidations et sautillements étranges dans mes UITableViews se sont arrêtés.

REMARQUE: je connais réellement la taille de mes cellules. Il n'y a que deux valeurs possibles. Si vos cellules ont vraiment une taille variable, vous pouvez alors mettre en cache le cell.bounds.size.height à partir de tableView:willDisplayCell:forRowAtIndexPath:

8
MarcWan

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

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

Mais, en général, vous pouvez également animer les changements de hauteur des cellules du tableau comme suit:

tableView.beginUpdates()
tableView.endUpdates()
6
Lyndsey Scott

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

Après avoir passé des heures et passé en revue toutes les frustrations possibles, nous avons trouvé un moyen de résoudre ce problème. Cette solution est un sauveur de vie! 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 afin de donner au code une apparence plus nette et d'éviter d'écrire toutes ces lignes chaque fois que je souhaite recharger.

extension UITableView {

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

enfin ..

tableView.reloadWithoutAnimation()
5
Srujan Simha

J'utilise plusieurs façons de 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

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

Voici une version un peu plus courte:

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

Celui-ci a fonctionné pour moi dans Swift4:

extension UITableView {

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

J'ai 2 hauteurs de cellules 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é installedHeightForRowAt , il n'y avait plus de sauts.

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

Vous pouvez utiliser ce qui suit 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
Vid

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

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

tableView.rowHeight = UITableView.automaticDimension

Également dans viewDidLoad (), enregistrez tous vos numéros de cellules 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, renvoyez la hauteur égale à la hauteur de chaque cellule dans 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
    } 

}

Maintenant, donnez une hauteur de ligne estimée pour chaque cellule de tableView installedHeightForRowAt. Soyez précis que vous pouvez ...

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 de sauvegarder et de définir contentOffset lors de l'appel de tableView.reloadData ()

0
Michael Colonna

Redéfinition de la méthode ratedHeightForRowAtIndexPath par une valeur élevée, par exemple 300f

Cela devrait résoudre le problème :)

0
Flappy

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

C’est à ce moment-là que vous faites une reload et que tableView contentOffSet est modifié de façon inattendue. En fait, contentOffset ne devrait pas changer après un rechargement. Cela a tendance à se produire en raison d'erreurs de calcul de UITableViewAutomaticDimension

Vous devez enregistrer votre contentOffSet et la rétablir sur votre valeur enregistrée une fois le 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 vous l'utilisez?

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

Cette réponse est dérivée de ici

0
Honey

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

0
CrimeZone