web-dev-qa-db-fra.com

Jerky Scrolling Après la mise à jour de UITableViewCell en place avec UITableViewAutomaticDimension

Je construis une application avec une vue de fil pour les publications soumises par les utilisateurs. Cette vue a une UITableView avec une implémentation UITableViewCell personnalisée. Dans cette cellule, j’ai une autre UITableView pour afficher les commentaires. The Gist est quelque chose comme ceci:

Feed TableView
  PostCell
    Comments (TableView)
      CommentCell
  PostCell
    Comments (TableView)
      CommentCell
      CommentCell
      CommentCell
      CommentCell
      CommentCell

Le flux initial sera téléchargé avec 3 commentaires pour la prévisualisation, mais s'il y a plus de commentaires, ou si l'utilisateur ajoute ou supprime un commentaire, je souhaite mettre à jour la variable PostCell à la place de la vue de la table des flux en ajoutant ou en supprimant CommentCells aux commentaires. table à l'intérieur de la PostCell. J'utilise actuellement l'aide suivante pour accomplir cela:

// (PostCell.Swift) Handle showing/hiding comments
func animateAddOrDeleteComments(startRow: Int, endRow: Int, operation: CellOperation) {
  let table = self.superview?.superview as UITableView

  // "table" is outer feed table
  // self is the PostCell that is updating it's comments
  // self.comments is UITableView for displaying comments inside of the PostCell
  table.beginUpdates()
  self.comments.beginUpdates()

  // This function handles inserting/removing/reloading a range of comments
  // so we build out an array of index paths for each row that needs updating
  var indexPaths = [NSIndexPath]()
  for var index = startRow; index <= endRow; index++ {
    indexPaths.append(NSIndexPath(forRow: index, inSection: 0))
  }

  switch operation {
  case .INSERT:
    self.comments.insertRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.None)
  case .DELETE:
    self.comments.deleteRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.None)
  case .RELOAD:
    self.comments.reloadRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.None)
  }

  self.comments.endUpdates()
  table.endUpdates()

  // trigger a call to updateConstraints so that we can update the height constraint 
  // of the comments table to fit all of the comments
  self.setNeedsUpdateConstraints()
}

override func updateConstraints() {
  super.updateConstraints()
  self.commentsHeight.constant = self.comments.sizeThatFits(UILayoutFittingCompressedSize).height
}

Ceci accomplit la mise à jour très bien. La publication est mise à jour sur place avec des commentaires ajoutés ou supprimés à l'intérieur de la PostCell comme prévu. J'utilise le redimensionnement automatique PostCells dans le tableau des flux. La table de commentaires de la variable PostCell se développe pour afficher tous les commentaires, mais l'animation est un peu saccadée et la sorte de table défile vers le haut et vers le bas d'une dizaine de pixels environ pendant que l'animation de mise à jour de cellule a lieu.

Le saut lors du redimensionnement est un peu gênant, mais mon problème principal vient ensuite. Maintenant, si je fais défiler le flux vers le bas, le défilement est lisse comme avant, mais si je fais défiler au-dessus de la cellule que je viens de redimensionner après avoir ajouté des commentaires, le flux basculera plusieurs fois en arrière avant d'atteindre le sommet du flux. J'ai configuré les cellules de redimensionnement automatique iOS8 comme suit:

// (FeedController.Swift)
// tableView is the feed table containing PostCells
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.estimatedRowHeight = 560

Si je supprime la estimatedRowHeight, le tableau défile vers le haut chaque fois qu'une hauteur de cellule change. Je me sens assez coincé là-dessus maintenant et en tant que nouveau développeur iOS, je pourrais utiliser vos conseils.

71
Bryan Alger

Voici la meilleure solution que j'ai trouvée pour résoudre ce genre de problème (problème de défilement + reloadRows + iOS 8 UITableViewAutomaticDimension);

Cela consiste à conserver toutes les hauteurs dans un dictionnaire et à les mettre à jour (dans le dictionnaire), car la vue tableau affichera la cellule.

Vous retournerez ensuite la hauteur sauvegardée dans la méthode - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath.

Vous devriez implémenter quelque chose comme ceci:

Objectif c

- (void)viewDidLoad {
    [super viewDidLoad];

    self.heightAtIndexPath = [NSMutableDictionary new];
    self.tableView.rowHeight = UITableViewAutomaticDimension;
}

- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSNumber *height = [self.heightAtIndexPath objectForKey:indexPath];
    if(height) {
        return height.floatValue;
    } else {
        return UITableViewAutomaticDimension;
    }
}

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    NSNumber *height = @(cell.frame.size.height);
    [self.heightAtIndexPath setObject:height forKey:indexPath];
}

Swift 3

@IBOutlet var tableView : UITableView?
var heightAtIndexPath = NSMutableDictionary()

override func viewDidLoad() {
    super.viewDidLoad()

    tableView?.rowHeight = UITableViewAutomaticDimension
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    if let height = heightAtIndexPath.object(forKey: indexPath) as? NSNumber {
        return CGFloat(height.floatValue)
    } else {
        return UITableViewAutomaticDimension
    }
}

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    let height = NSNumber(value: Float(cell.frame.size.height))
    heightAtIndexPath.setObject(height, forKey: indexPath as NSCopying)
}
109
dosdos

Nous avons eu le même problème. Cela provient d'une mauvaise estimation de la hauteur de la cellule qui force le SDK à forcer une hauteur incorrecte, ce qui provoque le saut de cellules lors du défilement. Selon la manière dont vous avez construit votre cellule, la meilleure solution consiste à implémenter la méthode UITableViewDelegate. - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath

Tant que votre estimation est assez proche de la valeur réelle de la hauteur de la cellule, cela annulera presque le saut et les secousses. Voici comment nous l'avons implémenté, vous obtiendrez la logique:

- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
    // This method will get your cell identifier based on your data
    NSString *cellType = [self reuseIdentifierForIndexPath:indexPath];

    if ([cellType isEqualToString:kFirstCellIdentifier])
        return kFirstCellHeight;
    else if ([cellType isEqualToString:kSecondCellIdentifier])
        return kSecondCellHeight;
    else if ([cellType isEqualToString:kThirdCellIdentifier])
        return kThirdCellHeight;
    else {
        return UITableViewAutomaticDimension;
    }
}

Ajout du support Swift 2

func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    // This method will get your cell identifier based on your data
    let cellType = reuseIdentifierForIndexPath(indexPath)

    if cellType == kFirstCellIdentifier 
        return kFirstCellHeight
    else if cellType == kSecondCellIdentifier
        return kSecondCellHeight
    else if cellType == kThirdCellIdentifier
        return kThirdCellHeight
    else
        return UITableViewAutomaticDimension  
}
29
Gabriel Cartier

la réponse dosdos a fonctionné pour moi dans Swift 2

Déclarer l'ivar

var heightAtIndexPath = NSMutableDictionary()

dans func viewDidLoad ()

func viewDidLoad() {
  .... your code
  self.tableView.rowHeight = UITableViewAutomaticDimension
}

Ajoutez ensuite les 2 méthodes suivantes:

override func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
   let height = self.heightAtIndexPath.objectForKey(indexPath)
   if ((height) != nil) {
     return CGFloat(height!.floatValue)
   } else {
    return UITableViewAutomaticDimension
   }
 }

override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
  let height = cell.frame.size.height
  self.heightAtIndexPath.setObject(height, forKey: indexPath)
}

Swift 3:

var heightAtIndexPath = [IndexPath: CGFloat]()

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

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

Je faisais face au même problème aussi. J'ai trouvé une solution de contournement, mais cela ne résout pas complètement le crétin. Mais il semble être beaucoup mieux comparé au défilement désordonné précédent.

Dans votre méthode de délégué UITableView:cellForRowAtIndexPath:, essayez d'utiliser les deux méthodes suivantes pour mettre à jour les contraintes avant de renvoyer la cellule. (Langue rapide)

cell.setNeedsUpdateConstraints()
cell.updateConstraintsIfNeeded()

EDIT: Vous devrez peut-être aussi jouer avec la valeur tableView.estimatedRowHeight pour obtenir un défilement plus fluide.

2
Vishal Chandran

Après @dosdos answer.

J'ai aussi trouvé intéressant d'implémenter: tableView(tableView: didEndDisplayingCell: forRowAtIndexPath:

Spécialement pour mon code, où la cellule modifie les contraintes de manière dynamique alors que la cellule est déjà affichée à l'écran. La mise à jour du dictionnaire comme ceci aide la deuxième fois que la cellule est affichée.

var heightAtIndexPath = [NSIndexPath : NSNumber]()

....

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = UITableViewAutomaticDimension

....

extension TableViewViewController: UITableViewDelegate {

    //MARK: - UITableViewDelegate

    func tableView(tableView: UITableView,
                   estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {

        let height = heightAtIndexPath[indexPath]

        if let height = height {

            return CGFloat(height)
        }
        else {

            return UITableViewAutomaticDimension
        }
    }

    func tableView(tableView: UITableView,
                   willDisplayCell cell: UITableViewCell,
                                   forRowAtIndexPath indexPath: NSIndexPath) {

        let height: NSNumber = CGRectGetHeight(cell.frame)
        heightAtIndexPath[indexPath] = height
    }

    func tableView(tableView: UITableView,
                   didEndDisplayingCell cell: UITableViewCell,
                                        forRowAtIndexPath indexPath: NSIndexPath) {

        let height: NSNumber = CGRectGetHeight(cell.frame)
        heightAtIndexPath[indexPath] = height
    }
}
1
Gabriel.Massana

La solution @dosdos fonctionne bien 

mais il y a quelque chose que vous devriez ajouter

suite à @dosdos answer

Swift 3/4

@IBOutlet var tableView : UITableView!
var heightAtIndexPath = NSMutableDictionary()

override func viewDidLoad() {
    super.viewDidLoad()

    tableView?.rowHeight = UITableViewAutomaticDimension
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    if let height = heightAtIndexPath.object(forKey: indexPath) as? NSNumber {
        return CGFloat(height.floatValue)
    } else {
        return UITableViewAutomaticDimension
    }
}

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    let height = NSNumber(value: Float(cell.frame.size.height))
    heightAtIndexPath.setObject(height, forKey: indexPath as NSCopying)
}

puis utilisez ces lignes quand vous le voulez, je l’utilise à l’intérieur de textDidChange

  1. premier rechargement Tableview 
  2. contrainte de mise à jour
  3. enfin passer en haut de la table

    tableView.reloadData()
    self.tableView.layoutIfNeeded()
    self.tableView.setContentOffset(CGPoint.zero, animated: true)
    
0
Basil